# Harmony Connector:MTP 会话恢复与能力探测 调研日期:2026-09-10。本文补充 [初始技术调研](harmony-connector.md),只记录源码、公开问题和建议验证路线,不声称完成了真机修复。支持范围按实际 MTP 能力判定,不以 HarmonyOS 版本、机型字符串或 USB product 字符串限制。 本次任务提供的现象:macOS、libmtp 1.1.23、libusb 1.0.30;Pura 70 Ultra 的 USB 标识为 `12d1:1101`,USB product 为 `HDC Device`,MTP friendly name 为 `HUAWEI Pura70Ultra`,MTP model 为 `HBP-AL00`。0.1.1 在 `LIBMTP_Release_Device` 的 CloseSession 等待约 61 秒后超时;界面已异步处理断开,但用户报告需要拔插才能重连。上述为主任务提供的真机信息,本研究代理未操作 USB。 ## 可用于实现决策的结论 1. **libmtp 现有路径已经会 reset,不能把“新增 reset”当成修复。** `12d1:1101` 表项使用 `DEVICE_FLAGS_ANDROID_BUGS`,其中同时包含 `LONG_TIMEOUT` 和 `FORCE_RESET_ON_CLOSE`。前者选择 60 秒 USB 超时,后者在释放接口后、关闭 USB handle 前 reset。约 61 秒现象与该等待路径吻合,但仍须日志确认实际失败阶段。[设备表](https://github.com/libmtp/libmtp/blob/v1.1.23/src/music-players.h#L3290)、[标志定义](https://github.com/libmtp/libmtp/blob/v1.1.23/src/device-flags.h#L320)、[USB 实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libusb1-glue.c#L2087) 2. **OpenHarmony 开源代码存在能产生 CloseSession 无响应的明确控制流。** 它把协议成功码 `0x2001` 当成内部函数返回值,但组包调用方要求 `0`;因此在写响应前退出。该路径也未清除会话状态。它能解释同类症状,尚不能证明这台商业固件包含相同代码。完整链条见下节。 3. **`LIBMTP_Reset_Device` 不是 USB reset。** 它仅在设备广告 `ResetDevice(0x1010)` 时发送 MTP 操作;上述 OpenHarmony 实现又把该操作路由到 CloseSession 的同一实现。不能用它替代 USB 层恢复。[libmtp 实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.c#L3633)、[OpenHarmony 路由](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_operation.cpp#L249) 4. **建议优先验证“短时限关闭 → 释放接口 → 一次 USB reset → 关闭 handle → 新枚举重连”。** 必须记录每阶段结果,不能静默保留会话并显示已断开。单纯更换 session ID、再加既有标志或一直重试,均没有充分依据。 ## OpenHarmony CloseSession 的具体问题 此次 GitHub API 查询到的 `openharmony/multimedia_media_library` 当前提交为 `4a411de74cfaf694c14820a51a0e900b04241fe6`。以下结论固定到该提交,描述其开源实现,不将 OpenHarmony 与商业 HarmonyOS 固件等同。 ### 响应没有写入的调用链 | 层次 | 所查行为 | 影响 | | --- | --- | --- | | 内部成功码 | `MTP_SUCCESS = 0`。 | C++ 内部处理成功应返回 0。[定义](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/frameworks/utils/include/medialibrary_errno.h#L30) | | 协议成功码 | `MTP_OK_CODE = 0x2001`。 | 这是 MTP 响应容器的 response code。[定义](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/include/mtp_constants.h#L413) | | CloseSession payload | `Parser` 在 session 已开时返回 `MTP_OK_CODE`;`Maker` 写入该数值后也返回 `MTP_OK_CODE`。 | 返回值与调用层的内部成功码约定不一致。[实现](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/payload_data/close_session_data.cpp#L34) | | MtpPacket | `ParserPayload` 传播返回值;`MakerPayload`、`Maker` 只有返回 `MTP_SUCCESS` 才继续。 | CloseSession 的组包结果为 `0x2001`,被当作失败返回。[实现](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_packet.cpp#L166) | | MtpOperation | 收包失败也尝试 `SendMakeResponsePacket`,但该方法在 `responsePacketPtr_->Maker(false) != MTP_SUCCESS` 时提前返回。 | 无论从正常还是错误响应路径进入,都会在 `Write` 之前结束。[实现](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_operation.cpp#L129) | 这是静态控制流分析:在所查代码和正常对象初始化前提下,可以追踪到“收到关闭请求,主机却收不到关闭响应”的路径;并非手机端日志或固件反编译结果。`CalculateSize` 也调用这个返回协议码的 `Maker`,不过无需依赖长度问题,调用方对返回值的检查已足以解释写入被跳过。 ### 会话状态与重新打开 `DealRequest` 在 OpenSession 时检查 `sessionOpen`,未打开时保存参数中的 `tempSessionID`,已打开时返回 SessionAlreadyOpen;`GetOpenSession` 在成功响应时设置 `sessionOpen=true`。CloseSession 的 `Parser` 只检查已有会话,`GetCloseSession` 只创建 payload,没有将 `sessionOpen` 设为 false 或将 `sessionID` 清零;`ResetOperation` 只重置当前命令、transaction ID 和数据阶段。所查 `services/media_mtp` 源文件未找到关闭时清除这些会话字段的其他赋值。[打开处理与每次操作重置](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_operation.cpp#L213)、[会话响应处理](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_operation_utils.cpp#L668)、[初始状态](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/include/mtp_operation_context.h#L49) 因此,新的非零 session ID 不会自动绕过 `sessionOpen` 检查。设备端 `ResetDevice` 和 CloseSession 又使用同一个 payload 与响应处理,不能从 OperationsSupported 含 `0x1010` 推断它真的重置了会话。[payload 工厂](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/packet_payload_factory.cpp#L65) ## libmtp 的关闭、打开与身份语义 1.1.23 的 `LIBMTP_Release_Device` 返回类型是 `void`:它调用 `close_device`,然后清错误栈、释放 `PTP_USB`、`PTPParams`、存储和 device 对象。因而 Release 返回后,旧 `device`、`usbinfo` 和 USB handle 都不可继续使用;仅靠该 API 的返回值也拿不到关闭是否成功。[释放实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.c#L2513) 关闭顺序为 `ptp_closesession → close_usb`;后者默认执行 `clear_stall → libusb_release_interface → 按标志 libusb_reset_device → libusb_close`。reset 和 release 的返回值在此没有向调用者报告。连接路径先以 5 秒时限尝试 OpenSession,遇 `PTP_ERROR_IO` 已会 reset、关闭 USB、重新初始化并重试;接受 `OK` 和 `SessionAlreadyOpened`。若回到 retry,正常设备超时可能又是 60 秒。[1.1.23 USB 流程](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libusb1-glue.c#L2304) 截至本次调研的 libmtp 当前提交 `9de46078d15d290ac5c46039937b1164b918dbf0`,CloseSession 和 OpenSession 的上述恢复路径仍存在,未发现可通过直接升级主分支获得的对应修复。[当前代码](https://github.com/libmtp/libmtp/blob/9de46078d15d290ac5c46039937b1164b918dbf0/src/libusb1-glue.c) libmtp 使用 session 参数 `1`。`ptp_opensession` 自行将 OpenSession transaction ID 归零,并在事务后将本地 session ID 设为传入值。修改随机 ID 必须连同调用约定理解;它不会修复无响应或设备端 `sessionOpen` 不清除。代码中 InvalidTransactionID 分支先加 10,再调用会归零的 `ptp_opensession`,因此也不能把该分支视为可靠的 ID 恢复保证。[PTP 实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/ptp.c#L1364) `12d1:1101` 的上游记录来自 Nova14 的 ID 报告,只证明这个 USB ID 已入表;它不是对 Pura 70 Ultra 的型号鉴定。USB product `HDC Device` 也不否定同一复合设备上的 MTP 接口。应用应展示实际 MTP friendly/model 信息,使用选中 USB 实例及实际接口建立会话;不能将表中的 Nova14 名称硬编码为真实机型。[上游 issue #334](https://github.com/libmtp/libmtp/issues/334)、[设备表](https://github.com/libmtp/libmtp/blob/v1.1.23/src/music-players.h#L3290) libmtp README 本身记录了设备 CloseSession 实现不佳、首次打开成功而后续失败的历史问题;这是固件兼容问题的先例,不是当前设备根因的独立证明。[libmtp README](https://github.com/libmtp/libmtp/blob/v1.1.23/README) ## macOS USB reset 的限制与调用位置 `libusb_reset_device` 的公开契约是 USB port reset,并尝试恢复先前 configuration/alternate settings;如果重枚举后的设备改变而返回 `LIBUSB_ERROR_NOT_FOUND`,调用者应关闭旧 handle 并重新发现设备。它不是“手机已经接受新的 MTP 会话”的保证。[libusb API](https://libusb.sourceforge.io/api-1.0/group__libusb__dev.html#gafee9c4638f1713ca5faa867948878111) libusb 1.0.30 Darwin 后端在普通、未 capture 的 macOS 路径调用 `USBDeviceReEnumerate`,等待重枚举,再恢复状态;源码说明 10.11 起旧 `ResetDevice` 路径不再有效。它有专门的重枚举等待和返回值,不能省略检查,也不能与另一线程的 close/open/reset 并发。[Darwin reset 实现](https://github.com/libusb/libusb/blob/v1.0.30/libusb/os/darwin_usb.c#L2139) 2026-08 的上游 [issue #1923](https://github.com/libusb/libusb/issues/1923) 报告 Darwin 在接口仍 claimed 时 reset、再 claim 的旧接口泄漏,以及 restore 失败后 claim 位丢失。对应 [PR #1942](https://github.com/libusb/libusb/pull/1942) 在本次查询时仍未合并。报告包含 macOS 实验,但设备是 SEGGER J-Link、部分失败来自故障注入,不能当成本项目手机已命中该问题。它足以说明:**不能因为其他库先 reset 就无条件把本项目 reset 移到 release_interface 之前。** 建议的调用位置: | 位置 | 判定 | | --- | --- | | 传输中、另一个线程仍执行 MTP 时 | 不使用;先串行收束当前事务,避免重置仍在使用的句柄。 | | CloseSession 失败后、USB handle 尚有效 | 可以由拥有该 handle 的底层实现处理;优先释放接口后 reset,检查结果,再 close。 | | `LIBMTP_Release_Device` 返回后,拿旧 `usbinfo` reset | 错误:对象和 handle 已释放。 | | Release 完成后,新枚举并打开同一 USB 实例的全新 libusb handle,再 reset | 可作为受控恢复路线;这时不要先 OpenSession 再 reset。需要准确定位设备、检查返回值并重新枚举,不能仅按 VID/PID 匹配第一台。 | | 正常关闭也无条件反复 reset | 不作为额外策略;现有 quirk 已可能 reset,重复执行增加重枚举和占用干扰。 | 在本项目固定版本的 C 边界内,关闭超时和原始 reset 状态若无法经公开 API取得,可用小范围、可重现的依赖补丁返回状态;避免在 Rust/前端按内存偏移解包私有结构。若用新 handle 做恢复,不应复用旧 bus/device address 作为永久身份;重枚举可能使它失效。[libusb 枚举与 handle 说明](https://libusb.sourceforge.io/api-1.0/group__libusb__dev.html) ## 其他 macOS MTP 工具能提供什么证据 | 实现 | 观察 | 可借鉴与边界 | | --- | --- | --- | | OpenMTP 的 go-mtpx/go-mtpfs | `go-mtpx` 固定到 `go-mtpfs` 提交 `1c3302b3c476`。`Close` 遇 CloseSession 错误会先 USB Reset,再 ReleaseInterface、Close;`Configure` 遇 SessionAlreadyOpened 先尝试 CloseSession/OpenSession,其余失败 reset、close、等约一秒、重新打开。 | 证明一种现实恢复路线存在,但不是这台设备或当前 libusb 1.0.30 的验证。不要直接继承“约一秒足够”的假设。[依赖](https://github.com/ganeshrvel/go-mtpx/blob/18f12db021cc4256396748dbd2996d15853d7033/go.mod)、[关闭和配置](https://github.com/ganeshrvel/go-mtpfs/blob/1c3302b3c476f69e787a0339122a804dd518a007/mtp/mtp.go#L94) | | go-mtpfs session ID | 每次选择非零、非 `0xffffffff` 的随机 session ID。 | 是它自身的实现选择;没有证明随机化能解决 OpenHarmony 的状态未清除。[实现](https://github.com/ganeshrvel/go-mtpfs/blob/1c3302b3c476f69e787a0339122a804dd518a007/mtp/ops.go#L16) | | Android File Transfer for Linux/macOS(whoozle) | Session Close 发 CloseSession 并等响应;通用 Device Open 有可选 reset,但其 Darwin backend 的 `Device::Reset()` 是空实现。 | 不能从工具支持 macOS 或方法名推断它进行了有效 USB reset。这是开源独立项目,非 Google 的同名 Android File Transfer。[项目](https://github.com/whoozle/android-file-transfer-linux)、[session](https://github.com/whoozle/android-file-transfer-linux/blob/88926930db41238c7b4d7237fc5849b9586cc7b8/mtp/ptp/Session.cpp#L105)、[Darwin 空实现](https://github.com/whoozle/android-file-transfer-linux/blob/88926930db41238c7b4d7237fc5849b9586cc7b8/mtp/backend/darwin/usb/Device.cpp#L38) | 本次未取得 Google 闭源 Android File Transfer 的第一方 host 源码,因此不声称其关闭顺序或私有恢复方式。OpenMTP 的 Huawei P30 Pro [公开问题 #452](https://github.com/ganeshrvel/openmtp/issues/452) 报告的是目录/传输故障,也不能作为 HarmonyOS 当前设备的重连成功证明。 ## 按实际设备能力开放功能 建立会话后读取设备 `OperationsSupported`、`DevicePropertiesSupported`,对象功能还要结合格式、属性描述、存储写权限和操作实际返回值。广告支持不等于本次操作成功,也不等于所有对象类型都可用。OpenHarmony 所查代码声明了 delete、move、copy、thumbnail、battery 和对象属性,但实际商业设备返回值才是应用开关依据。[OpenHarmony DeviceInfo 声明](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/payload_data/get_device_info_data.cpp#L32) 以下为 libmtp 1.1.23 的公开 API 映射;操作码由其 PTP 定义核对。[公开头文件](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.h.in)、[PTP 操作定义](https://github.com/libmtp/libmtp/blob/v1.1.23/src/ptp.h) | 功能 | 公开 API | 探测与实现边界 | | --- | --- | --- | | 改名 | `LIBMTP_Set_File_Name`、`LIBMTP_Set_Folder_Name` | 按实际格式查询 `ObjectFileName(0xdc07)`;要求 `GetObjectPropDesc(0x9802)` 的 GetSet 为可写,并有可用的 `SetObjectPropValue(0x9804)` 或 SetObjectPropList。库的改名 helper 自己检查 GetSet。不要新用已弃用的 `LIBMTP_Set_Object_Filename`。 | | 删除 | `LIBMTP_Delete_Object` | `DeleteObject(0x100b)`;仍处理 ObjectWriteProtected、AccessDenied 等对象级拒绝。目录递归由应用计划与执行,库文档不保证删除目录会删除全部子对象。 | | 移动 | `LIBMTP_Check_Capability(...MoveObject)`、`LIBMTP_Move_Object` | `MoveObject(0x1019)`;目标根 parent 参数是 `0`。目录和跨存储行为须验证。标准命令无字节进度,不显示虚假百分比。 | | 设备内复制 | `LIBMTP_Check_Capability(...CopyObject)`、`LIBMTP_Copy_Object` | `CopyObject(0x101a)`;无字节进度;API 仅返回成功/失败,不返回新对象 ID,完成后重新枚举目标目录。不支持时不可把移动偷偷变成复制后删除。 | | 缩略图 | `LIBMTP_Get_Thumbnail` | `GetThumb(0x100a)`,并按对象是否有缩略图处理。失败显示普通图标;它不是整图预览保证。RepresentativeSample 是另一组属性/API。 | | 电量 | `LIBMTP_Get_Batterylevel` | `BatteryLevel(0x5001)` 在设备属性列表中;API还检查 broken battery quirk。只有调用成功且最大值有效时换算,失败/不支持显示未知,不能显示 0%。 | | 对象属性读取 | `LIBMTP_Is_Property_Supported`、`LIBMTP_Get_String_From_Object`、`LIBMTP_Get_u64/u32/u16/u8_From_Object` | `GetObjectPropsSupported(0x9801)` 按格式,`GetObjectPropDesc(0x9802)`、`GetObjectPropValue(0x9803)` 按具体属性。`Is_Property_Supported` 返回正数/0/负数分别为支持/不支持/查询失败,不能把 -1 当成 true。 | | 对象属性写入 | `LIBMTP_Set_Object_String/u32/u16/u8`、`LIBMTP_Get_Allowed_Property_Values` | 存在不等于可写;遵循 GetSet、数据类型、枚举/范围。后一个 API 返回合法值,不直接暴露 GetSet;不能据它推断写权限。 | | 目录下载 | `LIBMTP_Get_Files_And_Folders` + `LIBMTP_Get_File_To_File_Descriptor` | 主机遍历对象树,使用各 storage ID/parent ID;不是单一“递归传输”操作。按实际项汇总大小、进度和失败。 | | 目录上传 | `LIBMTP_Create_Folder` + `LIBMTP_Send_File_From_File_Descriptor` | 以 association/folder 对象建立层级,再逐文件传输;基础操作为 SendObjectInfo/SendObject 或设备支持的 SendObjectPropList 路径。保留空目录、逐项处理重名和失败。 | 公开 `LIBMTP_Check_Capability` 仅覆盖 GetPartialObject、SendPartialObject、EditObjects、MoveObject、CopyObject,不能拿它询问任意操作码。若前端需要全部 OperationsSupported,应在与固定 libmtp 一起编译的 C 桥接层提供只读投影,或给依赖增加受控 API,不从 UI 解析 `mtp-detect` 文本。[能力实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.c#L4132) GetObjectPropList 也要遵循 libmtp 的已知兼容标志,不能为了“一次取全树”强制启用。对于当前 ID,上游已有 BROKEN_MTPGETOBJPROPLIST/BROKEN_SET_OBJECT_PROPLIST/BROKEN_SEND_OBJECT_PROPLIST;这些是库的兼容策略,不表示设备完全不能改名或传输。[兼容标志](https://github.com/libmtp/libmtp/blob/v1.1.23/src/device-flags.h#L320) 递归删除宜先枚举用户选中的子树,再从叶到根执行。复制/移动目录时,不能仅凭标准操作支持就承诺任意层级、跨存储、同名覆盖或取消的原子性。libmtp 文档明确警告目录操作语义与耗时;应用级递归必须保留部分成功状态,不能自动删除原文件作为未说明的降级。[删除/移动/复制契约](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.c#L6922) ## 最小验证路线 以下是建议给主任务执行的实验,不是本文已经执行的验证。 1. 串行记录一次连接、只读目录、断开、重连全过程。区分 CloseSession 写请求、等响应、release_interface、USB reset、USB close 的耗时与返回码;确认旧后端进程/线程是否退出、旧 handle 是否完成释放。不要把设备名可枚举当成 MTP 会话已恢复。 2. 将关闭独立于正常传输超时:CloseSession 超时后继续清理,避免反复调用它;不改变大文件传输的合理超时。先保留当前 release→reset 顺序,检查这次 reset 是否真的完成。若产品需要状态回传,在固定库边界内显式提供结果。 3. 旧 handle 全部关闭后,按新枚举结果重连,实际完成 GetStorageIDs 与至少一次目录读取。只观察 UI 按钮恢复、扫描返回设备、reset 返回 0 均不足以通过。 4. 只在基线有完整日志后比较候选方案:释放接口后 reset;必要时用全新 raw USB handle 做独立恢复。一次实验只变一处,避免短超时、随机 ID、不同 reset 顺序、额外库补丁同时变化而无法定位。 5. 若 reset 返回 NOT_FOUND,按 API 重新发现设备;若 reset 无响应或设备不重新出现,显示恢复失败和真实状态,不无限循环。硬件重新插拔能恢复,不足以证明软件重枚举一定具备等价效果。 6. 修复通过后,在同一 USB 插接周期连续完成多次 connect→browse→disconnect→connect,并包含插件关闭/退出再打开。写操作能力在专用测试目录验证:改名、删除、移动、复制、中文/emoji、空文件、空目录、嵌套传输,下载回传比对哈希;缩略图和电量失败应保留其余浏览能力。 本研究实际完成:读取项目固定依赖源码;在线查询并固定第一方仓库提交;核对 OpenHarmony 会话调用链、libmtp API、libusb Darwin reset、OpenMTP 与 whoozle 实现;查询相关公开 issue/PR 状态。未修改实现、未打开或重置 USB、未把开源静态分析标为商业设备实测根因。