@mikrojs/firmware 0.6.0 → 0.8.0-pr-85.g6768f8c

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.
@@ -83,6 +83,7 @@ idf_component_register(
83
83
  "mik_deploy.cpp"
84
84
  "mik_http.cpp"
85
85
  "mik_i2c.cpp"
86
+ "mik_logfile.cpp"
86
87
  "mik_neopixel.cpp"
87
88
  "mik_pin.cpp"
88
89
  "mik_pwm.cpp"
@@ -41,6 +41,26 @@ int mik__console_read(void* buf, size_t len);
41
41
  /* Write to the active console. Returns bytes written. */
42
42
  int mik__console_write(const void* buf, size_t len);
43
43
 
44
+ /* Tap installed by mik_logfile so console output is mirrored into the
45
+ * on-device log file. Called from mik__console_write before the actual
46
+ * UART/USB-JTAG write, so output is captured even when no host is
47
+ * attached. Single slot; mik_logfile owns it. */
48
+ typedef void (*mik_console_tap_fn)(const void* buf, size_t len);
49
+ void mik__console_set_tap(mik_console_tap_fn fn);
50
+
51
+ /* File logging (mik_logfile.cpp).
52
+ * Initialized after MIK_LoadConfig once the filesystem is mounted.
53
+ * No-op when MIKConfig.log_file is empty. */
54
+ typedef struct MIKConfig MIKConfig;
55
+ void mik_logfile_init(const MIKConfig* config);
56
+ void mik_logfile_close(void);
57
+ void mik_logfile_flush(void);
58
+ /* Release/restore the underlying FILE* so the host can read the file
59
+ * without contention. Output between suspend/resume is dropped. No-op
60
+ * when file logging is disabled. */
61
+ void mik_logfile_suspend(void);
62
+ void mik_logfile_resume(void);
63
+
44
64
  /* Serial binary I/O (mik_serial_io.cpp) */
45
65
  void mik__serial_binary_begin_no_echo(void);
46
66
 
@@ -582,3 +582,53 @@ bool mik__handle_deploy_command(MIKReplTransport* transport, uint8_t cmd_type,
582
582
  return false;
583
583
  }
584
584
  }
585
+
586
+ /*
587
+ * Handle MIK_CMD_FS_GET: stream the contents of a file off the device.
588
+ * Payload: u16le path_len | path.
589
+ * Suspends the file logger so its buffered writes are flushed to disk and
590
+ * its FILE* released before we open the same path for reading, then
591
+ * resumes it once streaming is done.
592
+ */
593
+ bool mik__handle_fs_get(MIKReplTransport* transport, uint32_t payload_len) {
594
+ if (payload_len < 2) {
595
+ mik__proto_drain(transport, payload_len);
596
+ mik__proto_send_err(transport, "fs get: short header");
597
+ return true;
598
+ }
599
+ uint8_t nl[2];
600
+ if (!mik__proto_read_exact(transport, nl, 2)) return false;
601
+ uint16_t path_len = nl[0] | (nl[1] << 8);
602
+ if (path_len == 0 || path_len >= 256 || (uint32_t)path_len + 2 > payload_len) {
603
+ mik__proto_drain(transport, payload_len - 2);
604
+ mik__proto_send_err(transport, "fs get: bad path length");
605
+ return true;
606
+ }
607
+ char path[256];
608
+ if (!mik__proto_read_exact(transport, path, path_len)) return false;
609
+ path[path_len] = '\0';
610
+ uint32_t consumed = 2 + (uint32_t)path_len;
611
+ if (payload_len > consumed) mik__proto_drain(transport, payload_len - consumed);
612
+
613
+ mik_logfile_suspend();
614
+
615
+ FILE* f = fopen(path, "r");
616
+ if (!f) {
617
+ char msg[160];
618
+ snprintf(msg, sizeof(msg), "fs get: open failed: %s", strerror(errno));
619
+ mik__proto_send_err(transport, msg);
620
+ mik_logfile_resume();
621
+ return true;
622
+ }
623
+
624
+ uint8_t buf[512];
625
+ for (;;) {
626
+ size_t n = fread(buf, 1, sizeof(buf), f);
627
+ if (n == 0) break;
628
+ mik__proto_send(transport, MIK_MSG_FS_CHUNK, buf, n);
629
+ }
630
+ fclose(f);
631
+ mik_logfile_resume();
632
+ mik__proto_send_ok(transport);
633
+ return true;
634
+ }
@@ -668,15 +668,39 @@ static JSValue mik__http_next_message(JSContext* ctx, JSValue this_val, int argc
668
668
  return promise;
669
669
  }
