cj-tauri 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +113 -99
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +632 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +526 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3740 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/prebuilt/linux-x64/cj-tauri +0 -0
  77. package/src/api_system.cj +67 -67
  78. package/src/app.cj +325 -969
  79. package/src/app_events.cj +166 -0
  80. package/src/app_host_ops.cj +287 -0
  81. package/src/app_run.cj +283 -0
  82. package/src/app_spec.cj +76 -76
  83. package/src/arkweb_menu.cj +216 -0
  84. package/src/arkweb_script.cj +142 -142
  85. package/src/capability.cj +409 -409
  86. package/src/clipboard_bridge.cj +178 -0
  87. package/src/dialog_spec.cj +79 -0
  88. package/src/dragdrop.cj +26 -26
  89. package/src/embedded_assets.cj +98 -98
  90. package/src/host.cj +752 -607
  91. package/src/host_arkweb.cj +924 -778
  92. package/src/host_webkit.cj +663 -570
  93. package/src/host_webview2.cj +663 -570
  94. package/src/hotkey.cj +66 -66
  95. package/src/ipc_hub.cj +337 -337
  96. package/src/ipc_message.cj +286 -286
  97. package/src/launch.cj +62 -62
  98. package/src/logging.cj +127 -127
  99. package/src/menu.cj +197 -197
  100. package/src/platform/platform_app_context.cj +77 -77
  101. package/src/platform/platform_assets.cj +33 -33
  102. package/src/platform/platform_discover.cj +176 -176
  103. package/src/platform/platform_env.cj +29 -29
  104. package/src/platform/platform_feature.cj +31 -31
  105. package/src/platform/platform_page_source.cj +87 -87
  106. package/src/platform/platform_target_info.cj +32 -32
  107. package/src/platform_host_provider.cj +87 -87
  108. package/src/plugin.cj +333 -333
  109. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  110. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  111. package/src/plugins/fs/plugin_fs.cj +105 -1
  112. package/src/plugins/http/http_backend.cj +131 -0
  113. package/src/plugins/http/http_desktop.cj +195 -0
  114. package/src/plugins/http/http_ohos.cj +204 -0
  115. package/src/plugins/http/plugin_http.cj +310 -459
  116. package/src/plugins/log/plugin_log.cj +105 -105
  117. package/src/plugins/menu/plugin_menu.cj +158 -158
  118. package/src/plugins/notification/plugin_notification.cj +154 -154
  119. package/src/plugins/serial/plugin_serial.cj +458 -454
  120. package/src/plugins/serial/serial_bridge.cj +194 -194
  121. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  122. package/src/plugins/serial/serial_ohos.cj +529 -0
  123. package/src/plugins/shell/plugin_shell.cj +258 -258
  124. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  125. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  126. package/src/plugins/ws/plugin_ws.cj +488 -470
  127. package/src/process_hidden.cj +174 -174
  128. package/src/scope.cj +391 -391
  129. package/src/shell_event.cj +69 -69
  130. package/src/tests/app_spec_test.cj +126 -127
  131. package/src/tests/capability_test.cj +243 -243
  132. package/src/tests/dialog_spec_test.cj +73 -0
  133. package/src/tests/fake_host.cj +201 -201
  134. package/src/tests/ipc_hub_test.cj +487 -487
  135. package/src/tests/logging_test.cj +62 -62
  136. package/src/tests/platform_contract_test.cj +251 -251
  137. package/src/tests/plugin_clipboard_test.cj +222 -222
  138. package/src/tests/plugin_dialog_test.cj +237 -237
  139. package/src/tests/plugin_fs_test.cj +207 -0
  140. package/src/tests/plugin_http_test.cj +124 -124
  141. package/src/tests/plugin_notification_test.cj +205 -205
  142. package/src/tests/plugin_serial_test.cj +48 -0
  143. package/src/tests/plugin_test.cj +605 -585
  144. package/src/tests/plugin_ws_test.cj +40 -40
  145. package/src/tests/process_hidden_test.cj +129 -129
  146. package/src/tests/scope_test.cj +350 -350
  147. package/src/tests/system_host_test.cj +99 -81
  148. package/src/tests/urlutil_test.cj +53 -53
  149. package/src/tests/version_test.cj +75 -75
  150. package/src/tests/window_registry_test.cj +123 -123
  151. package/src/tray.cj +90 -90
  152. package/src/urlutil.cj +159 -159
  153. package/src/version.cj +44 -44
  154. package/src/window.cj +51 -51
  155. package/src/window_registry.cj +116 -116
  156. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  157. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  158. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -0,0 +1,529 @@
