@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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/bin/idf.py +7 -0
  4. package/chips.json +3 -0
  5. package/cmake.js +9 -0
  6. package/components/mikrojs/CMakeLists.txt +187 -0
  7. package/components/mikrojs/Kconfig +55 -0
  8. package/components/mikrojs/idf_component.yml +6 -0
  9. package/components/mikrojs/include/mem.h +3 -0
  10. package/components/mikrojs/include/mik_color.h +3 -0
  11. package/components/mikrojs/include/mik_http_internal.h +77 -0
  12. package/components/mikrojs/include/mikrojs.h +5 -0
  13. package/components/mikrojs/include/mikrojs_esp32.h +65 -0
  14. package/components/mikrojs/include/private.h +10 -0
  15. package/components/mikrojs/include/utils.h +3 -0
  16. package/components/mikrojs/mik_ble.cpp +1588 -0
  17. package/components/mikrojs/mik_ble_c_shim.c +61 -0
  18. package/components/mikrojs/mik_ble_c_shim.h +37 -0
  19. package/components/mikrojs/mik_config.cpp +167 -0
  20. package/components/mikrojs/mik_deploy.cpp +584 -0
  21. package/components/mikrojs/mik_http.cpp +916 -0
  22. package/components/mikrojs/mik_i2c.cpp +364 -0
  23. package/components/mikrojs/mik_main.cpp +542 -0
  24. package/components/mikrojs/mik_neopixel.cpp +437 -0
  25. package/components/mikrojs/mik_nvs_kv.cpp +219 -0
  26. package/components/mikrojs/mik_pin.cpp +195 -0
  27. package/components/mikrojs/mik_pwm.cpp +525 -0
  28. package/components/mikrojs/mik_recovery.cpp +86 -0
  29. package/components/mikrojs/mik_rtc.cpp +305 -0
  30. package/components/mikrojs/mik_serial_io.cpp +362 -0
  31. package/components/mikrojs/mik_sleep.cpp +226 -0
  32. package/components/mikrojs/mik_sntp.cpp +275 -0
  33. package/components/mikrojs/mik_spi.cpp +330 -0
  34. package/components/mikrojs/mik_uart.cpp +497 -0
  35. package/components/mikrojs/mik_wifi.cpp +1434 -0
  36. package/components/mikrojs/platform_esp32.cpp +192 -0
  37. package/components/mikrojs/test/CMakeLists.txt +32 -0
  38. package/components/mikrojs/test/abort_test.cpp +254 -0
  39. package/components/mikrojs/test/ble_test.cpp +714 -0
  40. package/components/mikrojs/test/fs_js_test.cpp +458 -0
  41. package/components/mikrojs/test/fs_pub_test.cpp +312 -0
  42. package/components/mikrojs/test/http_test.cpp +475 -0
  43. package/components/mikrojs/test/i2c_test.cpp +138 -0
  44. package/components/mikrojs/test/modules_extended_test.cpp +137 -0
  45. package/components/mikrojs/test/modules_test.cpp +131 -0
  46. package/components/mikrojs/test/pins_test.cpp +47 -0
  47. package/components/mikrojs/test/pwm_test.cpp +166 -0
  48. package/components/mikrojs/test/repl_protocol_test.cpp +405 -0
  49. package/components/mikrojs/test/rtc_test.cpp +331 -0
  50. package/components/mikrojs/test/runtime_test.cpp +89 -0
  51. package/components/mikrojs/test/sleep_test.cpp +222 -0
  52. package/components/mikrojs/test/sntp_test.cpp +249 -0
  53. package/components/mikrojs/test/stdio_test.cpp +449 -0
  54. package/components/mikrojs/test/sys_test.cpp +165 -0
  55. package/components/mikrojs/test/text_encoding_test.cpp +224 -0
  56. package/components/mikrojs/test/timers_js_test.cpp +244 -0
  57. package/components/mikrojs/test/timers_test.cpp +79 -0
  58. package/components/mikrojs/test/wifi_test.cpp +599 -0
  59. package/default-app/main/CMakeLists.txt +3 -0
  60. package/default-app/main/main.cpp +5 -0
  61. package/discover.js +77 -0
  62. package/index.d.ts +7 -0
  63. package/index.js +20 -0
  64. package/package.json +61 -0
  65. package/partitions.csv +5 -0
  66. package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
  67. package/prebuilds/esp32/flasher_args.json +24 -0
  68. package/prebuilds/esp32/mikrojs.bin +0 -0
  69. package/prebuilds/esp32/partition_table/partition-table.bin +0 -0
  70. package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
  71. package/prebuilds/esp32c3/flasher_args.json +24 -0
  72. package/prebuilds/esp32c3/mikrojs.bin +0 -0
  73. package/prebuilds/esp32c3/partition_table/partition-table.bin +0 -0
  74. package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
  75. package/prebuilds/esp32c6/flasher_args.json +24 -0
  76. package/prebuilds/esp32c6/mikrojs.bin +0 -0
  77. package/prebuilds/esp32c6/partition_table/partition-table.bin +0 -0
  78. package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
  79. package/prebuilds/esp32s3/flasher_args.json +24 -0
  80. package/prebuilds/esp32s3/mikrojs.bin +0 -0
  81. package/prebuilds/esp32s3/partition_table/partition-table.bin +0 -0
  82. package/project.cmake +101 -0
  83. package/resolve.js +54 -0
  84. package/sdkconfig.defaults +127 -0
  85. package/sdkconfig.defaults.esp32 +8 -0
  86. package/sdkconfig.defaults.esp32c3 +15 -0
  87. package/sdkconfig.defaults.esp32c6 +26 -0
  88. package/sdkconfig.defaults.esp32s3 +22 -0