670
670
 
671
+ /* Forward decl: defined further down, called here so pendingCount() drains
672
+ * result_queue before reading the counter. */
673
+ void mik__http_consume(JSContext* ctx);
674
+
671
675
  /* pendingCount() — number of in-flight requests whose terminal message has
672
676
  * not yet been consumed. Useful for tests that want to verify cancel+drain
673
- * freed a slot without relying on heap-headroom for a follow-up request. */
677
+ * freed a slot without relying on heap-headroom for a follow-up request.
678
+ *
679
+ * Drains result_queue first via the consume loop. Without this, a request
680
+ * whose native task has already posted its terminal message but whose
681
+ * message hasn't been processed by the next event-loop tick reads as
682
+ * "still pending" — even though it'd drop microseconds later. Surfaced
683
+ * as an intermittent "in-flight HTTP request at teardown" warning when
684
+ * the test harness reads pendingCount() synchronously right after the
685
+ * last awaited test step. */
674
686
  static JSValue mik__http_pending_count(JSContext* ctx, JSValue this_val, int argc,
675
687
  JSValue* argv) {
676
688
  MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
677
689
  CHECK_NOT_NULL(mik_rt);
678
690
  if (!mik__http_st(mik_rt)) return JS_NewUint32(ctx, 0);
679
- return JS_NewUint32(ctx, mik__http_st(mik_rt)->pending_count);
691
+ mik__http_consume(ctx);
692
+ /* Exclude explicitly-cancelled entries. JS has signalled intent to
693
+ * release; the entry stays alive only until the native task wakes
694
+ * from a blocking call (TLS handshake, DNS) and posts its terminal
695
+ * message. That's not a leak — counting it as one fires false
696
+ * "in-flight HTTP at teardown" warnings on tests that abort or
697
+ * time out a request and exit before the native task has noticed. */
698
+ MIKHttpState* state = mik__http_st(mik_rt);
699
+ uint32_t live = 0;
700
+ for (size_t i = 0; i < state->pending_count; i++) {
701
+ if (!state->pending[i].js_cancelled) live++;
702
+ }
703
+ return JS_NewUint32(ctx, live);
680
704
  }
681
705
 
682
706
  /* cancel(id) — signal a pending request to abort */
