@mikrojs/firmware 0.12.0 → 0.14.0-pr-229.g0d8db1b

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 CHANGED
@@ -6,20 +6,19 @@ ESP32 firmware package for Mikro.js. Provides the ESP-IDF integration, build sys
6
6
 
7
7
  - **`project.cmake`** - CMake module that integrates with ESP-IDF. Handles component discovery, SDK config merging, and partition table defaults.
8
8
  - **`components/mikrojs/`** - ESP-IDF component that compiles the Mikro.js runtime, QuickJS engine, and platform-specific C modules.
9
- - **`idf.py` wrapper** - Runs `idf.py` through `eim run`, so you don't need to manually activate ESP-IDF.
10
9
  - **Default configs** - `sdkconfig.defaults` and `partitions.csv` for common setups.
11
10
 
12
11
  ## Usage
13
12
 
14
- A custom firmware project depends on this package and includes `project.cmake`:
13
+ A custom firmware project depends on this package and includes `project.cmake` (resolved via `resolve.js`, see the docs below for the CMakeLists boilerplate):
15
14
 
16
15
  ```
17
16
  my-firmware/
18
17
  ├── package.json # depends on @mikrojs/firmware
19
- ├── CMakeLists.txt # include($ENV{MIKROJS_PROJECT_CMAKE})
20
- └── main/
21
- ├── CMakeLists.txt
22
- └── main.cpp # calls MIK_Main()
18
+ ├── CMakeLists.txt # resolves and includes project.cmake via resolve.js
19
+ └── main/ # optional: omit it and the package's default main
20
+ ├── CMakeLists.txt # (which calls MIK_Main()) is used automatically
21
+ └── main.cpp
23
22
  ```
24
23
 
