@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,525 @@
|
|
|
1
|
+
#include <atomic>
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
#include "driver/ledc.h"
|
|
5
|
+
#include "esp_log.h"
|
|
6
|
+
#include "mikrojs/mikrojs.h"
|
|
7
|
+
#include "mikrojs/private.h"
|
|
8
|
+
#include "mikrojs/utils.h"
|
|
9
|
+
|
|
10
|
+
#define MIK_PWM_TAG "native:pwm"
|
|
11
|
+
#define MIK_PWM_MAX_CHANNELS LEDC_CHANNEL_MAX
|
|
12
|
+
#define MIK_PWM_MAX_TIMERS LEDC_TIMER_MAX
|
|
13
|
+
#define MIK_PWM_MAX_PENDING_FADES 8
|
|
14
|
+
|
|
15
|
+
static JSClassID mik_pwm_class_id;
|
|
16
|
+
|
|
17
|
+
/* ── Channel / timer pool ──────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
static uint8_t s_channel_used = 0; // bitmask of allocated channels
|
|
20
|
+
static uint32_t s_timer_freq[MIK_PWM_MAX_TIMERS] = {};
|
|
21
|
+
static uint8_t s_timer_refcount[MIK_PWM_MAX_TIMERS] = {};
|
|
22
|
+
static bool s_fade_installed = false;
|
|
23
|
+
|
|
24
|
+
static int mik__pwm_alloc_channel() {
|
|
25
|
+
for (int i = 0; i < MIK_PWM_MAX_CHANNELS; i++) {
|
|
26
|
+
if (!(s_channel_used & (1 << i))) {
|
|
27
|
+
s_channel_used |= (1 << i);
|
|
28
|
+
return i;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static void mik__pwm_free_channel(int ch) {
|
|
35
|
+
if (ch >= 0 && ch < MIK_PWM_MAX_CHANNELS) {
|
|
36
|
+
s_channel_used &= ~(1 << ch);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static int mik__pwm_alloc_timer(uint32_t freq) {
|
|
41
|
+
/* Try to share an existing timer with the same frequency */
|
|
42
|
+
for (int i = 0; i < MIK_PWM_MAX_TIMERS; i++) {
|
|
43
|
+
if (s_timer_refcount[i] > 0 && s_timer_freq[i] == freq) {
|
|
44
|
+
s_timer_refcount[i]++;
|
|
45
|
+
return i;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/* Allocate a new timer */
|
|
49
|
+
for (int i = 0; i < MIK_PWM_MAX_TIMERS; i++) {
|
|
50
|
+
if (s_timer_refcount[i] == 0) {
|
|
51
|
+
s_timer_freq[i] = freq;
|
|
52
|
+
s_timer_refcount[i] = 1;
|
|
53
|
+
return i;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return -1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static void mik__pwm_free_timer(int timer) {
|
|
60
|
+
if (timer >= 0 && timer < MIK_PWM_MAX_TIMERS) {
|
|
61
|
+
if (s_timer_refcount[timer] > 0) {
|
|
62
|
+
s_timer_refcount[timer]--;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ── Resolution helper ─────────────────────────────────────────────── */
|
|
68
|
+
|
|
69
|
+
static ledc_timer_bit_t mik__pwm_best_resolution(uint32_t freq) {
|
|
70
|
+
/* Pick the highest resolution that works for this frequency.
|
|
71
|
+
* Max duty resolution = log2(APB_CLK_FREQ / freq).
|
|
72
|
+
* APB clock is typically 80 MHz. Clamp to LEDC limits. */
|
|
73
|
+
uint32_t apb_clk = 80000000;
|
|
74
|
+
int max_bits = 0;
|
|
75
|
+
uint32_t ratio = apb_clk / freq;
|
|
76
|
+
while (ratio > 1) {
|
|
77
|
+
ratio >>= 1;
|
|
78
|
+
max_bits++;
|
|
79
|
+
}
|
|
80
|
+
if (max_bits < 1) max_bits = 1;
|
|
81
|
+
if (max_bits > LEDC_TIMER_14_BIT) max_bits = LEDC_TIMER_14_BIT;
|
|
82
|
+
return static_cast<ledc_timer_bit_t>(max_bits);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── Per-instance state ────────────────────────────────────────────── */
|
|
86
|
+
|
|
87
|
+
typedef struct {
|
|
88
|
+
int gpio;
|
|
89
|
+
int channel;
|
|
90
|
+
int timer;
|
|
91
|
+
uint32_t freq;
|
|
92
|
+
ledc_timer_bit_t resolution;
|
|
93
|
+
double duty; // 0.0–1.0
|
|
94
|
+
bool active;
|
|
95
|
+
} MIKPwmState;
|
|
96
|
+
|
|
97
|
+
/* ── Fade tracking ─────────────────────────────────────────────────── */
|
|
98
|
+
|
|
99
|
+
struct MIKPwmFadePending {
|
|
100
|
+
int channel; // LEDC channel that is fading
|
|
101
|
+
MIKPromise promise;
|
|
102
|
+
std::atomic<bool> complete; // set from ISR
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/* Dynamic module data slot, allocated on first import */
|
|
106
|
+
static int mik__pwm_slot = -1;
|
|
107
|
+
|
|
108
|
+
/* Helper to access PWM module state from runtime */
|
|
109
|
+
static inline MIKPwmFadePending*& mik__pwm_fades(MIKRuntime* rt) {
|
|
110
|
+
return reinterpret_cast<MIKPwmFadePending*&>(rt->module_data[mik__pwm_slot]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static int s_fade_count = 0;
|
|
114
|
+
|
|
115
|
+
static IRAM_ATTR bool mik__pwm_fade_cb(const ledc_cb_param_t* param, void* user_arg) {
|
|
116
|
+
auto* pending = static_cast<MIKPwmFadePending*>(user_arg);
|
|
117
|
+
if (param->event == LEDC_FADE_END_EVT) {
|
|
118
|
+
pending->complete.store(true, std::memory_order_release);
|
|
119
|
+
}
|
|
120
|
+
return false; // no high-priority task woken
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ── Helpers ───────────────────────────────────────────────────────── */
|
|
124
|
+
|
|
125
|
+
static MIKPwmState* mik__pwm_get(JSContext* ctx, JSValue this_val) {
|
|
126
|
+
return static_cast<MIKPwmState*>(JS_GetOpaque2(ctx, this_val, mik_pwm_class_id));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static uint32_t mik__pwm_duty_to_raw(double duty, ledc_timer_bit_t resolution) {
|
|
130
|
+
uint32_t max_duty = (1u << resolution) - 1;
|
|
131
|
+
if (duty <= 0.0) return 0;
|
|
132
|
+
if (duty >= 1.0) return max_duty;
|
|
133
|
+
return static_cast<uint32_t>(duty * max_duty + 0.5);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ── Finalizer ─────────────────────────────────────────────────────── */
|
|
137
|
+
|
|
138
|
+
static void mik__pwm_finalizer(JSRuntime* rt, JSValue val) {
|
|
139
|
+
auto* s = static_cast<MIKPwmState*>(JS_GetOpaque(val, mik_pwm_class_id));
|
|
140
|
+
if (!s) return;
|
|
141
|
+
if (s->active) {
|
|
142
|
+
ledc_stop(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel), 0);
|
|
143
|
+
mik__pwm_free_channel(s->channel);
|
|
144
|
+
mik__pwm_free_timer(s->timer);
|
|
145
|
+
}
|
|
146
|
+
free(s);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static JSClassDef mik_pwm_class = {
|
|
150
|
+
.class_name = "Pwm",
|
|
151
|
+
.finalizer = mik__pwm_finalizer,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/* ── Constructor ───────────────────────────────────────────────────── */
|
|
155
|
+
|
|
156
|
+
static JSValue js_pwm_constructor(JSContext* ctx, JSValue new_target, int argc, JSValue* argv) {
|
|
157
|
+
if (argc < 2)
|
|
158
|
+
return JS_ThrowTypeError(ctx, "Pwm requires (pin, freq) or (pin, freq, duty)");
|
|
159
|
+
|
|
160
|
+
int32_t gpio;
|
|
161
|
+
if (JS_ToInt32(ctx, &gpio, argv[0])) return JS_EXCEPTION;
|
|
162
|
+
|
|
163
|
+
double freq;
|
|
164
|
+
if (JS_ToFloat64(ctx, &freq, argv[1])) return JS_EXCEPTION;
|
|
165
|
+
if (freq <= 0) return JS_ThrowRangeError(ctx, "frequency must be > 0");
|
|
166
|
+
|
|
167
|
+
double duty = 0.0;
|
|
168
|
+
if (argc >= 3) {
|
|
169
|
+
if (JS_ToFloat64(ctx, &duty, argv[2])) return JS_EXCEPTION;
|
|
170
|
+
if (duty < 0.0 || duty > 1.0)
|
|
171
|
+
return JS_ThrowRangeError(ctx, "duty must be between 0.0 and 1.0");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
int ch = mik__pwm_alloc_channel();
|
|
175
|
+
if (ch < 0)
|
|
176
|
+
return JS_ThrowInternalError(ctx, "no free PWM channels (max %d)", MIK_PWM_MAX_CHANNELS);
|
|
177
|
+
|
|
178
|
+
int timer = mik__pwm_alloc_timer(static_cast<uint32_t>(freq));
|
|
179
|
+
if (timer < 0) {
|
|
180
|
+
mik__pwm_free_channel(ch);
|
|
181
|
+
return JS_ThrowInternalError(ctx, "no free PWM timers (max %d)", MIK_PWM_MAX_TIMERS);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
ledc_timer_bit_t resolution = mik__pwm_best_resolution(static_cast<uint32_t>(freq));
|
|
185
|
+
|
|
186
|
+
/* Configure timer */
|
|
187
|
+
ledc_timer_config_t timer_cfg = {};
|
|
188
|
+
timer_cfg.speed_mode = LEDC_LOW_SPEED_MODE;
|
|
189
|
+
timer_cfg.duty_resolution = resolution;
|
|
190
|
+
timer_cfg.timer_num = static_cast<ledc_timer_t>(timer);
|
|
191
|
+
timer_cfg.freq_hz = static_cast<uint32_t>(freq);
|
|
192
|
+
timer_cfg.clk_cfg = LEDC_AUTO_CLK;
|
|
193
|
+
|
|
194
|
+
esp_err_t err = ledc_timer_config(&timer_cfg);
|
|
195
|
+
if (err != ESP_OK) {
|
|
196
|
+
mik__pwm_free_channel(ch);
|
|
197
|
+
mik__pwm_free_timer(timer);
|
|
198
|
+
return JS_ThrowInternalError(ctx, "LEDC timer config failed: %s", esp_err_to_name(err));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Configure channel */
|
|
202
|
+
ledc_channel_config_t ch_cfg = {};
|
|
203
|
+
ch_cfg.gpio_num = gpio;
|
|
204
|
+
ch_cfg.speed_mode = LEDC_LOW_SPEED_MODE;
|
|
205
|
+
ch_cfg.channel = static_cast<ledc_channel_t>(ch);
|
|
206
|
+
ch_cfg.timer_sel = static_cast<ledc_timer_t>(timer);
|
|
207
|
+
ch_cfg.duty = mik__pwm_duty_to_raw(duty, resolution);
|
|
208
|
+
ch_cfg.hpoint = 0;
|
|
209
|
+
|
|
210
|
+
err = ledc_channel_config(&ch_cfg);
|
|
211
|
+
if (err != ESP_OK) {
|
|
212
|
+
mik__pwm_free_channel(ch);
|
|
213
|
+
mik__pwm_free_timer(timer);
|
|
214
|
+
return JS_ThrowInternalError(ctx, "LEDC channel config failed: %s", esp_err_to_name(err));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Install fade service (once) */
|
|
218
|
+
if (!s_fade_installed) {
|
|
219
|
+
err = ledc_fade_func_install(0);
|
|
220
|
+
if (err == ESP_OK || err == ESP_ERR_INVALID_STATE) {
|
|
221
|
+
s_fade_installed = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
auto* s = static_cast<MIKPwmState*>(calloc(1, sizeof(MIKPwmState)));
|
|
226
|
+
if (!s) {
|
|
227
|
+
mik__pwm_free_channel(ch);
|
|
228
|
+
mik__pwm_free_timer(timer);
|
|
229
|
+
return JS_ThrowOutOfMemory(ctx);
|
|
230
|
+
}
|
|
231
|
+
s->gpio = gpio;
|
|
232
|
+
s->channel = ch;
|
|
233
|
+
s->timer = timer;
|
|
234
|
+
s->freq = static_cast<uint32_t>(freq);
|
|
235
|
+
s->resolution = resolution;
|
|
236
|
+
s->duty = duty;
|
|
237
|
+
s->active = true;
|
|
238
|
+
|
|
239
|
+
JSValue obj = JS_NewObjectClass(ctx, mik_pwm_class_id);
|
|
240
|
+
if (JS_IsException(obj)) {
|
|
241
|
+
ledc_stop(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(ch), 0);
|
|
242
|
+
mik__pwm_free_channel(ch);
|
|
243
|
+
mik__pwm_free_timer(timer);
|
|
244
|
+
free(s);
|
|
245
|
+
return obj;
|
|
246
|
+
}
|
|
247
|
+
JS_SetOpaque(obj, s);
|
|
248
|
+
return obj;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ── Methods ───────────────────────────────────────────────────────── */
|
|
252
|
+
|
|
253
|
+
static JSValue js_pwm_duty(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
254
|
+
auto* s = mik__pwm_get(ctx, this_val);
|
|
255
|
+
if (!s) return JS_EXCEPTION;
|
|
256
|
+
if (!s->active) return mik__result_err_tag(ctx, "NotActive");
|
|
257
|
+
|
|
258
|
+
/* Getter */
|
|
259
|
+
if (argc == 0 || JS_IsUndefined(argv[0])) {
|
|
260
|
+
return mik__result_ok(ctx, JS_NewFloat64(ctx, s->duty));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* Setter */
|
|
264
|
+
double duty;
|
|
265
|
+
if (JS_ToFloat64(ctx, &duty, argv[0])) return JS_EXCEPTION;
|
|
266
|
+
if (duty < 0.0 || duty > 1.0)
|
|
267
|
+
return mik__result_err_named(ctx, "DutyFailed", "duty must be 0.0-1.0");
|
|
268
|
+
|
|
269
|
+
uint32_t raw = mik__pwm_duty_to_raw(duty, s->resolution);
|
|
270
|
+
esp_err_t err = ledc_set_duty(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel), raw);
|
|
271
|
+
if (err != ESP_OK)
|
|
272
|
+
return mik__result_err_named(ctx, "DutyFailed", "failed to set duty: %s",
|
|
273
|
+
esp_err_to_name(err));
|
|
274
|
+
|
|
275
|
+
err = ledc_update_duty(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel));
|
|
276
|
+
if (err != ESP_OK)
|
|
277
|
+
return mik__result_err_named(ctx, "DutyFailed", "failed to update duty: %s",
|
|
278
|
+
esp_err_to_name(err));
|
|
279
|
+
|
|
280
|
+
s->duty = duty;
|
|
281
|
+
return mik__result_ok_void(ctx);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
static JSValue js_pwm_freq(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
285
|
+
auto* s = mik__pwm_get(ctx, this_val);
|
|
286
|
+
if (!s) return JS_EXCEPTION;
|
|
287
|
+
if (!s->active) return mik__result_err_tag(ctx, "NotActive");
|
|
288
|
+
|
|
289
|
+
/* Getter */
|
|
290
|
+
if (argc == 0 || JS_IsUndefined(argv[0])) {
|
|
291
|
+
return mik__result_ok(ctx, JS_NewFloat64(ctx, static_cast<double>(s->freq)));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Setter */
|
|
295
|
+
double freq;
|
|
296
|
+
if (JS_ToFloat64(ctx, &freq, argv[0])) return JS_EXCEPTION;
|
|
297
|
+
if (freq <= 0) return mik__result_err_named(ctx, "FreqFailed", "frequency must be positive");
|
|
298
|
+
|
|
299
|
+
uint32_t new_freq = static_cast<uint32_t>(freq);
|
|
300
|
+
ledc_timer_bit_t new_resolution = mik__pwm_best_resolution(new_freq);
|
|
301
|
+
|
|
302
|
+
/* Release old timer, allocate new one */
|
|
303
|
+
mik__pwm_free_timer(s->timer);
|
|
304
|
+
int new_timer = mik__pwm_alloc_timer(new_freq);
|
|
305
|
+
if (new_timer < 0) {
|
|
306
|
+
/* Re-claim old timer */
|
|
307
|
+
s->timer = mik__pwm_alloc_timer(s->freq);
|
|
308
|
+
return mik__result_err_named(ctx, "NoTimer",
|
|
309
|
+
"no free PWM timers (max %d)", MIK_PWM_MAX_TIMERS);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
ledc_timer_config_t timer_cfg = {};
|
|
313
|
+
timer_cfg.speed_mode = LEDC_LOW_SPEED_MODE;
|
|
314
|
+
timer_cfg.duty_resolution = new_resolution;
|
|
315
|
+
timer_cfg.timer_num = static_cast<ledc_timer_t>(new_timer);
|
|
316
|
+
timer_cfg.freq_hz = new_freq;
|
|
317
|
+
timer_cfg.clk_cfg = LEDC_AUTO_CLK;
|
|
318
|
+
|
|
319
|
+
esp_err_t err = ledc_timer_config(&timer_cfg);
|
|
320
|
+
if (err != ESP_OK) {
|
|
321
|
+
mik__pwm_free_timer(new_timer);
|
|
322
|
+
s->timer = mik__pwm_alloc_timer(s->freq);
|
|
323
|
+
return mik__result_err_named(ctx, "FreqFailed",
|
|
324
|
+
"failed to configure timer: %s", esp_err_to_name(err));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Bind channel to new timer */
|
|
328
|
+
err = ledc_bind_channel_timer(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel),
|
|
329
|
+
static_cast<ledc_timer_t>(new_timer));
|
|
330
|
+
if (err != ESP_OK) {
|
|
331
|
+
mik__pwm_free_timer(new_timer);
|
|
332
|
+
s->timer = mik__pwm_alloc_timer(s->freq);
|
|
333
|
+
return mik__result_err_named(ctx, "FreqFailed",
|
|
334
|
+
"failed to bind channel to timer: %s", esp_err_to_name(err));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
s->timer = new_timer;
|
|
338
|
+
s->freq = new_freq;
|
|
339
|
+
s->resolution = new_resolution;
|
|
340
|
+
|
|
341
|
+
/* Re-apply duty at new resolution */
|
|
342
|
+
uint32_t raw = mik__pwm_duty_to_raw(s->duty, s->resolution);
|
|
343
|
+
ledc_set_duty(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel), raw);
|
|
344
|
+
ledc_update_duty(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel));
|
|
345
|
+
|
|
346
|
+
return mik__result_ok(ctx, JS_NewFloat64(ctx, static_cast<double>(s->freq)));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
static JSValue js_pwm_fade(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
350
|
+
auto* s = mik__pwm_get(ctx, this_val);
|
|
351
|
+
if (!s) return JS_EXCEPTION;
|
|
352
|
+
if (!s->active) return mik__result_err_tag(ctx, "NotActive");
|
|
353
|
+
if (!s_fade_installed)
|
|
354
|
+
return mik__result_err_named(ctx, "FadeFailed", "fade service not installed");
|
|
355
|
+
|
|
356
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
357
|
+
CHECK_NOT_NULL(mik_rt);
|
|
358
|
+
|
|
359
|
+
if (s_fade_count >= MIK_PWM_MAX_PENDING_FADES)
|
|
360
|
+
return mik__result_err_named(ctx, "FadeFailed",
|
|
361
|
+
"too many pending fades (max %d)",
|
|
362
|
+
MIK_PWM_MAX_PENDING_FADES);
|
|
363
|
+
|
|
364
|
+
double target;
|
|
365
|
+
if (JS_ToFloat64(ctx, &target, argv[0])) return JS_EXCEPTION;
|
|
366
|
+
if (target < 0.0 || target > 1.0)
|
|
367
|
+
return mik__result_err_named(ctx, "DutyFailed", "duty must be 0.0-1.0");
|
|
368
|
+
|
|
369
|
+
int32_t duration_ms;
|
|
370
|
+
if (JS_ToInt32(ctx, &duration_ms, argv[1])) return JS_EXCEPTION;
|
|
371
|
+
if (duration_ms < 0)
|
|
372
|
+
return mik__result_err_named(ctx, "FadeFailed", "fade duration must be non-negative");
|
|
373
|
+
|
|
374
|
+
/* Allocate fade tracking entry */
|
|
375
|
+
auto* fades = mik__pwm_fades(mik_rt);
|
|
376
|
+
if (!fades) {
|
|
377
|
+
fades = static_cast<MIKPwmFadePending*>(
|
|
378
|
+
calloc(MIK_PWM_MAX_PENDING_FADES, sizeof(MIKPwmFadePending)));
|
|
379
|
+
if (!fades) return JS_ThrowOutOfMemory(ctx);
|
|
380
|
+
for (int j = 0; j < MIK_PWM_MAX_PENDING_FADES; j++) {
|
|
381
|
+
MIK_ClearPromise(ctx, &fades[j].promise);
|
|
382
|
+
}
|
|
383
|
+
mik__pwm_fades(mik_rt) = fades;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Find free slot */
|
|
387
|
+
int slot = -1;
|
|
388
|
+
for (int i = 0; i < MIK_PWM_MAX_PENDING_FADES; i++) {
|
|
389
|
+
if (!MIK_IsPromisePending(ctx, &fades[i].promise)) {
|
|
390
|
+
slot = i;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (slot < 0)
|
|
395
|
+
return mik__result_err_named(ctx, "FadeFailed", "no free fade slots available");
|
|
396
|
+
|
|
397
|
+
/* Create promise */
|
|
398
|
+
fades[slot].channel = s->channel;
|
|
399
|
+
fades[slot].complete.store(false, std::memory_order_relaxed);
|
|
400
|
+
JSValue promise = MIK_InitPromise(ctx, &fades[slot].promise);
|
|
401
|
+
s_fade_count++;
|
|
402
|
+
|
|
403
|
+
/* Register fade callback */
|
|
404
|
+
ledc_cbs_t cbs = {};
|
|
405
|
+
cbs.fade_cb = mik__pwm_fade_cb;
|
|
406
|
+
ledc_cb_register(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel), &cbs,
|
|
407
|
+
&fades[slot]);
|
|
408
|
+
|
|
409
|
+
/* Start fade */
|
|
410
|
+
uint32_t target_raw = mik__pwm_duty_to_raw(target, s->resolution);
|
|
411
|
+
esp_err_t err = ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE,
|
|
412
|
+
static_cast<ledc_channel_t>(s->channel), target_raw,
|
|
413
|
+
duration_ms);
|
|
414
|
+
if (err != ESP_OK) {
|
|
415
|
+
s_fade_count--;
|
|
416
|
+
MIK_FreePromise(ctx, &fades[slot].promise);
|
|
417
|
+
return mik__result_err_named(ctx, "FadeFailed",
|
|
418
|
+
"failed to configure fade: %s", esp_err_to_name(err));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
err = ledc_fade_start(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel),
|
|
422
|
+
LEDC_FADE_NO_WAIT);
|
|
423
|
+
if (err != ESP_OK) {
|
|
424
|
+
s_fade_count--;
|
|
425
|
+
MIK_FreePromise(ctx, &fades[slot].promise);
|
|
426
|
+
return mik__result_err_named(ctx, "FadeFailed",
|
|
427
|
+
"failed to start fade: %s", esp_err_to_name(err));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* Update duty to target (will be accurate once fade completes) */
|
|
431
|
+
s->duty = target;
|
|
432
|
+
|
|
433
|
+
return mik__result_ok(ctx, promise);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
static JSValue js_pwm_end(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
437
|
+
auto* s = mik__pwm_get(ctx, this_val);
|
|
438
|
+
if (!s) return JS_EXCEPTION;
|
|
439
|
+
if (!s->active) return mik__result_ok_void(ctx); // idempotent
|
|
440
|
+
|
|
441
|
+
ledc_stop(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(s->channel), 0);
|
|
442
|
+
mik__pwm_free_channel(s->channel);
|
|
443
|
+
mik__pwm_free_timer(s->timer);
|
|
444
|
+
s->active = false;
|
|
445
|
+
return mik__result_ok_void(ctx);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* ── Prototype ─────────────────────────────────────────────────────── */
|
|
449
|
+
|
|
450
|
+
static const JSCFunctionListEntry mik_pwm_proto_funcs[] = {
|
|
451
|
+
MIK_CFUNC_DEF("duty", 1, js_pwm_duty),
|
|
452
|
+
MIK_CFUNC_DEF("freq", 1, js_pwm_freq),
|
|
453
|
+
MIK_CFUNC_DEF("fade", 2, js_pwm_fade),
|
|
454
|
+
MIK_CFUNC_DEF("end", 0, js_pwm_end),
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/* ── Module init ───────────────────────────────────────────────────── */
|
|
458
|
+
|
|
459
|
+
static int mik__pwm_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
460
|
+
JSValue ctor =
|
|
461
|
+
JS_NewCFunction2(ctx, js_pwm_constructor, "Pwm", 3, JS_CFUNC_constructor, 0);
|
|
462
|
+
JS_SetModuleExport(ctx, m, "Pwm", ctor);
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
static JSModuleDef* mik__pwm_init(JSContext* ctx) {
|
|
467
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
468
|
+
mik__pwm_slot = MIK_AllocModuleSlot(mik_rt);
|
|
469
|
+
|
|
470
|
+
JSRuntime* rt = JS_GetRuntime(ctx);
|
|
471
|
+
|
|
472
|
+
/* Register class (once per runtime) */
|
|
473
|
+
JS_NewClassID(rt, &mik_pwm_class_id);
|
|
474
|
+
JS_NewClass(rt, mik_pwm_class_id, &mik_pwm_class);
|
|
475
|
+
|
|
476
|
+
/* Create prototype with methods */
|
|
477
|
+
JSValue proto = JS_NewObject(ctx);
|
|
478
|
+
JS_SetPropertyFunctionList(ctx, proto, mik_pwm_proto_funcs, countof(mik_pwm_proto_funcs));
|
|
479
|
+
JS_SetClassProto(ctx, mik_pwm_class_id, proto); /* consumed */
|
|
480
|
+
|
|
481
|
+
/* Register module */
|
|
482
|
+
JSModuleDef* m = JS_NewCModule(ctx, "native:pwm", mik__pwm_module_init);
|
|
483
|
+
if (!m) return nullptr;
|
|
484
|
+
JS_AddModuleExport(ctx, m, "Pwm");
|
|
485
|
+
return m;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* ── Event loop: fade completion ───────────────────────────────────── */
|
|
489
|
+
|
|
490
|
+
void mik__pwm_consume(JSContext* ctx) {
|
|
491
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
492
|
+
CHECK_NOT_NULL(mik_rt);
|
|
493
|
+
auto* fades = mik__pwm_fades(mik_rt);
|
|
494
|
+
if (!fades) return;
|
|
495
|
+
|
|
496
|
+
for (int i = 0; i < MIK_PWM_MAX_PENDING_FADES; i++) {
|
|
497
|
+
if (!MIK_IsPromisePending(ctx, &fades[i].promise)) continue;
|
|
498
|
+
if (!fades[i].complete.load(std::memory_order_acquire)) continue;
|
|
499
|
+
|
|
500
|
+
/* Fade completed — resolve promise and mark slot as free */
|
|
501
|
+
JSValue undef = JS_UNDEFINED;
|
|
502
|
+
MIK_ResolvePromise(ctx, &fades[i].promise, 1, &undef);
|
|
503
|
+
MIK_ClearPromise(ctx, &fades[i].promise);
|
|
504
|
+
s_fade_count--;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
void mik__pwm_destroy(JSContext* ctx) {
|
|
509
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
510
|
+
CHECK_NOT_NULL(mik_rt);
|
|
511
|
+
auto* fades = mik__pwm_fades(mik_rt);
|
|
512
|
+
if (!fades) return;
|
|
513
|
+
|
|
514
|
+
for (int i = 0; i < MIK_PWM_MAX_PENDING_FADES; i++) {
|
|
515
|
+
if (MIK_IsPromisePending(ctx, &fades[i].promise)) {
|
|
516
|
+
MIK_FreePromise(ctx, &fades[i].promise);
|
|
517
|
+
s_fade_count--;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
free(fades);
|
|
522
|
+
mik__pwm_fades(mik_rt) = nullptr;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
MIK_REGISTER_MODULE(pwm, "native:pwm", mik__pwm_init, mik__pwm_consume, mik__pwm_destroy)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#include <stddef.h>
|
|
2
|
+
#include <stdint.h>
|
|
3
|
+
|
|
4
|
+
#include <esp_attr.h>
|
|
5
|
+
#include <esp_timer.h>
|
|
6
|
+
#include <freertos/FreeRTOS.h>
|
|
7
|
+
#include <freertos/task.h>
|
|
8
|
+
|
|
9
|
+
#include "mikrojs_esp32.h"
|
|
10
|
+
|
|
11
|
+
/* Magic word stored in RTC slow memory. RTC_NOINIT_ATTR keeps the value
|
|
12
|
+
* across software resets but leaves it uninitialized on power-on, so a
|
|
13
|
+
* cold boot will (almost certainly) see garbage rather than the magic. */
|
|
14
|
+
#define MIK_RECOVERY_MAGIC 0x4D494B53u /* 'M','I','K','S' */
|
|
15
|
+
|
|
16
|
+
RTC_NOINIT_ATTR static uint32_t s_recovery_magic;
|
|
17
|
+
|
|
18
|
+
/* Sync sequence host tools send during the recovery window. */
|
|
19
|
+
static const char SYNC_BYTES[] = "MIKSAFE\n";
|
|
20
|
+
|
|
21
|
+
/* Drain any bytes still sitting in stdin. Called after a successful
|
|
22
|
+
* recovery trigger to discard leftover flood bytes before the protocol
|
|
23
|
+
* loop starts reading — otherwise the tail of the host's "MIKSAFE\n"
|
|
24
|
+
* flood gets parsed as a bogus TLV frame and the device hangs in
|
|
25
|
+
* mik__proto_drain() waiting for GB of phantom payload.
|
|
26
|
+
*
|
|
27
|
+
* Reads with a short idle timeout: keep pulling while bytes are flowing,
|
|
28
|
+
* stop once no bytes arrive for drain_idle_ms. */
|
|
29
|
+
static void drain_stdin(int drain_idle_ms) {
|
|
30
|
+
int64_t idle_deadline_us = esp_timer_get_time() + ((int64_t)drain_idle_ms * 1000);
|
|
31
|
+
while (esp_timer_get_time() < idle_deadline_us) {
|
|
32
|
+
char buf[64];
|
|
33
|
+
int n = mik__console_read(buf, sizeof(buf));
|
|
34
|
+
if (n > 0) {
|
|
35
|
+
/* Fresh bytes — reset the idle window. */
|
|
36
|
+
idle_deadline_us = esp_timer_get_time() + ((int64_t)drain_idle_ms * 1000);
|
|
37
|
+
} else {
|
|
38
|
+
vTaskDelay(pdMS_TO_TICKS(10));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
bool mik__check_recovery(int timeout_ms) {
|
|
44
|
+
/* Double-reset path: previous boot armed the magic and got reset
|
|
45
|
+
* before it could clear it. Honor the request and clear immediately
|
|
46
|
+
* so the next boot is normal. */
|
|
47
|
+
if (s_recovery_magic == MIK_RECOVERY_MAGIC) {
|
|
48
|
+
s_recovery_magic = 0;
|
|
49
|
+
drain_stdin(200);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Arm the magic for the duration of the window. If the user resets
|
|
54
|
+
* the chip while we're here, the next boot will see the magic and
|
|
55
|
+
* enter safe mode. */
|
|
56
|
+
s_recovery_magic = MIK_RECOVERY_MAGIC;
|
|
57
|
+
|
|
58
|
+
const size_t sync_len = sizeof(SYNC_BYTES) - 1;
|
|
59
|
+
size_t matched = 0;
|
|
60
|
+
|
|
61
|
+
int64_t deadline_us = esp_timer_get_time() + ((int64_t)timeout_ms * 1000);
|
|
62
|
+
while (esp_timer_get_time() < deadline_us) {
|
|
63
|
+
char buf[16];
|
|
64
|
+
int n = mik__console_read(buf, sizeof(buf));
|
|
65
|
+
if (n > 0) {
|
|
66
|
+
for (int i = 0; i < n; i++) {
|
|
67
|
+
if (buf[i] == SYNC_BYTES[matched]) {
|
|
68
|
+
matched++;
|
|
69
|
+
if (matched == sync_len) {
|
|
70
|
+
s_recovery_magic = 0;
|
|
71
|
+
drain_stdin(200);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
matched = (buf[i] == SYNC_BYTES[0]) ? 1 : 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
vTaskDelay(pdMS_TO_TICKS(10));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Window expired with no trigger; disarm. */
|
|
84
|
+
s_recovery_magic = 0;
|
|
85
|
+
return false;
|
|
86
|
+
}
|