@@ -0,0 +1,269 @@
1
+ #include "mikrojs/mikrojs.h"
2
+ #include "mikrojs/platform.h"
3
+ #include "mikrojs/private.h"
4
+ #include "mikrojs_esp32.h"
5
+
6
+ #include <cstdarg>
7
+ #include <cstdio>
8
+ #include <cstring>
9
+ #include <ctime>
10
+ #include <sys/stat.h>
11
+ #include <sys/time.h>
12
+ #include <unistd.h>
13
+
14
+ #include "esp_log.h"
15
+ #include "esp_timer.h"
16
+ #include "freertos/FreeRTOS.h"
17
+ #include "freertos/semphr.h"
18
+
19
+ static const char* TAG = "mik_logfile";
20
+
21
+ /* Static buffers — total ~1.5 KiB plus a FreeRTOS mutex. */
22
+ static char s_line_buf[512]; /* current line being assembled */
23
+ static char s_stdio_buf[1024]; /* setvbuf target for the FILE* */
24
+ static char s_path_main[96]; /* "<dir>/log.txt" */
25
+ static char s_path_rot[100]; /* "<dir>/log.txt.1" */
26
+ static SemaphoreHandle_t s_mtx = nullptr;
27
+ static FILE* s_file = nullptr;
28
+ static size_t s_file_size = 0;
29
+ static size_t s_line_pos = 0;
30
+ static bool s_line_has_ts = false;
31
+ static uint32_t s_max_size = 0;
32
+ static MIKLogFlush s_flush_policy = MIK_LOG_FLUSH_ERROR;
33
+ static vprintf_like_t s_prev_vprintf = nullptr;
34
+
35
+ /* Time threshold for treating the RTC as wall-clock-set (Jan 1 2020). */
36
+ static const time_t WALL_CLOCK_THRESHOLD = 1577836800;
37
+
38
+ static int format_timestamp(char* buf, size_t buf_size) {
39
+ struct timeval tv;
40
+ gettimeofday(&tv, nullptr);
41
+ if (tv.tv_sec > WALL_CLOCK_THRESHOLD) {
42
+ struct tm tm;
43
+ gmtime_r(&tv.tv_sec, &tm);
44
+ return snprintf(buf, buf_size, "%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ ",
45
+ tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
46
+ tm.tm_sec, (long)(tv.tv_usec / 1000));
47
+ }
48
+ int64_t us = esp_timer_get_time();
49
+ int64_t s = us / 1000000;
50
+ int64_t ms = (us / 1000) % 1000;
51
+ return snprintf(buf, buf_size, "[+%lld.%03llds] ", (long long)s, (long long)ms);
52
+ }
53
+
54
+ /* Recognize lines that should trigger an immediate fflush under flush='error'.
55
+ * Patterns: ESP_LOG ("E (12345)" / "W (12345)") and mikrojs platform.log
56
+ * ("[ERROR]" / "[WARN]"). False negatives just delay the flush — they don't
57
+ * lose data — so a conservative match is fine. */
58
+ static bool line_is_error_level(const char* line, size_t len) {
59
+ if (len >= 2 && (line[0] == 'E' || line[0] == 'W') && line[1] == ' ') return true;
60
+ if (len >= 7 && memcmp(line, "[ERROR]", 7) == 0) return true;
61
+ if (len >= 6 && memcmp(line, "[WARN]", 6) == 0) return true;
62
+ return false;
63
+ }
64
+
65
+ /* Caller holds s_mtx. */
66
+ static void rotate_if_needed() {
67
+ if (s_file_size < s_max_size) return;
68
+ fclose(s_file);
69
+ s_file = nullptr;
70
+ unlink(s_path_rot);
71
+ rename(s_path_main, s_path_rot);
72
+ s_file = fopen(s_path_main, "a");
73
+ if (s_file) {
74
+ setvbuf(s_file, s_stdio_buf, _IOFBF, sizeof(s_stdio_buf));
75
+ s_file_size = 0;
76
+ }
77
+ }
78
+
79
+ /* Caller holds s_mtx. */
80
+ static void emit_line() {
81
+ if (s_line_pos == 0) return;
82
+ if (!s_file) {
83
+ s_line_pos = 0;
84
+ s_line_has_ts = false;
85
+ return;
86
+ }
87
+ size_t n = fwrite(s_line_buf, 1, s_line_pos, s_file);
88
+ s_file_size += n;
89
+ bool is_err = line_is_error_level(s_line_buf, s_line_pos);
90
+ if (s_flush_policy == MIK_LOG_FLUSH_LINE ||
91
+ (s_flush_policy == MIK_LOG_FLUSH_ERROR && is_err)) {
92
+ fflush(s_file);
93
+ }
94
+ rotate_if_needed();
95
+ s_line_pos = 0;
96
+ s_line_has_ts = false;
97
+ }
98
+
99
+ /* Caller holds s_mtx. */
100
+ static void append_byte(uint8_t c) {
101
+ if (!s_line_has_ts) {
102
+ int n = format_timestamp(s_line_buf, sizeof(s_line_buf) - 1);
103
+ if (n < 0 || (size_t)n >= sizeof(s_line_buf) - 1) n = 0;
104
+ s_line_pos = (size_t)n;
105
+ s_line_has_ts = true;
106
+ }
107
+ if (s_line_pos < sizeof(s_line_buf) - 1) {
108
+ s_line_buf[s_line_pos++] = (char)c;
109
+ }
110
+ if (c == '\n' || s_line_pos >= sizeof(s_line_buf) - 1) {
111
+ if (s_line_pos == 0 || s_line_buf[s_line_pos - 1] != '\n') {
112
+ s_line_buf[s_line_pos++] = '\n';
113
+ }
114
+ emit_line();
115
+ }
116
+ }
117
+
118
+ static void console_tap(const void* buf, size_t len) {
119
+ if (!s_mtx) return;
120
+ if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(50)) != pdTRUE) return;
121
+ const uint8_t* p = (const uint8_t*)buf;
122
+ for (size_t i = 0; i < len; i++) append_byte(p[i]);
123
+ xSemaphoreGive(s_mtx);
124
+ }
125
+
126
+ /* Fired by mik__repl_proto_send_output before TLV framing. Gives us the
127
+ * un-wrapped body bytes plus the message type, so console.error etc.
128
+ * forces an immediate flush without relying on prefix pattern matching. */
129
+ static void log_emit_tap(uint8_t msg_type, const void* data, size_t len) {
130
+ if (!s_mtx) return;
131
+ if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(50)) != pdTRUE) return;
132
+ const uint8_t* p = (const uint8_t*)data;
133
+ for (size_t i = 0; i < len; i++) append_byte(p[i]);
134
+ /* Ensure the line is closed even if the body lacks a trailing newline
135
+ * (mik__repl_proto_send_output is called once per logical message). */
136
+ if (s_line_pos > 0 && s_line_buf[s_line_pos - 1] != '\n') {
137
+ append_byte((uint8_t)'\n');
138
+ }
139
+ if (s_file && (msg_type == MIK_MSG_ERROR || msg_type == MIK_MSG_WARN ||
140
+ msg_type == MIK_MSG_EVAL_ERROR)) {
141
+ fflush(s_file);
142
+ }
143
+ xSemaphoreGive(s_mtx);
144
+ }
145
+
146
+ static int log_vprintf_tap(const char* fmt, va_list args) {
147
+ /* Forward to the previously-installed vprintf so terminal output is
148
+ * unchanged, then format a copy into the line buffer for the file. */
149
+ va_list copy;
150
+ va_copy(copy, args);
151
+ int rv = s_prev_vprintf ? s_prev_vprintf(fmt, args) : vprintf(fmt, args);
152
+
153
+ if (s_mtx) {
154
+ char line[256];
155
+ int n = vsnprintf(line, sizeof(line), fmt, copy);
156
+ if (n > 0) {
157
+ if ((size_t)n >= sizeof(line)) n = (int)sizeof(line) - 1;
158
+ if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(50)) == pdTRUE) {
159
+ for (int i = 0; i < n; i++) append_byte((uint8_t)line[i]);
160
+ xSemaphoreGive(s_mtx);
161
+ }
162
+ }
163
+ }
164
+ va_end(copy);
165
+ return rv;
166
+ }
167
+
168
+ void mik_logfile_init(const MIKConfig* config) {
169
+ const MIKPlatform* platform = MIK_GetPlatform();
170
+ if (!config || config->log_dir[0] == '\0') return;
171
+ if (s_file) return;
172
+
173
+ /* Ensure the directory exists. mkdir errors when it already exists,
174
+ * which is fine. */
175
+ mkdir(config->log_dir, 0775);
176
+
177
+ if ((size_t)snprintf(s_path_main, sizeof(s_path_main), "%s/log.txt", config->log_dir) >=
178
+ sizeof(s_path_main)) {
179
+ platform->log(MIK_LOG_WARN, TAG, "log dir path too long");
180
+ return;
181
+ }
182
+ snprintf(s_path_rot, sizeof(s_path_rot), "%s.1", s_path_main);
183
+
184
+ s_mtx = xSemaphoreCreateMutex();
185
+ if (!s_mtx) return;
186
+
187
+ s_file = fopen(s_path_main, "a");
188
+ if (!s_file) {
189
+ platform->log(MIK_LOG_WARN, TAG, "Could not open log file");
190
+ vSemaphoreDelete(s_mtx);
191
+ s_mtx = nullptr;
192
+ return;
193
+ }
194
+ setvbuf(s_file, s_stdio_buf, _IOFBF, sizeof(s_stdio_buf));
195
+ fseek(s_file, 0, SEEK_END);
196
+ long pos = ftell(s_file);
197
+ s_file_size = pos > 0 ? (size_t)pos : 0;
198
+ s_max_size = config->log_max_size;
199
+ s_flush_policy = config->log_flush;
200
+
201
+ mik__console_set_tap(console_tap);
202
+ mik__set_log_emit_tap(log_emit_tap);
203
+ s_prev_vprintf = esp_log_set_vprintf(log_vprintf_tap);
204
+
205
+ platform->log(MIK_LOG_INFO, TAG, "File log enabled (size=%u, max=%u)",
206
+ (unsigned)s_file_size, (unsigned)s_max_size);
207
+ }
208
+
209
+ void mik_logfile_suspend(void) {
210
+ if (!s_mtx) return;
211
+ if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(100)) != pdTRUE) return;
212
+ if (s_file) {
213
+ fflush(s_file);
214
+ fclose(s_file);
215
+ s_file = nullptr;
216
+ }
217
+ xSemaphoreGive(s_mtx);
218
+ }
219
+
220
+ void mik_logfile_resume(void) {
221
+ if (!s_mtx) return;
222
+ if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(100)) != pdTRUE) return;
223
+ if (!s_file) {
224
+ s_file = fopen(s_path_main, "a");
225
+ if (s_file) {
226
+ setvbuf(s_file, s_stdio_buf, _IOFBF, sizeof(s_stdio_buf));
227
+ fseek(s_file, 0, SEEK_END);
228
+ long pos = ftell(s_file);
229
+ s_file_size = pos > 0 ? (size_t)pos : 0;
230
+ }
231
+ }
232
+ xSemaphoreGive(s_mtx);
233
+ }
234
+
235
+ void mik_logfile_flush(void) {
236
+ if (!s_mtx || !s_file) return;
237
+ if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(50)) != pdTRUE) return;
238
+ fflush(s_file);
239
+ xSemaphoreGive(s_mtx);
240
+ }
241
+
242
+ void mik_logfile_close(void) {
243
+ if (!s_mtx) return;
244
+ if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(100)) != pdTRUE) return;
245
+ mik__console_set_tap(nullptr);
246
+ mik__set_log_emit_tap(nullptr);
247
+ if (s_prev_vprintf) {
248
+ esp_log_set_vprintf(s_prev_vprintf);
249
+ s_prev_vprintf = nullptr;
250
+ }
251
+ if (s_line_pos > 0) {
252
+ if (s_line_buf[s_line_pos - 1] != '\n' && s_line_pos < sizeof(s_line_buf)) {
253
+ s_line_buf[s_line_pos++] = '\n';
254
+ }
255
+ if (s_file) fwrite(s_line_buf, 1, s_line_pos, s_file);
256
+ s_line_pos = 0;
257
+ }
258
+ if (s_file) {
259
+ fflush(s_file);
260
+ fclose(s_file);
261
+ s_file = nullptr;
262
+ }
263
+ s_file_size = 0;
264
+ s_line_has_ts = false;
265
+ SemaphoreHandle_t mtx = s_mtx;
266
+ s_mtx = nullptr;
267
+ xSemaphoreGive(mtx);
268
+ vSemaphoreDelete(mtx);
269
+ }
@@ -127,6 +127,7 @@ bool mik__handle_deploy_command(MIKReplTransport* transport, uint8_t cmd_type,
127
127
  uint32_t payload_len);