25
24
  See the [Custom Firmware](https://mikrojs.dev/develop/custom-firmware) docs for details.
@@ -27,4 +26,6 @@ See the [Custom Firmware](https://mikrojs.dev/develop/custom-firmware) docs for
27
26
  ## Requirements
28
27
 
29
28
  - Node.js >= 24
30
- - ESP-IDF >= 6.0.1 (installed via [EIM](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html))
29
+ - ESP-IDF >= 6.0.1 (installed via [EIM](https://docs.espressif.com/projects/idf-im-ui/en/latest/))
30
+
31
+ To build, activate ESP-IDF in your shell first: run `eim select` and source the activation script it prints, then use `idf.py` as usual.
@@ -134,7 +134,7 @@ include("${MIK_BYTECODE_CMAKE}")
134
134
 
135
135
  # Force linker to include self-registering native modules
136
136
  set(_MIK_MODULES cbor pin i2c spi http http_server wifi rtc nvs_kv sntp sleep neopixel pwm uart)
137
- set(_MIK_BYTECODE_MODULES cbor env result schema fs http/helpers http/request http/server i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi)
137
+ set(_MIK_BYTECODE_MODULES abort cbor env result schema fs http/helpers http/request http/server i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi)
138
138
  if(CONFIG_BT_ENABLED)
139
139
  list(APPEND _MIK_MODULES ble)
140
140
  list(APPEND _MIK_BYTECODE_MODULES ble)
@@ -60,6 +60,10 @@ void mik_logfile_flush(void);
60
60
  * when file logging is disabled. */
61
61
  void mik_logfile_suspend(void);
62
62
  void mik_logfile_resume(void);
63
+ /* Clear the log files: close + unlink log.txt and log.txt.1, then reopen
64
+ * a fresh empty log.txt. Logging continues uninterrupted (no restart).
65
+ * No-op when file logging is disabled. */
66
+ void mik_logfile_reset(void);
63
67
 
64
68
  /* Serial binary I/O (mik_serial_io.cpp) */
65
69
  void mik__serial_binary_begin_no_echo(void);
@@ -218,9 +218,17 @@ static void mik__http_task(void* arg) {
218
218
  int tls_flags = 0;
219
219
  esp_err_t last_tls_err =
220
220
  esp_http_client_get_and_clear_last_tls_error(client, &tls_code, &tls_flags);
221
+ /* Alloc-class failures surface as opaque transport codes: 0x008d
222
+ * is PSA's INSUFFICIENT_MEMORY (-141) leaking through the mbedtls
223
+ * slot untranslated, 0x7f00 is MBEDTLS_ERR_SSL_ALLOC_FAILED. Name
224
+ * the condition when a trustworthy code is present; otherwise
225
+ * keep the plain connect error (no heap-level guessing). */
226
+ bool out_of_memory = err == ESP_ERR_NO_MEM || last_tls_err == ESP_ERR_NO_MEM ||
227
+ tls_code == 0x008d || tls_code == 0x7f00;
221
228
  snprintf(error_buf, sizeof(error_buf),
222
- "fetch failed: could not connect to %s (%s, errno=%d, "
229
+ "fetch failed: %s %s (%s, errno=%d, "
223
230
  "esp_tls=%s, mbedtls=-0x%04x, flags=0x%x)",
231
+ out_of_memory ? "out of memory connecting to" : "could not connect to",
224
232
  args->req.url, esp_err_to_name(err), sock_errno,
225
233
  esp_err_to_name(last_tls_err), tls_code, tls_flags);
226
234
  have_error = true;
@@ -573,6 +581,14 @@ static JSValue mik__http_request(JSContext* ctx, JSValue this_val, int argc, JSV
573
581
  MIKHttpPending pending = {};
574
582
  pending.id = mik__http_st(mik_rt)->next_id++;
575
583
  JSValue headers_promise = MIK_InitPromise(ctx, &pending.headers_promise);
584
+ if (JS_IsException(headers_promise)) {
585
+ /* OOM building the promise. Nothing is registered yet, so just free the
586
+ * request and propagate. Do NOT register this pending entry: its
587
+ * headers_promise.rfuncs are unwritten on failure, and a later
588
+ * consume/destroy would free dangling values (gc_decref underflow). */
589
+ mik__http_free_request(&req);
590
+ return JS_EXCEPTION;
591
+ }
576
592
 
577
593
  auto* cancelled = new std::atomic<bool>(false);
578
594
  pending.cancelled = cancelled;
@@ -664,6 +680,12 @@ static JSValue mik__http_next_message(JSContext* ctx, JSValue this_val, int argc
664
680
 
665
681
  /* Slow path: wait. Create a new promise stored on the pending entry. */
666
682
  JSValue promise = MIK_InitPromise(ctx, &p->next_promise);
683
+ if (JS_IsException(promise)) {
684
+ /* OOM building the promise. Do NOT mark next_promise_active: rfuncs are
685
+ * unwritten on failure, so a later resolve/destroy would free dangling
686
+ * values (gc_decref underflow). */
687
+ return JS_EXCEPTION;
688
+ }
667
689
  p->next_promise_active = true;
668
690
  return promise;
669
691
  }
@@ -232,6 +232,28 @@ void mik_logfile_resume(void) {
232
232
  xSemaphoreGive(s_mtx);
233
233
  }
234
234
 
235
+ void mik_logfile_reset(void) {
236
+ if (!s_mtx) return;
237
+ if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(100)) != pdTRUE) return;
238
+ if (s_file) {
239
+ fclose(s_file);
240
+ s_file = nullptr;
241
+ }
242
+ unlink(s_path_main);
243
+ unlink(s_path_rot);
244
+ /* Reopen the same path: unlinked, so this starts a fresh empty file. */
245
+ s_file = fopen(s_path_main, "a");
246
+ if (s_file) {
247
+ setvbuf(s_file, s_stdio_buf, _IOFBF, sizeof(s_stdio_buf));
248
+ s_file_size = 0;
249
+ }
250
+ /* Drop any half-assembled line so a stale prefix doesn't bleed into
251
+ * the fresh file. */
252
+ s_line_pos = 0;
253
+ s_line_has_ts = false;
254
+ xSemaphoreGive(s_mtx);
255
+ }
256
+
235
257
  void mik_logfile_flush(void) {
236
258
  if (!s_mtx || !s_file) return;
237
259
  if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(50)) != pdTRUE) return;
@@ -75,7 +75,11 @@ static void mik__apply_nvs_log_level(void) {
75
75
  #define MIK_SUP_PATH_MAX 384
76
76
  static char s_sup_dbg[MIK_SUP_PATH_MAX + 64];
77
77
  static char s_sup_esc[MIK_SUP_PATH_MAX * 2 + 8];
78
- static char s_sup_buf[MIK_SUP_PATH_MAX * 2 + 128];
78
+ static char s_sup_buf[MIK_SUP_PATH_MAX * 2 + 512];
79
+ /* Exception text captured by MIK_RunEntryErr when a test file fails to
80
+ * evaluate, and its JSON-escaped form for the synthesized test event. */
81
+ static char s_sup_err[192];
82
+ static char s_sup_err_esc[sizeof(s_sup_err) * 2 + 8];
79
83
 
80
84
  /* Minimal JSON string-escape into a bounded buffer. Handles `"`, `\`, and
81
85
  * control characters; everything else copies verbatim. Returns bytes
@@ -169,6 +173,14 @@ static bool platform_command_handler(MIKReplTransport* transport, uint8_t cmd_ty
169
173
  return mik__handle_fs_get(transport, payload_len);
170
174
  }
171
175
 
176
+ /* Log reset (0x2C): clear the on-device log files */
177
+ if (cmd_type == MIK_CMD_LOG_RESET) {
178
+ mik__proto_drain(transport, payload_len);
179
+ mik_logfile_reset();
180
+ mik__proto_send_ok(transport);
181
+ return true;
182
+ }
183
+
172
184
  /* Config commands (0x40-0x42) */
173
185
  if (cmd_type >= MIK_CMD_CONFIG_LIST && cmd_type <= MIK_CMD_CONFIG_DELETE) {
174
186
  return mik__handle_config_command(transport, cmd_type, payload_len);
@@ -476,7 +488,7 @@ void MIK_Main(void) {
476
488
  MIKRuntime* rt = create_runtime();
477
489
  MIK_EnableTestHelpers(rt);
478
490
  MIK_ProtocolAttach(rt);
479
- int rc = MIK_RunEntry(rt, test_paths[i]);
491
+ int rc = MIK_RunEntryErr(rt, test_paths[i], s_sup_err, sizeof(s_sup_err));
480
492
  const char* fail_reason = nullptr;
481
493
  if (rc == -ENOENT) {
482
494
  fail_reason = "Test file not found";
@@ -508,10 +520,26 @@ void MIK_Main(void) {
508
520
  s_sup_esc[1] = '\0';
509
521
  }
510
522
  }
511
- int n = snprintf(s_sup_buf, sizeof(s_sup_buf),
523
+ /* Append the captured exception text (escaped) so the CLI
524
+ * shows the actual error, not just "Evaluation threw". */
525
+ s_sup_err_esc[0] = '\0';
526
+ if (rc == -EFAULT && s_sup_err[0] != '\0') {
527
+ if (mik__json_escape(s_sup_err_esc, sizeof(s_sup_err_esc), s_sup_err) < 0) {
528
+ s_sup_err_esc[0] = '\0';
529
+ }
530
+ }
531
+ int n;
532
+ if (s_sup_err_esc[0] != '\0') {
533
+ n = snprintf(s_sup_buf, sizeof(s_sup_buf),
534
+ "{\"e\":3,\"s\":\"<load>\",\"t\":\"%s\",\"d\":0,"
535
+ "\"m\":\"%s: %s\"}",
536
+ s_sup_esc, fail_reason, s_sup_err_esc);
537
+ } else {
538
+ n = snprintf(s_sup_buf, sizeof(s_sup_buf),
512
539
  "{\"e\":3,\"s\":\"<load>\",\"t\":\"%s\",\"d\":0,"
513
540
  "\"m\":\"%s\"}",
514
541
  s_sup_esc, fail_reason);
542
+ }
515
543
  if (n > 0 && n < (int)sizeof(s_sup_buf)) {
516
544
  mik__proto_send(&transport, MIK_MSG_TEST, s_sup_buf, n);
517
545
  }
@@ -144,6 +144,15 @@ static JSValue mik__sntp_sync(JSContext* ctx, JSValue this_val, int argc, JSValu
144
144
 
145
145
  /* Create and return promise wrapped in result */
146
146
  JSValue promise = MIK_InitPromise(ctx, &state->sync_promise);
147
+ if (JS_IsException(promise)) {
148
+ /* OOM building the promise. Tear down so we don't leave a half-started
149
+ * sync, and crucially do NOT set sync_pending: MIK_InitPromise leaves
150
+ * sync_promise.rfuncs unwritten on failure, so a later consume/destroy
151
+ * would free dangling values (gc_decref refcount underflow). */
152
+ esp_netif_sntp_deinit();
153
+ state->running = false;
154
+ return JS_EXCEPTION;
155
+ }
147
156
  state->sync_pending = true;
148
157
 
149
158
  return mik__result_ok(ctx, promise);
@@ -2,6 +2,7 @@
2
2
  #include <vector>
3
3
 
4
4
  #include "esp_event.h"
5
+ #include "esp_heap_caps.h"
5
6
  #include "esp_log.h"
6
7
  #include "esp_mac.h"
7
8
  #include "esp_netif.h"
@@ -332,6 +333,17 @@ fail_after_init:
332
333
  return err;
333
334
  }
334
335
 
336
+ /* Minimum free internal (DMA-capable) RAM required before bringing up the
337
+ * radio. esp_wifi_start() enables the PHY, which calloc()s its RF
338
+ * calibration data from internal RAM and calls abort() on failure
339
+ * (esp_phy/src/phy_init.c: "failed to allocate memory for RF calibration
340
+ * data"). That hard-abort reboots the device instead of surfacing a
341
+ * catchable error, so we pre-flight the heap here and refuse gracefully
342
+ * when it's too low. The figure covers the PHY cal buffers plus the WiFi
343
+ * driver's start-time internal allocations with margin; it is a heuristic,
344
+ * not an exact bound. Tune if a healthy device ever trips it. */
345
+ static constexpr size_t MIK_WIFI_MIN_INTERNAL_HEAP = 40 * 1024;
346
+
335
347
  /* Start the WiFi radio. Deferred from init so the radio is not active
336
348
  * until connect() or scan() is actually called. */
337
349
  static esp_err_t mik__wifi_ensure_started(JSContext* ctx) {
@@ -339,6 +351,13 @@ static esp_err_t mik__wifi_ensure_started(JSContext* ctx) {
339
351
  if (err != ESP_OK) return err;
340
352
  if (s_wifi_started) return ESP_OK;
341
353
 
354
+ /* Refuse to start under low internal RAM rather than let the PHY init
355
+ * abort() the whole device. The caller turns ESP_ERR_NO_MEM into a
356
+ * catchable StartFailed result. */
357
+ if (heap_caps_get_free_size(MALLOC_CAP_INTERNAL) < MIK_WIFI_MIN_INTERNAL_HEAP) {
358
+ return ESP_ERR_NO_MEM;
359
+ }
360
+
342
361
  err = esp_wifi_start();
343
362
  if (err != ESP_OK) return err;
344
363
 
@@ -167,6 +167,43 @@ static const char* esp32_get_device_id(void) {
167
167
  return id;
168
168
  }
169
169
 
170
+ static const char* esp32_get_reset_reason(void) {
171
+ switch (esp_reset_reason()) {
172
+ case ESP_RST_POWERON:
173
+ return "power-on";
174
+ case ESP_RST_EXT:
175
+ return "external";
176
+ case ESP_RST_SW:
177
+ return "software";
178
+ case ESP_RST_PANIC:
179
+ return "panic";
180
+ case ESP_RST_INT_WDT:
181
+ return "interrupt-watchdog";
182
+ case ESP_RST_TASK_WDT:
183
+ return "task-watchdog";
184
+ case ESP_RST_WDT:
185
+ return "watchdog";
186
+ case ESP_RST_DEEPSLEEP:
187
+ return "deep-sleep";
188
+ case ESP_RST_BROWNOUT:
189
+ return "brownout";
190
+ case ESP_RST_SDIO:
191
+ return "sdio";
192
+ case ESP_RST_USB:
193
+ return "usb";
194
+ case ESP_RST_JTAG:
195
+ return "jtag";
196
+ case ESP_RST_EFUSE:
197
+ return "efuse";
198
+ case ESP_RST_PWR_GLITCH:
199
+ return "power-glitch";
200
+ case ESP_RST_CPU_LOCKUP:
201
+ return "cpu-lockup";
202
+ default:
203
+ return "unknown";
204
+ }
205
+ }
206
+
170
207
  static void esp32_log(int level, const char* tag, const char* fmt, ...) {
171
208
  /* Map MIK_LOG_xxx to ESP_LOG_xxx for the runtime filter check. */
172
209
  esp_log_level_t esp_level;
@@ -238,6 +275,7 @@ static const MIKPlatform esp32_platform = {
238
275
  .stderr_write = esp32_stderr_write,
239
276
  .stdin_read = esp32_stdin_read,
240
277
  .get_device_id = esp32_get_device_id,
278
+ .get_reset_reason = esp32_get_reset_reason,
241
279
  };
242
280
 
243
281
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/firmware",
3
- "version": "0.12.0",
3
+ "version": "0.14.0-pr-229.g0d8db1b",
4
4
  "description": "Mikro.js ESP32 firmware: ESP-IDF component, build tools, and project template",
5
5
  "keywords": [
6
6
  "esp-idf",
@@ -18,9 +18,6 @@
18
18
  "type": "git",
19
19
  "url": "git+https://github.com/mikrojs/mikro.git"
20
20
  },
21
- "bin": {
22
- "idf.py": "./bin/idf.py"
23
- },
24
21
  "files": [
25
22
  "components",
26
23
  "default-app",
@@ -28,7 +25,6 @@
28
25
  "sdkconfig.defaults",
29
26
  "sdkconfig.defaults.*",
30
27
  "partitions.csv",
31
- "bin",
32
28
  "chips.json",
33
29
  "cmake.js",
34
30
  "discover.js",
@@ -51,8 +47,8 @@
51
47
  },
52
48
  "dependencies": {
53
49
  "esbuild": "^0.28.0",
54
- "@mikrojs/native": "0.12.0",
55
- "@mikrojs/quickjs": "0.12.0"
50
+ "@mikrojs/native": "0.14.0-pr-229.g0d8db1b",
51
+ "@mikrojs/quickjs": "0.14.0-pr-229.g0d8db1b"
56
52
  },
57
53
  "engines": {
58
54
  "node": ">=24.0.0"
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -21,11 +21,11 @@ CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
21
21
  # Use -Os to reduce code size (less IRAM pressure, smaller function frames)
22
22
  CONFIG_COMPILER_OPTIMIZATION_SIZE=y
23
23
 
24
- # Move system functions from IRAM to flash (IRAM and DRAM share the same
25
- # SRAM on single-core chips like C3/C6, so freeing IRAM also frees DRAM)
24
+ # Move heap functions from IRAM to flash (IRAM and DRAM share the same
25
+ # SRAM on single-core chips like C3/C6, so freeing IRAM also frees DRAM).
26
+ # FreeRTOS and esp_ringbuf functions are placed in flash by default since
27
+ # ESP-IDF 6.0; their *_PLACE_FUNCTIONS_INTO_FLASH options were removed.
26
28
  CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH=y
27
- CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y
28
- CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH=y
29
29
 
30
30
  # Trade WiFi throughput for ~15-20KB IRAM savings. Fine for mikrojs
31
31
  # workloads (HTTP requests, small payloads — not high-throughput streaming).
package/bin/idf.py DELETED
@@ -1,7 +0,0 @@
1
- #!/bin/sh
2
- # Thin wrapper: runs idf.py via eim in the activated ESP-IDF environment.
3
- # Usage: idf.py <args>
4
- # idf.py build
5
- # idf.py set-target esp32c6
6
- # idf.py build flash monitor
7
- exec eim run "idf.py $*"