@@ -0,0 +1,137 @@
1
+ #include <string.h>
2
+
3
+ #include "mikrojs.h"
4
+ #include "private.h"
5
+ #include "quickjs.h"
6
+ #include "unity.h"
7
+
8
+ TEST_CASE("import.meta.dirname for absolute path module", "[modules]") {
9
+ const auto mik_rt = MIK_NewRuntime();
10
+ const auto ctx = MIK_GetJSContext(mik_rt);
11
+
12
+ const char* code = "globalThis.__dirname = import.meta.dirname;";
13
+ JSValue ret = MIK_EvalModuleContent(ctx, "/appfs/lib/main.js", code, strlen(code));
14
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
15
+ JS_FreeValue(ctx, ret);
16
+
17
+ mik__execute_jobs(ctx);
18
+
19
+ JSValue global = JS_GetGlobalObject(ctx);
20
+ JSValue dirname = JS_GetPropertyStr(ctx, global, "__dirname");
21
+ TEST_ASSERT_TRUE_MESSAGE(JS_IsString(dirname), "Expected __dirname to be a string");
22
+
23
+ const char* str = JS_ToCString(ctx, dirname);
24
+ TEST_ASSERT_EQUAL_STRING("/appfs/lib", str);
25
+ JS_FreeCString(ctx, str);
26
+
27
+ JS_FreeValue(ctx, dirname);
28
+ JS_FreeValue(ctx, global);
29
+ MIK_FreeRuntime(mik_rt);
30
+ }
31
+
32
+ TEST_CASE("import.meta.basename for absolute path module", "[modules]") {
33
+ const auto mik_rt = MIK_NewRuntime();
34
+ const auto ctx = MIK_GetJSContext(mik_rt);
35
+
36
+ const char* code = "globalThis.__basename = import.meta.basename;";
37
+ JSValue ret = MIK_EvalModuleContent(ctx, "/appfs/lib/main.js", code, strlen(code));
38
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
39
+ JS_FreeValue(ctx, ret);
40
+
41
+ mik__execute_jobs(ctx);
42
+
43
+ JSValue global = JS_GetGlobalObject(ctx);
44
+ JSValue basename = JS_GetPropertyStr(ctx, global, "__basename");
45
+ TEST_ASSERT_TRUE_MESSAGE(JS_IsString(basename), "Expected __basename to be a string");
46
+
47
+ const char* str = JS_ToCString(ctx, basename);
48
+ TEST_ASSERT_EQUAL_STRING("main.js", str);
49
+ JS_FreeCString(ctx, str);
50
+
51
+ JS_FreeValue(ctx, basename);
52
+ JS_FreeValue(ctx, global);
53
+ MIK_FreeRuntime(mik_rt);
54
+ }
55
+
56
+ TEST_CASE("import.meta.path for absolute path module", "[modules]") {
57
+ const auto mik_rt = MIK_NewRuntime();
58
+ const auto ctx = MIK_GetJSContext(mik_rt);
59
+
60
+ const char* code = "globalThis.__path = import.meta.path;";
61
+ JSValue ret = MIK_EvalModuleContent(ctx, "/appfs/lib/main.js", code, strlen(code));
62
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
63
+ JS_FreeValue(ctx, ret);
64
+
65
+ mik__execute_jobs(ctx);
66
+
67
+ JSValue global = JS_GetGlobalObject(ctx);
68
+ JSValue path = JS_GetPropertyStr(ctx, global, "__path");
69
+ TEST_ASSERT_TRUE_MESSAGE(JS_IsString(path), "Expected __path to be a string");
70
+
71
+ const char* str = JS_ToCString(ctx, path);
72
+ TEST_ASSERT_EQUAL_STRING("/appfs/lib/main.js", str);
73
+ JS_FreeCString(ctx, str);
74
+
75
+ JS_FreeValue(ctx, path);
76
+ JS_FreeValue(ctx, global);
77
+ MIK_FreeRuntime(mik_rt);
78
+ }
79
+
80
+ TEST_CASE("import.meta.url has file:// prefix", "[modules]") {
81
+ const auto mik_rt = MIK_NewRuntime();
82
+ const auto ctx = MIK_GetJSContext(mik_rt);
83
+
84
+ const char* code = "globalThis.__url = import.meta.url;";
85
+ JSValue ret = MIK_EvalModuleContent(ctx, "/appfs/main.js", code, strlen(code));
86
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
87
+ JS_FreeValue(ctx, ret);
88
+
89
+ mik__execute_jobs(ctx);
90
+
91
+ JSValue global = JS_GetGlobalObject(ctx);
92
+ JSValue url = JS_GetPropertyStr(ctx, global, "__url");
93
+ const char* str = JS_ToCString(ctx, url);
94
+ TEST_ASSERT_EQUAL_STRING("file:///appfs/main.js", str);
95
+ JS_FreeCString(ctx, str);
96
+
97
+ JS_FreeValue(ctx, url);
98
+ JS_FreeValue(ctx, global);
99
+ MIK_FreeRuntime(mik_rt);
100
+ }
101
+
102
+ TEST_CASE("native:* internal import blocked from user code", "[modules]") {
103
+ JSRuntime* rt = JS_NewRuntime();
104
+ JSContext* ctx = JS_NewContext(rt);
105
+
106
+ // User code at "/appfs/main.js" should NOT be able to import native:sys
107
+ char* result = mik_module_normalizer(ctx, "/appfs/main.js", "native:sys", nullptr);
108
+ TEST_ASSERT_NULL_MESSAGE(result, "native:sys should be blocked from user code");
109
+
110
+ // But a built-in module should be allowed to import native:sys
111
+ result = mik_module_normalizer(ctx, "mikrojs/sys", "native:sys", nullptr);
112
+ TEST_ASSERT_NOT_NULL_MESSAGE(result, "native:sys should be allowed from mikrojs/ modules");
113
+ TEST_ASSERT_EQUAL_STRING("native:sys", result);
114
+ js_free(ctx, result);
115
+
116
+ // native: modules should also be allowed to import other native:* modules
117
+ result = mik_module_normalizer(ctx, "native:sys", "native:stdio", nullptr);
118
+ TEST_ASSERT_NOT_NULL_MESSAGE(result, "native:stdio should be allowed from native: modules");
119
+ js_free(ctx, result);
120
+
121
+ JS_FreeContext(ctx);
122
+ JS_FreeRuntime(rt);
123
+ }
124
+
125
+ TEST_CASE("Module normalization: root file with no dir", "[modules]") {
126
+ JSRuntime* rt = JS_NewRuntime();
127
+ JSContext* ctx = JS_NewContext(rt);
128
+
129
+ // Edge case: base has no directory component
130
+ char* result = mik_module_normalizer(ctx, "main.js", "./utils.js", nullptr);
131
+ TEST_ASSERT_NOT_NULL(result);
132
+ TEST_ASSERT_EQUAL_STRING("utils.js", result);
133
+ js_free(ctx, result);
134
+
135
+ JS_FreeContext(ctx);
136
+ JS_FreeRuntime(rt);
137
+ }
@@ -0,0 +1,131 @@
1
+ #include <string.h>
2
+
3
+ #include "mikrojs.h"
4
+ #include "private.h"
5
+ #include "quickjs.h"
6
+ #include "unity.h"
7
+
8
+ static void assert_normalizes_to(JSContext* ctx, const char* base, const char* name,
9
+ const char* expected) {
10
+ char* result = mik_module_normalizer(ctx, base, name, nullptr);
11
+ TEST_ASSERT_NOT_NULL_MESSAGE(result, "mik_module_normalizer returned NULL");
12
+ TEST_ASSERT_EQUAL_STRING(expected, result);
13
+ js_free(ctx, result);
14
+ }
15
+
16
+ TEST_CASE("Bare module names pass through unchanged", "[modules]") {
17
+ JSRuntime* rt = JS_NewRuntime();
18
+ JSContext* ctx = JS_NewContext(rt);
19
+
20
+ assert_normalizes_to(ctx, "main.js", "mikrojs/fs", "mikrojs/fs");
21
+ assert_normalizes_to(ctx, "main.js", "lodash", "lodash");
22
+
23
+ JS_FreeContext(ctx);
24
+ JS_FreeRuntime(rt);
25
+ }
26
+
27
+ TEST_CASE("Relative ./import resolves against base dirname", "[modules]") {
28
+ JSRuntime* rt = JS_NewRuntime();
29
+ JSContext* ctx = JS_NewContext(rt);
30
+
31
+ assert_normalizes_to(ctx, "lib/foo.js", "./bar.js", "lib/bar.js");
32
+
33
+ JS_FreeContext(ctx);
34
+ JS_FreeRuntime(rt);
35
+ }
36
+
37
+ TEST_CASE("Parent ../traversal resolves correctly", "[modules]") {
38
+ JSRuntime* rt = JS_NewRuntime();
39
+ JSContext* ctx = JS_NewContext(rt);
40
+
41
+ assert_normalizes_to(ctx, "lib/sub/foo.js", "../bar.js", "lib/bar.js");
42
+
43
+ JS_FreeContext(ctx);
44
+ JS_FreeRuntime(rt);
45
+ }
46
+
47
+ TEST_CASE("Multiple ../traversals resolve correctly", "[modules]") {
48
+ JSRuntime* rt = JS_NewRuntime();
49
+ JSContext* ctx = JS_NewContext(rt);
50
+
51
+ assert_normalizes_to(ctx, "a/b/c/foo.js", "../../bar.js", "a/bar.js");
52
+
53
+ JS_FreeContext(ctx);
54
+ JS_FreeRuntime(rt);
55
+ }
56
+
57
+ TEST_CASE("Root-level relative import", "[modules]") {
58
+ JSRuntime* rt = JS_NewRuntime();
59
+ JSContext* ctx = JS_NewContext(rt);
60
+
61
+ assert_normalizes_to(ctx, "foo.js", "./bar.js", "bar.js");
62
+
63
+ JS_FreeContext(ctx);
64
+ JS_FreeRuntime(rt);
65
+ }
66
+
67
+ TEST_CASE("Simple module eval works", "[modules]") {
68
+ const auto mik_rt = MIK_NewRuntime();
69
+ const auto ctx = MIK_GetJSContext(mik_rt);
70
+
71
+ const char* code = "var result = 1 + 2;";
72
+ JSValue ret = MIK_EvalModuleContent(ctx, "/test/main.js", code, strlen(code));
73
+ TEST_ASSERT_FALSE(JS_IsException(ret));
74
+ JS_FreeValue(ctx, ret);
75
+
76
+ MIK_FreeRuntime(mik_rt);
77
+ }
78
+
79
+ TEST_CASE("Module import.meta.main is true for entry module", "[modules]") {
80
+ const auto mik_rt = MIK_NewRuntime();
81
+ const auto ctx = MIK_GetJSContext(mik_rt);
82
+
83
+ const char* code = "globalThis.__isMain = import.meta.main;";
84
+ JSValue ret = MIK_EvalModuleContent(ctx, "/test/main.js", code, strlen(code));
85
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
86
+ JS_FreeValue(ctx, ret);
87
+
88
+ /* Execute pending jobs so the module body runs */
89
+ mik__execute_jobs(ctx);
90
+
91
+ JSValue global = JS_GetGlobalObject(ctx);
92
+ JSValue is_main = JS_GetPropertyStr(ctx, global, "__isMain");
93
+ TEST_ASSERT_TRUE_MESSAGE(JS_IsBool(is_main), "Expected __isMain to be a boolean");
94
+ TEST_ASSERT_EQUAL(1, JS_ToBool(ctx, is_main));
95
+ JS_FreeValue(ctx, is_main);
96
+ JS_FreeValue(ctx, global);
97
+
98
+ MIK_FreeRuntime(mik_rt);
99
+ }
100
+
101
+ TEST_CASE("Bytecode roundtrip: compile then load", "[modules]") {
102
+ JSRuntime* rt = JS_NewRuntime();
103
+ JSContext* ctx = JS_NewContext(rt);
104
+
105
+ const char* code = "var x = 40 + 2;";
106
+
107
+ /* Compile to bytecode */
108
+ JSValue compiled = JS_Eval(ctx, code, strlen(code), "test.js",
109
+ JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
110
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(compiled), "Compilation should succeed");
111
+
112
+ size_t bytecode_len;
113
+ uint8_t* bytecode = JS_WriteObject(ctx, &bytecode_len, compiled, JS_WRITE_OBJ_BYTECODE);
114
+ JS_FreeValue(ctx, compiled);
115
+ TEST_ASSERT_NOT_NULL_MESSAGE(bytecode, "JS_WriteObject should produce bytecode");
116
+ TEST_ASSERT_TRUE_MESSAGE(bytecode_len > 0, "Bytecode should not be empty");
117
+
118
+ /* Load from bytecode */
119
+ JSValue loaded = JS_ReadObject(ctx, bytecode, bytecode_len, JS_READ_OBJ_BYTECODE);
120
+ js_free(ctx, bytecode);
121
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(loaded), "JS_ReadObject should succeed");
122
+ TEST_ASSERT_TRUE_MESSAGE(JS_VALUE_GET_TAG(loaded) == JS_TAG_MODULE, "Should be a module");
123
+
124
+ /* Execute */
125
+ JSValue result = JS_EvalFunction(ctx, loaded);
126
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(result), "JS_EvalFunction should succeed");
127
+ JS_FreeValue(ctx, result);
128
+
129
+ JS_FreeContext(ctx);
130
+ JS_FreeRuntime(rt);
131
+ }
@@ -0,0 +1,47 @@
1
+ #include "../mik_pin.cpp"
2
+ #include "freertos/FreeRTOS.h"
3
+ #include "freertos/task.h"
4
+ #include "quickjs.h"
5
+ #include "unity.h"
6
+
7
+ #define PIN CONFIG_MIKROJS_TEST_GPIO_PIN
8
+ TEST_CASE("Test write output", "[gpio]") {
9
+ mik_gpio_set_pin_mode(PIN, GPIO_MODE_OUTPUT);
10
+ mik_gpio_digital_write(PIN, 0);
11
+ vTaskDelay(100 / portTICK_PERIOD_MS);
12
+ mik_gpio_digital_write(PIN, 1);
13
+ }
14
+
15
+ TEST_CASE("digitalRead returns a value after write", "[gpio]") {
16
+ mik_gpio_reset_pin(PIN);
17
+ mik_gpio_set_pin_mode(PIN, GPIO_MODE_INPUT_OUTPUT);
18
+ mik_gpio_digital_write(PIN, 1);
19
+ TEST_ASSERT_EQUAL_INT(1, mik_gpio_digital_read(PIN));
20
+ mik_gpio_digital_write(PIN, 0);
21
+ TEST_ASSERT_EQUAL_INT(0, mik_gpio_digital_read(PIN));
22
+ }
23
+
24
+ #define ADC_PIN CONFIG_MIKROJS_TEST_ADC_PIN
25
+
26
+ TEST_CASE("analogRead returns a value in range", "[gpio]") {
27
+ TEST_ASSERT_EQUAL(ESP_OK, mik_adc_ensure_init());
28
+ int raw = mik_adc_read_raw(ADC_PIN, ADC_ATTEN_DB_12);
29
+ TEST_ASSERT_GREATER_OR_EQUAL(0, raw);
30
+ TEST_ASSERT_LESS_OR_EQUAL(4095, raw);
31
+ }
32
+
33
+ TEST_CASE("analogReadMillivolts returns a value", "[gpio]") {
34
+ TEST_ASSERT_EQUAL(ESP_OK, mik_adc_ensure_init());
35
+ int mv = mik_adc_read_millivolts(ADC_PIN, ADC_ATTEN_DB_12);
36
+ TEST_ASSERT_GREATER_OR_EQUAL(0, mv);
37
+ }
38
+
39
+ TEST_CASE("analogRead works with all attenuations", "[gpio]") {
40
+ TEST_ASSERT_EQUAL(ESP_OK, mik_adc_ensure_init());
41
+ adc_atten_t attens[] = {ADC_ATTEN_DB_0, ADC_ATTEN_DB_2_5, ADC_ATTEN_DB_6, ADC_ATTEN_DB_12};
42
+ for (int i = 0; i < 4; i++) {
43
+ int raw = mik_adc_read_raw(ADC_PIN, attens[i]);
44
+ TEST_ASSERT_GREATER_OR_EQUAL(0, raw);
45
+ TEST_ASSERT_LESS_OR_EQUAL(4095, raw);
46
+ }
47
+ }
@@ -0,0 +1,166 @@
1
+ #include "../mik_pwm.cpp"
2
+ #include "freertos/FreeRTOS.h"
3
+ #include "freertos/task.h"
4
+ #include "unity.h"
5
+
6
+ #define PWM_PIN CONFIG_MIKROJS_TEST_PWM_PIN
7
+
8
+ TEST_CASE("PWM channel alloc and release", "[pwm]") {
9
+ int ch = mik__pwm_alloc_channel();
10
+ TEST_ASSERT_GREATER_OR_EQUAL(0, ch);
11
+ mik__pwm_free_channel(ch);
12
+ }
13
+
14
+ TEST_CASE("PWM timer alloc shares same frequency", "[pwm]") {
15
+ int t1 = mik__pwm_alloc_timer(5000);
16
+ int t2 = mik__pwm_alloc_timer(5000);
17
+ TEST_ASSERT_GREATER_OR_EQUAL(0, t1);
18
+ TEST_ASSERT_EQUAL(t1, t2); // same frequency shares timer
19
+ mik__pwm_free_timer(t1);
20
+ mik__pwm_free_timer(t2);
21
+ }
22
+
23
+ TEST_CASE("PWM timer alloc different frequencies", "[pwm]") {
24
+ int t1 = mik__pwm_alloc_timer(5000);
25
+ int t2 = mik__pwm_alloc_timer(1000);
26
+ TEST_ASSERT_GREATER_OR_EQUAL(0, t1);
27
+ TEST_ASSERT_GREATER_OR_EQUAL(0, t2);
28
+ TEST_ASSERT_NOT_EQUAL(t1, t2); // different frequency = different timer
29
+ mik__pwm_free_timer(t1);
30
+ mik__pwm_free_timer(t2);
31
+ }
32
+
33
+ TEST_CASE("PWM best resolution for common frequencies", "[pwm]") {
34
+ /* 5 kHz → 80 MHz / 5000 = 16000, log2(16000) ≈ 13 */
35
+ ledc_timer_bit_t res = mik__pwm_best_resolution(5000);
36
+ TEST_ASSERT_GREATER_OR_EQUAL(10, res);
37
+ TEST_ASSERT_LESS_OR_EQUAL(LEDC_TIMER_14_BIT, res);
38
+
39
+ /* 1 MHz → 80 MHz / 1000000 = 80, log2(80) ≈ 6 */
40
+ res = mik__pwm_best_resolution(1000000);
41
+ TEST_ASSERT_GREATER_OR_EQUAL(4, res);
42
+ TEST_ASSERT_LESS_OR_EQUAL(8, res);
43
+ }
44
+
45
+ TEST_CASE("PWM duty to raw conversion", "[pwm]") {
46
+ /* 13-bit resolution: max = 8191 */
47
+ TEST_ASSERT_EQUAL(0, mik__pwm_duty_to_raw(0.0, LEDC_TIMER_13_BIT));
48
+ TEST_ASSERT_EQUAL(8191, mik__pwm_duty_to_raw(1.0, LEDC_TIMER_13_BIT));
49
+
50
+ uint32_t half = mik__pwm_duty_to_raw(0.5, LEDC_TIMER_13_BIT);
51
+ TEST_ASSERT_GREATER_OR_EQUAL(4090, half);
52
+ TEST_ASSERT_LESS_OR_EQUAL(4100, half);
53
+ }
54
+
55
+ TEST_CASE("PWM channel exhaustion", "[pwm]") {
56
+ int channels[MIK_PWM_MAX_CHANNELS];
57
+ for (int i = 0; i < MIK_PWM_MAX_CHANNELS; i++) {
58
+ channels[i] = mik__pwm_alloc_channel();
59
+ TEST_ASSERT_GREATER_OR_EQUAL(0, channels[i]);
60
+ }
61
+ /* Next alloc should fail */
62
+ TEST_ASSERT_EQUAL(-1, mik__pwm_alloc_channel());
63
+
64
+ /* Release all */
65
+ for (int i = 0; i < MIK_PWM_MAX_CHANNELS; i++) {
66
+ mik__pwm_free_channel(channels[i]);
67
+ }
68
+ /* Should be able to alloc again */
69
+ int ch = mik__pwm_alloc_channel();
70
+ TEST_ASSERT_GREATER_OR_EQUAL(0, ch);
71
+ mik__pwm_free_channel(ch);
72
+ }
73
+
74
+ TEST_CASE("PWM LEDC set duty produces output", "[pwm]") {
75
+ int ch = mik__pwm_alloc_channel();
76
+ int timer = mik__pwm_alloc_timer(5000);
77
+ TEST_ASSERT_GREATER_OR_EQUAL(0, ch);
78
+ TEST_ASSERT_GREATER_OR_EQUAL(0, timer);
79
+
80
+ ledc_timer_bit_t resolution = mik__pwm_best_resolution(5000);
81
+
82
+ ledc_timer_config_t timer_cfg = {};
83
+ timer_cfg.speed_mode = LEDC_LOW_SPEED_MODE;
84
+ timer_cfg.duty_resolution = resolution;
85
+ timer_cfg.timer_num = static_cast<ledc_timer_t>(timer);
86
+ timer_cfg.freq_hz = 5000;
87
+ timer_cfg.clk_cfg = LEDC_AUTO_CLK;
88
+ TEST_ASSERT_EQUAL(ESP_OK, ledc_timer_config(&timer_cfg));
89
+
90
+ ledc_channel_config_t ch_cfg = {};
91
+ ch_cfg.gpio_num = PWM_PIN;
92
+ ch_cfg.speed_mode = LEDC_LOW_SPEED_MODE;
93
+ ch_cfg.channel = static_cast<ledc_channel_t>(ch);
94
+ ch_cfg.timer_sel = static_cast<ledc_timer_t>(timer);
95
+ ch_cfg.duty = mik__pwm_duty_to_raw(0.5, resolution);
96
+ ch_cfg.hpoint = 0;
97
+ TEST_ASSERT_EQUAL(ESP_OK, ledc_channel_config(&ch_cfg));
98
+
99
+ /* Let it run briefly */
100
+ vTaskDelay(pdMS_TO_TICKS(50));
101
+
102
+ /* Stop and release */
103
+ ledc_stop(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(ch), 0);
104
+ mik__pwm_free_channel(ch);
105
+ mik__pwm_free_timer(timer);
106
+ }
107
+
108
+ TEST_CASE("PWM fade completes", "[pwm]") {
109
+ int ch = mik__pwm_alloc_channel();
110
+ int timer = mik__pwm_alloc_timer(5000);
111
+ TEST_ASSERT_GREATER_OR_EQUAL(0, ch);
112
+ TEST_ASSERT_GREATER_OR_EQUAL(0, timer);
113
+
114
+ ledc_timer_bit_t resolution = mik__pwm_best_resolution(5000);
115
+
116
+ ledc_timer_config_t timer_cfg = {};
117
+ timer_cfg.speed_mode = LEDC_LOW_SPEED_MODE;
118
+ timer_cfg.duty_resolution = resolution;
119
+ timer_cfg.timer_num = static_cast<ledc_timer_t>(timer);
120
+ timer_cfg.freq_hz = 5000;
121
+ timer_cfg.clk_cfg = LEDC_AUTO_CLK;
122
+ TEST_ASSERT_EQUAL(ESP_OK, ledc_timer_config(&timer_cfg));
123
+
124
+ ledc_channel_config_t ch_cfg = {};
125
+ ch_cfg.gpio_num = PWM_PIN;
126
+ ch_cfg.speed_mode = LEDC_LOW_SPEED_MODE;
127
+ ch_cfg.channel = static_cast<ledc_channel_t>(ch);
128
+ ch_cfg.timer_sel = static_cast<ledc_timer_t>(timer);
129
+ ch_cfg.duty = 0;
130
+ ch_cfg.hpoint = 0;
131
+ TEST_ASSERT_EQUAL(ESP_OK, ledc_channel_config(&ch_cfg));
132
+
133
+ /* Install fade and run a short fade */
134
+ ledc_fade_func_install(0);
135
+
136
+ std::atomic<bool> fade_done{false};
137
+
138
+ ledc_cbs_t cbs = {};
139
+ cbs.fade_cb = [](const ledc_cb_param_t* param, void* arg) -> bool {
140
+ if (param->event == LEDC_FADE_END_EVT) {
141
+ static_cast<std::atomic<bool>*>(arg)->store(true, std::memory_order_release);
142
+ }
143
+ return false;
144
+ };
145
+ ledc_cb_register(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(ch), &cbs, &fade_done);
146
+
147
+ uint32_t target = mik__pwm_duty_to_raw(1.0, resolution);
148
+ TEST_ASSERT_EQUAL(ESP_OK,
149
+ ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE,
150
+ static_cast<ledc_channel_t>(ch), target, 100));
151
+ TEST_ASSERT_EQUAL(ESP_OK,
152
+ ledc_fade_start(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(ch),
153
+ LEDC_FADE_NO_WAIT));
154
+
155
+ /* Wait for fade to complete (max 500ms) */
156
+ for (int i = 0; i < 50; i++) {
157
+ if (fade_done.load(std::memory_order_acquire)) break;
158
+ vTaskDelay(pdMS_TO_TICKS(10));
159
+ }
160
+ TEST_ASSERT_TRUE(fade_done.load(std::memory_order_acquire));
161
+
162
+ /* Cleanup */
163
+ ledc_stop(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(ch), 0);
164
+ mik__pwm_free_channel(ch);
165
+ mik__pwm_free_timer(timer);
166
+ }