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.
- package/README.md +113 -99
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +632 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +526 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -969
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -251
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +605 -585
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
|
@@ -1,454 +1,458 @@
|
|
|
1
|
-
package cjTauri.plugins.serial
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
|
|
5
|
-
import std.collection.*
|
|
6
|
-
// std.fs / std.sort
|
|
7
|
-
@When[env != "ohos"]
|
|
8
|
-
import std.fs.*
|
|
9
|
-
@When[env != "ohos"]
|
|
10
|
-
import std.sort.*
|
|
11
|
-
import stdx.encoding.json.*
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 官方插件 serial:串口(对标 Tauri 的 serialport 插件,RFC-001 §7.3)
|
|
15
|
-
*
|
|
16
|
-
* 命令:serial:list / serial:open / serial:read / serial:write / serial:close
|
|
17
|
-
* 权限:**不自动放行**——应用要在 capabilities 里显式声明,两种写法等价:
|
|
18
|
-
* "commands": ["serial:open", "serial:read", "serial:close"] // 明文逐条
|
|
19
|
-
* "permissions": ["serial:readonly"] // 命名权限集,一次放行一组
|
|
20
|
-
*
|
|
21
|
-
* 能力落在 C 桥(`cj_bridge_serial_*`,见 native/bridge_core.h)而不是纯仓颉:
|
|
22
|
-
* std / stdx 都没有串口,也没有 termios / ioctl / fcntl 绑定,设备参数只能交给 C。
|
|
23
|
-
* **路径白名单与参数校验在 C 侧 core**(原生层不是可信边界之外 —— 页面能直接 invoke,
|
|
24
|
-
* 仓颉侧的检查只是为了给前端一句可读的人话,真正的边界在 native/ 那边)。
|
|
25
|
-
*
|
|
26
|
-
* 前端:window.__CJ_TAURI__.serial.open({ path: "/dev/ttyUSB0", baud: 115200 })
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
// serial:list 的候选目录:/dev 顶层 + udev 稳定符号链接目录(不存在就跳过,不是错误)。
|
|
30
|
-
// (顶层 let 是全局不可变量;仓颉的 const 只收编译期常量表达式,Array 字面量不合法——实测 2026-10-09)
|
|
31
|
-
let SERIAL_LIST_DIRS: Array<String> = ["/dev", "/dev/serial/by-id", "/dev/serial/by-path"]
|
|
32
|
-
|
|
33
|
-
/** /dev 顶层只收「设备节点类」前缀(与 C 桥白名单同一套设备面,排除 /dev/pts 伪终端) */
|
|
34
|
-
let SERIAL_LIST_PREFIXES: Array<String> = [
|
|
35
|
-
"ttyUSB", "ttyACM", "ttyS", "ttyAMA", "ttyPS", "ttyTHS", "ttyXRUSB", "ttyV",
|
|
36
|
-
"tty.", "cu.",
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
/** 枚举本机串口设备节点(serial:list 的实现;拿不到 /dev 就回空数组,不抛)。
|
|
40
|
-
* Windows 优先走 C 桥的 QueryDosDeviceW 枚举(返回的是「COM 名」,页面直接当 path 用,
|
|
41
|
-
* C 桥 open 会自动补 \\.\\ 前缀);C 桥返回 -4(未实现)或异常时回落到 /dev 目录扫描——
|
|
42
|
-
* 后者在 Windows 下天然为空,不算错误。 */
|
|
43
|
-
@When[env != "ohos"]
|
|
44
|
-
public func serialPortList(): ArrayList<String> {
|
|
45
|
-
let viaC = serialListC()
|
|
46
|
-
if (let Some(list) <- viaC) {
|
|
47
|
-
return list
|
|
48
|
-
}
|
|
49
|
-
let found = ArrayList<String>()
|
|
50
|
-
for (dir in SERIAL_LIST_DIRS) {
|
|
51
|
-
if (!exists(dir)) {
|
|
52
|
-
continue
|
|
53
|
-
}
|
|
54
|
-
var entries: Array<FileInfo>
|
|
55
|
-
try {
|
|
56
|
-
entries = Directory.readFrom(dir)
|
|
57
|
-
} catch (e: Exception) {
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
// by-id / by-path 里全是串口设备(udev 链接名不含 ttyUSB 这类前缀),整目录收录
|
|
61
|
-
let scanAll = dir != "/dev"
|
|
62
|
-
for (info in entries) {
|
|
63
|
-
if (info.isDirectory()) {
|
|
64
|
-
continue
|
|
65
|
-
}
|
|
66
|
-
let name = info.path.toString()
|
|
67
|
-
if (scanAll) {
|
|
68
|
-
found.add(name)
|
|
69
|
-
continue
|
|
70
|
-
}
|
|
71
|
-
for (p in SERIAL_LIST_PREFIXES) {
|
|
72
|
-
if (name.contains(p)) {
|
|
73
|
-
found.add(name)
|
|
74
|
-
break
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
sort(found)
|
|
80
|
-
return found
|
|
81
|
-
}
|
|
82
|
-
public class SerialPlugin <: Plugin {
|
|
83
|
-
public init() {}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
public func describe(): PluginManifest {
|
|
87
|
-
let hosts = ArrayList<String>()
|
|
88
|
-
hosts.add("webkitgtk")
|
|
89
|
-
hosts.add("webview2")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
m
|
|
100
|
-
m.add("
|
|
101
|
-
m.add("
|
|
102
|
-
m.add("
|
|
103
|
-
m.add("
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let
|
|
115
|
-
ro
|
|
116
|
-
ro.add("
|
|
117
|
-
ro.add("
|
|
118
|
-
ro.add("
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
all
|
|
122
|
-
all.add("
|
|
123
|
-
all.add("
|
|
124
|
-
all.add("
|
|
125
|
-
all.add("
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
i
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
i
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
j
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
case "serial:
|
|
289
|
-
case
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
case
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
case
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
throw CommandException("serial: argument \"handle\"
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
let
|
|
368
|
-
if (
|
|
369
|
-
throw CommandException("serial:open refused:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
let
|
|
377
|
-
if (
|
|
378
|
-
throw CommandException("serial:open refused:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
let
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
out
|
|
395
|
-
out.put("
|
|
396
|
-
out.put("
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
let
|
|
406
|
-
let
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
let
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
1
|
+
package cjTauri.plugins.serial
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
import std.collection.*
|
|
6
|
+
// std.fs / std.sort 被 serialPortList 用到(桌面走 C 桥枚举回落 + 目录扫描;ohos 走 serial_ohos.cj 的扫描)
|
|
7
|
+
@When[env != "ohos"]
|
|
8
|
+
import std.fs.*
|
|
9
|
+
@When[env != "ohos"]
|
|
10
|
+
import std.sort.*
|
|
11
|
+
import stdx.encoding.json.*
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 官方插件 serial:串口(对标 Tauri 的 serialport 插件,RFC-001 §7.3)
|
|
15
|
+
*
|
|
16
|
+
* 命令:serial:list / serial:open / serial:read / serial:write / serial:close
|
|
17
|
+
* 权限:**不自动放行**——应用要在 capabilities 里显式声明,两种写法等价:
|
|
18
|
+
* "commands": ["serial:open", "serial:read", "serial:close"] // 明文逐条
|
|
19
|
+
* "permissions": ["serial:readonly"] // 命名权限集,一次放行一组
|
|
20
|
+
*
|
|
21
|
+
* 能力落在 C 桥(`cj_bridge_serial_*`,见 native/bridge_core.h)而不是纯仓颉:
|
|
22
|
+
* std / stdx 都没有串口,也没有 termios / ioctl / fcntl 绑定,设备参数只能交给 C。
|
|
23
|
+
* **路径白名单与参数校验在 C 侧 core**(原生层不是可信边界之外 —— 页面能直接 invoke,
|
|
24
|
+
* 仓颉侧的检查只是为了给前端一句可读的人话,真正的边界在 native/ 那边)。
|
|
25
|
+
*
|
|
26
|
+
* 前端:window.__CJ_TAURI__.serial.open({ path: "/dev/ttyUSB0", baud: 115200 })
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// serial:list 的候选目录:/dev 顶层 + udev 稳定符号链接目录(不存在就跳过,不是错误)。
|
|
30
|
+
// (顶层 let 是全局不可变量;仓颉的 const 只收编译期常量表达式,Array 字面量不合法——实测 2026-10-09)
|
|
31
|
+
let SERIAL_LIST_DIRS: Array<String> = ["/dev", "/dev/serial/by-id", "/dev/serial/by-path"]
|
|
32
|
+
|
|
33
|
+
/** /dev 顶层只收「设备节点类」前缀(与 C 桥白名单同一套设备面,排除 /dev/pts 伪终端) */
|
|
34
|
+
let SERIAL_LIST_PREFIXES: Array<String> = [
|
|
35
|
+
"ttyUSB", "ttyACM", "ttyS", "ttyAMA", "ttyPS", "ttyTHS", "ttyXRUSB", "ttyV",
|
|
36
|
+
"tty.", "cu.",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
/** 枚举本机串口设备节点(serial:list 的实现;拿不到 /dev 就回空数组,不抛)。
|
|
40
|
+
* Windows 优先走 C 桥的 QueryDosDeviceW 枚举(返回的是「COM 名」,页面直接当 path 用,
|
|
41
|
+
* C 桥 open 会自动补 \\.\\ 前缀);C 桥返回 -4(未实现)或异常时回落到 /dev 目录扫描——
|
|
42
|
+
* 后者在 Windows 下天然为空,不算错误。 */
|
|
43
|
+
@When[env != "ohos"]
|
|
44
|
+
public func serialPortList(): ArrayList<String> {
|
|
45
|
+
let viaC = serialListC()
|
|
46
|
+
if (let Some(list) <- viaC) {
|
|
47
|
+
return list
|
|
48
|
+
}
|
|
49
|
+
let found = ArrayList<String>()
|
|
50
|
+
for (dir in SERIAL_LIST_DIRS) {
|
|
51
|
+
if (!exists(dir)) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
var entries: Array<FileInfo>
|
|
55
|
+
try {
|
|
56
|
+
entries = Directory.readFrom(dir)
|
|
57
|
+
} catch (e: Exception) {
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
// by-id / by-path 里全是串口设备(udev 链接名不含 ttyUSB 这类前缀),整目录收录
|
|
61
|
+
let scanAll = dir != "/dev"
|
|
62
|
+
for (info in entries) {
|
|
63
|
+
if (info.isDirectory()) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
let name = info.path.toString()
|
|
67
|
+
if (scanAll) {
|
|
68
|
+
found.add(name)
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
for (p in SERIAL_LIST_PREFIXES) {
|
|
72
|
+
if (name.contains(p)) {
|
|
73
|
+
found.add(name)
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
sort(found)
|
|
80
|
+
return found
|
|
81
|
+
}
|
|
82
|
+
public class SerialPlugin <: Plugin {
|
|
83
|
+
public init() {}
|
|
84
|
+
|
|
85
|
+
/** 自描述:桌面两端走 C 桥、ohos 走 NDK FFI 直连(serial_ohos.cj),三端都实现 */
|
|
86
|
+
public func describe(): PluginManifest {
|
|
87
|
+
let hosts = ArrayList<String>()
|
|
88
|
+
hosts.add("webkitgtk")
|
|
89
|
+
hosts.add("webview2")
|
|
90
|
+
hosts.add("arkweb")
|
|
91
|
+
return PluginManifest(hosts, ArrayList<String>())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public func name(): String {
|
|
95
|
+
return "serial"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
99
|
+
let m = HashMap<String, CommandHandler>()
|
|
100
|
+
m.add("list", SerialCommands())
|
|
101
|
+
m.add("open", SerialCommands())
|
|
102
|
+
m.add("read", SerialCommands())
|
|
103
|
+
m.add("write", SerialCommands())
|
|
104
|
+
m.add("close", SerialCommands())
|
|
105
|
+
return m
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 命名权限集:readonly 只含「列出 + 打开 + 读 + 关」——只想读设备的应用引用它即可,
|
|
110
|
+
* **写能力不会被顺带打开**;要写就必须显式引用 default 或把 serial:write 写成明文。
|
|
111
|
+
* list 归入 readonly:枚举端口是只读操作,且连接前的「选端口」离不开它。
|
|
112
|
+
*/
|
|
113
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
114
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
115
|
+
let ro = ArrayList<String>()
|
|
116
|
+
ro.add("list")
|
|
117
|
+
ro.add("open")
|
|
118
|
+
ro.add("read")
|
|
119
|
+
ro.add("close")
|
|
120
|
+
m.add("readonly", ro)
|
|
121
|
+
let all = ArrayList<String>()
|
|
122
|
+
all.add("list")
|
|
123
|
+
all.add("open")
|
|
124
|
+
all.add("read")
|
|
125
|
+
all.add("write")
|
|
126
|
+
all.add("close")
|
|
127
|
+
m.add("default", all)
|
|
128
|
+
return m
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 前端 shim:只往 __CJ_TAURI__ 挂一个 serial 命名空间,不动 invoke / listen / emit
|
|
133
|
+
*/
|
|
134
|
+
public func jsShim(): String {
|
|
135
|
+
return "(function () { var t = window.__CJ_TAURI__ || (window.__CJ_TAURI__ = {});" +
|
|
136
|
+
" t.serial = {" +
|
|
137
|
+
" list: function () { return window.__CJ_TAURI__.invoke('serial:list', {}); }," +
|
|
138
|
+
" open: function (args) { return window.__CJ_TAURI__.invoke('serial:open', args); }," +
|
|
139
|
+
" read: function (args) { return window.__CJ_TAURI__.invoke('serial:read', args); }," +
|
|
140
|
+
" write: function (args) { return window.__CJ_TAURI__.invoke('serial:write', args); }," +
|
|
141
|
+
" close: function (args) { return window.__CJ_TAURI__.invoke('serial:close', args); }" +
|
|
142
|
+
" }; })();\n"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ===== 可与 C 侧对齐的纯函数(无副作用,便于单测)===== */
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 支持的波特率档位:与 native/bridge_linux.c 的 serial_speed_of 一字不差地对齐——
|
|
150
|
+
* 档位在那边是「有宏才收」,这里提前给前端一句可读原因,而不是让 C 返回一个数字码。
|
|
151
|
+
*/
|
|
152
|
+
let SERIAL_BAUDS: Array<Int32> = [300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
|
|
153
|
+
230400, 460800, 500000, 921600, 1000000]
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 设备路径前缀白名单:与 native/bridge_core.c 的 CJ_SERIAL_PREFIXES 对齐;
|
|
157
|
+
* /dev/bus/tty* 是鸿蒙特有的 USB 串口落点(桌面 C 桥白名单没有它们,但桌面侧不会出现
|
|
158
|
+
* 这类路径,加了不影响;ohos 侧的真正边界在 serial_ohos.cj 的 open,白名单同源)
|
|
159
|
+
*/
|
|
160
|
+
let SERIAL_PATH_PREFIXES: Array<String> = ["/dev/ttyUSB", "/dev/ttyACM", "/dev/ttyS", "/dev/ttyAMA",
|
|
161
|
+
"/dev/ttyPS", "/dev/ttyTHS", "/dev/ttyXRUSB", "/dev/ttyV", "/dev/tty.", "/dev/cu.",
|
|
162
|
+
"/dev/bus/ttyUSB", "/dev/bus/ttyACM", "/dev/bus/ttyS",
|
|
163
|
+
"/dev/serial/by-id/", "/dev/serial/by-path/", "/dev/pts/", "COM", "\\\\.\\COM"]
|
|
164
|
+
|
|
165
|
+
public func serialBaudSupported(baud: Int32): Bool {
|
|
166
|
+
for (b in SERIAL_BAUDS) {
|
|
167
|
+
if (b == baud) {
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 支持档位的一句话列表(报错信息里用;从 SERIAL_BAUDS 现拼,避免两处维护) */
|
|
175
|
+
public func serialBaudListText(): String {
|
|
176
|
+
var s = ""
|
|
177
|
+
var i = 0
|
|
178
|
+
while (i < SERIAL_BAUDS.size) {
|
|
179
|
+
if (i > 0) {
|
|
180
|
+
s = s + "/"
|
|
181
|
+
}
|
|
182
|
+
s = s + SERIAL_BAUDS[i].toString()
|
|
183
|
+
i++
|
|
184
|
+
}
|
|
185
|
+
return s
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 校验位文案 → 数值(0=无 1=奇 2=偶);无法识别返回 -1,由调用方报错 */
|
|
189
|
+
public func serialParityCode(text: String): Int32 {
|
|
190
|
+
let t = serialAsciiLower(text)
|
|
191
|
+
if (t == "none" || t == "n") {
|
|
192
|
+
return 0
|
|
193
|
+
}
|
|
194
|
+
if (t == "odd" || t == "o") {
|
|
195
|
+
return 1
|
|
196
|
+
}
|
|
197
|
+
if (t == "even" || t == "e") {
|
|
198
|
+
return 2
|
|
199
|
+
}
|
|
200
|
+
return -1
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 设备路径白名单的**仓颉侧镜像**:只为给前端一句可读提示,**不是安全边界**
|
|
205
|
+
* (边界在 C 侧 core:JS 绕过这个函数直接 invoke 也一样会被拒)。
|
|
206
|
+
* 与 C 侧同样两条硬规则:不得含 ".."、前缀本身不算设备名;COM 名要求前缀后是 1..3 位数字。
|
|
207
|
+
* 路径按字节比较——串口设备名是 ASCII,非 ASCII 路径本就到不了这里。
|
|
208
|
+
*/
|
|
209
|
+
public func serialPathLooksAllowed(path: String): Bool {
|
|
210
|
+
let bytes = path.toArray()
|
|
211
|
+
if (bytes.size == 0 || bytes.size > 255) {
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
if (path.contains("..")) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
for (p in SERIAL_PATH_PREFIXES) {
|
|
218
|
+
let pb = p.toArray()
|
|
219
|
+
if (bytes.size <= pb.size) {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
var same = true
|
|
223
|
+
var i = 0
|
|
224
|
+
while (i < pb.size) {
|
|
225
|
+
if (bytes[i] != pb[i]) {
|
|
226
|
+
same = false
|
|
227
|
+
break
|
|
228
|
+
}
|
|
229
|
+
i++
|
|
230
|
+
}
|
|
231
|
+
if (!same) {
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (p != "COM" && p != "\\\\.\\COM") {
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
if (bytes.size - pb.size > 3) {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
var j = pb.size
|
|
241
|
+
while (j < bytes.size) {
|
|
242
|
+
let c = bytes[j]
|
|
243
|
+
if (c < 48 || c > 57) {
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
j++
|
|
247
|
+
}
|
|
248
|
+
return true
|
|
249
|
+
}
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 字节 → 文本(无 BOM 假设):不是合法 UTF-8 就返回 None,调用方保留 hex 即可 */
|
|
254
|
+
public func serialBytesToText(bytes: Array<UInt8>): Option<String> {
|
|
255
|
+
try {
|
|
256
|
+
return Some(String.fromUtf8(bytes))
|
|
257
|
+
} catch (e: Exception) {
|
|
258
|
+
return None
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** ASCII 小写(不引 toAsciiLower:只处理选项文案,自己转就够,行为一目了然) */
|
|
263
|
+
func serialAsciiLower(s: String): String {
|
|
264
|
+
let bytes = s.toArray()
|
|
265
|
+
let out = Array<UInt8>(bytes.size, repeat: 0)
|
|
266
|
+
var i = 0
|
|
267
|
+
while (i < bytes.size) {
|
|
268
|
+
let b = bytes[i]
|
|
269
|
+
if (b >= UInt8(65) && b <= UInt8(90)) {
|
|
270
|
+
out[i] = b + UInt8(32)
|
|
271
|
+
} else {
|
|
272
|
+
out[i] = b
|
|
273
|
+
}
|
|
274
|
+
i++
|
|
275
|
+
}
|
|
276
|
+
return String.fromUtf8(out)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* serial 插件的命令处理器(四个命令共用一份,按全名分派,与内置 SystemCommands 同形)
|
|
281
|
+
*/
|
|
282
|
+
class SerialCommands <: CommandHandler {
|
|
283
|
+
public init() {}
|
|
284
|
+
|
|
285
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
286
|
+
let _ = ipc
|
|
287
|
+
match (cmd) {
|
|
288
|
+
case "serial:list" => this.listCmd()
|
|
289
|
+
case "serial:open" => this.openCmd(args)
|
|
290
|
+
case "serial:read" => this.readCmd(args)
|
|
291
|
+
case "serial:write" => this.writeCmd(args)
|
|
292
|
+
case "serial:close" => this.closeCmd(args)
|
|
293
|
+
case _ => throw CommandException("unknown serial command: ${cmd}")
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private func requireString(args: JsonObject, key: String): String {
|
|
298
|
+
match (args.get(key)) {
|
|
299
|
+
case Some(v) => match (v.kind()) {
|
|
300
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
301
|
+
case _ => throw CommandException("serial: argument \"${key}\" must be a string")
|
|
302
|
+
}
|
|
303
|
+
case None => throw CommandException("serial: missing argument \"${key}\"")
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private func optionalString(args: JsonObject, key: String, fallback: String): String {
|
|
308
|
+
match (args.get(key)) {
|
|
309
|
+
case Some(_) => return this.requireString(args, key)
|
|
310
|
+
case None => return fallback
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private func optionalInt(args: JsonObject, key: String, fallback: Int64): Int64 {
|
|
315
|
+
match (args.get(key)) {
|
|
316
|
+
case Some(v) => match (v.kind()) {
|
|
317
|
+
case JsonKind.JsInt => return v.asInt().getValue()
|
|
318
|
+
case _ => throw CommandException("serial: argument \"${key}\" must be an integer")
|
|
319
|
+
}
|
|
320
|
+
case None => return fallback
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 句柄必须由 serial:open 给出(>0);拿别的数字来只会得到一句可读拒绝 */
|
|
325
|
+
private func requireHandle(args: JsonObject): Int64 {
|
|
326
|
+
match (args.get("handle")) {
|
|
327
|
+
case Some(_) => ()
|
|
328
|
+
case None => throw CommandException("serial: missing argument \"handle\"")
|
|
329
|
+
}
|
|
330
|
+
let h = this.optionalInt(args, "handle", 0)
|
|
331
|
+
if (h <= 0) {
|
|
332
|
+
throw CommandException("serial: argument \"handle\" must be a handle returned by serial:open")
|
|
333
|
+
}
|
|
334
|
+
return h
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private func clamp(v: Int64, lo: Int64, hi: Int64): Int64 {
|
|
338
|
+
if (v < lo) {
|
|
339
|
+
return lo
|
|
340
|
+
}
|
|
341
|
+
if (v > hi) {
|
|
342
|
+
return hi
|
|
343
|
+
}
|
|
344
|
+
return v
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 枚举本机串口:页面拿它填「端口」下拉(连接前就要用,故归 readonly 集)。
|
|
349
|
+
* 返回 { ports: ["…"] }——没有设备就给空数组,页面据此显示「未发现端口」。
|
|
350
|
+
*/
|
|
351
|
+
private func listCmd(): JsonValue {
|
|
352
|
+
let out = JsonObject()
|
|
353
|
+
let ports = ArrayList<JsonValue>()
|
|
354
|
+
for (p in serialPortList()) {
|
|
355
|
+
ports.add(JsonString(p))
|
|
356
|
+
}
|
|
357
|
+
out.put("ports", JsonArray(ports))
|
|
358
|
+
return out
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private func openCmd(args: JsonObject): JsonValue {
|
|
362
|
+
let path = this.requireString(args, "path")
|
|
363
|
+
if (!serialPathLooksAllowed(path)) {
|
|
364
|
+
throw CommandException("serial:open refused: \"${path}\" 不在允许的设备名单内" +
|
|
365
|
+
"(只收串口设备节点 / 伪终端,且不得含 ..;最终以 C 桥白名单为准)")
|
|
366
|
+
}
|
|
367
|
+
let baud = this.optionalInt(args, "baud", 115200)
|
|
368
|
+
if (baud <= 0 || baud > 4000000) {
|
|
369
|
+
throw CommandException("serial:open refused: baud 超出范围(1..4000000,拿到 ${baud})")
|
|
370
|
+
}
|
|
371
|
+
let baud32 = Int32(baud)
|
|
372
|
+
if (!serialBaudSupported(baud32)) {
|
|
373
|
+
throw CommandException("serial:open refused: 不支持的波特率 ${baud}" +
|
|
374
|
+
"(可选档位:${serialBaudListText()})")
|
|
375
|
+
}
|
|
376
|
+
let dataBits = Int32(this.optionalInt(args, "dataBits", 8))
|
|
377
|
+
if (dataBits < 5 || dataBits > 8) {
|
|
378
|
+
throw CommandException("serial:open refused: dataBits 只收 5..8(拿到 ${dataBits})")
|
|
379
|
+
}
|
|
380
|
+
let parity = serialParityCode(this.optionalString(args, "parity", "none"))
|
|
381
|
+
if (parity < 0) {
|
|
382
|
+
throw CommandException("serial:open refused: parity 只认 none|odd|even(拿到 " +
|
|
383
|
+
this.optionalString(args, "parity", "none") + ")")
|
|
384
|
+
}
|
|
385
|
+
let stopBits = Int32(this.optionalInt(args, "stopBits", 1))
|
|
386
|
+
if (stopBits != 1 && stopBits != 2) {
|
|
387
|
+
throw CommandException("serial:open refused: stopBits 只收 1|2(拿到 ${stopBits})")
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let h = serialOpen(path, baud32, dataBits, parity, stopBits)
|
|
391
|
+
if (h <= 0) {
|
|
392
|
+
throw CommandException("serial:open failed (code ${h}): ${serialLastError()}")
|
|
393
|
+
}
|
|
394
|
+
let out = JsonObject()
|
|
395
|
+
out.put("handle", JsonInt(h))
|
|
396
|
+
out.put("path", JsonString(path))
|
|
397
|
+
out.put("baud", JsonInt(Int64(baud32)))
|
|
398
|
+
out.put("dataBits", JsonInt(Int64(dataBits)))
|
|
399
|
+
out.put("parity", JsonInt(Int64(parity)))
|
|
400
|
+
out.put("stopBits", JsonInt(Int64(stopBits)))
|
|
401
|
+
return out
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private func readCmd(args: JsonObject): JsonValue {
|
|
405
|
+
let h = this.requireHandle(args)
|
|
406
|
+
let maxBytes = this.clamp(this.optionalInt(args, "maxBytes", 256), 1, 4096)
|
|
407
|
+
let timeoutMs = this.clamp(this.optionalInt(args, "timeoutMs", 200), 0, 60000)
|
|
408
|
+
|
|
409
|
+
let bytes = serialRead(h, Int32(maxBytes), Int32(timeoutMs))
|
|
410
|
+
let out = JsonObject()
|
|
411
|
+
// 超时没数据不是错误:前端按 count == 0 判断即可,不必拿 rejected 当「空」
|
|
412
|
+
out.put("count", JsonInt(Int64(bytes.size)))
|
|
413
|
+
out.put("hex", JsonString(bytesToHex(bytes)))
|
|
414
|
+
match (serialBytesToText(bytes)) {
|
|
415
|
+
case Some(t) => out.put("text", JsonString(t))
|
|
416
|
+
// 二进制帧:给 hex 就够,不硬塞一个坏字符串
|
|
417
|
+
case None => ()
|
|
418
|
+
}
|
|
419
|
+
return out
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private func writeCmd(args: JsonObject): JsonValue {
|
|
423
|
+
let h = this.requireHandle(args)
|
|
424
|
+
let payload = this.requireString(args, "data")
|
|
425
|
+
let encoding = serialAsciiLower(this.optionalString(args, "encoding", "utf8"))
|
|
426
|
+
var bytes = payload.toArray()
|
|
427
|
+
if (encoding == "hex") {
|
|
428
|
+
match (hexToBytes(payload)) {
|
|
429
|
+
case Some(b) => bytes = b
|
|
430
|
+
case None => throw CommandException("serial:write refused: data 不是合法十六进制串" +
|
|
431
|
+
"(应偶数长度、只含 0-9a-f)")
|
|
432
|
+
}
|
|
433
|
+
} else if (encoding != "utf8" && encoding != "text") {
|
|
434
|
+
throw CommandException("serial:write refused: encoding 只认 \"utf8\" 或 \"hex\"")
|
|
435
|
+
}
|
|
436
|
+
if (bytes.size > 4096) {
|
|
437
|
+
throw CommandException("serial:write refused: 单次最多 4096 字节(拿到 ${bytes.size})")
|
|
438
|
+
}
|
|
439
|
+
let timeoutMs = this.clamp(this.optionalInt(args, "timeoutMs", 1000), 0, 60000)
|
|
440
|
+
let written = serialWrite(h, bytes, Int32(timeoutMs))
|
|
441
|
+
if (written < 0) {
|
|
442
|
+
throw CommandException("serial:write failed (code ${written}): ${serialLastError()}")
|
|
443
|
+
}
|
|
444
|
+
let out = JsonObject()
|
|
445
|
+
out.put("written", JsonInt(Int64(written)))
|
|
446
|
+
out.put("bytes", JsonInt(Int64(bytes.size)))
|
|
447
|
+
return out
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private func closeCmd(args: JsonObject): JsonValue {
|
|
451
|
+
let h = this.requireHandle(args)
|
|
452
|
+
let rc = serialClose(h)
|
|
453
|
+
if (rc != 0) {
|
|
454
|
+
throw CommandException("serial:close failed (code ${rc}): ${serialLastError()}")
|
|
455
|
+
}
|
|
456
|
+
return JsonBool(true)
|
|
457
|
+
}
|
|
458
|
+
}
|