20 KiB
Harmony Connector:MTP 会话恢复与能力探测
调研日期:2026-09-10。本文补充 初始技术调研,只记录源码、公开问题和建议验证路线,不声称完成了真机修复。支持范围按实际 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。
可用于实现决策的结论
- libmtp 现有路径已经会 reset,不能把“新增 reset”当成修复。
12d1:1101表项使用DEVICE_FLAGS_ANDROID_BUGS,其中同时包含LONG_TIMEOUT和FORCE_RESET_ON_CLOSE。前者选择 60 秒 USB 超时,后者在释放接口后、关闭 USB handle 前 reset。约 61 秒现象与该等待路径吻合,但仍须日志确认实际失败阶段。设备表、标志定义、USB 实现 - OpenHarmony 开源代码存在能产生 CloseSession 无响应的明确控制流。 它把协议成功码
0x2001当成内部函数返回值,但组包调用方要求0;因此在写响应前退出。该路径也未清除会话状态。它能解释同类症状,尚不能证明这台商业固件包含相同代码。完整链条见下节。 LIBMTP_Reset_Device不是 USB reset。 它仅在设备广告ResetDevice(0x1010)时发送 MTP 操作;上述 OpenHarmony 实现又把该操作路由到 CloseSession 的同一实现。不能用它替代 USB 层恢复。libmtp 实现、OpenHarmony 路由- 建议优先验证“短时限关闭 → 释放接口 → 一次 USB reset → 关闭 handle → 新枚举重连”。 必须记录每阶段结果,不能静默保留会话并显示已断开。单纯更换 session ID、再加既有标志或一直重试,均没有充分依据。
OpenHarmony CloseSession 的具体问题
此次 GitHub API 查询到的 openharmony/multimedia_media_library 当前提交为 4a411de74cfaf694c14820a51a0e900b04241fe6。以下结论固定到该提交,描述其开源实现,不将 OpenHarmony 与商业 HarmonyOS 固件等同。
响应没有写入的调用链
| 层次 | 所查行为 | 影响 |
|---|---|---|
| 内部成功码 | MTP_SUCCESS = 0。 |
C++ 内部处理成功应返回 0。定义 |
| 协议成功码 | MTP_OK_CODE = 0x2001。 |
这是 MTP 响应容器的 response code。定义 |
| CloseSession payload | Parser 在 session 已开时返回 MTP_OK_CODE;Maker 写入该数值后也返回 MTP_OK_CODE。 |
返回值与调用层的内部成功码约定不一致。实现 |
| MtpPacket | ParserPayload 传播返回值;MakerPayload、Maker 只有返回 MTP_SUCCESS 才继续。 |
CloseSession 的组包结果为 0x2001,被当作失败返回。实现 |
| MtpOperation | 收包失败也尝试 SendMakeResponsePacket,但该方法在 responsePacketPtr_->Maker(false) != MTP_SUCCESS 时提前返回。 |
无论从正常还是错误响应路径进入,都会在 Write 之前结束。实现 |
这是静态控制流分析:在所查代码和正常对象初始化前提下,可以追踪到“收到关闭请求,主机却收不到关闭响应”的路径;并非手机端日志或固件反编译结果。CalculateSize 也调用这个返回协议码的 Maker,不过无需依赖长度问题,调用方对返回值的检查已足以解释写入被跳过。
会话状态与重新打开
DealRequest 在 OpenSession 时检查 sessionOpen,未打开时保存参数中的 tempSessionID,已打开时返回 SessionAlreadyOpen;GetOpenSession 在成功响应时设置 sessionOpen=true。CloseSession 的 Parser 只检查已有会话,GetCloseSession 只创建 payload,没有将 sessionOpen 设为 false 或将 sessionID 清零;ResetOperation 只重置当前命令、transaction ID 和数据阶段。所查 services/media_mtp 源文件未找到关闭时清除这些会话字段的其他赋值。打开处理与每次操作重置、会话响应处理、初始状态
因此,新的非零 session ID 不会自动绕过 sessionOpen 检查。设备端 ResetDevice 和 CloseSession 又使用同一个 payload 与响应处理,不能从 OperationsSupported 含 0x1010 推断它真的重置了会话。payload 工厂
libmtp 的关闭、打开与身份语义
1.1.23 的 LIBMTP_Release_Device 返回类型是 void:它调用 close_device,然后清错误栈、释放 PTP_USB、PTPParams、存储和 device 对象。因而 Release 返回后,旧 device、usbinfo 和 USB handle 都不可继续使用;仅靠该 API 的返回值也拿不到关闭是否成功。释放实现
关闭顺序为 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 流程
截至本次调研的 libmtp 当前提交 9de46078d15d290ac5c46039937b1164b918dbf0,CloseSession 和 OpenSession 的上述恢复路径仍存在,未发现可通过直接升级主分支获得的对应修复。当前代码
libmtp 使用 session 参数 1。ptp_opensession 自行将 OpenSession transaction ID 归零,并在事务后将本地 session ID 设为传入值。修改随机 ID 必须连同调用约定理解;它不会修复无响应或设备端 sessionOpen 不清除。代码中 InvalidTransactionID 分支先加 10,再调用会归零的 ptp_opensession,因此也不能把该分支视为可靠的 ID 恢复保证。PTP 实现
12d1:1101 的上游记录来自 Nova14 的 ID 报告,只证明这个 USB ID 已入表;它不是对 Pura 70 Ultra 的型号鉴定。USB product HDC Device 也不否定同一复合设备上的 MTP 接口。应用应展示实际 MTP friendly/model 信息,使用选中 USB 实例及实际接口建立会话;不能将表中的 Nova14 名称硬编码为真实机型。上游 issue #334、设备表
libmtp README 本身记录了设备 CloseSession 实现不佳、首次打开成功而后续失败的历史问题;这是固件兼容问题的先例,不是当前设备根因的独立证明。libmtp README
macOS USB reset 的限制与调用位置
libusb_reset_device 的公开契约是 USB port reset,并尝试恢复先前 configuration/alternate settings;如果重枚举后的设备改变而返回 LIBUSB_ERROR_NOT_FOUND,调用者应关闭旧 handle 并重新发现设备。它不是“手机已经接受新的 MTP 会话”的保证。libusb API
libusb 1.0.30 Darwin 后端在普通、未 capture 的 macOS 路径调用 USBDeviceReEnumerate,等待重枚举,再恢复状态;源码说明 10.11 起旧 ResetDevice 路径不再有效。它有专门的重枚举等待和返回值,不能省略检查,也不能与另一线程的 close/open/reset 并发。Darwin reset 实现
2026-08 的上游 issue #1923 报告 Darwin 在接口仍 claimed 时 reset、再 claim 的旧接口泄漏,以及 restore 失败后 claim 位丢失。对应 PR #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 说明
其他 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 的验证。不要直接继承“约一秒足够”的假设。依赖、关闭和配置 |
| go-mtpfs session ID | 每次选择非零、非 0xffffffff 的随机 session ID。 |
是它自身的实现选择;没有证明随机化能解决 OpenHarmony 的状态未清除。实现 |
| Android File Transfer for Linux/macOS(whoozle) | Session Close 发 CloseSession 并等响应;通用 Device Open 有可选 reset,但其 Darwin backend 的 Device::Reset() 是空实现。 |
不能从工具支持 macOS 或方法名推断它进行了有效 USB reset。这是开源独立项目,非 Google 的同名 Android File Transfer。项目、session、Darwin 空实现 |
本次未取得 Google 闭源 Android File Transfer 的第一方 host 源码,因此不声称其关闭顺序或私有恢复方式。OpenMTP 的 Huawei P30 Pro 公开问题 #452 报告的是目录/传输故障,也不能作为 HarmonyOS 当前设备的重连成功证明。
按实际设备能力开放功能
建立会话后读取设备 OperationsSupported、DevicePropertiesSupported,对象功能还要结合格式、属性描述、存储写权限和操作实际返回值。广告支持不等于本次操作成功,也不等于所有对象类型都可用。OpenHarmony 所查代码声明了 delete、move、copy、thumbnail、battery 和对象属性,但实际商业设备返回值才是应用开关依据。OpenHarmony DeviceInfo 声明
以下为 libmtp 1.1.23 的公开 API 映射;操作码由其 PTP 定义核对。公开头文件、PTP 操作定义
| 功能 | 公开 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 文本。能力实现
GetObjectPropList 也要遵循 libmtp 的已知兼容标志,不能为了“一次取全树”强制启用。对于当前 ID,上游已有 BROKEN_MTPGETOBJPROPLIST/BROKEN_SET_OBJECT_PROPLIST/BROKEN_SEND_OBJECT_PROPLIST;这些是库的兼容策略,不表示设备完全不能改名或传输。兼容标志
递归删除宜先枚举用户选中的子树,再从叶到根执行。复制/移动目录时,不能仅凭标准操作支持就承诺任意层级、跨存储、同名覆盖或取消的原子性。libmtp 文档明确警告目录操作语义与耗时;应用级递归必须保留部分成功状态,不能自动删除原文件作为未说明的降级。删除/移动/复制契约
最小验证路线
以下是建议给主任务执行的实验,不是本文已经执行的验证。
- 串行记录一次连接、只读目录、断开、重连全过程。区分 CloseSession 写请求、等响应、release_interface、USB reset、USB close 的耗时与返回码;确认旧后端进程/线程是否退出、旧 handle 是否完成释放。不要把设备名可枚举当成 MTP 会话已恢复。
- 将关闭独立于正常传输超时:CloseSession 超时后继续清理,避免反复调用它;不改变大文件传输的合理超时。先保留当前 release→reset 顺序,检查这次 reset 是否真的完成。若产品需要状态回传,在固定库边界内显式提供结果。
- 旧 handle 全部关闭后,按新枚举结果重连,实际完成 GetStorageIDs 与至少一次目录读取。只观察 UI 按钮恢复、扫描返回设备、reset 返回 0 均不足以通过。
- 只在基线有完整日志后比较候选方案:释放接口后 reset;必要时用全新 raw USB handle 做独立恢复。一次实验只变一处,避免短超时、随机 ID、不同 reset 顺序、额外库补丁同时变化而无法定位。
- 若 reset 返回 NOT_FOUND,按 API 重新发现设备;若 reset 无响应或设备不重新出现,显示恢复失败和真实状态,不无限循环。硬件重新插拔能恢复,不足以证明软件重枚举一定具备等价效果。
- 修复通过后,在同一 USB 插接周期连续完成多次 connect→browse→disconnect→connect,并包含插件关闭/退出再打开。写操作能力在专用测试目录验证:改名、删除、移动、复制、中文/emoji、空文件、空目录、嵌套传输,下载回传比对哈希;缩略图和电量失败应保留其余浏览能力。
本研究实际完成:读取项目固定依赖源码;在线查询并固定第一方仓库提交;核对 OpenHarmony 会话调用链、libmtp API、libusb Darwin reset、OpenMTP 与 whoozle 实现;查询相关公开 issue/PR 状态。未修改实现、未打开或重置 USB、未把开源静态分析标为商业设备实测根因。