@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,599 @@
1
+ #include "esp_event.h"
2
+ #include "esp_mac.h"
3
+ #include "esp_wifi.h"
4
+ #include "freertos/FreeRTOS.h"
5
+ #include "freertos/task.h"
6
+ #include "mikrojs.h"
7
+ #include "private.h"
8
+ #include "quickjs.h"
9
+ #include "unity.h"
10
+
11
+ /* WiFi status codes — must match MIKWifiStatus in mik_wifi.cpp */
12
+ #define WIFI_STATUS_IDLE 0
13
+ #define WIFI_STATUS_NO_SSID_AVAIL 1
14
+ #define WIFI_STATUS_CONNECTED 3
15
+ #define WIFI_STATUS_CONNECT_FAILED 4
16
+ #define WIFI_STATUS_CONNECTION_LOST 5
17
+ #define WIFI_STATUS_DISCONNECTED 6
18
+
19
+ static MIKRuntime* rt;
20
+ static JSContext* ctx;
21
+
22
+ /* WiFi consume is needed to drain stale events between tests */
23
+ extern void mik__wifi_consume(JSContext* ctx);
24
+
25
+ static void setup() {
26
+ // Ensure WiFi is idle before each test
27
+ esp_wifi_disconnect();
28
+ vTaskDelay(pdMS_TO_TICKS(100));
29
+ rt = MIK_NewRuntime();
30
+ ctx = MIK_GetJSContext(rt);
31
+ /* WiFi module is self-registered and lazily initialized.
32
+ * The import below triggers init + loop consumer registration.
33
+ * Then drain any stale events from previous tests. */
34
+ const char* init_code =
35
+ "import { Wifi } from \"native:wifi\"; new Wifi().setCountry(\"US\");";
36
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test-setup", init_code, strlen(init_code));
37
+ if (!JS_IsException(ret)) {
38
+ JS_FreeValue(ctx, ret);
39
+ mik__execute_jobs(ctx);
40
+ }
41
+ // Drain any stale events from previous tests
42
+ mik__wifi_consume(ctx);
43
+ }
44
+
45
+ static void teardown() {
46
+ // Disconnect WiFi to avoid leaking "connecting" state into next test
47
+ esp_wifi_disconnect();
48
+ vTaskDelay(pdMS_TO_TICKS(100));
49
+ // Drain events before destroying to prevent dangling promise refs
50
+ mik__wifi_consume(ctx);
51
+ MIK_FreeRuntime(rt);
52
+ }
53
+
54
+ static JSValue eval_module(const char* code) {
55
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
56
+ if (!JS_IsException(ret)) {
57
+ JS_FreeValue(ctx, ret);
58
+ mik__execute_jobs(ctx);
59
+ }
60
+ return ret;
61
+ }
62
+
63
+ static int32_t get_wifi_status() {
64
+ JSValue ret = eval_module("globalThis.__s = globalThis.__wifi.status();");
65
+ if (JS_IsException(ret)) return -1;
66
+ JSValue global = JS_GetGlobalObject(ctx);
67
+ JSValue v = JS_GetPropertyStr(ctx, global, "__s");
68
+ int32_t s;
69
+ JS_ToInt32(ctx, &s, v);
70
+ JS_FreeValue(ctx, v);
71
+ JS_FreeValue(ctx, global);
72
+ return s;
73
+ }
74
+
75
+ /* ── Module structure tests ───────────────────────────────────────── */
76
+
77
+ TEST_CASE("native:wifi exports Wifi with expected methods", "[wifi]") {
78
+ setup();
79
+
80
+ JSValue ret = eval_module(R"(
81
+ import { Wifi } from "native:wifi";
82
+ globalThis.__isFunc = typeof Wifi === "function";
83
+ const wifi = new Wifi();
84
+ globalThis.__isObj = typeof wifi === "object" && wifi !== null;
85
+ globalThis.__methods = ["connect","disconnect","rssi","ip","status","scan","on","off"]
86
+ .every(m => typeof wifi[m] === "function");
87
+ )");
88
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
89
+
90
+ JSValue global = JS_GetGlobalObject(ctx);
91
+
92
+ JSValue isFunc = JS_GetPropertyStr(ctx, global, "__isFunc");
93
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, isFunc), "Wifi should be a function");
94
+ JS_FreeValue(ctx, isFunc);
95
+
96
+ JSValue isObj = JS_GetPropertyStr(ctx, global, "__isObj");
97
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, isObj), "new Wifi() should return an object");
98
+ JS_FreeValue(ctx, isObj);
99
+
100
+ JSValue methods = JS_GetPropertyStr(ctx, global, "__methods");
101
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, methods), "All expected methods should exist");
102
+ JS_FreeValue(ctx, methods);
103
+
104
+ JS_FreeValue(ctx, global);
105
+ teardown();
106
+ }
107
+
108
+ TEST_CASE("Wifi ip returns 0.0.0.0 and rssi returns 0 when not connected", "[wifi]") {
109
+ setup();
110
+
111
+ JSValue ret = eval_module(R"(
112
+ import { Wifi } from "native:wifi";
113
+ const wifi = new Wifi();
114
+ globalThis.__ip = wifi.ip();
115
+ globalThis.__rssi = wifi.rssi();
116
+ globalThis.__statusType = typeof wifi.status();
117
+ )");
118
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
119
+
120
+ JSValue global = JS_GetGlobalObject(ctx);
121
+
122
+ JSValue ipVal = JS_GetPropertyStr(ctx, global, "__ip");
123
+ const char* ipStr = JS_ToCString(ctx, ipVal);
124
+ TEST_ASSERT_EQUAL_STRING("0.0.0.0", ipStr);
125
+ JS_FreeCString(ctx, ipStr);
126
+ JS_FreeValue(ctx, ipVal);
127
+
128
+ int32_t rssi;
129
+ JSValue rssiVal = JS_GetPropertyStr(ctx, global, "__rssi");
130
+ JS_ToInt32(ctx, &rssi, rssiVal);
131
+ JS_FreeValue(ctx, rssiVal);
132
+ TEST_ASSERT_EQUAL_INT32(0, rssi);
133
+
134
+ JSValue stType = JS_GetPropertyStr(ctx, global, "__statusType");
135
+ const char* typeStr = JS_ToCString(ctx, stType);
136
+ TEST_ASSERT_EQUAL_STRING("number", typeStr);
137
+ JS_FreeCString(ctx, typeStr);
138
+ JS_FreeValue(ctx, stType);
139
+
140
+ JS_FreeValue(ctx, global);
141
+ teardown();
142
+ }
143
+
144
+ /* ── Connect returns a promise ─────────────────────────────────────── */
145
+
146
+ TEST_CASE("Wifi connect returns a promise", "[wifi]") {
147
+ setup();
148
+
149
+ // Only test that connect() returns a Promise — don't actually wait for connection.
150
+ // We call disconnect() right after to avoid leaving WiFi in "connecting" state.
151
+ JSValue ret = eval_module(R"(
152
+ import { Wifi } from "native:wifi";
153
+ const wifi = new Wifi();
154
+ const result = wifi.connect("test", "pass");
155
+ globalThis.__isPromise = result.ok && result.value instanceof Promise;
156
+ wifi.disconnect();
157
+ )");
158
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
159
+
160
+ JSValue global = JS_GetGlobalObject(ctx);
161
+ JSValue isPromise = JS_GetPropertyStr(ctx, global, "__isPromise");
162
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, isPromise), "connect() should return a Promise");
163
+ JS_FreeValue(ctx, isPromise);
164
+ JS_FreeValue(ctx, global);
165
+ teardown();
166
+ }
167
+
168
+ /* ── Scan returns a promise ────────────────────────────────────────── */
169
+
170
+ TEST_CASE("Wifi scan returns a promise", "[wifi]") {
171
+ setup();
172
+
173
+ JSValue ret = eval_module(R"(
174
+ import { Wifi } from "native:wifi";
175
+ const wifi = new Wifi();
176
+ const result = wifi.scan();
177
+ globalThis.__isPromise = result.ok && result.value instanceof Promise;
178
+ )");
179
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
180
+
181
+ JSValue global = JS_GetGlobalObject(ctx);
182
+ JSValue isPromise = JS_GetPropertyStr(ctx, global, "__isPromise");
183
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, isPromise), "scan() should return a Promise");
184
+ JS_FreeValue(ctx, isPromise);
185
+ JS_FreeValue(ctx, global);
186
+ teardown();
187
+ }
188
+
189
+ /* ── Event-driven state machine tests (mock via esp_event_post) ──── */
190
+
191
+ TEST_CASE("WiFi status reflects events through connect-disconnect cycle", "[wifi]") {
192
+ setup();
193
+
194
+ JSValue ret = eval_module(R"(
195
+ import { Wifi } from "native:wifi";
196
+ globalThis.__wifi = new Wifi();
197
+ )");
198
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
199
+
200
+ // Connected
201
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, nullptr, 0, portMAX_DELAY);
202
+ vTaskDelay(pdMS_TO_TICKS(50));
203
+ mik__wifi_consume(ctx);
204
+ TEST_ASSERT_EQUAL_INT32(WIFI_STATUS_CONNECTED, get_wifi_status());
205
+
206
+ // Disconnect: NO_AP_FOUND → NO_SSID_AVAIL
207
+ wifi_event_sta_disconnected_t disc = {};
208
+ disc.reason = WIFI_REASON_NO_AP_FOUND;
209
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disc, sizeof(disc), portMAX_DELAY);
210
+ vTaskDelay(pdMS_TO_TICKS(50));
211
+ mik__wifi_consume(ctx);
212
+ TEST_ASSERT_EQUAL_INT32(WIFI_STATUS_NO_SSID_AVAIL, get_wifi_status());
213
+
214
+ // Disconnect: AUTH_FAIL → CONNECT_FAILED
215
+ disc.reason = WIFI_REASON_AUTH_FAIL;
216
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disc, sizeof(disc), portMAX_DELAY);
217
+ vTaskDelay(pdMS_TO_TICKS(50));
218
+ mik__wifi_consume(ctx);
219
+ TEST_ASSERT_EQUAL_INT32(WIFI_STATUS_CONNECT_FAILED, get_wifi_status());
220
+
221
+ // Disconnect: BEACON_TIMEOUT → CONNECTION_LOST
222
+ disc.reason = WIFI_REASON_BEACON_TIMEOUT;
223
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disc, sizeof(disc), portMAX_DELAY);
224
+ vTaskDelay(pdMS_TO_TICKS(50));
225
+ mik__wifi_consume(ctx);
226
+ TEST_ASSERT_EQUAL_INT32(WIFI_STATUS_CONNECTION_LOST, get_wifi_status());
227
+
228
+ // Disconnect: generic → DISCONNECTED
229
+ disc.reason = WIFI_REASON_UNSPECIFIED;
230
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disc, sizeof(disc), portMAX_DELAY);
231
+ vTaskDelay(pdMS_TO_TICKS(50));
232
+ mik__wifi_consume(ctx);
233
+ TEST_ASSERT_EQUAL_INT32(WIFI_STATUS_DISCONNECTED, get_wifi_status());
234
+
235
+ // Reconnect
236
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, nullptr, 0, portMAX_DELAY);
237
+ vTaskDelay(pdMS_TO_TICKS(50));
238
+ mik__wifi_consume(ctx);
239
+ TEST_ASSERT_EQUAL_INT32(WIFI_STATUS_CONNECTED, get_wifi_status());
240
+
241
+ teardown();
242
+ }
243
+
244
+ /* ── Event listener tests ──────────────────────────────────────────── */
245
+
246
+ TEST_CASE("Wifi on/off registers and removes event listeners", "[wifi]") {
247
+ setup();
248
+
249
+ JSValue ret = eval_module(R"(
250
+ import { Wifi } from "native:wifi";
251
+ const wifi = new Wifi();
252
+ globalThis.__disconnectCount = 0;
253
+ function onDisconnect(reason) {
254
+ globalThis.__disconnectCount++;
255
+ globalThis.__lastReason = reason;
256
+ }
257
+ wifi.on("disconnect", onDisconnect);
258
+ globalThis.__onDisconnect = onDisconnect;
259
+ globalThis.__wifi = wifi;
260
+ )");
261
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
262
+
263
+ // Fire a disconnect event
264
+ wifi_event_sta_disconnected_t disc = {};
265
+ disc.reason = WIFI_REASON_AUTH_FAIL;
266
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disc, sizeof(disc), portMAX_DELAY);
267
+ vTaskDelay(pdMS_TO_TICKS(50));
268
+ mik__wifi_consume(ctx);
269
+ mik__execute_jobs(ctx);
270
+
271
+ JSValue global = JS_GetGlobalObject(ctx);
272
+ JSValue countVal = JS_GetPropertyStr(ctx, global, "__disconnectCount");
273
+ int32_t count;
274
+ JS_ToInt32(ctx, &count, countVal);
275
+ JS_FreeValue(ctx, countVal);
276
+ TEST_ASSERT_EQUAL_INT32(1, count);
277
+
278
+ JSValue reasonVal = JS_GetPropertyStr(ctx, global, "__lastReason");
279
+ const char* reason = JS_ToCString(ctx, reasonVal);
280
+ TEST_ASSERT_EQUAL_STRING("auth-failed", reason);
281
+ JS_FreeCString(ctx, reason);
282
+ JS_FreeValue(ctx, reasonVal);
283
+
284
+ // Remove listener and verify it stops receiving events
285
+ eval_module(R"(
286
+ globalThis.__wifi.off("disconnect", globalThis.__onDisconnect);
287
+ )");
288
+
289
+ disc.reason = WIFI_REASON_BEACON_TIMEOUT;
290
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disc, sizeof(disc), portMAX_DELAY);
291
+ vTaskDelay(pdMS_TO_TICKS(50));
292
+ mik__wifi_consume(ctx);
293
+ mik__execute_jobs(ctx);
294
+
295
+ countVal = JS_GetPropertyStr(ctx, global, "__disconnectCount");
296
+ JS_ToInt32(ctx, &count, countVal);
297
+ JS_FreeValue(ctx, countVal);
298
+ TEST_ASSERT_EQUAL_INT32(1, count); // still 1, listener was removed
299
+
300
+ JS_FreeValue(ctx, global);
301
+ teardown();
302
+ }
303
+
304
+ /* ── Phase 4: Network configuration tests ────────────────────────── */
305
+
306
+ TEST_CASE("Wifi mac returns a valid MAC address format", "[wifi]") {
307
+ setup();
308
+
309
+ JSValue ret = eval_module(R"(
310
+ import { Wifi } from "native:wifi";
311
+ const wifi = new Wifi();
312
+ const result = wifi.mac();
313
+ globalThis.__mac = result.ok ? result.value : "";
314
+ )");
315
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
316
+
317
+ JSValue global = JS_GetGlobalObject(ctx);
318
+ JSValue macVal = JS_GetPropertyStr(ctx, global, "__mac");
319
+ const char* mac = JS_ToCString(ctx, macVal);
320
+ TEST_ASSERT_NOT_NULL_MESSAGE(mac, "mac() should return a string");
321
+ // MAC format: XX:XX:XX:XX:XX:XX (17 chars)
322
+ TEST_ASSERT_EQUAL_INT(17, strlen(mac));
323
+ // Check colons at expected positions
324
+ TEST_ASSERT_EQUAL_CHAR(':', mac[2]);
325
+ TEST_ASSERT_EQUAL_CHAR(':', mac[5]);
326
+ TEST_ASSERT_EQUAL_CHAR(':', mac[8]);
327
+ TEST_ASSERT_EQUAL_CHAR(':', mac[11]);
328
+ TEST_ASSERT_EQUAL_CHAR(':', mac[14]);
329
+ JS_FreeCString(ctx, mac);
330
+ JS_FreeValue(ctx, macVal);
331
+ JS_FreeValue(ctx, global);
332
+ teardown();
333
+ }
334
+
335
+ TEST_CASE("Wifi hostname get/set round-trips", "[wifi]") {
336
+ setup();
337
+
338
+ JSValue ret = eval_module(R"(
339
+ import { Wifi } from "native:wifi";
340
+ const wifi = new Wifi();
341
+ wifi.setHostname("test-device");
342
+ globalThis.__hostname = wifi.getHostname();
343
+ )");
344
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
345
+
346
+ JSValue global = JS_GetGlobalObject(ctx);
347
+ JSValue hnVal = JS_GetPropertyStr(ctx, global, "__hostname");
348
+ const char* hn = JS_ToCString(ctx, hnVal);
349
+ TEST_ASSERT_EQUAL_STRING("test-device", hn);
350
+ JS_FreeCString(ctx, hn);
351
+ JS_FreeValue(ctx, hnVal);
352
+ JS_FreeValue(ctx, global);
353
+ teardown();
354
+ }
355
+
356
+ TEST_CASE("Wifi getIpConfig returns object with expected keys", "[wifi]") {
357
+ setup();
358
+
359
+ JSValue ret = eval_module(R"(
360
+ import { Wifi } from "native:wifi";
361
+ const wifi = new Wifi();
362
+ const result = wifi.getIpConfig();
363
+ const cfg = result.ok ? result.value : undefined;
364
+ globalThis.__hasIp = cfg !== undefined && "ip" in cfg;
365
+ globalThis.__hasNetmask = cfg !== undefined && "netmask" in cfg;
366
+ globalThis.__hasGateway = cfg !== undefined && "gateway" in cfg;
367
+ globalThis.__hasDns = cfg !== undefined && "dns" in cfg;
368
+ )");
369
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
370
+
371
+ JSValue global = JS_GetGlobalObject(ctx);
372
+
373
+ JSValue hasIp = JS_GetPropertyStr(ctx, global, "__hasIp");
374
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, hasIp), "IpConfig should have 'ip' key");
375
+ JS_FreeValue(ctx, hasIp);
376
+
377
+ JSValue hasNetmask = JS_GetPropertyStr(ctx, global, "__hasNetmask");
378
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, hasNetmask), "IpConfig should have 'netmask' key");
379
+ JS_FreeValue(ctx, hasNetmask);
380
+
381
+ JSValue hasGateway = JS_GetPropertyStr(ctx, global, "__hasGateway");
382
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, hasGateway), "IpConfig should have 'gateway' key");
383
+ JS_FreeValue(ctx, hasGateway);
384
+
385
+ JSValue hasDns = JS_GetPropertyStr(ctx, global, "__hasDns");
386
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, hasDns), "IpConfig should have 'dns' key");
387
+ JS_FreeValue(ctx, hasDns);
388
+
389
+ JS_FreeValue(ctx, global);
390
+ teardown();
391
+ }
392
+
393
+ /* ── Phase 5: AP mode tests ──────────────────────────────────────── */
394
+
395
+ TEST_CASE("Wifi AP methods exist and return expected types", "[wifi]") {
396
+ setup();
397
+
398
+ JSValue ret = eval_module(R"(
399
+ import { Wifi } from "native:wifi";
400
+ const wifi = new Wifi();
401
+ globalThis.__apMethods = ["apStart","apStop","apIsActive","apIp","apStations"]
402
+ .every(m => typeof wifi[m] === "function");
403
+ globalThis.__apInactive = wifi.apIsActive() === false;
404
+ globalThis.__apStationsArray = Array.isArray(wifi.apStations());
405
+ )");
406
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
407
+
408
+ JSValue global = JS_GetGlobalObject(ctx);
409
+
410
+ JSValue apMethods = JS_GetPropertyStr(ctx, global, "__apMethods");
411
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, apMethods), "All AP methods should exist");
412
+ JS_FreeValue(ctx, apMethods);
413
+
414
+ JSValue apInactive = JS_GetPropertyStr(ctx, global, "__apInactive");
415
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, apInactive), "AP should be inactive initially");
416
+ JS_FreeValue(ctx, apInactive);
417
+
418
+ JSValue apStations = JS_GetPropertyStr(ctx, global, "__apStationsArray");
419
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, apStations), "apStations() should return an array");
420
+ JS_FreeValue(ctx, apStations);
421
+
422
+ JS_FreeValue(ctx, global);
423
+ teardown();
424
+ }
425
+
426
+ /* ── Phase 6: Power management & misc tests ──────────────────────── */
427
+
428
+ TEST_CASE("Wifi power save get/set round-trips", "[wifi]") {
429
+ setup();
430
+
431
+ JSValue ret = eval_module(R"(
432
+ import { Wifi } from "native:wifi";
433
+ const wifi = new Wifi();
434
+ wifi.setPowerSave("min");
435
+ globalThis.__ps = wifi.getPowerSave();
436
+ )");
437
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
438
+
439
+ JSValue global = JS_GetGlobalObject(ctx);
440
+ JSValue psVal = JS_GetPropertyStr(ctx, global, "__ps");
441
+ const char* ps = JS_ToCString(ctx, psVal);
442
+ TEST_ASSERT_EQUAL_STRING("min", ps);
443
+ JS_FreeCString(ctx, ps);
444
+ JS_FreeValue(ctx, psVal);
445
+ JS_FreeValue(ctx, global);
446
+ teardown();
447
+ }
448
+
449
+ TEST_CASE("Wifi country get/set round-trips", "[wifi]") {
450
+ setup();
451
+
452
+ JSValue ret = eval_module(R"(
453
+ import { Wifi } from "native:wifi";
454
+ const wifi = new Wifi();
455
+ wifi.setCountry("US");
456
+ globalThis.__cc = wifi.getCountry();
457
+ )");
458
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
459
+
460
+ JSValue global = JS_GetGlobalObject(ctx);
461
+ JSValue ccVal = JS_GetPropertyStr(ctx, global, "__cc");
462
+ const char* cc = JS_ToCString(ctx, ccVal);
463
+ TEST_ASSERT_EQUAL_STRING("US", cc);
464
+ JS_FreeCString(ctx, cc);
465
+ JS_FreeValue(ctx, ccVal);
466
+ JS_FreeValue(ctx, global);
467
+ teardown();
468
+ }
469
+
470
+ /* ── Scan with filter options ─────────────────────────────────────── */
471
+
472
+ TEST_CASE("Wifi scan accepts filter options", "[wifi]") {
473
+ setup();
474
+
475
+ JSValue ret = eval_module(R"(
476
+ import { Wifi } from "native:wifi";
477
+ const wifi = new Wifi();
478
+ const result = wifi.scan({channel: 1, passive: true});
479
+ globalThis.__isPromise = result.ok && result.value instanceof Promise;
480
+ )");
481
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
482
+
483
+ JSValue global = JS_GetGlobalObject(ctx);
484
+ JSValue isPromise = JS_GetPropertyStr(ctx, global, "__isPromise");
485
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, isPromise), "scan({...}) should return a Promise");
486
+ JS_FreeValue(ctx, isPromise);
487
+ JS_FreeValue(ctx, global);
488
+ teardown();
489
+ }
490
+
491
+ /* ── TX power get/set ─────────────────────────────────────────────── */
492
+
493
+ TEST_CASE("Wifi txPower get/set round-trips", "[wifi]") {
494
+ setup();
495
+
496
+ JSValue ret = eval_module(R"(
497
+ import { Wifi } from "native:wifi";
498
+ const wifi = new Wifi();
499
+ wifi.setTxPower(8);
500
+ const result = wifi.getTxPower();
501
+ globalThis.__txPower = result.ok ? result.value : 0;
502
+ )");
503
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
504
+
505
+ JSValue global = JS_GetGlobalObject(ctx);
506
+ JSValue txVal = JS_GetPropertyStr(ctx, global, "__txPower");
507
+ double txPower;
508
+ JS_ToFloat64(ctx, &txPower, txVal);
509
+ JS_FreeValue(ctx, txVal);
510
+ // ESP-IDF may clamp; just verify it's a reasonable positive number
511
+ TEST_ASSERT_TRUE_MESSAGE(txPower > 0, "TX power should be positive");
512
+ JS_FreeValue(ctx, global);
513
+ teardown();
514
+ }
515
+
516
+ /* ── RSSI threshold ───────────────────────────────────────────────── */
517
+
518
+ TEST_CASE("Wifi RSSI threshold get/set and event listener", "[wifi]") {
519
+ setup();
520
+
521
+ JSValue ret = eval_module(R"(
522
+ import { Wifi } from "native:wifi";
523
+ const wifi = new Wifi();
524
+ wifi.setRssiThreshold(-70);
525
+ globalThis.__threshold = wifi.getRssiThreshold();
526
+ globalThis.__rssiLowCount = 0;
527
+ wifi.on("rssi-low", (rssi) => {
528
+ globalThis.__rssiLowCount++;
529
+ globalThis.__lastRssi = rssi;
530
+ });
531
+ globalThis.__wifi = wifi;
532
+ )");
533
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
534
+
535
+ JSValue global = JS_GetGlobalObject(ctx);
536
+ JSValue threshVal = JS_GetPropertyStr(ctx, global, "__threshold");
537
+ int32_t threshold;
538
+ JS_ToInt32(ctx, &threshold, threshVal);
539
+ JS_FreeValue(ctx, threshVal);
540
+ TEST_ASSERT_EQUAL_INT32(-70, threshold);
541
+
542
+ // Fire a mock RSSI low event
543
+ wifi_event_bss_rssi_low_t rssi_evt = {};
544
+ rssi_evt.rssi = -75;
545
+ esp_event_post(WIFI_EVENT, WIFI_EVENT_STA_BSS_RSSI_LOW, &rssi_evt, sizeof(rssi_evt),
546
+ portMAX_DELAY);
547
+ vTaskDelay(pdMS_TO_TICKS(50));
548
+ mik__wifi_consume(ctx);
549
+ mik__execute_jobs(ctx);
550
+
551
+ JSValue countVal = JS_GetPropertyStr(ctx, global, "__rssiLowCount");
552
+ int32_t count;
553
+ JS_ToInt32(ctx, &count, countVal);
554
+ JS_FreeValue(ctx, countVal);
555
+ TEST_ASSERT_EQUAL_INT32(1, count);
556
+
557
+ JSValue rssiVal = JS_GetPropertyStr(ctx, global, "__lastRssi");
558
+ int32_t rssi;
559
+ JS_ToInt32(ctx, &rssi, rssiVal);
560
+ JS_FreeValue(ctx, rssiVal);
561
+ TEST_ASSERT_EQUAL_INT32(-75, rssi);
562
+
563
+ JS_FreeValue(ctx, global);
564
+ teardown();
565
+ }
566
+
567
+ /* ── AP deauthStation and inactiveTimeout methods exist ───────────── */
568
+
569
+ TEST_CASE("Wifi AP extra methods exist", "[wifi]") {
570
+ setup();
571
+
572
+ JSValue ret = eval_module(R"(
573
+ import { Wifi } from "native:wifi";
574
+ const wifi = new Wifi();
575
+ globalThis.__hasDeauth = typeof wifi.apDeauthStation === "function";
576
+ globalThis.__hasGetTimeout = typeof wifi.apGetInactiveTimeout === "function";
577
+ globalThis.__hasSetTimeout = typeof wifi.apSetInactiveTimeout === "function";
578
+ )");
579
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
580
+
581
+ JSValue global = JS_GetGlobalObject(ctx);
582
+
583
+ JSValue hasDeauth = JS_GetPropertyStr(ctx, global, "__hasDeauth");
584
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, hasDeauth), "apDeauthStation should be a function");
585
+ JS_FreeValue(ctx, hasDeauth);
586
+
587
+ JSValue hasGetTimeout = JS_GetPropertyStr(ctx, global, "__hasGetTimeout");
588
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, hasGetTimeout),
589
+ "apGetInactiveTimeout should be a function");
590
+ JS_FreeValue(ctx, hasGetTimeout);
591
+
592
+ JSValue hasSetTimeout = JS_GetPropertyStr(ctx, global, "__hasSetTimeout");
593
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, hasSetTimeout),
594
+ "apSetInactiveTimeout should be a function");
595
+ JS_FreeValue(ctx, hasSetTimeout);
596
+
597
+ JS_FreeValue(ctx, global);
598
+ teardown();
599
+ }
@@ -0,0 +1,3 @@
1
+ idf_component_register(SRCS "main.cpp"
2
+ PRIV_REQUIRES spi_flash mikrojs littlefs esp_driver_uart esp_driver_usb_serial_jtag
3
+ INCLUDE_DIRS "")
@@ -0,0 +1,5 @@
1
+ #include "mikrojs_esp32.h"
2
+
3
+ extern "C" void app_main(void) {
4
+ MIK_Main();
5
+ }
package/discover.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Discover ESP-IDF components and board sdkconfig defaults from
3
+ * a firmware project's dependencies.
4
+ *
5
+ * Can be used as a module (import {discover} from '@mikrojs/firmware/discover')
6
+ * or run directly via node (reads cwd).
7
+ */
8
+ import {readFileSync, existsSync} from 'node:fs'
9
+ import {createRequire} from 'node:module'
10
+ import {dirname, join, resolve} from 'node:path'
11
+
12
+ /** Find a package's directory by walking up node_modules from startDir */
13
+ function findPackageDir(name, startDir) {
14
+ let dir = startDir
15
+ while (true) {
16
+ const candidate = join(dir, 'node_modules', name)
17
+ if (existsSync(join(candidate, 'package.json'))) return candidate
18
+ const parent = dirname(dir)
19
+ if (parent === dir) return null
20
+ dir = parent
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Scan dependencies in the given project directory for ESP-IDF components
26
+ * and board sdkconfig defaults.
27
+ *
28
+ * @param {string} projectDir - Directory containing package.json
29
+ * @returns {{components: string, sdkconfigs: string}} Semicolon-separated paths
30
+ */
31
+ export function discover(projectDir) {
32
+ const require = createRequire(join(projectDir, 'package.json'))
33
+ const pkg = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8'))
34
+ const dependencies = pkg.dependencies ?? {}
35
+ const board = process.env.MIKROJS_BOARD ?? ''
36
+ const components = []
37
+ const sdkconfigs = []
38
+
39
+ for (const dep of Object.keys(dependencies)) {
40
+ if (dep === '@mikrojs/native' || dep === '@mikrojs/quickjs' || dep === '@mikrojs/firmware')
41
+ continue
42
+
43
+ const depDir = findPackageDir(dep, projectDir)
44
+ if (!depDir) continue
45
+
46
+ // Check for cmake component exports (cmake.js files use CJS)
47
+ let cmake
48
+ try {
49
+ cmake = require(dep + '/cmake')
50
+ } catch {
51
+ continue
52
+ }
53
+
54
+ if (cmake.componentPath) components.push(cmake.componentPath)
55
+ if (Array.isArray(cmake.componentPaths)) components.push(...cmake.componentPaths)
56
+
57
+ // Read board-specific sdkconfig from mikrojs.boards manifest
58
+ try {
59
+ const depPkg = JSON.parse(readFileSync(join(depDir, 'package.json'), 'utf8'))
60
+ if (depPkg.mikrojs?.boards) {
61
+ for (const [subpath, config] of Object.entries(depPkg.mikrojs.boards)) {
62
+ const boardName = subpath.startsWith('./') ? subpath.slice(2) : subpath
63
+ if ((!board || board === boardName) && config.sdkconfig) {
64
+ sdkconfigs.push(resolve(depDir, config.sdkconfig))
65
+ }
66
+ }
67
+ }
68
+ } catch {
69
+ // No board manifest
70
+ }
71
+ }
72
+
73
+ return {
74
+ components: components.join(';'),
75
+ sdkconfigs: sdkconfigs.join(';'),
76
+ }
77
+ }
package/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export const componentDir: string
2
+ export const configDir: string
3
+ export const defaultAppDir: string
4
+ export const projectCmakePath: string
5
+
6
+ export function prebuiltFirmwareDir(chip: string): string
7
+ export function hasPrebuiltFirmware(chip: string): boolean