@mikrojs/firmware 0.21.1-next.20260927212749 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,12 +13,16 @@
13
13
  #include "esp_littlefs.h"
14
14
  #include "esp_log.h"
15
15
  #include "esp_partition.h"
16
+ #include "esp_pm.h"
16
17
  #include "esp_system.h"
17
18
  #if CONFIG_ESP_TASK_WDT_EN
18
19
  #include "esp_task_wdt.h"
19
20
  #endif
20
21
  #include "freertos/FreeRTOS.h"
21
22
  #include "freertos/task.h"
23
+ #if CONFIG_IDF_TARGET_ESP32 && CONFIG_BT_ENABLED
24
+ #include "mik_ble_c_shim.h"
25
+ #endif
22
26
  #include "mikrojs.h"
23
27
  #include "mikrojs/platform.h"
24
28
  #include "mikrojs/private.h"
@@ -37,7 +41,7 @@ static_assert(MIK_WATCHDOG_BLOCKING_DEFAULT_MS / 1000 + MIK_TWDT_MARGIN_S <
37
41
  CONFIG_ESP_TASK_WDT_TIMEOUT_S,
38
42
  "watchdog.blocking default (30 s) must fire before the task watchdog: set "
39
43
  "CONFIG_ESP_TASK_WDT_TIMEOUT_S=60 in sdkconfig, or delete sdkconfig and re-run "
40
- "`idf.py set-target` so sdkconfig.defaults applies");
44
+ "`mikro idf set-target` so sdkconfig.defaults applies");
41
45
  #endif
42
46
 
43
47
  /* Error handler armed during a normal boot: when the app hits a fatal JS error
@@ -116,26 +120,13 @@ static void mik__apply_nvs_log_level(void) {
116
120
  esp_log_level_set("mik_app_config", level);
117
121
  }
118
122
 
119
- /* Scratch buffers for the supervisor loop, heap-allocated for the duration
120
- * of a test-manifest run so normal boots don't carry them in BSS (~3 KB).
121
- *
122
- * Kept off the stack because the main task stack is ~24 KB and cannot
123
- * accommodate 1-2 KB of local buffers on top of the nested eval/module
124
- * normalizer call chain (observed as a stack-protection fault in
125
- * mik_module_normalizer on the first test file). Fixed sizes instead of
126
- * PATH_MAX scaling — on newlib PATH_MAX can be 4096, which is absurd for
127
- * our purposes. These sizes fit any realistic on-device test path. Only
128
- * one test manifest runs per boot, so single-ownership is safe. */
123
+ /* Fixed sizes instead of PATH_MAX scaling — on newlib PATH_MAX can be 4096,
124
+ * which is absurd for our purposes. These fit any realistic on-device test
125
+ * path. */
129
126
  #define MIK_SUP_PATH_MAX 384
130
- struct MIKSupScratch {
131
- char dbg[MIK_SUP_PATH_MAX + 64];
132
- char esc[MIK_SUP_PATH_MAX * 2 + 8];
133
- char buf[MIK_SUP_PATH_MAX * 2 + 512];
134
- /* Exception text captured by MIK_RunEntryErr when a test file fails to
135
- * evaluate, and its JSON-escaped form for the synthesized test event. */
136
- char err[192];
137
- char err_esc[sizeof(err) * 2 + 8];
138
- };
127
+ /* Exception text captured by MIK_RunEntryErr when a test file fails to
128
+ * evaluate. */
129
+ #define MIK_SUP_ERR_MAX 192
139
130
 
