@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.
- package/components/mikrojs/CMakeLists.txt +1 -0
- package/components/mikrojs/include/mikrojs_esp32.h +20 -0
- package/components/mikrojs/mik_deploy.cpp +50 -0
- package/components/mikrojs/mik_http.cpp +26 -2
- package/components/mikrojs/mik_logfile.cpp +269 -0
- package/components/mikrojs/mik_main.cpp +9 -0
- package/components/mikrojs/mik_serial_io.cpp +14 -0
- package/components/mikrojs/mik_uart.cpp +34 -9
- package/components/mikrojs/platform_esp32.cpp +13 -3
- package/package.json +3 -3
- package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32/mikrojs.bin +0 -0
- package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c3/mikrojs.bin +0 -0
- package/prebuilds/esp32c5/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c5/mikrojs.bin +0 -0
- package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c6/mikrojs.bin +0 -0
- package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32s3/mikrojs.bin +0 -0
- package/sdkconfig.defaults +0 -10
|
@@ -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
|
-
|
|
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
|
-
/*
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
193
|
+
int m = vsnprintf(buf + n, sizeof(buf) - (size_t)n, fmt, args);
|
|
189
194
|
va_end(args);
|
|
190
|
-
|
|
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.
|
|
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.
|
|
55
|
-
"@mikrojs/quickjs": "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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/sdkconfig.defaults
CHANGED
|
@@ -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
|
-
|