@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.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/bin/idf.py +7 -0
- package/chips.json +3 -0
- package/cmake.js +9 -0
- package/components/mikrojs/CMakeLists.txt +187 -0
- package/components/mikrojs/Kconfig +55 -0
- package/components/mikrojs/idf_component.yml +6 -0
- package/components/mikrojs/include/mem.h +3 -0
- package/components/mikrojs/include/mik_color.h +3 -0
- package/components/mikrojs/include/mik_http_internal.h +77 -0
- package/components/mikrojs/include/mikrojs.h +5 -0
- package/components/mikrojs/include/mikrojs_esp32.h +65 -0
- package/components/mikrojs/include/private.h +10 -0
- package/components/mikrojs/include/utils.h +3 -0
- package/components/mikrojs/mik_ble.cpp +1588 -0
- package/components/mikrojs/mik_ble_c_shim.c +61 -0
- package/components/mikrojs/mik_ble_c_shim.h +37 -0
- package/components/mikrojs/mik_config.cpp +167 -0
- package/components/mikrojs/mik_deploy.cpp +584 -0
- package/components/mikrojs/mik_http.cpp +916 -0
- package/components/mikrojs/mik_i2c.cpp +364 -0
- package/components/mikrojs/mik_main.cpp +542 -0
- package/components/mikrojs/mik_neopixel.cpp +437 -0
- package/components/mikrojs/mik_nvs_kv.cpp +219 -0
- package/components/mikrojs/mik_pin.cpp +195 -0
- package/components/mikrojs/mik_pwm.cpp +525 -0
- package/components/mikrojs/mik_recovery.cpp +86 -0
- package/components/mikrojs/mik_rtc.cpp +305 -0
- package/components/mikrojs/mik_serial_io.cpp +362 -0
- package/components/mikrojs/mik_sleep.cpp +226 -0
- package/components/mikrojs/mik_sntp.cpp +275 -0
- package/components/mikrojs/mik_spi.cpp +330 -0
- package/components/mikrojs/mik_uart.cpp +497 -0
- package/components/mikrojs/mik_wifi.cpp +1434 -0
- package/components/mikrojs/platform_esp32.cpp +192 -0
- package/components/mikrojs/test/CMakeLists.txt +32 -0
- package/components/mikrojs/test/abort_test.cpp +254 -0
- package/components/mikrojs/test/ble_test.cpp +714 -0
- package/components/mikrojs/test/fs_js_test.cpp +458 -0
- package/components/mikrojs/test/fs_pub_test.cpp +312 -0
- package/components/mikrojs/test/http_test.cpp +475 -0
- package/components/mikrojs/test/i2c_test.cpp +138 -0
- package/components/mikrojs/test/modules_extended_test.cpp +137 -0
- package/components/mikrojs/test/modules_test.cpp +131 -0
- package/components/mikrojs/test/pins_test.cpp +47 -0
- package/components/mikrojs/test/pwm_test.cpp +166 -0
- package/components/mikrojs/test/repl_protocol_test.cpp +405 -0
- package/components/mikrojs/test/rtc_test.cpp +331 -0
- package/components/mikrojs/test/runtime_test.cpp +89 -0
- package/components/mikrojs/test/sleep_test.cpp +222 -0
- package/components/mikrojs/test/sntp_test.cpp +249 -0
- package/components/mikrojs/test/stdio_test.cpp +449 -0
- package/components/mikrojs/test/sys_test.cpp +165 -0
- package/components/mikrojs/test/text_encoding_test.cpp +224 -0
- package/components/mikrojs/test/timers_js_test.cpp +244 -0
- package/components/mikrojs/test/timers_test.cpp +79 -0
- package/components/mikrojs/test/wifi_test.cpp +599 -0
- package/default-app/main/CMakeLists.txt +3 -0
- package/default-app/main/main.cpp +5 -0
- package/discover.js +77 -0
- package/index.d.ts +7 -0
- package/index.js +20 -0
- package/package.json +61 -0
- package/partitions.csv +5 -0
- package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32/flasher_args.json +24 -0
- package/prebuilds/esp32/mikrojs.bin +0 -0
- package/prebuilds/esp32/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c3/flasher_args.json +24 -0
- package/prebuilds/esp32c3/mikrojs.bin +0 -0
- package/prebuilds/esp32c3/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c6/flasher_args.json +24 -0
- package/prebuilds/esp32c6/mikrojs.bin +0 -0
- package/prebuilds/esp32c6/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32s3/flasher_args.json +24 -0
- package/prebuilds/esp32s3/mikrojs.bin +0 -0
- package/prebuilds/esp32s3/partition_table/partition-table.bin +0 -0
- package/project.cmake +101 -0
- package/resolve.js +54 -0
- package/sdkconfig.defaults +127 -0
- package/sdkconfig.defaults.esp32 +8 -0
- package/sdkconfig.defaults.esp32c3 +15 -0
- package/sdkconfig.defaults.esp32c6 +26 -0
- 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
|
+
}
|