140
131
  /* Minimal JSON string-escape into a bounded buffer. Handles `"`, `\`, and
141
132
  * control characters; everything else copies verbatim. Returns bytes
@@ -166,6 +157,68 @@ static int mik__json_escape(char* dst, size_t dst_size, const char* src) {
166
157
  return (int)w;
167
158
  }
168
159
 
160
+ /* Supervisor frame buffers live on these noinline helpers' stacks, called
161
+ * only while no JS runs. Not in MIK_Main's frame: it stays live under the
162
+ * eval chain, and 1-2 KB of locals there faulted in mik_module_normalizer.
163
+ * Not on the heap: that held ~3 KB through every test file's network calls. */
164
+
165
+ /* Announce the file about to run so the CLI can confirm the supervisor's
166
+ * iteration matches its own testFiles order. The MSG_DEBUG frame is
167
+ * rendered as a dim log line. */
168
+ [[gnu::noinline]] static void mik__sup_announce(MIKReplTransport* transport, size_t i, size_t count,
169
+ const char* path) {
170
+ char dbg[MIK_SUP_PATH_MAX + 64];
171
+ int n = snprintf(dbg, sizeof(dbg), "[supervisor] running %zu/%zu: %s", i + 1, count, path);
172
+ if (n > 0 && n < (int)sizeof(dbg)) {
173
+ mik__proto_send(transport, MIK_MSG_DEBUG, dbg, n);
174
+ }
175
+ }
176
+
177
+ /* Synthesize a failing test + run_done so the CLI accounts for this file
178
+ * instead of stalling waiting for a run_done the runtime will never emit.
179
+ * `err` is the captured exception text, or null. */
180
+ [[gnu::noinline]] static void mik__sup_report_failure(MIKReplTransport* transport,
181
+ const char* path, const char* reason,
182
+ const char* err) {
183
+ char esc[MIK_SUP_PATH_MAX * 2 + 8];
184
+ char err_esc[MIK_SUP_ERR_MAX * 2 + 8];
185
+ char buf[MIK_SUP_PATH_MAX * 2 + 512];
186
+
187
+ ESP_LOGE(TAG, "%s: %s", reason, path);
188
+ /* Escape the path so any `"` or `\` in it doesn't corrupt the JSON frame. */
189
+ if (mik__json_escape(esc, sizeof(esc), path) < 0) {
190
+ /* Path too long to fit even escaped — fall back to basename so the
191
+ * frame at least identifies something. */
192
+ const char* base = strrchr(path, '/');
193
+ if (!base || mik__json_escape(esc, sizeof(esc), base + 1) < 0) {
194
+ esc[0] = '?';
195
+ esc[1] = '\0';
196
+ }
197
+ }
198
+ /* Append the captured exception text (escaped) so the CLI shows the
199
+ * actual error, not just "Evaluation threw". */
200
+ err_esc[0] = '\0';
201
+ if (err && err[0] != '\0') {
202
+ if (mik__json_escape(err_esc, sizeof(err_esc), err) < 0) {
203
+ err_esc[0] = '\0';
204
+ }
205
+ }
206
+ int n;
207
+ if (err_esc[0] != '\0') {
208
+ n = snprintf(buf, sizeof(buf),
209
+ "{\"e\":3,\"s\":\"<load>\",\"t\":\"%s\",\"d\":0,\"m\":\"%s: %s\"}", esc,
210
+ reason, err_esc);
211
+ } else {
212
+ n = snprintf(buf, sizeof(buf),
213
+ "{\"e\":3,\"s\":\"<load>\",\"t\":\"%s\",\"d\":0,\"m\":\"%s\"}", esc, reason);
214
+ }
215
+ if (n > 0 && n < (int)sizeof(buf)) {
216
+ mik__proto_send(transport, MIK_MSG_TEST, buf, n);
217
+ }
218
+ static const char kRunDone[] = "{\"e\":6,\"p\":0,\"f\":1,\"k\":0,\"o\":0,\"d\":0}";
219
+ mik__proto_send(transport, MIK_MSG_TEST, kRunDone, sizeof(kRunDone) - 1);
220
+ }
221
+
169
222
  /* ── Serial transport ───────────────────────────────────────────── */
170
223
 