1
+ package cjTauri.plugins.serial
2
+
3
+ /**
4
+ * ohos 侧 serial 真实现(批次 4,issue #5):NDK FFI 直连 musl libc 的 POSIX 串口面。
5
+ * 前身是 P-1 桩(serial_ohos_stub.cj,已删);写法照剪贴板那次的「NDK FFI 直连」模式
6
+ * (`src/clipboard_bridge.cj`),不经 C 桥、不加能力位。
7
+ *
8
+ * 为什么不引 serialport 式的现成库:std / stdx 都没有串口与 termios 绑定;同类项目 sscom
9
+ * 实测 serialport 依赖 nix/libudev 在 OHOS target 编不过,只能手写 POSIX——本文件同理,
10
+ * termios 结构体按 musl(aarch64 linux)布局手工按字节偏移读写,不依赖 @C 结构体映射。
11
+ *
12
+ * 边界与桌面两端的差异(对齐 RFC §4.6.4 P-2 的口径):
13
+ * - 桌面侧「真正的白名单在 C 桥 core」;ohos 没有 C 桥,本文件**就是**安全边界,
14
+ * open 前必须再过一次 `serialPathLooksAllowed`(仓颉侧镜像,plugin_serial.cj)——
15
+ * 页面可控字符串绝不能只靠上层命令层的校验。
16
+ * - 错误明细是包级共享槽(仓颉没有线程局部),Mutex 守着;并发失败时可能被覆盖,
17
+ * 代价只是错误文案不准,与桌面「线程局部」的语义差异如实记在这里。
18
+ * - 读超时用 poll(有数据才 read,与桌面 Linux 桥同口径);写按预算轮转。
19
+ * - 设备节点访问在真机上可能要 DDK/USB 权限甚至系统签名(sscom 同款风险),装不上 /
20
+ * 打不开时本实现如实报错,不伪装成功。
21
+ */
22
+
23
+ @When[env == "ohos"]
24
+ import std.collection.*
25
+
26
+ // std.sync 只被 ohos 门禁代码用(Mutex 状态槽),import 随之门禁——桌面侧整段摘除
27
+ @When[env == "ohos"]
28
+ import std.sync.*
29
+
30
+ @When[env == "ohos"]
31
+ import std.time.*
32
+
33
+ @When[env == "ohos"]
34
+ import std.fs.*
35
+
36
+ @When[env == "ohos"]
37
+ import std.sort.*
38
+
39
+ // ---- musl libc 的 POSIX 串口面(@C foreign 直连;符号都在 libc,无需 [links] 追加) ----
40
+
41
+ /** open 只按双参声明:aarch64(AAPCS64)上可变参与固定参的整型传参相同, musl 的 open 直接可用 */
42
+ @When[env == "ohos"]
43
+ @C
44
+ foreign func open(path: CString, flags: Int32): Int32
45
+
46
+ @When[env == "ohos"]
47
+ @C
48
+ foreign func close(fd: Int32): Int32
49
+
50
+ @When[env == "ohos"]
51
+ @C
52
+ foreign func read(fd: Int32, buf: CPointer<UInt8>, count: UInt64): Int64
53
+
54
+ @When[env == "ohos"]
55
+ @C
56
+ foreign func write(fd: Int32, buf: CPointer<UInt8>, count: UInt64): Int64
57
+
58
+ /** termios 按 64 字节裸缓冲传递(musl aarch64 布局,实际 60 字节) */
59
+ @When[env == "ohos"]
60
+ @C
61
+ foreign func tcgetattr(fd: Int32, termios: CPointer<UInt8>): Int32
62
+
63
+ @When[env == "ohos"]
64
+ @C
65
+ foreign func tcsetattr(fd: Int32, actions: Int32, termios: CPointer<UInt8>): Int32
66
+
67
+ @When[env == "ohos"]
68
+ @C
69
+ foreign func cfsetispeed(termios: CPointer<UInt8>, speed: UInt32): Int32
70
+
71
+ @When[env == "ohos"]
72
+ @C
73
+ foreign func cfsetospeed(termios: CPointer<UInt8>, speed: UInt32): Int32
74
+
75
+ /** pollfd 按 8 字节裸缓冲:fd(Int32) | events(Int16) | revents(Int16) */
76
+ @When[env == "ohos"]
77
+ @C
78
+ foreign func poll(fds: CPointer<UInt8>, nfds: UInt64, timeout: Int32): Int32
79
+
80
+ /** errno 与 strerror:给 serialLastError 一句人话(strerror 返回静态串,不释放) */
81
+ @When[env == "ohos"]
82
+ @C
83
+ foreign func __errno_location(): CPointer<Int32>
84
+
85
+ @When[env == "ohos"]
86
+ @C
87
+ foreign func strerror(errnum: Int32): CString
88
+
89
+ // ---- Linux/asm-generic 常量(与 sscom 的取值一字不差) ----
90
+
91
+ // open flags
92
+ const OHOS_O_RDWR: Int32 = 0x2
93
+ const OHOS_O_NOCTTY: Int32 = 0x100
94
+
95
+ // tcsetattr actions:TCSANOW
96
+ const OHOS_TCSANOW: Int32 = 0
97
+
98
+ // cflag 位
99
+ const OHOS_CSIZE: UInt32 = 0x30
100
+ const OHOS_CS8: UInt32 = 0x30
101
+ const OHOS_CSTOPB: UInt32 = 0x40
102
+ const OHOS_CREAD: UInt32 = 0x80
103
+ const OHOS_PARENB: UInt32 = 0x100
104
+ const OHOS_PARODD: UInt32 = 0x200
105
+ const OHOS_CLOCAL: UInt32 = 0x800
106
+
107
+ // iflag 原始位(cfmakeraw 要清的):IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON
108
+ const OHOS_IFLAG_RAW: UInt32 = 0x5eb
109
+
110
+ // lflag 原始位:ECHO|ICANON|ISIG|IEXTEN
111
+ const OHOS_LFLAG_RAW: UInt32 = 0x800b
112
+
113
+ // poll 事件
114
+ const OHOS_POLLIN: Int16 = 0x1
115
+ const OHOS_POLLOUT: Int16 = 0x4
116
+ // errno:EAGAIN
117
+ const OHOS_EAGAIN: Int32 = 11
118
+
119
+ /** termios 结构体里 speed 字段的字节偏移(musl aarch64:c_ispeed@52 / c_ospeed@56) */
120
+ const OHOS_TERMIOS_ISPEED: Int64 = 52
121
+ const OHOS_TERMIOS_OSPEED: Int64 = 56
122
+
123
+ /** c_cc 里 VTIME / VMIN 的下标(Linux 布局);poll+read 模式下两个都置 0 */
124
+ const OHOS_VTIME: Int64 = 5
125
+ const OHOS_VMIN: Int64 = 6
126
+
127
+ /** 波特率 → termios 速度常量(asm-generic:B38400 以下连续,往上 0x1000+n);
128
+ * 档位与 plugin_serial.cj 的 SERIAL_BAUDS 对齐(那层已拦过一次,这里兜底)。
129
+ * public:单测要直接构造(跨包可见性,AGENTS §2) */
130
+ public func serialOhosBaudConst(baud: Int32): Option<UInt32> {
131
+ let low: Option<UInt32> = match (baud) {
132
+ case 300 => Some(0x7)
133
+ case 600 => Some(0x8)
134
+ case 1200 => Some(0x9)
135
+ case 2400 => Some(0xb)
136
+ case 4800 => Some(0xc)
137
+ case 9600 => Some(0xd)
138
+ case 19200 => Some(0xe)
139
+ case 38400 => Some(0xf)
140
+ case _ => None
141
+ }
142
+ if (let Some(v) <- low) {
143
+ return low
144
+ }
145
+ match (baud) {
146
+ case 57600 => Some(0x1001)
147
+ case 115200 => Some(0x1002)
148
+ case 230400 => Some(0x1003)
149
+ case 460800 => Some(0x1004)
150
+ case 500000 => Some(0x1005)
151
+ case 921600 => Some(0x1006)
152
+ case 1000000 => Some(0x1008)
153
+ case _ => None
154
+ }
155
+ }
156
+
157
+ // ---- 错误明细(包级共享槽,Mutex 守护;见文件头与桌面线程局部语义的差异说明) ----
158
+ // 引用 ohos 专属 foreign(strerror / __errno_location / poll)的辅助一律带门禁,
159
+ // 否则桌面构建在链接期找不到符号(AGENTS §4:跨平台文件别按单平台的 warning 清 import)
160
+
161
+ @When[env == "ohos"]
162
+ let OHOS_SERIAL_ERR_MUTEX = Mutex()
163
+
164
+ @When[env == "ohos"]
165
+ var ohosSerialLastError = ""
166
+
167
+ @When[env == "ohos"]
168
+ func serialOhosSetError(msg: String): Unit {
169
+ OHOS_SERIAL_ERR_MUTEX.lock()
170
+ ohosSerialLastError = msg
171
+ OHOS_SERIAL_ERR_MUTEX.unlock()
172
+ }
173
+
174
+ @When[env == "ohos"]
175
+ func serialOhosErrno(): Int32 {
176
+ unsafe {
177
+ let p = __errno_location()
178
+ if (p.isNull()) {
179
+ return 0
180
+ }
181
+ return p.read(0)
182
+ }
183
+ }
184
+
185
+ /** 最近一次 errno 的人话(strerror 返回静态串,不释放);调用处无需 unsafe */
186
+ @When[env == "ohos"]
187
+ func serialOhosErrnoText(): String {
188
+ unsafe {
189
+ return strerror(serialOhosErrno()).toString()
190
+ }
191
+ }
192
+
193
+ // ---- termios 裸缓冲的读写辅助(aarch64 是小端;public 供单测直接构造,AGENTS §2) ----
194
+
195
+ public func serialOhosGetU32(t: CPointer<UInt8>, off: Int64): UInt32 {
196
+ unsafe {
197
+ let b0 = UInt32(t.read(off))
198
+ let b1 = UInt32(t.read(off + 1)) << 8
199
+ let b2 = UInt32(t.read(off + 2)) << 16
200
+ let b3 = UInt32(t.read(off + 3)) << 24
201
+ return b0 | b1 | b2 | b3
202
+ }
203
+ }
204
+
205
+ public func serialOhosPutU32(t: CPointer<UInt8>, off: Int64, v: UInt32): Unit {
206
+ unsafe {
207
+ t.write(off, UInt8(v & 0xff))
208
+ t.write(off + 1, UInt8((v >> 8) & 0xff))
209
+ t.write(off + 2, UInt8((v >> 16) & 0xff))
210
+ t.write(off + 3, UInt8((v >> 24) & 0xff))
211
+ }
212
+ }
213
+
214
+ /** poll 单 fd 单事件:>0 就绪,0 超时,<0 出错(errno 已置) */
215
+ @When[env == "ohos"]
216
+ func serialOhosPollOne(fd: Int32, events: Int16, timeoutMs: Int32): Int32 {
217
+ let pfd = LibC.malloc<UInt8>(count: 8)
218
+ unsafe {
219
+ // fd 低 4 字节小端写入;events 高位偏移 4;revents(偏移 6)由内核填,预清零
220
+ pfd.write(0, UInt8(Int32(fd) & 0xff))
221
+ pfd.write(1, UInt8((fd >> 8) & 0xff))
222
+ pfd.write(2, UInt8((fd >> 16) & 0xff))
223
+ pfd.write(3, UInt8((fd >> 24) & 0xff))
224
+ pfd.write(4, UInt8(UInt16(events) & 0xff))
225
+ pfd.write(5, UInt8((UInt16(events) >> 8) & 0xff))
226
+ pfd.write(6, UInt8(0))
227
+ pfd.write(7, UInt8(0))
228
+ }
229
+ var r: Int32 = 0
230
+ unsafe {
231
+ r = poll(pfd, 1, timeoutMs)
232
+ }
233
+ unsafe {
234
+ LibC.free(pfd)
235
+ }
236
+ return r
237
+ }
238
+
239
+ /**
240
+ * 打开并配置串口:成功返回 >0 句柄(fd+1,保证恒 >0),失败负码:
241
+ * -1 参数 / 白名单、-3 open 或 termios 配置失败(明细在 serialLastError())。
242
+ * 线程安全:fd 由调用方独占使用;重复 open 同一路径是两次独立连接(与桌面同形)。
243
+ */
244
+ @When[env == "ohos"]
245
+ public func serialOpen(path: String, baud: Int32, dataBits: Int32, parity: Int32,
246
+ stopBits: Int32): Int64 {
247
+ // ohos 没有桌面 C 桥那层白名单,本函数就是边界:页面可控路径必须在这里再拦一次
248
+ if (!serialPathLooksAllowed(path)) {
249
+ serialOhosSetError("path \"${path}\" 不在允许的设备名单内(serialPathLooksAllowed)")
250
+ return -1
251
+ }
252
+ if (serialOhosBaudConst(baud).isNone()) {
253
+ serialOhosSetError("不支持的波特率 ${baud}")
254
+ return -1
255
+ }
256
+ if (dataBits < 5 || dataBits > 8) {
257
+ serialOhosSetError("dataBits 只收 5..8(拿到 ${dataBits})")
258
+ return -1
259
+ }
260
+ if (parity < 0 || parity > 2) {
261
+ serialOhosSetError("parity 只收 0|1|2(拿到 ${parity})")
262
+ return -1
263
+ }
264
+ if (stopBits != 1 && stopBits != 2) {
265
+ serialOhosSetError("stopBits 只收 1|2(拿到 ${stopBits})")
266
+ return -1
267
+ }
268
+
269
+ var fd: Int32 = 0
270
+ unsafe {
271
+ let cpath = LibC.mallocCString(path)
272
+ fd = open(cpath, OHOS_O_RDWR | OHOS_O_NOCTTY)
273
+ LibC.free(cpath)
274
+ }
275
+ if (fd < 0) {
276
+ // POSIX 打不开就走 serialManager(serial_manager_ohos.cj)。普通三方应用对 /dev/bus
277
+ // 整体不可达(SELinux,连 hdc shell 都进不去),seriaManager 由系统代管设备节点、
278
+ // 用户点一次授权即可——这是三方应用碰 USB 串口的正路,不是「降级兜底」。
279
+ let posixErr = serialOhosErrnoText()
280
+ let usbHandle = usbSerialOpenByPath(path, baud, dataBits, parity, stopBits)
281
+ if (usbHandle > 0) {
282
+ return usbHandle
283
+ }
284
+ // 两条路都失败:POSIX 的 errno 与 USB 侧原因一并写清,否则只看到「端口表里没有」
285
+ serialOhosSetError("open failed: POSIX ${posixErr};USB ${serialLastError()}")
286
+ return -3
287
+ }
288
+
289
+ let t = LibC.malloc<UInt8>(count: 64)
290
+ unsafe {
291
+ for (i in 0..64) {
292
+ t.write(i, 0)
293
+ }
294
+ }
295
+ var rc: Int32 = 0
296
+ unsafe {
297
+ rc = tcgetattr(fd, t)
298
+ }
299
+ if (rc != 0) {
300
+ serialOhosSetError("tcgetattr failed: ${serialOhosErrnoText()}")
301
+ unsafe {
302
+ close(fd)
303
+ LibC.free(t)
304
+ }
305
+ return -3
306
+ }
307
+
308
+ // cfmakeraw 等价:清 iflag/lflag 原始位,oflag/lflag 归零,cflag 重设数据位/校验/停止位
309
+ // (取反用「异或全 1 掩码」:仓颉没有 ~ 运算符,2026-10-24 编译实测)
310
+ let iflag = serialOhosGetU32(t, 0) & (OHOS_IFLAG_RAW ^ 0xFFFF_FFFF)
311
+ serialOhosPutU32(t, 0, iflag)
312
+ serialOhosPutU32(t, 4, 0)
313
+ var cflag = serialOhosGetU32(t, 8) & ((OHOS_CSIZE | OHOS_PARENB | OHOS_PARODD | OHOS_CSTOPB) ^ 0xFFFF_FFFF)
314
+ cflag |= OHOS_CREAD | OHOS_CLOCAL
315
+ // dataBits:CS5..CS8 = 0x00/0x10/0x20/0x30
316
+ cflag |= UInt32(dataBits - 5) << 4
317
+ if (parity == 1) {
318
+ cflag |= OHOS_PARODD | OHOS_PARENB
319
+ } else if (parity == 2) {
320
+ cflag |= OHOS_PARENB
321
+ }
322
+ if (stopBits == 2) {
323
+ cflag |= OHOS_CSTOPB
324
+ }
325
+ serialOhosPutU32(t, 8, cflag)
326
+ // lflag 清原始位(c_line@16 与 c_cc 其余位保留,buffer 已按 tcgetattr 的返回初始化)
327
+ serialOhosPutU32(t, 12, serialOhosGetU32(t, 12) & (OHOS_LFLAG_RAW ^ 0xFFFF_FFFF))
328
+ // VTIME / VMIN 置 0:read 不自带阻塞预算,超时由 poll 负责(与桌面 Linux 桥同口径)
329
+ unsafe {
330
+ t.write(17 + OHOS_VTIME, UInt8(0))
331
+ t.write(17 + OHOS_VMIN, UInt8(0))
332
+ }
333
+
334
+ match (serialOhosBaudConst(baud)) {
335
+ case Some(speed) =>
336
+ unsafe {
337
+ cfsetispeed(t, speed)
338
+ cfsetospeed(t, speed)
339
+ }
340
+ case None => () // 上面已拦,不可能走到
341
+ }
342
+ unsafe {
343
+ rc = tcsetattr(fd, OHOS_TCSANOW, t)
344
+ }
345
+ if (rc != 0) {
346
+ serialOhosSetError("tcsetattr failed: ${serialOhosErrnoText()}")
347
+ unsafe {
348
+ close(fd)
349
+ LibC.free(t)
350
+ }
351
+ return -3
352
+ }
353
+ unsafe {
354
+ LibC.free(t)
355
+ }
356
+ return Int64(fd) + 1
357
+ }
358
+
359
+ /** 最近一次失败的明细(包级共享槽;见文件头与桌面线程局部语义的差异说明) */
360
+ @When[env == "ohos"]
361
+ public func serialLastError(): String {
362
+ OHOS_SERIAL_ERR_MUTEX.lock()
363
+ let msg = ohosSerialLastError
364
+ OHOS_SERIAL_ERR_MUTEX.unlock()
365
+ return msg
366
+ }
367
+
368
+ /** ohos 不走桌面 C 桥的枚举(返回 None,list 命令走下面的 serialPortList 目录扫描) */
369
+ @When[env == "ohos"]
370
+ public func serialListC(): Option<ArrayList<String>> {
371
+ return None
372
+ }
373
+
374
+ /**
375
+ * 读:最多 maxBytes 字节、最多等 timeoutMs 毫秒(0 = 立即返回)。
376
+ * 返回实际读到的字节(空数组 = 超时无数据)。poll 有数据才 read 一次——
377
+ * 与桌面 Linux 桥「poll + read」同口径,不会在没数据时白等。
378
+ */
379
+ @When[env == "ohos"]
380
+ public func serialRead(h: Int64, maxBytes: Int32, timeoutMs: Int32): Array<UInt8> {
381
+ if (usbSerialIsHandle(h)) {
382
+ return usbSerialReadHandle(h, maxBytes, timeoutMs)
383
+ }
384
+ let fd = Int32(h - 1)
385
+ let ready = serialOhosPollOne(fd, OHOS_POLLIN, timeoutMs)
386
+ if (ready <= 0) {
387
+ if (ready < 0) {
388
+ serialOhosSetError("poll failed: ${serialOhosErrnoText()}")
389
+ }
390
+ return Array<UInt8>(0, repeat: 0)
391
+ }
392
+ let cap = Int64(maxBytes)
393
+ let buf = LibC.malloc<UInt8>(count: cap)
394
+ var n: Int64 = 0
395
+ unsafe {
396
+ n = read(fd, buf, UInt64(cap))
397
+ }
398
+ var out = Array<UInt8>(0, repeat: 0)
399
+ if (n > 0) {
400
+ out = Array<UInt8>(n, repeat: 0)
401
+ unsafe {
402
+ for (i in 0..n) {
403
+ out[i] = buf.read(i)
404
+ }
405
+ }
406
+ } else if (n < 0) {
407
+ serialOhosSetError("read failed: ${serialOhosErrnoText()}")
408
+ }
409
+ unsafe {
410
+ LibC.free(buf)
411
+ }
412
+ return out
413
+ }
414
+
415
+ /**
416
+ * 写:在 timeoutMs 预算内尽量写完,返回实际写入字节数(可能 < data.size;负码 = 失败)。
417
+ * EAGAIN 时 poll(POLLOUT) 等剩余预算,与桌面「预算内尽量写完」的口径一致。
418
+ */
419
+ @When[env == "ohos"]
420
+ public func serialWrite(h: Int64, data: Array<UInt8>, timeoutMs: Int32): Int32 {
421
+ if (data.size == 0) {
422
+ return 0
423
+ }
424
+ if (usbSerialIsHandle(h)) {
425
+ return usbSerialWriteHandle(h, data, timeoutMs)
426
+ }
427
+ let fd = Int32(h - 1)
428
+ let n = data.size
429
+ let buf = LibC.malloc<UInt8>(count: Int64(n))
430
+ unsafe {
431
+ for (i in 0..n) {
432
+ buf.write(i, data[i])
433
+ }
434
+ }
435
+ let deadline = MonoTime.now() + Duration.millisecond * Int64(timeoutMs)
436
+ var total: Int64 = 0
437
+ while (total < n) {
438
+ var w: Int64 = 0
439
+ unsafe {
440
+ w = write(fd, buf + total, UInt64(n - total))
441
+ }
442
+ if (w > 0) {
443
+ total += w
444
+ continue
445
+ }
446
+ if (w < 0 && serialOhosErrno() != OHOS_EAGAIN) {
447
+ serialOhosSetError("write failed: ${serialOhosErrnoText()}")
448
+ unsafe {
449
+ LibC.free(buf)
450
+ }
451
+ return -1
452
+ }
453
+ let remainMs = (deadline - MonoTime.now()).toMilliseconds()
454
+ if (remainMs <= 0) {
455
+ break
456
+ }
457
+ let pr = serialOhosPollOne(fd, OHOS_POLLOUT, Int32(remainMs))
458
+ if (pr < 0) {
459
+ serialOhosSetError("poll failed: ${serialOhosErrnoText()}")
460
+ unsafe {
461
+ LibC.free(buf)
462
+ }
463
+ return -1
464
+ }
465
+ }
466
+ unsafe {
467
+ LibC.free(buf)
468
+ }
469
+ return Int32(total)
470
+ }
471
+
472
+ /** 关闭:0 = 成功;fd 已关(EBADF)按桌面口径返回 -4(幂等拒绝重复关) */
473
+ @When[env == "ohos"]
474
+ public func serialClose(h: Int64): Int32 {
475
+ if (usbSerialIsHandle(h)) {
476
+ return usbSerialCloseHandle(h)
477
+ }
478
+ let fd = Int32(h - 1)
479
+ let rc = unsafe {
480
+ close(fd)
481
+ }
482
+ if (rc == 0) {
483
+ return 0
484
+ }
485
+ serialOhosSetError("close failed: ${serialOhosErrnoText()}")
486
+ return -4
487
+ }
488
+
489
+ /**
490
+ * 枚举本机串口(ohos 版):扫 /dev 顶层 + /dev/bus(鸿蒙特有的 USB 串口落点,sscom 同款),
491
+ * 按前缀过滤。拿不到目录就跳过;没有设备回空数组——空数组在页面侧语义就是「未发现端口」。
492
+ */
493
+ @When[env == "ohos"]
494
+ public func serialPortList(): ArrayList<String> {
495
+ let prefixes = ["ttyUSB", "ttyACM", "ttyS", "ttyAMA", "ttyGS", "ttySC", "ttyPS", "ttyTHS", "tty.", "cu."]
496
+ let found = ArrayList<String>()
497
+ for (dir in ["/dev", "/dev/bus"]) {
498
+ if (!exists(dir)) {
499
+ continue
500
+ }
501
+ var entries: Array<FileInfo>
502
+ try {
503
+ entries = Directory.readFrom(dir)
504
+ } catch (e: Exception) {
505
+ continue
506
+ }
507
+ for (info in entries) {
508
+ if (info.isDirectory()) {
509
+ continue
510
+ }
511
+ let name = info.path.toString()
512
+ for (p in prefixes) {
513
+ if (name.contains(p)) {
514
+ found.add(name)
515
+ break
516
+ }
517
+ }
518
+ }
519
+ }
520
+ // USB 串口只有 serialManager 认得(设备节点由系统代管,/dev 扫描看不见);
521
+ // 有权限时两边可能都出现同一个名字,去重后再排序
522
+ for (name in usbSerialListNames()) {
523
+ if (!found.contains(name)) {
524
+ found.add(name)
525
+ }
526
+ }
527
+ sort(found)
528
+ return found
529
+ }