128
128
  bool mik__handle_config_command(MIKReplTransport* transport, uint8_t cmd_type,
129
129
  uint32_t payload_len);
130
+ bool mik__handle_fs_get(MIKReplTransport* transport, uint32_t payload_len);
130
131
  void mik__deploy_session_reset(void);
131
132
 
132
133
  static void platform_session_end(void* ctx) {
@@ -163,6 +164,11 @@ static bool platform_command_handler(MIKReplTransport* transport, uint8_t cmd_ty
163
164
  return mik__handle_deploy_command(transport, cmd_type, payload_len);
164
165
  }
165
166
 
167
+ /* File pull (0x2B) */
168
+ if (cmd_type == MIK_CMD_FS_GET) {
169
+ return mik__handle_fs_get(transport, payload_len);
170
+ }
171
+
166
172
  /* Config commands (0x40-0x42) */
167
173
  if (cmd_type >= MIK_CMD_CONFIG_LIST && cmd_type <= MIK_CMD_CONFIG_DELETE) {
168
174
  return mik__handle_config_command(transport, cmd_type, payload_len);
@@ -277,6 +283,9 @@ void MIK_Main(void) {
277
283
  MIKConfig app_config;
278
284
  MIK_LoadConfig("/appfs", &app_config);
279
285
 
286
+ /* Start file logging if configured (no-op when log_file is empty). */
287
+ mik_logfile_init(&app_config);
288
+
280
289
  /* Create JS runtime — reserve heap for WiFi, TLS, HTTP, LittleFS, and other
281
290
  * ESP-IDF subsystems that allocate after the runtime is created.
282
291
  * WiFi + lwIP ≈ 50-65 KB, TLS ≈ 40 KB, HTTP task ≈ 8 KB, misc ≈ 15 KB. */
@@ -162,7 +162,21 @@ int mik__console_read(void* buf, size_t len) {
162
162
  #endif
163
163
  }
164
164
 
165
+ static mik_console_tap_fn s_console_tap = nullptr;
166
+
167
+ void mik__console_set_tap(mik_console_tap_fn fn) {
168
+ s_console_tap = fn;
169
+ }
170
+
165
171
  int mik__console_write(const void* buf, size_t len) {
172
+ /* Mirror to file-log tap (if installed) before the host write. This
173
+ * captures output even when USB-JTAG has no host attached. Skip the
174
+ * tap while a TLV protocol frame is being written — the file logger
175
+ * has a higher-level tap on the un-framed body via the log-emit
176
+ * hook, so capturing the wire bytes here would just embed the
177
+ * [type][len] header in the log file. */
178
+ mik_console_tap_fn tap = s_console_tap;
179
+ if (tap && !mik__proto_send_in_progress) tap(buf, len);
166
180
  #if SOC_USB_SERIAL_JTAG_SUPPORTED
167
181
  if (s_console == CONSOLE_USB_SERIAL_JTAG) {
168
182
  /* No host attached: the ring buffer would fill and block writes
@@ -260,6 +260,7 @@ struct MIKUartIterState {
260
260
  * iterator could outlive its parent Uart, leading to a UAF here. */
261
261
  MIKUartState* uart;
262
262
  JSValue uart_jsval;
263
+ bool ended; // sticky: once true, next() returns {done:true} immediately
263
264
  };
264
265
 
265
266
  static void mik__uart_iter_finalizer(JSRuntime* rt, JSValue val) {
@@ -286,7 +287,24 @@ static JSClassDef mik_uart_iter_class = {
286
287
  .gc_mark = mik__uart_iter_gc_mark,
287
288
  };
288
289
 
289
- /* Try to read available data and return {done: false, value: Uint8Array} synchronously */
290
+ /* Build {done:false, value: Result<Uint8Array, UartError>} wrapper. Takes
291
+ * ownership of the inner Result value. */
292
+ static JSValue mik__uart_iter_yield(JSContext* ctx, JSValue inner_result) {
293
+ JSValue out = JS_NewObject(ctx);
294
+ JS_DefinePropertyValueStr(ctx, out, "done", JS_FALSE, JS_PROP_C_W_E);
295
+ JS_DefinePropertyValueStr(ctx, out, "value", inner_result, JS_PROP_C_W_E);
296
+ return out;
297
+ }
298
+
299
+ static JSValue mik__uart_iter_done(JSContext* ctx) {
300
+ JSValue out = JS_NewObject(ctx);
301
+ JS_DefinePropertyValueStr(ctx, out, "done", JS_TRUE, JS_PROP_C_W_E);
302
+ JS_DefinePropertyValueStr(ctx, out, "value", JS_UNDEFINED, JS_PROP_C_W_E);
303
+ return out;
304
+ }
305
+
306
+ /* Try to read available data and return {done:false, value: ok(Uint8Array)}
307
+ * synchronously, or JS_UNDEFINED if no data is buffered yet. */
290
308
  static JSValue mik__uart_try_read(JSContext* ctx, MIKUartState* s) {
291
309
  size_t buffered = 0;
292
310
  uart_get_buffered_data_len(s->port, &buffered);
@@ -302,18 +320,27 @@ static JSValue mik__uart_try_read(JSContext* ctx, MIKUartState* s) {
302
320
  }
303
321
 
304
322
  JSValue arr = MIK_NewUint8Array(ctx, buf, read);
305
- JSValue result = JS_NewObject(ctx);
306
- JS_DefinePropertyValueStr(ctx, result, "done", JS_FALSE, JS_PROP_C_W_E);
307
- JS_DefinePropertyValueStr(ctx, result, "value", arr, JS_PROP_C_W_E);
308
- return result;
323
+ return mik__uart_iter_yield(ctx, mik__result_ok(ctx, arr));
309
324
  }
310
325
 
311
326
  static JSValue js_uart_iter_next(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
312
327
  auto* it = static_cast<MIKUartIterState*>(JS_GetOpaque2(ctx, this_val, mik_uart_iter_class_id));
313
328
  if (!it || !it->uart) return JS_ThrowInternalError(ctx, "invalid uart iterator");
329
+
330
+ if (it->ended) {
331
+ JSValue done_result = mik__uart_iter_done(ctx);
332
+ return MIK_NewResolvedPromise(ctx, 1, &done_result);
333
+ }
334
+
314
335
  MIKUartState* s = it->uart;
315
336
 
316
- if (!s->begun) return JS_Throw(ctx, JS_NewString(ctx, "UART not started"));
337
+ /* If end() has invalidated the underlying port, surface NotStarted as
338
+ * a single err item and mark the iterator done. */
339
+ if (!s->begun) {
340
+ it->ended = true;
341
+ JSValue err_yield = mik__uart_iter_yield(ctx, mik__result_err_tag(ctx, "NotStarted"));
342
+ return MIK_NewResolvedPromise(ctx, 1, &err_yield);
343
+ }
317
344
 
318
345
  /* Try synchronous read first */
319
346
  JSValue sync_result = mik__uart_try_read(ctx, s);
@@ -468,9 +495,7 @@ void mik__uart_consume(JSContext* ctx) {
468
495
  }
469
496
 
470
497
  JSValue arr = MIK_NewUint8Array(ctx, buf, read_bytes);
471
- JSValue result = JS_NewObject(ctx);
472
- JS_DefinePropertyValueStr(ctx, result, "done", JS_FALSE, JS_PROP_C_W_E);
473
- JS_DefinePropertyValueStr(ctx, result, "value", arr, JS_PROP_C_W_E);
498
+ JSValue result = mik__uart_iter_yield(ctx, mik__result_ok(ctx, arr));
474
499
  MIK_ResolvePromise(ctx, &s->read_promise, 1, &result);
475
500
  MIK_ClearPromise(ctx, &s->read_promise);
476
501
  }
@@ -182,12 +182,22 @@ static void esp32_log(int level, const char* tag, const char* fmt, ...) {
182
182
  esp_log_level_t tag_level = esp_log_level_get(tag);
183
183
  if (tag_level == ESP_LOG_NONE || esp_level > tag_level) return;
184
184
 
185
- printf("[%s] %s: ", mik_log_level_name(level), tag);
185
+ /* Route through mik__console_write rather than printf so the file-log
186
+ * tap captures these lines along with JS console output. */
187
+ char buf[256];
188
+ int n = snprintf(buf, sizeof(buf), "[%s] %s: ", mik_log_level_name(level), tag);
189
+ if (n < 0) return;
190
+ if ((size_t)n >= sizeof(buf)) n = (int)sizeof(buf) - 1;
186
191
  va_list args;
187
192
  va_start(args, fmt);
188
- vprintf(fmt, args);
193
+ int m = vsnprintf(buf + n, sizeof(buf) - (size_t)n, fmt, args);
189
194
  va_end(args);
190
- putchar('\n');
195
+ if (m < 0) m = 0;
196
+ size_t total = (size_t)n + (size_t)m;
197
+ if (total >= sizeof(buf)) total = sizeof(buf) - 1;
198
+ /* Replace trailing NUL with newline; if exactly full, overwrite last byte. */
199
+ buf[total] = '\n';
200
+ mik__console_write(buf, total + 1);
191
201
  }
192
202
 
193
203
  static const MIKPlatform esp32_platform = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/firmware",
3
- "version": "0.6.0",
3
+ "version": "0.8.0-pr-85.g6768f8c",
4
4
  "description": "Mikro.js ESP32 firmware: ESP-IDF component, build tools, and project template",
5
5
  "keywords": [
6
6
  "esp-idf",
@@ -51,8 +51,8 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "esbuild": "^0.28.0",
54
- "@mikrojs/native": "0.6.0",
55
- "@mikrojs/quickjs": "0.6.0"
54
+ "@mikrojs/native": "0.8.0-pr-85.g6768f8c",
55
+ "@mikrojs/quickjs": "0.8.0-pr-85.g6768f8c"
56
56
  },
57
57
  "engines": {
58
58
  "node": ">=24.0.0"
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -132,13 +132,3 @@ CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=n
132
132
  CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y
133
133
  CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y
134
134
 
135
- # TLS record buffers must come from internal SRAM. The ESP-IDF default for
136
- # the inbound buffer is 16 KB, which competes with QuickJS heap allocations
137
- # for contiguous internal SRAM and causes HTTPS handshake failures (error
138
- # 0x7002) on apps with significant retained JS state. Halving it to 8 KB
139
- # leaves enough room for typical TLS records (handshake fragments are well
140
- # under 4 KB; modern servers rarely send >4 KB application records, and
141
- # mbedTLS will fragment larger records). The outbound buffer was already
142
- # 4 KB via CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y.
143
- CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=8192
144
-