@mikrojs/firmware 0.0.7

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/bin/idf.py +7 -0
  4. package/chips.json +3 -0
  5. package/cmake.js +9 -0
  6. package/components/mikrojs/CMakeLists.txt +187 -0
  7. package/components/mikrojs/Kconfig +55 -0
  8. package/components/mikrojs/idf_component.yml +6 -0
  9. package/components/mikrojs/include/mem.h +3 -0
  10. package/components/mikrojs/include/mik_color.h +3 -0
  11. package/components/mikrojs/include/mik_http_internal.h +77 -0
  12. package/components/mikrojs/include/mikrojs.h +5 -0
  13. package/components/mikrojs/include/mikrojs_esp32.h +65 -0
  14. package/components/mikrojs/include/private.h +10 -0
  15. package/components/mikrojs/include/utils.h +3 -0
  16. package/components/mikrojs/mik_ble.cpp +1588 -0
  17. package/components/mikrojs/mik_ble_c_shim.c +61 -0
  18. package/components/mikrojs/mik_ble_c_shim.h +37 -0
  19. package/components/mikrojs/mik_config.cpp +167 -0
  20. package/components/mikrojs/mik_deploy.cpp +584 -0
  21. package/components/mikrojs/mik_http.cpp +916 -0
  22. package/components/mikrojs/mik_i2c.cpp +364 -0
  23. package/components/mikrojs/mik_main.cpp +542 -0
  24. package/components/mikrojs/mik_neopixel.cpp +437 -0
  25. package/components/mikrojs/mik_nvs_kv.cpp +219 -0
  26. package/components/mikrojs/mik_pin.cpp +195 -0
  27. package/components/mikrojs/mik_pwm.cpp +525 -0
  28. package/components/mikrojs/mik_recovery.cpp +86 -0
  29. package/components/mikrojs/mik_rtc.cpp +305 -0
  30. package/components/mikrojs/mik_serial_io.cpp +362 -0
  31. package/components/mikrojs/mik_sleep.cpp +226 -0
  32. package/components/mikrojs/mik_sntp.cpp +275 -0
  33. package/components/mikrojs/mik_spi.cpp +330 -0
  34. package/components/mikrojs/mik_uart.cpp +497 -0
  35. package/components/mikrojs/mik_wifi.cpp +1434 -0
  36. package/components/mikrojs/platform_esp32.cpp +192 -0
  37. package/components/mikrojs/test/CMakeLists.txt +32 -0
  38. package/components/mikrojs/test/abort_test.cpp +254 -0
  39. package/components/mikrojs/test/ble_test.cpp +714 -0
  40. package/components/mikrojs/test/fs_js_test.cpp +458 -0
  41. package/components/mikrojs/test/fs_pub_test.cpp +312 -0
  42. package/components/mikrojs/test/http_test.cpp +475 -0
  43. package/components/mikrojs/test/i2c_test.cpp +138 -0
  44. package/components/mikrojs/test/modules_extended_test.cpp +137 -0
  45. package/components/mikrojs/test/modules_test.cpp +131 -0
  46. package/components/mikrojs/test/pins_test.cpp +47 -0
  47. package/components/mikrojs/test/pwm_test.cpp +166 -0
  48. package/components/mikrojs/test/repl_protocol_test.cpp +405 -0
  49. package/components/mikrojs/test/rtc_test.cpp +331 -0
  50. package/components/mikrojs/test/runtime_test.cpp +89 -0
  51. package/components/mikrojs/test/sleep_test.cpp +222 -0
  52. package/components/mikrojs/test/sntp_test.cpp +249 -0
  53. package/components/mikrojs/test/stdio_test.cpp +449 -0
  54. package/components/mikrojs/test/sys_test.cpp +165 -0
  55. package/components/mikrojs/test/text_encoding_test.cpp +224 -0
  56. package/components/mikrojs/test/timers_js_test.cpp +244 -0
  57. package/components/mikrojs/test/timers_test.cpp +79 -0
  58. package/components/mikrojs/test/wifi_test.cpp +599 -0
  59. package/default-app/main/CMakeLists.txt +3 -0
  60. package/default-app/main/main.cpp +5 -0
  61. package/discover.js +77 -0
  62. package/index.d.ts +7 -0
  63. package/index.js +20 -0
  64. package/package.json +61 -0
  65. package/partitions.csv +5 -0
  66. package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
  67. package/prebuilds/esp32/flasher_args.json +24 -0
  68. package/prebuilds/esp32/mikrojs.bin +0 -0
  69. package/prebuilds/esp32/partition_table/partition-table.bin +0 -0
  70. package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
  71. package/prebuilds/esp32c3/flasher_args.json +24 -0
  72. package/prebuilds/esp32c3/mikrojs.bin +0 -0
  73. package/prebuilds/esp32c3/partition_table/partition-table.bin +0 -0
  74. package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
  75. package/prebuilds/esp32c6/flasher_args.json +24 -0
  76. package/prebuilds/esp32c6/mikrojs.bin +0 -0
  77. package/prebuilds/esp32c6/partition_table/partition-table.bin +0 -0
  78. package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
  79. package/prebuilds/esp32s3/flasher_args.json +24 -0
  80. package/prebuilds/esp32s3/mikrojs.bin +0 -0
  81. package/prebuilds/esp32s3/partition_table/partition-table.bin +0 -0
  82. package/project.cmake +101 -0
  83. package/resolve.js +54 -0
  84. package/sdkconfig.defaults +127 -0
  85. package/sdkconfig.defaults.esp32 +8 -0
  86. package/sdkconfig.defaults.esp32c3 +15 -0
  87. package/sdkconfig.defaults.esp32c6 +26 -0
  88. package/sdkconfig.defaults.esp32s3 +22 -0
