@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.
- package/components/mikrojs/CMakeLists.txt +2 -2
- package/components/mikrojs/Kconfig +73 -1
- package/components/mikrojs/include/mikrojs_esp32.h +4 -3
- package/components/mikrojs/mik_ble.cpp +15 -4
- package/components/mikrojs/mik_ble_c_shim.c +10 -0
- package/components/mikrojs/mik_ble_c_shim.h +9 -0
- package/components/mikrojs/mik_deploy.cpp +92 -10
- package/components/mikrojs/mik_gpio.cpp +6 -1
- package/components/mikrojs/mik_http.cpp +10 -4
- package/components/mikrojs/mik_http_server.cpp +2 -0
- package/components/mikrojs/mik_i2s.cpp +17 -0
- package/components/mikrojs/mik_logfile.cpp +0 -7
- package/components/mikrojs/mik_main.cpp +130 -98
- package/components/mikrojs/mik_pwm.cpp +28 -1
- package/components/mikrojs/mik_serial_io.cpp +35 -23
- package/components/mikrojs/mik_sntp.cpp +2 -0
- package/components/mikrojs/mik_uart.cpp +41 -0
- package/components/mikrojs/mik_wifi.cpp +1 -0
- package/components/mikrojs/platform_esp32.cpp +76 -2
- package/dist/boards.d.ts +73 -12
- package/dist/boards.d.ts.map +1 -1
- package/dist/boards.js +137 -60
- package/dist/boards.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/sdkconfig.defaults +2 -0
- package/components/mikrojs/include/mem.h +0 -3
- package/components/mikrojs/include/mik_color.h +0 -3
|
@@ -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
|
|
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
|
-
/*
|
|
120
|
-
*
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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:
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
bool safe_mode =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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; //
|
|
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
|
-
|
|
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);
|