@mikrojs/firmware 0.6.1 → 0.7.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 +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 +37 -4
- 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 +66 -13
- 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
|
@@ -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,48 @@ static JSValue mik__http_next_message(JSContext* ctx, JSValue this_val, int argc
|
|
|
668
668
|
return promise;
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
-
/*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
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
|
+
|
|
675
|
+
/* pendingCount() — number of in-flight requests with work still visible to
|
|
676
|
+
* JS (headers/body not yet delivered, not cancelled). Useful for tests that
|
|
677
|
+
* want to verify cancel+drain freed a slot without relying on heap-headroom
|
|
678
|
+
* for a follow-up request.
|
|
679
|
+
*
|
|
680
|
+
* Semantic shift vs. a "raw slot count": this returns the JS-visible pending
|
|
681
|
+
* count, not the native slot count. Cancelled-but-not-yet-drained entries
|
|
682
|
+
* still occupy a native slot (until the BG task wakes from a blocking call
|
|
683
|
+
* and posts its terminal message) but are excluded from this count. If you
|
|
684
|
+
* need to know whether a follow-up `request()` will hit TooManyPending, this
|
|
685
|
+
* is not the right number — the slot may still be held. See
|
|
686
|
+
* MIK_HTTP_MAX_PENDING and the js_cancelled exclusion below.
|
|
687
|
+
*
|
|
688
|
+
* Drains result_queue first via the consume loop. Without this, a request
|
|
689
|
+
* whose native task has already posted its terminal message but whose
|
|
690
|
+
* message hasn't been processed by the next event-loop tick reads as
|
|
691
|
+
* "still pending" — even though it'd drop microseconds later. Surfaced
|
|
692
|
+
* as an intermittent "in-flight HTTP request at teardown" warning when
|
|
693
|
+
* the test harness reads pendingCount() synchronously right after the
|
|
694
|
+
* last awaited test step. */
|
|
674
695
|
static JSValue mik__http_pending_count(JSContext* ctx, JSValue this_val, int argc,
|
|
675
696
|
JSValue* argv) {
|
|
676
697
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
677
698
|
CHECK_NOT_NULL(mik_rt);
|
|
678
699
|
if (!mik__http_st(mik_rt)) return JS_NewUint32(ctx, 0);
|
|
679
|
-
|
|
700
|
+
mik__http_consume(ctx);
|
|
701
|
+
/* Exclude explicitly-cancelled entries. JS has signalled intent to
|
|
702
|
+
* release; the entry stays alive only until the native task wakes
|
|
703
|
+
* from a blocking call (TLS handshake, DNS) and posts its terminal
|
|
704
|
+
* message. That's not a leak — counting it as one fires false
|
|
705
|
+
* "in-flight HTTP at teardown" warnings on tests that abort or
|
|
706
|
+
* time out a request and exit before the native task has noticed. */
|
|
707
|
+
MIKHttpState* state = mik__http_st(mik_rt);
|
|
708
|
+
uint32_t live = 0;
|
|
709
|
+
for (size_t i = 0; i < state->pending_count; i++) {
|
|
710
|
+
if (!state->pending[i].js_cancelled) live++;
|
|
711
|
+
}
|
|
712
|
+
return JS_NewUint32(ctx, live);
|
|
680
713
|
}
|
|
681
714
|
|
|
682
715
|
/* 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
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
static JSClassID mik_uart_class_id;
|
|
13
13
|
static int mik__uart_slot = -1;
|
|
14
14
|
|
|
15
|
+
/* Forward decl: defined alongside the iterator class further down. The Uart
|
|
16
|
+
* state holds a non-owning backpointer so end() can mark the active iterator
|
|
17
|
+
* as exhausted without having to reach through JS. */
|
|
18
|
+
struct MIKUartIterState;
|
|
19
|
+
|
|
15
20
|
/* ── State ────────────────────────────────────────────────────────── */
|
|
16
21
|
|
|
17
22
|
struct MIKUartState {
|
|
@@ -22,6 +27,11 @@ struct MIKUartState {
|
|
|
22
27
|
bool begun;
|
|
23
28
|
bool reading; // active read() iterator exists
|
|
24
29
|
MIKPromise read_promise; // pending next() promise (when waiting for data)
|
|
30
|
+
/* Non-owning ref to the active iterator (when reading == true). The iter
|
|
31
|
+
* holds a strong ref to this Uart via uart_jsval, so this back-edge is
|
|
32
|
+
* always cleared (iter_return / finalizer) before the Uart can outlive
|
|
33
|
+
* the iterator. */
|
|
34
|
+
MIKUartIterState* iter;
|
|
25
35
|
};
|
|
26
36
|
|
|
27
37
|
/* Per-runtime tracking of all Uart instances for the loop consumer */
|
|
@@ -98,6 +108,7 @@ static JSValue js_uart_constructor(JSContext* ctx, JSValue new_target, int argc,
|
|
|
98
108
|
s->baud_rate = 115200;
|
|
99
109
|
s->begun = false;
|
|
100
110
|
s->reading = false;
|
|
111
|
+
s->iter = nullptr;
|
|
101
112
|
s->read_promise.p = JS_UNDEFINED;
|
|
102
113
|
s->read_promise.rfuncs[0] = JS_UNDEFINED;
|
|
103
114
|
s->read_promise.rfuncs[1] = JS_UNDEFINED;
|
|
@@ -193,19 +204,29 @@ static JSValue js_uart_begin(JSContext* ctx, JSValue this_val, int argc, JSValue
|
|
|
193
204
|
return mik__result_ok_void(ctx);
|
|
194
205
|
}
|
|
195
206
|
|
|
207
|
+
/* Forward decls — bodies live with the iterator class. */
|
|
208
|
+
static JSValue mik__uart_iter_yield(JSContext* ctx, JSValue inner_result);
|
|
209
|
+
static void mik__uart_iter_mark_ended(MIKUartIterState* it);
|
|
210
|
+
|
|
196
211
|
static JSValue js_uart_end(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
197
212
|
auto* s = mik__uart_get(ctx, this_val);
|
|
198
213
|
if (!s) return JS_EXCEPTION;
|
|
199
214
|
if (!s->begun) return mik__result_ok_void(ctx); // idempotent
|
|
200
215
|
|
|
201
|
-
/*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
216
|
+
/* If a read() iterator is active, deliver a terminal err item so any
|
|
217
|
+
* awaiting next() unblocks with err(NotStarted) rather than hanging.
|
|
218
|
+
* Also flip the iterator's `ended` flag so its next call resolves with
|
|
219
|
+
* {done:true} — without this, the awaiter consumes the err yield, then
|
|
220
|
+
* the next call hits the `!s->begun` branch in iter_next and emits a
|
|
221
|
+
* second err yield. The active iterator backpointer makes that flip
|
|
222
|
+
* possible without reaching through JS. */
|
|
204
223
|
if (s->reading) {
|
|
205
224
|
if (MIK_IsPromisePending(ctx, &s->read_promise)) {
|
|
206
|
-
|
|
225
|
+
JSValue err_yield = mik__uart_iter_yield(ctx, mik__result_err_tag(ctx, "NotStarted"));
|
|
226
|
+
MIK_ResolvePromise(ctx, &s->read_promise, 1, &err_yield);
|
|
207
227
|
MIK_ClearPromise(ctx, &s->read_promise);
|
|
208
228
|
}
|
|
229
|
+
if (s->iter) mik__uart_iter_mark_ended(s->iter);
|
|
209
230
|
s->reading = false;
|
|
210
231
|
}
|
|
211
232
|
|
|
@@ -260,6 +281,7 @@ struct MIKUartIterState {
|
|
|
260
281
|
* iterator could outlive its parent Uart, leading to a UAF here. */
|
|
261
282
|
MIKUartState* uart;
|
|
262
283
|
JSValue uart_jsval;
|
|
284
|
+
bool ended; // sticky: once true, next() returns {done:true} immediately
|
|
263
285
|
};
|
|
264
286
|
|
|
265
287
|
static void mik__uart_iter_finalizer(JSRuntime* rt, JSValue val) {
|
|
@@ -269,11 +291,16 @@ static void mik__uart_iter_finalizer(JSRuntime* rt, JSValue val) {
|
|
|
269
291
|
* safe here — except after iterator.return() nulled it out. */
|
|
270
292
|
if (it->uart) {
|
|
271
293
|
it->uart->reading = false;
|
|
294
|
+
if (it->uart->iter == it) it->uart->iter = nullptr;
|
|
272
295
|
}
|
|
273
296
|
JS_FreeValueRT(rt, it->uart_jsval);
|
|
274
297
|
free(it);
|
|
275
298
|
}
|
|
276
299
|
|
|
300
|
+
static void mik__uart_iter_mark_ended(MIKUartIterState* it) {
|
|
301
|
+
if (it) it->ended = true;
|
|
302
|
+
}
|
|
303
|
+
|
|
277
304
|
static void mik__uart_iter_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
278
305
|
auto* it = static_cast<MIKUartIterState*>(JS_GetOpaque(val, mik_uart_iter_class_id));
|
|
279
306
|
if (!it) return;
|
|
@@ -286,7 +313,24 @@ static JSClassDef mik_uart_iter_class = {
|
|
|
286
313
|
.gc_mark = mik__uart_iter_gc_mark,
|
|
287
314
|
};
|
|
288
315
|
|
|
289
|
-
/*
|
|
316
|
+
/* Build {done:false, value: Result<Uint8Array, UartError>} wrapper. Takes
|
|
317
|
+
* ownership of the inner Result value. */
|
|
318
|
+
static JSValue mik__uart_iter_yield(JSContext* ctx, JSValue inner_result) {
|
|
319
|
+
JSValue out = JS_NewObject(ctx);
|
|
320
|
+
JS_DefinePropertyValueStr(ctx, out, "done", JS_FALSE, JS_PROP_C_W_E);
|
|
321
|
+
JS_DefinePropertyValueStr(ctx, out, "value", inner_result, JS_PROP_C_W_E);
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
static JSValue mik__uart_iter_done(JSContext* ctx) {
|
|
326
|
+
JSValue out = JS_NewObject(ctx);
|
|
327
|
+
JS_DefinePropertyValueStr(ctx, out, "done", JS_TRUE, JS_PROP_C_W_E);
|
|
328
|
+
JS_DefinePropertyValueStr(ctx, out, "value", JS_UNDEFINED, JS_PROP_C_W_E);
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Try to read available data and return {done:false, value: ok(Uint8Array)}
|
|
333
|
+
* synchronously, or JS_UNDEFINED if no data is buffered yet. */
|
|
290
334
|
static JSValue mik__uart_try_read(JSContext* ctx, MIKUartState* s) {
|
|
291
335
|
size_t buffered = 0;
|
|
292
336
|
uart_get_buffered_data_len(s->port, &buffered);
|
|
@@ -302,18 +346,27 @@ static JSValue mik__uart_try_read(JSContext* ctx, MIKUartState* s) {
|
|
|
302
346
|
}
|
|
303
347
|
|
|
304
348
|
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;
|
|
349
|
+
return mik__uart_iter_yield(ctx, mik__result_ok(ctx, arr));
|
|
309
350
|
}
|
|
310
351
|
|
|
311
352
|
static JSValue js_uart_iter_next(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
312
353
|
auto* it = static_cast<MIKUartIterState*>(JS_GetOpaque2(ctx, this_val, mik_uart_iter_class_id));
|
|
313
354
|
if (!it || !it->uart) return JS_ThrowInternalError(ctx, "invalid uart iterator");
|
|
355
|
+
|
|
356
|
+
if (it->ended) {
|
|
357
|
+
JSValue done_result = mik__uart_iter_done(ctx);
|
|
358
|
+
return MIK_NewResolvedPromise(ctx, 1, &done_result);
|
|
359
|
+
}
|
|
360
|
+
|
|
314
361
|
MIKUartState* s = it->uart;
|
|
315
362
|
|
|
316
|
-
|
|
363
|
+
/* If end() has invalidated the underlying port, surface NotStarted as
|
|
364
|
+
* a single err item and mark the iterator done. */
|
|
365
|
+
if (!s->begun) {
|
|
366
|
+
it->ended = true;
|
|
367
|
+
JSValue err_yield = mik__uart_iter_yield(ctx, mik__result_err_tag(ctx, "NotStarted"));
|
|
368
|
+
return MIK_NewResolvedPromise(ctx, 1, &err_yield);
|
|
369
|
+
}
|
|
317
370
|
|
|
318
371
|
/* Try synchronous read first */
|
|
319
372
|
JSValue sync_result = mik__uart_try_read(ctx, s);
|
|
@@ -343,6 +396,7 @@ static JSValue js_uart_iter_return(JSContext* ctx, JSValue this_val, int argc, J
|
|
|
343
396
|
}
|
|
344
397
|
|
|
345
398
|
it->uart->reading = false;
|
|
399
|
+
if (it->uart->iter == it) it->uart->iter = nullptr;
|
|
346
400
|
it->uart = nullptr;
|
|
347
401
|
|
|
348
402
|
done:
|
|
@@ -384,6 +438,7 @@ static JSValue js_uart_read(JSContext* ctx, JSValue this_val, int argc, JSValue*
|
|
|
384
438
|
return JS_EXCEPTION;
|
|
385
439
|
}
|
|
386
440
|
JS_SetOpaque(iter_obj, it);
|
|
441
|
+
s->iter = it;
|
|
387
442
|
|
|
388
443
|
return mik__result_ok(ctx, iter_obj);
|
|
389
444
|
}
|
|
@@ -468,9 +523,7 @@ void mik__uart_consume(JSContext* ctx) {
|
|
|
468
523
|
}
|
|
469
524
|
|
|
470
525
|
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);
|
|
526
|
+
JSValue result = mik__uart_iter_yield(ctx, mik__result_ok(ctx, arr));
|
|
474
527
|
MIK_ResolvePromise(ctx, &s->read_promise, 1, &result);
|
|
475
528
|
MIK_ClearPromise(ctx, &s->read_promise);
|
|
476
529
|
}
|
|
@@ -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.7.0",
|
|
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/
|
|
55
|
-
"@mikrojs/
|
|
54
|
+
"@mikrojs/native": "0.7.0",
|
|
55
|
+
"@mikrojs/quickjs": "0.7.0"
|
|
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
|