@@ -0,0 +1,714 @@
1
+ #include <cstring>
2
+
3
+ #include "freertos/FreeRTOS.h"
4
+ #include "freertos/task.h"
5
+ #include "mikrojs.h"
6
+ #include "private.h"
7
+ #include "quickjs.h"
8
+ #include "unity.h"
9
+
10
+ /* The M1a BLE test suite exercises the broadcaster lifecycle through the
11
+ * `native:ble` module. Tests do not require an external central — they only
12
+ * verify that advertise/stop/teardown return cleanly and the module exports
13
+ * match the expected shape. Real GATT and connection behavior belongs to M2.
14
+ */
15
+
16
+ static MIKRuntime* rt;
17
+ static JSContext* ctx;
18
+
19
+ extern void mik__ble_consume(JSContext* ctx);
20
+
21
+ static void setup() {
22
+ rt = MIK_NewRuntime();
23
+ ctx = MIK_GetJSContext(rt);
24
+ mik__ble_consume(ctx);
25
+ }
26
+
27
+ static void teardown() {
28
+ /* Ensure the BLE stack is stopped between tests so every test starts
29
+ * from a clean state. stop() is idempotent. */
30
+ JSValue ret = MIK_EvalModuleContent(
31
+ ctx, "mikrojs/ble-test-teardown",
32
+ "import {Ble} from \"native:ble\"; new Ble().stop();", 48);
33
+ if (!JS_IsException(ret)) {
34
+ JS_FreeValue(ctx, ret);
35
+ mik__execute_jobs(ctx);
36
+ } else {
37
+ JS_FreeValue(ctx, ret);
38
+ }
39
+ mik__ble_consume(ctx);
40
+ /* Let NimBLE tasks settle before freeing the runtime. */
41
+ vTaskDelay(pdMS_TO_TICKS(50));
42
+ MIK_FreeRuntime(rt);
43
+ }
44
+
45
+ static JSValue eval_module(const char* code) {
46
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/ble-test", code, strlen(code));
47
+ if (!JS_IsException(ret)) {
48
+ JS_FreeValue(ctx, ret);
49
+ mik__execute_jobs(ctx);
50
+ }
51
+ return ret;
52
+ }
53
+
54
+ /* ── Module shape ─────────────────────────────────────────────────── */
55
+
56
+ TEST_CASE("native:ble exports Ble with expected methods", "[ble]") {
57
+ setup();
58
+
59
+ JSValue ret = eval_module(R"(
60
+ import {Ble} from "native:ble";
61
+ globalThis.__isFunc = typeof Ble === "function";
62
+ const ble = new Ble();
63
+ globalThis.__isObj = typeof ble === "object" && ble !== null;
64
+ globalThis.__methods = [
65
+ "getName","setName","getAddress","getTxPower","setTxPower",
66
+ "advertise","stopAdvertising","stop"
67
+ ].every(m => typeof ble[m] === "function");
68
+ )");
69
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
70
+
71
+ JSValue global = JS_GetGlobalObject(ctx);
72
+
73
+ JSValue isFunc = JS_GetPropertyStr(ctx, global, "__isFunc");
74
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, isFunc), "Ble should be a function");
75
+ JS_FreeValue(ctx, isFunc);
76
+
77
+ JSValue isObj = JS_GetPropertyStr(ctx, global, "__isObj");
78
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, isObj), "new Ble() should return an object");
79
+ JS_FreeValue(ctx, isObj);
80
+
81
+ JSValue methods = JS_GetPropertyStr(ctx, global, "__methods");
82
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, methods), "All expected methods should exist");
83
+ JS_FreeValue(ctx, methods);
84
+
85
+ JS_FreeValue(ctx, global);
86
+ teardown();
87
+ }
88
+
89
+ /* ── Default name follows the mikrojs-xxxxxx pattern ───────────────── */
90
+
91
+ TEST_CASE("Ble.getName returns a mikrojs- prefixed name by default", "[ble]") {
92
+ setup();
93
+
94
+ JSValue ret = eval_module(R"(
95
+ import {Ble} from "native:ble";
96
+ const ble = new Ble();
97
+ globalThis.__name = ble.getName();
98
+ )");
99
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
100
+
101
+ JSValue global = JS_GetGlobalObject(ctx);
102
+ JSValue nameVal = JS_GetPropertyStr(ctx, global, "__name");
103
+ const char* name = JS_ToCString(ctx, nameVal);
104
+ TEST_ASSERT_NOT_NULL(name);
105
+ TEST_ASSERT_TRUE_MESSAGE(strncmp(name, "mikrojs", 7) == 0,
106
+ "Default name should start with 'mikrojs'");
107
+ JS_FreeCString(ctx, name);
108
+ JS_FreeValue(ctx, nameVal);
109
+ JS_FreeValue(ctx, global);
110
+ teardown();
111
+ }
112
+
113
+ /* ── setName persists across getName ──────────────────────────────── */
114
+
115
+ TEST_CASE("Ble.setName updates the cached name", "[ble]") {
116
+ setup();
117
+
118
+ JSValue ret = eval_module(R"(
119
+ import {Ble} from "native:ble";
120
+ const ble = new Ble();
121
+ const setResult = ble.setName("mikrojs-test");
122
+ globalThis.__setOk = setResult.ok;
123
+ globalThis.__nameAfter = ble.getName();
124
+ )");
125
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
126
+
127
+ JSValue global = JS_GetGlobalObject(ctx);
128
+
129
+ JSValue setOk = JS_GetPropertyStr(ctx, global, "__setOk");
130
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, setOk), "setName should succeed");
131
+ JS_FreeValue(ctx, setOk);
132
+
133
+ JSValue nameVal = JS_GetPropertyStr(ctx, global, "__nameAfter");
134
+ const char* name = JS_ToCString(ctx, nameVal);
135
+ TEST_ASSERT_EQUAL_STRING("mikrojs-test", name);
136
+ JS_FreeCString(ctx, name);
137
+ JS_FreeValue(ctx, nameVal);
138
+
139
+ JS_FreeValue(ctx, global);
140
+ teardown();
141
+ }
142
+
143
+ /* ── advertise/stopAdvertising round-trip ─────────────────────────── */
144
+
145
+ TEST_CASE("Ble.advertise then stopAdvertising returns ok", "[ble]") {
146
+ setup();
147
+
148
+ JSValue ret = eval_module(R"(
149
+ import {Ble} from "native:ble";
150
+ const ble = new Ble();
151
+ const startResult = ble.advertise({
152
+ name: "mikrojs-test",
153
+ connectable: false,
154
+ interval: {min: 1000, max: 1500},
155
+ });
156
+ globalThis.__startOk = startResult.ok;
157
+ globalThis.__startErrName = startResult.ok ? null : startResult.error.name;
158
+ const stopResult = ble.stopAdvertising();
159
+ globalThis.__stopOk = stopResult.ok;
160
+ )");
161
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
162
+
163
+ JSValue global = JS_GetGlobalObject(ctx);
164
+
165
+ JSValue startOk = JS_GetPropertyStr(ctx, global, "__startOk");
166
+ if (!JS_ToBool(ctx, startOk)) {
167
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__startErrName");
168
+ const char* errStr = JS_ToCString(ctx, errName);
169
+ TEST_FAIL_MESSAGE(errStr ? errStr : "advertise returned err with no name");
170
+ if (errStr) JS_FreeCString(ctx, errStr);
171
+ JS_FreeValue(ctx, errName);
172
+ }
173
+ JS_FreeValue(ctx, startOk);
174
+
175
+ JSValue stopOk = JS_GetPropertyStr(ctx, global, "__stopOk");
176
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, stopOk), "stopAdvertising should return ok");
177
+ JS_FreeValue(ctx, stopOk);
178
+
179
+ JS_FreeValue(ctx, global);
180
+ teardown();
181
+ }
182
+
183
+ /* ── Double-advertise returns AlreadyAdvertising ──────────────────── */
184
+
185
+ TEST_CASE("Ble.advertise twice returns AlreadyAdvertising", "[ble]") {
186
+ setup();
187
+
188
+ JSValue ret = eval_module(R"(
189
+ import {Ble} from "native:ble";
190
+ const ble = new Ble();
191
+ ble.advertise({name: "t", connectable: false});
192
+ const second = ble.advertise({name: "t", connectable: false});
193
+ globalThis.__isErr = !second.ok;
194
+ globalThis.__errName = second.ok ? null : second.error.name;
195
+ ble.stopAdvertising();
196
+ )");
197
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
198
+
199
+ JSValue global = JS_GetGlobalObject(ctx);
200
+
201
+ JSValue isErr = JS_GetPropertyStr(ctx, global, "__isErr");
202
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, isErr), "Second advertise should be an error");
203
+ JS_FreeValue(ctx, isErr);
204
+
205
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__errName");
206
+ const char* name = JS_ToCString(ctx, errName);
207
+ TEST_ASSERT_EQUAL_STRING("AlreadyAdvertising", name);
208
+ if (name) JS_FreeCString(ctx, name);
209
+ JS_FreeValue(ctx, errName);
210
+
211
+ JS_FreeValue(ctx, global);
212
+ teardown();
213
+ }
214
+
215
+ /* ── getAddress returns a MAC-shaped string after init ────────────── */
216
+
217
+ TEST_CASE("Ble.getAddress returns a colon-separated MAC", "[ble]") {
218
+ setup();
219
+
220
+ JSValue ret = eval_module(R"(
221
+ import {Ble} from "native:ble";
222
+ const ble = new Ble();
223
+ const result = ble.getAddress();
224
+ globalThis.__ok = result.ok;
225
+ globalThis.__addr = result.ok ? result.value : null;
226
+ )");
227
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
228
+
229
+ JSValue global = JS_GetGlobalObject(ctx);
230
+
231
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
232
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, okVal), "getAddress should return ok");
233
+ JS_FreeValue(ctx, okVal);
234
+
235
+ JSValue addrVal = JS_GetPropertyStr(ctx, global, "__addr");
236
+ const char* addr = JS_ToCString(ctx, addrVal);
237
+ TEST_ASSERT_NOT_NULL(addr);
238
+ TEST_ASSERT_EQUAL_INT_MESSAGE(17, strlen(addr), "MAC should be 17 chars (aa:bb:cc:dd:ee:ff)");
239
+ if (addr) JS_FreeCString(ctx, addr);
240
+ JS_FreeValue(ctx, addrVal);
241
+
242
+ JS_FreeValue(ctx, global);
243
+ teardown();
244
+ }
245
+
246
+ /* ── Full teardown via stop() is idempotent ───────────────────────── */
247
+
248
+ TEST_CASE("Ble.stop is idempotent", "[ble]") {
249
+ setup();
250
+
251
+ JSValue ret = eval_module(R"(
252
+ import {Ble} from "native:ble";
253
+ const ble = new Ble();
254
+ const first = ble.stop();
255
+ const second = ble.stop();
256
+ globalThis.__firstOk = first.ok;
257
+ globalThis.__secondOk = second.ok;
258
+ )");
259
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
260
+
261
+ JSValue global = JS_GetGlobalObject(ctx);
262
+
263
+ JSValue firstOk = JS_GetPropertyStr(ctx, global, "__firstOk");
264
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, firstOk), "First stop should be ok");
265
+ JS_FreeValue(ctx, firstOk);
266
+
267
+ JSValue secondOk = JS_GetPropertyStr(ctx, global, "__secondOk");
268
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, secondOk), "Second stop should also be ok");
269
+ JS_FreeValue(ctx, secondOk);
270
+
271
+ JS_FreeValue(ctx, global);
272
+ teardown();
273
+ }
274
+
275
+ /* ── GATT registration ─────────────────────────────────────────────── */
276
+
277
+ TEST_CASE("Ble.advertise with services registers a GATT table", "[ble]") {
278
+ setup();
279
+
280
+ /* Properties bitmask: read (0x01) | notify (0x08) = 0x09 */
281
+ JSValue ret = eval_module(R"(
282
+ import {Ble} from "native:ble";
283
+ const ble = new Ble();
284
+ const result = ble.advertise({
285
+ connectable: true,
286
+ services: [{
287
+ uuid: "180f",
288
+ characteristics: [{
289
+ uuid: "2a19",
290
+ properties: 0x09,
291
+ value: new Uint8Array([100]),
292
+ }],
293
+ }],
294
+ });
295
+ globalThis.__ok = result.ok;
296
+ globalThis.__errName = result.ok ? null : result.error.name;
297
+ if (result.ok) ble.stopAdvertising();
298
+ )");
299
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
300
+
301
+ JSValue global = JS_GetGlobalObject(ctx);
302
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
303
+ if (!JS_ToBool(ctx, okVal)) {
304
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__errName");
305
+ const char* errStr = JS_ToCString(ctx, errName);
306
+ TEST_FAIL_MESSAGE(errStr ? errStr : "advertise returned err with no name");
307
+ if (errStr) JS_FreeCString(ctx, errStr);
308
+ JS_FreeValue(ctx, errName);
309
+ }
310
+ JS_FreeValue(ctx, okVal);
311
+ JS_FreeValue(ctx, global);
312
+ teardown();
313
+ }
314
+
315
+ TEST_CASE("Ble.advertise with matching services on re-advertise succeeds", "[ble]") {
316
+ setup();
317
+
318
+ JSValue ret = eval_module(R"(
319
+ import {Ble} from "native:ble";
320
+ const ble = new Ble();
321
+ const services = [{
322
+ uuid: "180f",
323
+ characteristics: [{
324
+ uuid: "2a19",
325
+ properties: 0x09,
326
+ value: new Uint8Array([100]),
327
+ }],
328
+ }];
329
+ const first = ble.advertise({connectable: true, services});
330
+ ble.stopAdvertising();
331
+ const second = ble.advertise({connectable: true, services});
332
+ globalThis.__firstOk = first.ok;
333
+ globalThis.__secondOk = second.ok;
334
+ globalThis.__secondErr = second.ok ? null : second.error.name;
335
+ if (second.ok) ble.stopAdvertising();
336
+ )");
337
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
338
+
339
+ JSValue global = JS_GetGlobalObject(ctx);
340
+
341
+ JSValue firstOk = JS_GetPropertyStr(ctx, global, "__firstOk");
342
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, firstOk), "First advertise should succeed");
343
+ JS_FreeValue(ctx, firstOk);
344
+
345
+ JSValue secondOk = JS_GetPropertyStr(ctx, global, "__secondOk");
346
+ if (!JS_ToBool(ctx, secondOk)) {
347
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__secondErr");
348
+ const char* errStr = JS_ToCString(ctx, errName);
349
+ TEST_FAIL_MESSAGE(errStr ? errStr : "second advertise returned err");
350
+ if (errStr) JS_FreeCString(ctx, errStr);
351
+ JS_FreeValue(ctx, errName);
352
+ }
353
+ JS_FreeValue(ctx, secondOk);
354
+
355
+ JS_FreeValue(ctx, global);
356
+ teardown();
357
+ }
358
+
359
+ TEST_CASE("Ble.advertise with different services on re-advertise errors", "[ble]") {
360
+ setup();
361
+
362
+ JSValue ret = eval_module(R"(
363
+ import {Ble} from "native:ble";
364
+ const ble = new Ble();
365
+ const first = ble.advertise({
366
+ connectable: true,
367
+ services: [{
368
+ uuid: "180f",
369
+ characteristics: [{uuid: "2a19", properties: 0x01, value: new Uint8Array([1])}],
370
+ }],
371
+ });
372
+ ble.stopAdvertising();
373
+ const second = ble.advertise({
374
+ connectable: true,
375
+ services: [{
376
+ uuid: "180a",
377
+ characteristics: [{uuid: "2a29", properties: 0x01, value: new Uint8Array([2])}],
378
+ }],
379
+ });
380
+ globalThis.__firstOk = first.ok;
381
+ globalThis.__secondOk = second.ok;
382
+ globalThis.__secondErr = second.ok ? null : second.error.name;
383
+ )");
384
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
385
+
386
+ JSValue global = JS_GetGlobalObject(ctx);
387
+
388
+ JSValue firstOk = JS_GetPropertyStr(ctx, global, "__firstOk");
389
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, firstOk), "First advertise should succeed");
390
+ JS_FreeValue(ctx, firstOk);
391
+
392
+ JSValue secondOk = JS_GetPropertyStr(ctx, global, "__secondOk");
393
+ TEST_ASSERT_FALSE_MESSAGE(JS_ToBool(ctx, secondOk),
394
+ "Second advertise with different services should fail");
395
+ JS_FreeValue(ctx, secondOk);
396
+
397
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__secondErr");
398
+ const char* name = JS_ToCString(ctx, errName);
399
+ TEST_ASSERT_EQUAL_STRING("GattAlreadyRegistered", name);
400
+ if (name) JS_FreeCString(ctx, name);
401
+ JS_FreeValue(ctx, errName);
402
+
403
+ JS_FreeValue(ctx, global);
404
+ teardown();
405
+ }
406
+
407
+ TEST_CASE("Ble.advertise with invalid UUID errors", "[ble]") {
408
+ setup();
409
+
410
+ JSValue ret = eval_module(R"(
411
+ import {Ble} from "native:ble";
412
+ const ble = new Ble();
413
+ const result = ble.advertise({
414
+ services: [{
415
+ uuid: "not-a-uuid",
416
+ characteristics: [{uuid: "2a19", properties: 0x01, value: new Uint8Array([1])}],
417
+ }],
418
+ });
419
+ globalThis.__ok = result.ok;
420
+ globalThis.__errName = result.ok ? null : result.error.name;
421
+ )");
422
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
423
+
424
+ JSValue global = JS_GetGlobalObject(ctx);
425
+
426
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
427
+ TEST_ASSERT_FALSE_MESSAGE(JS_ToBool(ctx, okVal), "advertise should fail on invalid UUID");
428
+ JS_FreeValue(ctx, okVal);
429
+
430
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__errName");
431
+ const char* name = JS_ToCString(ctx, errName);
432
+ TEST_ASSERT_EQUAL_STRING("InvalidUuid", name);
433
+ if (name) JS_FreeCString(ctx, name);
434
+ JS_FreeValue(ctx, errName);
435
+
436
+ JS_FreeValue(ctx, global);
437
+ teardown();
438
+ }
439
+
440
+ TEST_CASE("Ble.advertise with 128-bit UUIDs registers", "[ble]") {
441
+ setup();
442
+
443
+ JSValue ret = eval_module(R"(
444
+ import {Ble} from "native:ble";
445
+ const ble = new Ble();
446
+ const result = ble.advertise({
447
+ connectable: true,
448
+ services: [{
449
+ uuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
450
+ characteristics: [{
451
+ uuid: "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
452
+ properties: 0x02,
453
+ value: new Uint8Array([]),
454
+ }],
455
+ }],
456
+ });
457
+ globalThis.__ok = result.ok;
458
+ globalThis.__errName = result.ok ? null : result.error.name;
459
+ if (result.ok) ble.stopAdvertising();
460
+ )");
461
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
462
+
463
+ JSValue global = JS_GetGlobalObject(ctx);
464
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
465
+ if (!JS_ToBool(ctx, okVal)) {
466
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__errName");
467
+ const char* errStr = JS_ToCString(ctx, errName);
468
+ TEST_FAIL_MESSAGE(errStr ? errStr : "advertise with 128-bit UUIDs failed");
469
+ if (errStr) JS_FreeCString(ctx, errStr);
470
+ JS_FreeValue(ctx, errName);
471
+ }
472
+ JS_FreeValue(ctx, okVal);
473
+ JS_FreeValue(ctx, global);
474
+ teardown();
475
+ }
476
+
477
+ /* ── setValue ─────────────────────────────────────────────────────── */
478
+
479
+ TEST_CASE("Ble.setValue on a registered characteristic succeeds", "[ble]") {
480
+ setup();
481
+
482
+ JSValue ret = eval_module(R"(
483
+ import {Ble} from "native:ble";
484
+ const ble = new Ble();
485
+ const advResult = ble.advertise({
486
+ connectable: true,
487
+ services: [{
488
+ uuid: "180f",
489
+ characteristics: [{
490
+ uuid: "2a19",
491
+ properties: 0x01,
492
+ value: new Uint8Array([50]),
493
+ }],
494
+ }],
495
+ });
496
+ globalThis.__advOk = advResult.ok;
497
+ const setResult = ble.setValue("180f", "2a19", new Uint8Array([99]));
498
+ globalThis.__setOk = setResult.ok;
499
+ globalThis.__setErr = setResult.ok ? null : setResult.error.name;
500
+ ble.stopAdvertising();
501
+ )");
502
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
503
+
504
+ JSValue global = JS_GetGlobalObject(ctx);
505
+
506
+ JSValue advOk = JS_GetPropertyStr(ctx, global, "__advOk");
507
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, advOk), "advertise should succeed");
508
+ JS_FreeValue(ctx, advOk);
509
+
510
+ JSValue setOk = JS_GetPropertyStr(ctx, global, "__setOk");
511
+ if (!JS_ToBool(ctx, setOk)) {
512
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__setErr");
513
+ const char* errStr = JS_ToCString(ctx, errName);
514
+ TEST_FAIL_MESSAGE(errStr ? errStr : "setValue returned err with no name");
515
+ if (errStr) JS_FreeCString(ctx, errStr);
516
+ JS_FreeValue(ctx, errName);
517
+ }
518
+ JS_FreeValue(ctx, setOk);
519
+
520
+ JS_FreeValue(ctx, global);
521
+ teardown();
522
+ }
523
+
524
+ TEST_CASE("Ble.setValue on an unknown service errors", "[ble]") {
525
+ setup();
526
+
527
+ JSValue ret = eval_module(R"(
528
+ import {Ble} from "native:ble";
529
+ const ble = new Ble();
530
+ ble.advertise({
531
+ connectable: true,
532
+ services: [{
533
+ uuid: "180f",
534
+ characteristics: [{uuid: "2a19", properties: 0x01, value: new Uint8Array([1])}],
535
+ }],
536
+ });
537
+ const result = ble.setValue("1809", "2a19", new Uint8Array([1]));
538
+ globalThis.__ok = result.ok;
539
+ globalThis.__err = result.ok ? null : result.error.name;
540
+ ble.stopAdvertising();
541
+ )");
542
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
543
+
544
+ JSValue global = JS_GetGlobalObject(ctx);
545
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
546
+ TEST_ASSERT_FALSE_MESSAGE(JS_ToBool(ctx, okVal),
547
+ "setValue with unknown service should fail");
548
+ JS_FreeValue(ctx, okVal);
549
+
550
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__err");
551
+ const char* name = JS_ToCString(ctx, errName);
552
+ TEST_ASSERT_EQUAL_STRING("NoSuchCharacteristic", name);
553
+ if (name) JS_FreeCString(ctx, name);
554
+ JS_FreeValue(ctx, errName);
555
+
556
+ JS_FreeValue(ctx, global);
557
+ teardown();
558
+ }
559
+
560
+ /* ── notify ───────────────────────────────────────────────────────── */
561
+
562
+ TEST_CASE("Ble.notify with no subscribers is a silent no-op", "[ble]") {
563
+ setup();
564
+
565
+ JSValue ret = eval_module(R"(
566
+ import {Ble} from "native:ble";
567
+ const ble = new Ble();
568
+ ble.advertise({
569
+ connectable: true,
570
+ services: [{
571
+ uuid: "180f",
572
+ characteristics: [{
573
+ uuid: "2a19",
574
+ properties: 0x09, // read | notify
575
+ value: new Uint8Array([50]),
576
+ }],
577
+ }],
578
+ });
579
+ const result = ble.notify("180f", "2a19", new Uint8Array([42]));
580
+ globalThis.__ok = result.ok;
581
+ globalThis.__err = result.ok ? null : result.error.name;
582
+ ble.stopAdvertising();
583
+ )");
584
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
585
+
586
+ JSValue global = JS_GetGlobalObject(ctx);
587
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
588
+ if (!JS_ToBool(ctx, okVal)) {
589
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__err");
590
+ const char* errStr = JS_ToCString(ctx, errName);
591
+ TEST_FAIL_MESSAGE(errStr ? errStr : "notify returned err");
592
+ if (errStr) JS_FreeCString(ctx, errStr);
593
+ JS_FreeValue(ctx, errName);
594
+ }
595
+ JS_FreeValue(ctx, okVal);
596
+ JS_FreeValue(ctx, global);
597
+ teardown();
598
+ }
599
+
600
+ TEST_CASE("Ble.notify on an unknown characteristic errors", "[ble]") {
601
+ setup();
602
+
603
+ JSValue ret = eval_module(R"(
604
+ import {Ble} from "native:ble";
605
+ const ble = new Ble();
606
+ ble.advertise({
607
+ connectable: true,
608
+ services: [{
609
+ uuid: "180f",
610
+ characteristics: [{
611
+ uuid: "2a19",
612
+ properties: 0x09,
613
+ value: new Uint8Array([1]),
614
+ }],
615
+ }],
616
+ });
617
+ const result = ble.notify("180f", "2a1a", new Uint8Array([1]));
618
+ globalThis.__ok = result.ok;
619
+ globalThis.__err = result.ok ? null : result.error.name;
620
+ ble.stopAdvertising();
621
+ )");
622
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
623
+
624
+ JSValue global = JS_GetGlobalObject(ctx);
625
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
626
+ TEST_ASSERT_FALSE_MESSAGE(JS_ToBool(ctx, okVal),
627
+ "notify with unknown char should fail");
628
+ JS_FreeValue(ctx, okVal);
629
+
630
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__err");
631
+ const char* name = JS_ToCString(ctx, errName);
632
+ TEST_ASSERT_EQUAL_STRING("NoSuchCharacteristic", name);
633
+ if (name) JS_FreeCString(ctx, name);
634
+ JS_FreeValue(ctx, errName);
635
+
636
+ JS_FreeValue(ctx, global);
637
+ teardown();
638
+ }
639
+
640
+ TEST_CASE("Ble.notify on a characteristic without notify property errors", "[ble]") {
641
+ setup();
642
+
643
+ JSValue ret = eval_module(R"(
644
+ import {Ble} from "native:ble";
645
+ const ble = new Ble();
646
+ ble.advertise({
647
+ connectable: true,
648
+ services: [{
649
+ uuid: "180f",
650
+ characteristics: [{
651
+ uuid: "2a19",
652
+ properties: 0x01, // read only, no notify
653
+ value: new Uint8Array([1]),
654
+ }],
655
+ }],
656
+ });
657
+ const result = ble.notify("180f", "2a19", new Uint8Array([1]));
658
+ globalThis.__ok = result.ok;
659
+ globalThis.__err = result.ok ? null : result.error.name;
660
+ ble.stopAdvertising();
661
+ )");
662
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
663
+
664
+ JSValue global = JS_GetGlobalObject(ctx);
665
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
666
+ TEST_ASSERT_FALSE_MESSAGE(JS_ToBool(ctx, okVal),
667
+ "notify on non-notify char should fail");
668
+ JS_FreeValue(ctx, okVal);
669
+
670
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__err");
671
+ const char* name = JS_ToCString(ctx, errName);
672
+ TEST_ASSERT_EQUAL_STRING("InvalidProperties", name);
673
+ if (name) JS_FreeCString(ctx, name);
674
+ JS_FreeValue(ctx, errName);
675
+
676
+ JS_FreeValue(ctx, global);
677
+ teardown();
678
+ }
679
+
680
+ TEST_CASE("Ble.setValue on an unknown characteristic errors", "[ble]") {
681
+ setup();
682
+
683
+ JSValue ret = eval_module(R"(
684
+ import {Ble} from "native:ble";
685
+ const ble = new Ble();
686
+ ble.advertise({
687
+ connectable: true,
688
+ services: [{
689
+ uuid: "180f",
690
+ characteristics: [{uuid: "2a19", properties: 0x01, value: new Uint8Array([1])}],
691
+ }],
692
+ });
693
+ const result = ble.setValue("180f", "2a1a", new Uint8Array([1]));
694
+ globalThis.__ok = result.ok;
695
+ globalThis.__err = result.ok ? null : result.error.name;
696
+ ble.stopAdvertising();
697
+ )");
698
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
699
+
700
+ JSValue global = JS_GetGlobalObject(ctx);
701
+ JSValue okVal = JS_GetPropertyStr(ctx, global, "__ok");
702
+ TEST_ASSERT_FALSE_MESSAGE(JS_ToBool(ctx, okVal),
703
+ "setValue with unknown char should fail");
704
+ JS_FreeValue(ctx, okVal);
705
+
706
+ JSValue errName = JS_GetPropertyStr(ctx, global, "__err");
707
+ const char* name = JS_ToCString(ctx, errName);
708
+ TEST_ASSERT_EQUAL_STRING("NoSuchCharacteristic", name);
709
+ if (name) JS_FreeCString(ctx, name);
710
+ JS_FreeValue(ctx, errName);
711
+
712
+ JS_FreeValue(ctx, global);
713
+ teardown();
714
+ }