171
224
  static int serial_transport_read(uint8_t* buf, size_t size, void* ctx) {
@@ -222,9 +275,10 @@ static bool platform_command_handler(MIKReplTransport* transport, uint8_t cmd_ty
222
275
  return true;
223
276
  }
224
277
 
225
- /* Deploy commands (0x20-0x27) plus the build-stage command (0x2D) */
278
+ /* Deploy commands (0x20-0x27, 0x2F, 0x30) plus the build-stage command (0x2D) */
226
279
  if ((cmd_type >= MIK_CMD_DEPLOY_PUT && cmd_type <= MIK_CMD_DEPLOY_CHECKSUM) ||
227
- cmd_type == MIK_CMD_DEPLOY_BUILD) {
280
+ cmd_type == MIK_CMD_DEPLOY_BUILD || cmd_type == MIK_CMD_DEPLOY_CHECKSUM_LIST ||
281
+ cmd_type == MIK_CMD_DEPLOY_KEEP_MANY) {
228
282
  return mik__handle_deploy_command(transport, cmd_type, payload_len);
229
283
  }
230
284
 
@@ -354,6 +408,21 @@ void MIK_Main(void) {
354
408
  * for USB SOF packets; if absent it falls back to UART. */
355
409
  mik__console_init();
356
410
 
411
+ #if CONFIG_MIKROJS_AUTO_LIGHT_SLEEP
412
+ /* Keeps the DFS range from PM_DFS_INIT_AUTO and adds light sleep. */
413
+ {
414
+ esp_pm_config_t pm_config;
415
+ esp_err_t err = esp_pm_get_configuration(&pm_config);
416
+ if (err == ESP_OK) {
417
+ pm_config.light_sleep_enable = true;
418
+ err = esp_pm_configure(&pm_config);
419
+ }
420
+ if (err != ESP_OK) {
421
+ ESP_LOGW(TAG, "Automatic light sleep not enabled: %s", esp_err_to_name(err));
422
+ }
423
+ }
424
+ #endif
425
+
357
426
  /* Gather chip/feature info early — cheap, no side effects, used to
358
427
  * print the banner AFTER runtime init succeeds. We deliberately defer
359
428
  * the banner until everything is up so its mere presence signals
@@ -387,6 +456,20 @@ void MIK_Main(void) {
387
456
  bool has_ble = false;
388
457
  #endif
389
458
 
459
+ #if CONFIG_IDF_TARGET_ESP32 && CONFIG_BT_ENABLED
460
+ /* The BT controller reserves its DRAM at boot whether or not BLE is ever
461
+ * started. Classic BT is never used (NimBLE is BLE-only), so hand its
462
+ * 15 KB with the default config (0x3ffb2730-0x3ffb6388) to the heap now.
463
+ * nimble_port_init makes the same call later and treats the repeat as
464
+ * already released. */
465
+ {
466
+ int err = mik_ble_release_classic_bt_mem();
467
+ if (err != 0) {
468
+ ESP_LOGI(TAG, "Classic BT memory not released: %s", esp_err_to_name(err));
469
+ }
470
+ }
471
+ #endif
472
+
390
473
  /* Capture starting heap for the later runtime-cost INFO log. */
391
474
  size_t heap_free_at_boot = esp_get_free_heap_size();
392
475
  size_t heap_total_at_boot = heap_free_at_boot;
@@ -396,18 +479,22 @@ void MIK_Main(void) {
396
479
  heap_total_at_boot = info.total_free_bytes + info.total_allocated_bytes;
397
480
  }
398
481
 
399
- /* Recovery window: gives the host (or a double-reset) a chance to
400
- * skip the autorun if the deployed app is crash-looping. Costs ~500ms
401
- * on every cold boot but is the only window we have before user code
402
- * runs and may panic. */
403
- bool safe_mode = mik__check_recovery(500);
482
+ /* Recovery window: lets the host (or a double reset) skip the autorun when
483
+ * the app crash-loops. Costs CONFIG_MIKROJS_RECOVERY_WINDOW_MS on every boot,
484
+ * deep-sleep wakes included; at 0 the call is skipped and a second reset has
485
+ * no window to land in. */
486
+ bool safe_mode = false;
487
+ #if CONFIG_MIKROJS_RECOVERY_WINDOW_MS > 0
488
+ safe_mode = mik__check_recovery(CONFIG_MIKROJS_RECOVERY_WINDOW_MS);
489
+ #endif
404
490
  if (safe_mode) {
405
491
  printf("\n*** SAFE MODE: autorun skipped, dropping to REPL ***\n\n");
406
492
  }
407
493
 
408
494
  #if CONFIG_ESP_TASK_WDT_EN
409
495
  /* Watch the main task: a hang in native code below JS now resets the
410
- * chip instead of sitting forever. Fed from MIK_Loop and platform->yield. */
496
+ * chip instead of sitting forever. Fed from MIK_Loop, platform->yield and
497
+ * platform->wait. */
411
498
  {
412
499
  esp_err_t err = esp_task_wdt_add(NULL);
413
500
  if (err != ESP_OK) {
@@ -528,7 +615,12 @@ void MIK_Main(void) {
528
615
  options.stack_size = app_config.stack_size;
529
616
  }
530
617
  #ifdef CONFIG_MIKROJS_QUICKJS_HEAP_PSRAM
531
- options.use_psram_heap = true;
618
+ /* Route the JS heap to PSRAM only when the heap allocator has some: with
619
+ * SPIRAM_IGNORE_NOTFOUND a board without the chip boots too, and every failed
620
+ * heap_caps_malloc(MALLOC_CAP_SPIRAM) would walk the heap registry first. */
621
+ if (heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0) {
622
+ options.use_psram_heap = true;
623
+ }
532
624
  #endif
533
625
 
534
626
  auto create_runtime = [&]() -> MIKRuntime* {
@@ -678,47 +770,27 @@ void MIK_Main(void) {
678
770
  MIK_ProtocolOpen(&transport);
679
771
 
680
772
  if (test_mode) {
681
- auto* sup = static_cast<MIKSupScratch*>(malloc(sizeof(MIKSupScratch)));
682
- if (!sup) {
683
- ESP_LOGE(TAG, "Not enough memory for the test supervisor");
684
- /* Fail loud: a synthesized failing run-done plus end-of-manifest
685
- * lets the CLI report the failure instead of hanging on a stream
686
- * that will never produce results. */
687
- static const char kOomRunDone[] = "{\"e\":6,\"p\":0,\"f\":1,\"k\":0,\"o\":0,\"d\":0}";
688
- mik__proto_send(&transport, MIK_MSG_TEST, kOomRunDone, sizeof(kOomRunDone) - 1);
689
- mik__proto_send(&transport, MIK_MSG_MANIFEST_DONE, nullptr, 0);
690
- return;
691
- }
692
-
693
773
  /* Discard the primary runtime — each test gets a fresh one. */
694
774
  MIK_FreeRuntime(mik_rt);
695
775
  mik_rt = nullptr;
696
776
 
697
777
  for (size_t i = 0; i < test_count; i++) {
698
- /* Diagnostic: announce the file about to run so the CLI can
699
- * confirm the supervisor's iteration matches its own testFiles
700
- * order. The MSG_DEBUG frame is rendered as a dim log line.
701
- * Uses heap scratch to avoid bloating the main task stack —
702
- * the normalizer call chain during module resolution already
703
- * sits a few KB deep. */
704
- {
705
- int n = snprintf(sup->dbg, sizeof(sup->dbg),
706
- "[supervisor] running %zu/%zu: %s", i + 1, test_count,
707
- test_paths[i]);
708
- if (n > 0 && n < (int)sizeof(sup->dbg)) {
709
- mik__proto_send(&transport, MIK_MSG_DEBUG, sup->dbg, n);
710
- }
711
- }
778
+ mik__sup_announce(&transport, i, test_count, test_paths[i]);
712
779
  MIKRuntime* rt = create_runtime();
713
780
  MIK_EnableTestHelpers(rt);
714
781
  MIK_ProtocolAttach(rt);
715
- int rc = MIK_RunEntryErr(rt, test_paths[i], sup->err, sizeof(sup->err));
782
+ /* Held only across the entry eval; a null buffer just drops the
783
+ * exception text. */
784
+ auto* err = static_cast<char*>(malloc(MIK_SUP_ERR_MAX));
785
+ int rc = MIK_RunEntryErr(rt, test_paths[i], err, err ? MIK_SUP_ERR_MAX : 0);
716
786
  const char* fail_reason = nullptr;
717
787
  if (rc == -ENOENT) {
718
788
  fail_reason = "Test file not found";
719
789
  } else if (rc == -EFAULT) {
720
790
  fail_reason = "Evaluation threw";
721
791
  } else {
792
+ free(err);
793
+ err = nullptr;
722
794
  /* Entry eval returned successfully (rc == 0). The test module
723
795
  * may still asynchronously reject (e.g. top-level throw in a
724
796
  * module eval'd as a Promise) — that's caught after ServeLoop
@@ -729,52 +801,12 @@ void MIK_Main(void) {
729
801
  }
730
802
  }
731
803
  if (fail_reason) {
732
- /* Synthesize a failing test + run_done so the CLI accounts
733
- * for this file instead of stalling waiting for a run_done
734
- * the runtime will never emit. Escape the path so any `"`
735
- * or `\` in it doesn't corrupt the JSON frame. */
736
- ESP_LOGE(TAG, "%s: %s", fail_reason, test_paths[i]);
737
- if (mik__json_escape(sup->esc, sizeof(sup->esc), test_paths[i]) < 0) {
738
- /* Path too long to fit even escaped — fall back to
739
- * basename so the frame at least identifies something. */
740
- const char* base = strrchr(test_paths[i], '/');
741
- if (!base ||
742
- mik__json_escape(sup->esc, sizeof(sup->esc), base + 1) < 0) {
743
- sup->esc[0] = '?';
744
- sup->esc[1] = '\0';
745
- }
746
- }
747
- /* Append the captured exception text (escaped) so the CLI
748
- * shows the actual error, not just "Evaluation threw". */
749
- sup->err_esc[0] = '\0';
750
- if (rc == -EFAULT && sup->err[0] != '\0') {
751
- if (mik__json_escape(sup->err_esc, sizeof(sup->err_esc), sup->err) < 0) {
752
- sup->err_esc[0] = '\0';
753
- }
754
- }
755
- int n;
756
- if (sup->err_esc[0] != '\0') {
757
- n = snprintf(sup->buf, sizeof(sup->buf),
758
- "{\"e\":3,\"s\":\"<load>\",\"t\":\"%s\",\"d\":0,"
759
- "\"m\":\"%s: %s\"}",
760
- sup->esc, fail_reason, sup->err_esc);
761
- } else {
762
- n = snprintf(sup->buf, sizeof(sup->buf),
763
- "{\"e\":3,\"s\":\"<load>\",\"t\":\"%s\",\"d\":0,"
764
- "\"m\":\"%s\"}",
765
- sup->esc, fail_reason);
766
- }
767
- if (n > 0 && n < (int)sizeof(sup->buf)) {
768
- mik__proto_send(&transport, MIK_MSG_TEST, sup->buf, n);
769
- }
770
- static const char kRunDone[] =
771
- "{\"e\":6,\"p\":0,\"f\":1,\"k\":0,\"o\":0,\"d\":0}";
772
- mik__proto_send(&transport, MIK_MSG_TEST, kRunDone, sizeof(kRunDone) - 1);
804
+ mik__sup_report_failure(&transport, test_paths[i], fail_reason, err);
773
805
  }
806
+ free(err);
774
807
  MIK_ProtocolDetach();
775
808
  MIK_FreeRuntime(rt);
776
809
  }
777
- free(sup);
778
810
 
779
811
  /* Signal end-of-manifest so the CLI can finalize its report
780
812
  * without waiting on a silent stream. */
@@ -4,6 +4,7 @@
4
4
  #include "driver/ledc.h"
5
5
  #include "soc/soc_caps.h"
6
6
  #include "esp_log.h"
7
+ #include "esp_pm.h"
7
8
  #include "mikrojs/mikrojs.h"
8
9
  #include "mikrojs/private.h"
9
10
  #include "mikrojs/utils.h"
@@ -23,6 +24,17 @@ static uint32_t s_timer_freq[MIK_PWM_MAX_TIMERS] = {};
23
24
  static uint8_t s_timer_refcount[MIK_PWM_MAX_TIMERS] = {};
24
25
  static bool s_fade_installed = false;
25
26
 
27
+ #if CONFIG_PM_ENABLE
28
+ /* Held per live handle: LEDC_AUTO_CLK picks APB where the chip has it, which DFS
29
+ * slows; XTAL and PLL_F80M (C6/C5) keep their rate but stop in light sleep. */
30
+ #if SOC_LEDC_SUPPORT_APB_CLOCK
31
+ #define MIK_PWM_PM_LOCK ESP_PM_APB_FREQ_MAX
32
+ #else
33
+ #define MIK_PWM_PM_LOCK ESP_PM_NO_LIGHT_SLEEP
34
+ #endif
35
+ static esp_pm_lock_handle_t s_pwm_pm_lock = nullptr;
36
+ #endif
37
+
26
38
  static int mik__pwm_alloc_channel() {
27
39
  for (int i = 0; i < MIK_PWM_MAX_CHANNELS; i++) {
28
40
  if (!(s_channel_used & (1 << i))) {
@@ -120,8 +132,9 @@ static IRAM_ATTR bool mik__pwm_fade_cb(const ledc_cb_param_t* param, void* user_
120
132
  auto* pending = static_cast<MIKPwmFadePending*>(user_arg);
121
133
  if (param->event == LEDC_FADE_END_EVT) {
122
134
  pending->complete.store(true, std::memory_order_release);
135
+ MIK_WakeFromISR();
123
136
  }
124
- return false; // no high-priority task woken
137
+ return false; // the wake yields itself when it unblocks the main task
125
138
  }
126
139
 
127
140
  /* The channel's fade that is still running, or nullptr. */
@@ -166,6 +179,9 @@ static void mik__pwm_release(MIKPwmState* s) {
166
179
  mik__pwm_free_channel(s->channel);
167
180
  mik__pwm_free_timer(s->timer);
168
181
  MIK_ReleaseGpio(s->gpio, "Pwm");
182
+ #if CONFIG_PM_ENABLE
183
+ esp_pm_lock_release(s_pwm_pm_lock);
184
+ #endif
169
185
  s->active = false;
170
186
  }
171
187
 
@@ -231,6 +247,14 @@ static JSValue js_pwm(JSContext* ctx, JSValue this_val, int argc, JSValue* argv)
231
247
  const MIKGpioCheck check = {gpio, true};
232
248
  invalid = mik__gpio_check(ctx, &check, 1);
233
249
  if (!JS_IsUndefined(invalid)) return invalid;
250
+ #if CONFIG_PM_ENABLE
251
+ if (!s_pwm_pm_lock) {
252
+ esp_err_t lock_err = esp_pm_lock_create(MIK_PWM_PM_LOCK, 0, "pwm", &s_pwm_pm_lock);
253
+ if (lock_err != ESP_OK)
254
+ return mik__result_err_named(ctx, "ConfigFailed", "esp_pm_lock_create failed: %s",
255
+ esp_err_to_name(lock_err));
256
+ }
257
+ #endif
234
258
  const int gpios[] = {gpio};
235
259
  JSValue claim_failed = MIK_ClaimGpios(ctx, gpios, 1, "Pwm");
236
260
  if (!JS_IsUndefined(claim_failed)) return claim_failed;
@@ -304,6 +328,9 @@ static JSValue js_pwm(JSContext* ctx, JSValue this_val, int argc, JSValue* argv)
304
328
  s->resolution = resolution;
305
329
  s->duty = duty;
306
330
  s->active = true;
331
+ #if CONFIG_PM_ENABLE
332
+ esp_pm_lock_acquire(s_pwm_pm_lock);
333
+ #endif
307
334
 
308
335
  JSValue obj = JS_NewObjectClass(ctx, mik_pwm_class_id);
309
336
  if (JS_IsException(obj)) {
@@ -3,6 +3,8 @@
3
3
  #include <string.h>
4
4
 
5
5
  #include "driver/uart.h"
6
+ #include "driver/uart_select.h"
7
+ #include "esp_attr.h"
6
8
  #include "esp_intr_alloc.h"
7
9
  #include "esp_log.h"
8
10
  #include "freertos/FreeRTOS.h"
@@ -17,6 +19,7 @@
17
19
 
18
20
  #if SOC_USB_SERIAL_JTAG_SUPPORTED
19
21
  #include "driver/usb_serial_jtag.h"
22
+ #include "driver/usb_serial_jtag_select.h"
20
23
  #include "esp_rom_sys.h"
21
24
  #include "hal/usb_serial_jtag_ll.h"
22
25
  #include "soc/io_mux_reg.h"
@@ -66,6 +69,24 @@
66
69
  #define MIK_CONSOLE_HAS_UART 0
67
70
  #endif
68
71
 
72
+ /* RX data wakes the sleeping serve loop through each driver's select-notify
73
+ * hook, its only ISR-level RX signal; the VFS layer that would own it is unused. */
74
+ #if SOC_USB_SERIAL_JTAG_SUPPORTED
75
+ static void mik__usj_rx_notify(usj_select_notif_t notif, int* task_woken) {
76
+ (void)task_woken; // the wake yields itself
77
+ if (notif == USJ_SELECT_READ_NOTIF) MIK_WakeFromISR();
78
+ }
79
+ #endif
80
+ #if MIK_CONSOLE_HAS_UART
81
+ /* UART0's ISR is in IRAM and runs with the flash cache off. */
82
+ static void IRAM_ATTR mik__uart0_rx_notify(uart_port_t port, uart_select_notif_t notif,
83
+ BaseType_t* task_woken) {
84
+ (void)port;
85
+ (void)task_woken;
86
+ if (notif == UART_SELECT_READ_NOTIF) MIK_WakeFromISR();
87
+ }
88
+ #endif
89
+
69
90
  static enum { CONSOLE_UART, CONSOLE_USB_SERIAL_JTAG } s_console =
70
91
  #if SOC_USB_SERIAL_JTAG_SUPPORTED
71
92
  CONSOLE_USB_SERIAL_JTAG;
@@ -105,7 +126,11 @@ static bool mik__usj_install_with_default_config(void) {
105
126
  usb_serial_jtag_driver_config_t usj_cfg = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT();
106
127
  usj_cfg.rx_buffer_size = 4096;
107
128
  usj_cfg.tx_buffer_size = 2048;
108
- return usb_serial_jtag_driver_install(&usj_cfg) == ESP_OK;
129
+ if (usb_serial_jtag_driver_install(&usj_cfg) != ESP_OK) return false;
130
+ /* Per install: the driver object that holds the hook is freed on uninstall
131
+ * (light sleep detaches and re-attaches). */
132
+ usb_serial_jtag_set_select_notif_callback(mik__usj_rx_notify);
133
+ return true;
109
134
  }
110
135
  #endif
111
136
 
@@ -131,12 +156,21 @@ void mik__console_init(void) {
131
156
  uart_config.parity = UART_PARITY_DISABLE;
132
157
  uart_config.stop_bits = UART_STOP_BITS_1;
133
158
  uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
159
+ /* DFS slows the APB clock; keep the DFS-safe clock that esp_pm_impl_init
160
+ * gave the console, or RX garbles whenever the chip idles. */
161
+ #if CONFIG_PM_ENABLE && SOC_UART_SUPPORT_REF_TICK
162
+ uart_config.source_clk = UART_SCLK_REF_TICK;
163
+ #elif CONFIG_PM_ENABLE
164
+ uart_config.source_clk = UART_SCLK_XTAL;
165
+ #else
134
166
  uart_config.source_clk = UART_SCLK_DEFAULT;
167
+ #endif
135
168
  uart_param_config(UART_NUM_0, &uart_config);
136
169
  uart_set_pin(UART_NUM_0, U0TXD_GPIO_NUM, U0RXD_GPIO_NUM,
137
170
  UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
138
171
  /* IRAM ISR keeps UART alive during flash writes (LittleFS deploys). */
139
172
  if (uart_driver_install(UART_NUM_0, 4096, 0, 0, NULL, ESP_INTR_FLAG_IRAM) == ESP_OK) {
173
+ uart_set_select_notif_callback(UART_NUM_0, mik__uart0_rx_notify);
140
174
  MIK_ClaimGpio(U0TXD_GPIO_NUM, "console");
141
175
  MIK_ClaimGpio(U0RXD_GPIO_NUM, "console");
142
176
  }
@@ -329,28 +363,6 @@ void mik__serial_io_attach_usb(void) {
329
363
  const char* MIK__NVS_NS_ENV = "mik.env";
330
364
  const char* MIK__NVS_NS_SEC = "mik.sec";
331
365
 
332
- void mik__nvs_clear_namespace(const char* ns) {
333
- nvs_handle_t handle;
334
- if (nvs_open(ns, NVS_READWRITE, &handle) == ESP_OK) {
335
- nvs_erase_all(handle);
336
- nvs_commit(handle);
337
- nvs_close(handle);
338
- }
339
- }
340
-
341
- bool mik__nvs_put_string(const char* ns, const char* key, const char* value) {
342
- nvs_handle_t handle;
343
- esp_err_t err = nvs_open(ns, NVS_READWRITE, &handle);
344
- if (err != ESP_OK) return false;
345
-
346
- err = nvs_set_str(handle, key, value);
347
- if (err == ESP_OK) {
348
- err = nvs_commit(handle);
349
- }
350
- nvs_close(handle);
351
- return err == ESP_OK;
352
- }
353
-
354
366
  /* Write only if the value differs from what's already stored. `out_changed`
355
367
  * is set to true when the stored value ends up different than before, false
356
368
  * if the existing value already matched. Returns false on I/O failure. */
@@ -39,6 +39,7 @@ static void mik__sntp_sync_cb(struct timeval* tv) {
39
39
  if (!s_event_queue) return;
40
40
  struct timeval evt = *tv;
41
41
  xQueueSend(s_event_queue, &evt, 0);
42
+ MIK_Wake();
42
43
  }
43
44
 
44
45
  /* ── Test helper: inject a fake sync event ─────────────────────────── */
@@ -47,6 +48,7 @@ void mik__sntp_inject_sync(int64_t epoch_sec) {
47
48
  if (!s_event_queue) return;
48
49
  struct timeval tv = {.tv_sec = static_cast<time_t>(epoch_sec), .tv_usec = 0};
49
50
  xQueueSend(s_event_queue, &tv, 0);
51
+ MIK_Wake();
50
52
  }
51
53
 
52
54
  /* ── JS functions ──────────────────────────────────────────────────── */
@@ -2,10 +2,14 @@
2
2
  #include <cstring>
3
3
 
4
4
  #include "driver/uart.h"
5
+ #include "driver/uart_select.h"
6
+ #include "esp_attr.h"
7
+ #include "esp_pm.h"
5
8
  #include "mikrojs/mikrojs.h"
6
9
  #include "mikrojs/private.h"
7
10
  #include "mikrojs/utils.h"
8
11
  #include "mikrojs_esp32.h"
12
+ #include "soc/soc_caps.h"
9
13
 
10
14
  #define MIK_UART_RX_BUF_SIZE 2048
11
15
  #define MIK_UART_MAX_INSTANCES 3
@@ -13,6 +17,25 @@
13
17
  static JSClassID mik_uart_class_id;
14
18
  static int mik__uart_slot = -1;
15
19
 
20
+ #if CONFIG_PM_ENABLE
21
+ /* Held per live handle (the driver guards only TX): the default source is APB where the
22
+ * chip has it, which DFS slows; PLL_F80M (C6/C5) keeps its rate but stops in light sleep. */
23
+ #if SOC_UART_SUPPORT_APB_CLK
24
+ #define MIK_UART_PM_LOCK ESP_PM_APB_FREQ_MAX
25
+ #else
26
+ #define MIK_UART_PM_LOCK ESP_PM_NO_LIGHT_SLEEP
27
+ #endif
28
+ static esp_pm_lock_handle_t s_uart_pm_lock = nullptr;
29
+ #endif
30
+
31
+ /* UART ISR (IRAM, so it runs with the flash cache off): RX data landed. */
32
+ static void IRAM_ATTR mik__uart_rx_notify(uart_port_t port, uart_select_notif_t notif,
33
+ BaseType_t* task_woken) {
34
+ (void)port;
35
+ (void)task_woken; // the wake yields itself
36
+ if (notif == UART_SELECT_READ_NOTIF) MIK_WakeFromISR();
37
+ }
38
+
16
39
  /* Forward decl: defined alongside the iterator class further down. The Uart
17
40
  * state holds a non-owning backpointer so end() can mark the active iterator
18
41
  * as exhausted without having to reach through JS. */
@@ -75,6 +98,9 @@ static void mik__uart_release(MIKRuntime* mik_rt, MIKUartState* s) {
75
98
  uart_driver_delete(s->port);
76
99
  const int gpios[] = {s->tx_pin, s->rx_pin};
77
100
  MIK_ReleaseGpios(gpios, countof(gpios), "Uart");
101
+ #if CONFIG_PM_ENABLE
102
+ esp_pm_lock_release(s_uart_pm_lock);
103
+ #endif
78
104
  s->active = false;
79
105
  }
80
106
 
@@ -129,6 +155,15 @@ static JSValue js_uart(JSContext* ctx, JSValue this_val, int argc, JSValue* argv
129
155
  const MIKGpioCheck checks[] = {{tx, true}, {rx, false}};
130
156
  JSValue invalid = mik__gpio_check(ctx, checks, countof(checks));
131
157
  if (!JS_IsUndefined(invalid)) return invalid;
158
+ #if CONFIG_PM_ENABLE
159
+ if (!s_uart_pm_lock) {
160
+ esp_err_t lock_err = esp_pm_lock_create(MIK_UART_PM_LOCK, 0, "uart", &s_uart_pm_lock);
161
+ if (lock_err != ESP_OK)
162
+ return mik__result_err_named(ctx, "DriverInstallFailed",
163
+ "esp_pm_lock_create failed: %s",
164
+ esp_err_to_name(lock_err));
165
+ }
166
+ #endif
132
167
  const int gpios[] = {tx, rx};
133
168
  JSValue claim_failed = MIK_ClaimGpios(ctx, gpios, countof(gpios), "Uart");
134
169
  if (!JS_IsUndefined(claim_failed)) return claim_failed;
@@ -178,6 +213,9 @@ static JSValue js_uart(JSContext* ctx, JSValue this_val, int argc, JSValue* argv
178
213
  "uart_driver_install failed on port %d: %s", (int)port,
179
214
  esp_err_to_name(err));
180
215
  }
216
+ /* The loop consumer polls the RX buffer; this ends its wait when bytes land.
217
+ * uart_driver_delete clears the callback. */
218
+ uart_set_select_notif_callback(uart_port, mik__uart_rx_notify);
181
219
 
182
220
  auto* s = static_cast<MIKUartState*>(calloc(1, sizeof(MIKUartState)));
183
221
  if (!s) {
@@ -199,6 +237,9 @@ static JSValue js_uart(JSContext* ctx, JSValue this_val, int argc, JSValue* argv
199
237
  return obj;
200
238
  }
201
239
  JS_SetOpaque(obj, s);
240
+ #if CONFIG_PM_ENABLE
241
+ esp_pm_lock_acquire(s_uart_pm_lock);
242
+ #endif
202
243
  mik__uart_track(MIK_GetRuntime(ctx), s);
203
244
  MIK_KeepHandle(ctx, obj);
204
245
  return mik__result_ok(ctx, obj);
@@ -250,6 +250,7 @@ static void mik__wifi_event_handler(void* arg, esp_event_base_t event_base, int3
250
250
 
251
251
  if (s_event_queue) {
252
252
  xQueueSend(s_event_queue, &evt, 0);
253
+ MIK_Wake();
253
254
  }
254
255
  }
255
256