@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,542 @@
1
+ #include <cerrno>
2
+ #include <stdio.h>
3
+ #include <string.h>
4
+ #include <strings.h>
5
+ #include <sys/time.h>
6
+ #include <unistd.h>
7
+
8
+ #include <cinttypes>
9
+
10
+ #include "esp_chip_info.h"
11
+ #include "esp_heap_caps.h"
12
+ #include "esp_littlefs.h"
13
+ #include "esp_log.h"
14
+ #include "esp_system.h"
15
+ #include "freertos/FreeRTOS.h"
16
+ #include "freertos/task.h"
17
+ #include "mikrojs.h"
18
+ #include "mikrojs/platform.h"
19
+ #include "mikrojs/private.h"
20
+ #include "nvs_flash.h"
21
+
22
+ static const char* TAG = "mikrojs";
23
+
24
+ /* Apply the MIK_LOG_LEVEL env var (if set in NVS) to our log tags.
25
+ *
26
+ * The firmware compiles in all levels up to DEBUG
27
+ * (CONFIG_LOG_MAXIMUM_LEVEL=4) but keeps the runtime default at NONE so
28
+ * the serial console stays clean in production. Setting
29
+ * `MIK_LOG_LEVEL=debug` (or info/warn/error/verbose) via `mikro env set`
30
+ * flips the listed tags to that level on the next boot.
31
+ *
32
+ * We target specific tags rather than "*" to avoid flooding the output
33
+ * with unrelated ESP-IDF subsystems (WiFi, lwIP, driver framework etc.).
34
+ * Add more tags here if new mikrojs-component modules are introduced. */
35
+ static void mik__apply_nvs_log_level(void) {
36
+ nvs_handle_t handle;
37
+ if (nvs_open("mik.env", NVS_READONLY, &handle) != ESP_OK) return;
38
+
39
+ char buf[16];
40
+ size_t buf_len = sizeof(buf);
41
+ esp_err_t err = nvs_get_str(handle, "MIK_LOG_LEVEL", buf, &buf_len);
42
+ nvs_close(handle);
43
+ if (err != ESP_OK) return;
44
+
45
+ esp_log_level_t level;
46
+ if (strcasecmp(buf, "error") == 0) {
47
+ level = ESP_LOG_ERROR;
48
+ } else if (strcasecmp(buf, "warn") == 0 || strcasecmp(buf, "warning") == 0) {
49
+ level = ESP_LOG_WARN;
50
+ } else if (strcasecmp(buf, "info") == 0) {
51
+ level = ESP_LOG_INFO;
52
+ } else if (strcasecmp(buf, "debug") == 0) {
53
+ level = ESP_LOG_DEBUG;
54
+ } else if (strcasecmp(buf, "verbose") == 0 || strcasecmp(buf, "trace") == 0) {
55
+ level = ESP_LOG_VERBOSE;
56
+ } else {
57
+ return; /* unknown value — leave defaults alone */
58
+ }
59
+
60
+ esp_log_level_set("mikrojs", level);
61
+ esp_log_level_set("mik_repl", level);
62
+ esp_log_level_set("mik_app_config", level);
63
+ }
64
+
65
+ /* File-scope scratch buffers for the supervisor loop.
66
+ *
67
+ * Kept off the stack because the main task stack is ~24 KB and cannot
68
+ * accommodate 1-2 KB of local buffers on top of the nested eval/module
69
+ * normalizer call chain (observed as a stack-protection fault in
70
+ * mik_module_normalizer on the first test file). Fixed sizes instead of
71
+ * PATH_MAX scaling — on newlib PATH_MAX can be 4096, which is absurd for
72
+ * our purposes and would still blow the BSS budget. These sizes fit any
73
+ * realistic on-device test path. Only one test manifest runs per boot,
74
+ * so single-ownership is safe. */
75
+ #define MIK_SUP_PATH_MAX 384
76
+ static char s_sup_dbg[MIK_SUP_PATH_MAX + 64];
77
+ static char s_sup_esc[MIK_SUP_PATH_MAX * 2 + 8];
78
+ static char s_sup_buf[MIK_SUP_PATH_MAX * 2 + 128];
79
+
80
+ /* Minimal JSON string-escape into a bounded buffer. Handles `"`, `\`, and
81
+ * control characters; everything else copies verbatim. Returns bytes
82
+ * written (excluding NUL) on success, or -1 on overflow. Used to synthesize
83
+ * test-event JSON frames from raw C strings where the path may contain
84
+ * characters that would otherwise corrupt the frame. */
85
+ static int mik__json_escape(char* dst, size_t dst_size, const char* src) {
86
+ if (dst_size == 0) return -1;
87
+ size_t w = 0;
88
+ for (const unsigned char* p = (const unsigned char*)src; *p; p++) {
89
+ unsigned char c = *p;
90
+ if (c == '"' || c == '\\') {
91
+ if (w + 2 >= dst_size) return -1;
92
+ dst[w++] = '\\';
93
+ dst[w++] = (char)c;
94
+ } else if (c < 0x20) {
95
+ if (w + 7 >= dst_size) return -1;
96
+ int n = snprintf(dst + w, dst_size - w, "\\u%04x", c);
97
+ if (n < 0 || (size_t)n >= dst_size - w) return -1;
98
+ w += (size_t)n;
99
+ } else {
100
+ if (w + 1 >= dst_size) return -1;
101
+ dst[w++] = (char)c;
102
+ }
103
+ }
104
+ if (w >= dst_size) return -1;
105
+ dst[w] = '\0';
106
+ return (int)w;
107
+ }
108
+
109
+ /* ── Serial transport ───────────────────────────────────────────── */
110
+
111
+ static int serial_transport_read(uint8_t* buf, size_t size, void* ctx) {
112
+ (void)ctx;
113
+ return mik__console_read(buf, size);
114
+ }
115
+
116
+ static void serial_transport_write(const void* buf, size_t len, void* ctx) {
117
+ (void)ctx;
118
+ mik__console_write(buf, len);
119
+ }
120
+
121
+ /* ── Platform command handler (deploy, config, restart) ─────────── */
122
+
123
+ /* Forward declarations for deploy and config handlers.
124
+ * These receive the TLV header info but must read the payload bytes
125
+ * themselves from the transport via mik__proto_read_exact(). */
126
+ bool mik__handle_deploy_command(MIKReplTransport* transport, uint8_t cmd_type,
127
+ uint32_t payload_len);
128
+ bool mik__handle_config_command(MIKReplTransport* transport, uint8_t cmd_type,
129
+ uint32_t payload_len);
130
+ void mik__deploy_session_reset(void);
131
+
132
+ static void platform_session_end(void* ctx) {
133
+ (void)ctx;
134
+ mik__deploy_session_reset();
135
+ /* Safety net: if the CLI paused the runtime via CMD_RUNTIME_PAUSE and
136
+ * disconnected before sending RESUME or RESTART, the app would stay
137
+ * frozen until the next reboot. Always resume on session end. */
138
+ mik__repl_set_paused(false);
139
+ }
140
+
141
+ static bool platform_command_handler(MIKReplTransport* transport, uint8_t cmd_type,
142
+ uint32_t payload_len, void* ctx) {
143
+ (void)ctx;
144
+
145
+ /* Restart: drain payload (should be 0), then restart */
146
+ if (cmd_type == MIK_CMD_RESTART) {
147
+ mik__proto_drain(transport, payload_len);
148
+ esp_restart();
149
+ return true; /* unreachable */
150
+ }
151
+
152
+ /* Runtime pause/resume: freeze the JS event loop so user code can't
153
+ * interleave log output with TLV frames during a deploy. */
154
+ if (cmd_type == MIK_CMD_RUNTIME_PAUSE || cmd_type == MIK_CMD_RUNTIME_RESUME) {
155
+ mik__proto_drain(transport, payload_len);
156
+ mik__repl_set_paused(cmd_type == MIK_CMD_RUNTIME_PAUSE);
157
+ mik__proto_send_ok(transport);
158
+ return true;
159
+ }
160
+
161
+ /* Deploy commands (0x20-0x27) */
162
+ if (cmd_type >= MIK_CMD_DEPLOY_PUT && cmd_type <= MIK_CMD_DEPLOY_CHECKSUM) {
163
+ return mik__handle_deploy_command(transport, cmd_type, payload_len);
164
+ }
165
+
166
+ /* Config commands (0x40-0x42) */
167
+ if (cmd_type >= MIK_CMD_CONFIG_LIST && cmd_type <= MIK_CMD_CONFIG_DELETE) {
168
+ return mik__handle_config_command(transport, cmd_type, payload_len);
169
+ }
170
+
171
+ /* Unknown command: drain payload to stay in sync */
172
+ mik__proto_drain(transport, payload_len);
173
+ return false;
174
+ }
175
+
176
+ /* ── LittleFS mount ─────────────────────────────────────────────── */
177
+
178
+ static esp_err_t mount_littlefs(const char* base_path, const char* partition_label) {
179
+ esp_vfs_littlefs_conf_t conf = {};
180
+ conf.base_path = base_path;
181
+ conf.partition_label = partition_label;
182
+ conf.format_if_mount_failed = true;
183
+
184
+ esp_err_t ret = esp_vfs_littlefs_register(&conf);
185
+ if (ret != ESP_OK) {
186
+ ESP_LOGE(TAG, "Failed to mount LittleFS partition '%s' at '%s': %s", partition_label,
187
+ base_path, esp_err_to_name(ret));
188
+ } else {
189
+ ESP_LOGI(TAG, "Mounted LittleFS partition '%s' at '%s'", partition_label, base_path);
190
+ }
191
+ return ret;
192
+ }
193
+
194
+ /* ── Main entry point ───────────────────────────────────────────── */
195
+
196
+ void MIK_Main(void) {
197
+ /* Detect whether USB Serial/JTAG or UART is connected and set up
198
+ * the active console. On chips with USB Serial/JTAG this checks
199
+ * for USB SOF packets; if absent it falls back to UART. */
200
+ mik__console_init();
201
+
202
+ /* Gather chip/feature info early — cheap, no side effects, used to
203
+ * print the banner AFTER runtime init succeeds. We deliberately defer
204
+ * the banner until everything is up so its mere presence signals
205
+ * "mikrojs is alive and the JS runtime is ready." If anything between
206
+ * here and the printf below crashes or aborts, the banner doesn't
207
+ * print — the absence IS the diagnostic.
208
+ *
209
+ * Feature advertising: we only include features for which mikrojs has
210
+ * an actual JS API. ESP-IDF Kconfig flags like CONFIG_IEEE802154_ENABLED
211
+ * get set by default on capable silicon (e.g. ESP32-C6) just because
212
+ * the low-level radio driver is linked, even when no higher-level
213
+ * stack is compiled in. WiFi and BLE each require both silicon support
214
+ * and a corresponding `mikrojs/wifi`/`mikrojs/ble` JS module — we gate
215
+ * on the CONFIG_* flags as a proxy. 802.15.4 is deliberately omitted
216
+ * until a `mikrojs/zigbee` or `mikrojs/thread` module ships. */
217
+ esp_chip_info_t chip_info;
218
+ esp_chip_info(&chip_info);
219
+
220
+ #if CONFIG_ESP_WIFI_ENABLED
221
+ bool has_wifi = (chip_info.features & CHIP_FEATURE_WIFI_BGN) != 0;
222
+ #else
223
+ bool has_wifi = false;
224
+ #endif
225
+ #if CONFIG_BT_ENABLED
226
+ bool has_bt = (chip_info.features & CHIP_FEATURE_BT) != 0;
227
+ bool has_ble = (chip_info.features & CHIP_FEATURE_BLE) != 0;
228
+ #else
229
+ bool has_bt = false;
230
+ bool has_ble = false;
231
+ #endif
232
+
233
+ /* Capture starting heap for the later runtime-cost INFO log. */
234
+ size_t heap_free_at_boot = esp_get_free_heap_size();
235
+ size_t heap_total_at_boot = heap_free_at_boot;
236
+ {
237
+ multi_heap_info_t info;
238
+ heap_caps_get_info(&info, MALLOC_CAP_8BIT);
239
+ heap_total_at_boot = info.total_free_bytes + info.total_allocated_bytes;
240
+ }
241
+
242
+ /* Recovery window: gives the host (or a double-reset) a chance to
243
+ * skip the autorun if the deployed app is crash-looping. Costs ~500ms
244
+ * on every cold boot but is the only window we have before user code
245
+ * runs and may panic. */
246
+ bool safe_mode = mik__check_recovery(500);
247
+ if (safe_mode) {
248
+ printf("\n*** SAFE MODE: autorun skipped, dropping to REPL ***\n\n");
249
+ }
250
+
251
+ /* Initialize NVS (needed for env vars, secrets, and WiFi) */
252
+ {
253
+ esp_err_t err = nvs_flash_init();
254
+ if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
255
+ nvs_flash_erase();
256
+ err = nvs_flash_init();
257
+ }
258
+ if (err != ESP_OK) {
259
+ ESP_LOGE(TAG, "NVS init failed: %s", esp_err_to_name(err));
260
+ }
261
+ }
262
+
263
+ /* Apply MIK_LOG_LEVEL from NVS as early as possible so subsequent
264
+ * initialization logs respect the configured level. */
265
+ mik__apply_nvs_log_level();
266
+
267
+ /* Mount filesystem */
268
+ if (mount_littlefs("/appfs", "user") != ESP_OK) {
269
+ ESP_LOGE(TAG, "Cannot mount /appfs partition, aborting");
270
+ return;
271
+ }
272
+
273
+ /* Recover from incomplete deploys */
274
+ MIK_DeployRecover();
275
+
276
+ /* Load app config (mikro.config.json) if present */
277
+ MIKConfig app_config;
278
+ MIK_LoadConfig("/appfs", &app_config);
279
+
280
+ /* Create JS runtime — reserve heap for WiFi, TLS, HTTP, LittleFS, and other
281
+ * ESP-IDF subsystems that allocate after the runtime is created.
282
+ * WiFi + lwIP ≈ 50-65 KB, TLS ≈ 40 KB, HTTP task ≈ 8 KB, misc ≈ 15 KB. */
283
+ MIKRunOptions options;
284
+ MIK_DefaultOptions(&options);
285
+ uint32_t free_heap = esp_get_free_heap_size();
286
+ uint32_t reserved = app_config.mem_reserved;
287
+ if (free_heap < reserved) {
288
+ ESP_LOGE(TAG, "Not enough memory to start JS runtime (free: %" PRIu32
289
+ ", need: %" PRIu32 ")",
290
+ free_heap, reserved);
291
+ return;
292
+ }
293
+ options.mem_limit = (int)(free_heap - reserved);
294
+ /* QuickJS's stack overflow check compares the current SP against
295
+ * (stack_top − stack_size). The library default (1 MB) assumes a desktop
296
+ * stack and leaves the limit far below the real task stack bottom, so the
297
+ * hardware faults before QuickJS can throw. Cap at ~2/3 of the main task
298
+ * stack to leave headroom for the throw path itself. */
299
+ options.stack_size = (CONFIG_ESP_MAIN_TASK_STACK_SIZE * 2) / 3;
300
+ if (app_config.stack_size > 0) {
301
+ options.stack_size = app_config.stack_size;
302
+ }
303
+
304
+ auto create_runtime = [&]() -> MIKRuntime* {
305
+ MIKRuntime* rt = MIK_NewRuntimeOptions(&options);
306
+ MIK_LoadEnvFromNVS(rt);
307
+ MIK_RebuildEnv(rt);
308
+ MIK_SetFSBasePath(rt, "/appfs");
309
+ MIK_SetConfig(rt, &app_config);
310
+ return rt;
311
+ };
312
+
313
+ MIKRuntime* mik_rt = create_runtime();
314
+
315
+ /* ── Boot banner ──
316
+ * Printed here, AFTER the runtime is fully initialized and the app
317
+ * config is applied, because its mere presence signals "everything
318
+ * came up successfully." If a step earlier in boot panicked or
319
+ * aborted, the banner doesn't print — the absence is the diagnostic.
320
+ *
321
+ * Two lines: first identifies the device (version, chip, cores,
322
+ * radios), second reports free app heap.
323
+ *
324
+ * The "free app heap" number is QuickJS's `malloc_limit − malloc_size`
325
+ * immediately after runtime init — i.e. how much the user's JS code
326
+ * can still allocate before hitting the configured soft cap. This is
327
+ * the number users actually care about when they see "out of memory."
328
+ * It's distinct from raw system heap, which includes memory reserved
329
+ * for native subsystems (WiFi/lwIP/TLS) and already counts the
330
+ * ~78 KB cold-start floor (QuickJS atoms + class tables + builtin
331
+ * bytecode) that's unavailable for user allocations. */
332
+ {
333
+ /* Build the banner into a local buffer and push it through
334
+ * mik__console_write so it lands on the user-visible console
335
+ * (USB-Serial/JTAG). `printf` goes to newlib stdio, which
336
+ * ESP-IDF routes to UART0 and is invisible over USB. */
337
+ const char* core_word = chip_info.cores == 1 ? "core" : "cores";
338
+ char banner[160];
339
+ int off = 0;
340
+ #ifdef MIK_FW_VERSION
341
+ off += snprintf(banner + off, sizeof(banner) - off, "Mikro.js v%s on %s, %d %s",
342
+ MIK_FW_VERSION, CONFIG_IDF_TARGET, chip_info.cores, core_word);
343
+ #else
344
+ off += snprintf(banner + off, sizeof(banner) - off, "Mikro.js on %s, %d %s",
345
+ CONFIG_IDF_TARGET, chip_info.cores, core_word);
346
+ #endif
347
+ if (has_wifi || has_bt || has_ble) {
348
+ off += snprintf(banner + off, sizeof(banner) - off, ", ");
349
+ bool first = true;
350
+ if (has_wifi) {
351
+ off += snprintf(banner + off, sizeof(banner) - off, "WiFi");
352
+ first = false;
353
+ }
354
+ if (has_bt) {
355
+ off += snprintf(banner + off, sizeof(banner) - off, "%sBT", first ? "" : "/");
356
+ first = false;
357
+ }
358
+ if (has_ble) {
359
+ off += snprintf(banner + off, sizeof(banner) - off, "%sBLE", first ? "" : "/");
360
+ }
361
+ }
362
+ off += snprintf(banner + off, sizeof(banner) - off, "\n");
363
+ if (off > 0 && off < (int)sizeof(banner)) {
364
+ mik__console_write(banner, (size_t)off);
365
+ }
366
+
367
+ JSMemoryUsage mem;
368
+ JS_ComputeMemoryUsage(JS_GetRuntime(MIK_GetJSContext(mik_rt)), &mem);
369
+ long heap_available =
370
+ mem.malloc_limit > 0 ? (long)mem.malloc_limit - (long)mem.malloc_size : 0;
371
+ char heap_line[48];
372
+ int hl = snprintf(heap_line, sizeof(heap_line), "%.0f KB free heap\n",
373
+ heap_available / 1024.0);
374
+ if (hl > 0 && hl < (int)sizeof(heap_line)) {
375
+ mik__console_write(heap_line, (size_t)hl);
376
+ }
377
+ }
378
+
379
+ /* Detailed memory breakdown, gated behind INFO logging. The banner
380
+ * already shows the headline "free app heap" number; these logs add
381
+ * the detail you want when diagnosing OOM or tuning memReserved, but
382
+ * would be noise on every normal boot. Set `mikro env set
383
+ * MIK_LOG_LEVEL info` to see them. */
384
+ const MIKPlatform* platform = MIK_GetPlatform();
385
+ {
386
+ size_t heap_free_now = esp_get_free_heap_size();
387
+ size_t runtime_cost =
388
+ heap_free_at_boot > heap_free_now ? (heap_free_at_boot - heap_free_now) : 0;
389
+ platform->log(MIK_LOG_INFO, "mikrojs",
390
+ "System heap: %.1f / %.1f KB free (runtime init used %.1f KB)",
391
+ heap_free_now / 1024.0, (double)heap_total_at_boot / 1024.0,
392
+ runtime_cost / 1024.0);
393
+
394
+ JSMemoryUsage mem;
395
+ JS_ComputeMemoryUsage(JS_GetRuntime(MIK_GetJSContext(mik_rt)), &mem);
396
+ if (mem.malloc_limit > 0) {
397
+ double used_pct = (double)mem.malloc_size / (double)mem.malloc_limit * 100.0;
398
+ long available = (long)mem.malloc_limit - (long)mem.malloc_size;
399
+ platform->log(MIK_LOG_INFO, "mikrojs",
400
+ "App heap: %.1f KB available (%.1f / %.1f KB used, %.0f%%)",
401
+ available / 1024.0, mem.malloc_size / 1024.0,
402
+ mem.malloc_limit / 1024.0, used_pct);
403
+ }
404
+ }
405
+
406
+ /* Enter binary serial mode (suppresses ESP logging, disables line-ending
407
+ * translation) and switch to unified protocol mode. From here on,
408
+ * everything goes through TLV framing — including app console output. */
409
+ mik__serial_binary_begin_no_echo();
410
+
411
+ MIKReplTransport transport = {};
412
+ transport.read = serial_transport_read;
413
+ transport.write = serial_transport_write;
414
+ transport.ctx = nullptr;
415
+ transport.chip_name = CONFIG_IDF_TARGET;
416
+ transport.command_handler = platform_command_handler;
417
+ transport.command_handler_ctx = nullptr;
418
+ transport.session_end = platform_session_end;
419
+ transport.session_end_ctx = nullptr;
420
+
421
+ /* Test-harness mode: if package.json carries a non-empty "tests" array,
422
+ * run each listed file in its own fresh runtime through one transport
423
+ * session. The test runtime calls __testFileDone after emitting its
424
+ * final run_done event, which signals MIK_ProtocolExit so the serve
425
+ * loop returns and the supervisor can swap in the next runtime.
426
+ * Skipped in safe mode. */
427
+ char** test_paths = nullptr;
428
+ size_t test_count = 0;
429
+ if (!safe_mode) {
430
+ MIK_LoadTests("/appfs", &test_paths, &test_count);
431
+ }
432
+ bool test_mode = test_count > 0;
433
+
434
+ /* Open the protocol session. In test mode we'll attach/detach per
435
+ * test runtime; otherwise we attach the single primary runtime. */
436
+ MIK_ProtocolOpen(&transport);
437
+
438
+ if (test_mode) {
439
+ /* Discard the primary runtime — each test gets a fresh one. */
440
+ MIK_FreeRuntime(mik_rt);
441
+ mik_rt = nullptr;
442
+
443
+ for (size_t i = 0; i < test_count; i++) {
444
+ /* Diagnostic: announce the file about to run so the CLI can
445
+ * confirm the supervisor's iteration matches its own testFiles
446
+ * order. The MSG_DEBUG frame is rendered as a dim log line.
447
+ * Uses file-scope s_sup_dbg to avoid bloating the main task
448
+ * stack — the normalizer call chain during module resolution
449
+ * already sits a few KB deep. */
450
+ {
451
+ int n = snprintf(s_sup_dbg, sizeof(s_sup_dbg),
452
+ "[supervisor] running %zu/%zu: %s", i + 1, test_count,
453
+ test_paths[i]);
454
+ if (n > 0 && n < (int)sizeof(s_sup_dbg)) {
455
+ mik__proto_send(&transport, MIK_MSG_DEBUG, s_sup_dbg, n);
456
+ }
457
+ }
458
+ MIKRuntime* rt = create_runtime();
459
+ MIK_EnableTestHelpers(rt);
460
+ MIK_ProtocolAttach(rt);
461
+ int rc = MIK_RunEntry(rt, test_paths[i]);
462
+ const char* fail_reason = nullptr;
463
+ if (rc == -ENOENT) {
464
+ fail_reason = "Test file not found";
465
+ } else if (rc == -EFAULT) {
466
+ fail_reason = "Evaluation threw";
467
+ } else {
468
+ /* Entry eval returned successfully (rc == 0). The test module
469
+ * may still asynchronously reject (e.g. top-level throw in a
470
+ * module eval'd as a Promise) — that's caught after ServeLoop
471
+ * by inspecting MIK_IsStopRequested below. */
472
+ MIK_ProtocolServeLoop();
473
+ if (MIK_IsStopRequested(rt)) {
474
+ fail_reason = "Unhandled rejection";
475
+ }
476
+ }
477
+ if (fail_reason) {
478
+ /* Synthesize a failing test + run_done so the CLI accounts
479
+ * for this file instead of stalling waiting for a run_done
480
+ * the runtime will never emit. Escape the path so any `"`
481
+ * or `\` in it doesn't corrupt the JSON frame. */
482
+ ESP_LOGE(TAG, "%s: %s", fail_reason, test_paths[i]);
483
+ if (mik__json_escape(s_sup_esc, sizeof(s_sup_esc), test_paths[i]) < 0) {
484
+ /* Path too long to fit even escaped — fall back to
485
+ * basename so the frame at least identifies something. */
486
+ const char* base = strrchr(test_paths[i], '/');
487
+ if (!base ||
488
+ mik__json_escape(s_sup_esc, sizeof(s_sup_esc), base + 1) < 0) {
489
+ s_sup_esc[0] = '?';
490
+ s_sup_esc[1] = '\0';
491
+ }
492
+ }
493
+ int n = snprintf(s_sup_buf, sizeof(s_sup_buf),
494
+ "{\"e\":3,\"s\":\"<load>\",\"t\":\"%s\",\"d\":0,"
495
+ "\"m\":\"%s\"}",
496
+ s_sup_esc, fail_reason);
497
+ if (n > 0 && n < (int)sizeof(s_sup_buf)) {
498
+ mik__proto_send(&transport, MIK_MSG_TEST, s_sup_buf, n);
499
+ }
500
+ static const char kRunDone[] =
501
+ "{\"e\":6,\"p\":0,\"f\":1,\"k\":0,\"o\":0,\"d\":0}";
502
+ mik__proto_send(&transport, MIK_MSG_TEST, kRunDone, sizeof(kRunDone) - 1);
503
+ }
504
+ MIK_ProtocolDetach();
505
+ MIK_FreeRuntime(rt);
506
+ }
507
+
508
+ /* Signal end-of-manifest so the CLI can finalize its report
509
+ * without waiting on a silent stream. */
510
+ mik__proto_send(&transport, MIK_MSG_MANIFEST_DONE, nullptr, 0);
511
+
512
+ /* Keep the session alive for REPL / deploy commands. */
513
+ mik_rt = create_runtime();
514
+ MIK_ProtocolAttach(mik_rt);
515
+ } else {
516
+ /* Normal mode: attach primary runtime and evaluate the main entry. */
517
+ MIK_ProtocolAttach(mik_rt);
518
+
519
+ if (safe_mode) {
520
+ ESP_LOGW(TAG, "Safe mode: skipping entry point %s", app_config.entry_point);
521
+ } else {
522
+ int rc = MIK_RunEntry(mik_rt, app_config.entry_point);
523
+ if (rc == -EINVAL) {
524
+ ESP_LOGW(TAG, "No entry point configured (no \"main\" field in package.json)");
525
+ } else if (rc == -ENOENT) {
526
+ ESP_LOGW(TAG, "Entry point not found: %s", app_config.entry_point);
527
+ } else if (rc == -EFAULT) {
528
+ ESP_LOGE(TAG, "Failed to evaluate %s", app_config.entry_point);
529
+ }
530
+ }
531
+ }
532
+
533
+ /* Serve until transport error or CMD_EXIT. */
534
+ MIK_ProtocolServeLoop();
535
+ MIK_ProtocolDetach();
536
+ MIK_FreeRuntime(mik_rt);
537
+ MIK_ProtocolClose();
538
+ MIK_FreeTests(test_paths, test_count);
539
+
540
+ /* If we get here, the transport died. Restart. */
541
+ esp_restart();
542
+ }