From 5632ab3eeb8de7b16a097de446e994db706df0a6 Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Wed, 6 Dec 2023 16:19:10 +0100 Subject: [PATCH 01/12] add event types with a timestamp --- main/espFeatures/freeRTOSEventQueue.h | 164 ++++++++++++++++++-------- 1 file changed, 115 insertions(+), 49 deletions(-) diff --git a/main/espFeatures/freeRTOSEventQueue.h b/main/espFeatures/freeRTOSEventQueue.h index 23694cc..800791e 100644 --- a/main/espFeatures/freeRTOSEventQueue.h +++ b/main/espFeatures/freeRTOSEventQueue.h @@ -7,21 +7,46 @@ #include #include #include +#include template class FreeRTOSEventQueueFeature : public Next { public: + using TimePoint = std::chrono::time_point; + struct Event { + using EvStdF = std::function*; + using EvFreeF = std::tuple; + using EvTmrStdF = std::tuple*, uint64_t>; + using EvTmrFreeF = std::tuple; + + std::variant< std::monostate, - std::function*, - std::tuple + EvStdF, + EvFreeF, + EvTmrStdF, + EvTmrFreeF > event = std::monostate(); Event() : event(std::monostate()) {} Event(std::function func) : event(new std::function(std::move(func))) {} Event(void(*func)(void*), void* arg) : event(std::make_tuple(func, arg)) {} + Event(std::function func): + event(std::make_tuple( + new std::function(std::move(func)), + // TODO: use efficient time source, maybe add time argument? + std::chrono::steady_clock::now().time_since_epoch().count() + )) + {} + Event(void(*func)(void*, TimePoint), void* arg): + event(std::make_tuple( + func, + arg, + std::chrono::steady_clock::now().time_since_epoch().count() + )) + {} Event& operator=(const Event& other) = delete; Event(const Event& other) = delete; @@ -34,27 +59,50 @@ class FreeRTOSEventQueueFeature : public Next { } ~Event() { - if (std::holds_alternative*>(event)) { - delete std::get*>(event); - } + std::visit([](auto& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + delete arg; + } + else if constexpr (std::is_same_v) { + delete std::get<0>(arg); + } + else { + // no cleanup + } + }, event); } void operator()() { - if (std::holds_alternative*>(event)) { - auto func = std::get*>(event); - if (func) { - (*func)(); + std::visit([](auto& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + if (arg) { + (*arg)(); + } + } + else if constexpr (std::is_same_v) { + auto [func, a] = arg; + if (func) { + func(a); + } + } + else if constexpr (std::is_same_v) { + auto [func, time] = arg; + if (func) { + (*func)(std::chrono::steady_clock::time_point(std::chrono::steady_clock::duration(time))); + } + } + else if constexpr (std::is_same_v) { + auto [func, a, time] = arg; + if (func) { + func(a, std::chrono::steady_clock::time_point(std::chrono::steady_clock::duration(time))); + } } - } - else if (std::holds_alternative>(event)) { - auto [func, arg] = std::get>(event); - if (func) { - func(arg); + else { + // empty event } - } - else { - // empty event - } + }, event); } operator bool() const { @@ -77,6 +125,30 @@ class FreeRTOSEventQueueFeature : public Next { return Event(); } } + + void _scheduleImpl(auto&&... args) { + Event e(std::forward(args)...); + auto res = xQueueSend(_eventQueue, &e, portMAX_DELAY); + if (res != pdPASS) { + // TODO: handle error + return; + } + e.release(); + } + + void _scheduleIsrImpl(auto&&... args) { + Event e(std::forward(args)...); + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + auto res = xQueueSendFromISR(_eventQueue, &e, &xHigherPriorityTaskWoken); + if (res != pdPASS) { + // TODO: handle error + return; + } + e.release(); + if (xHigherPriorityTaskWoken) { + portYIELD_FROM_ISR(); + } + } public: FreeRTOSEventQueueFeature() { @@ -100,47 +172,41 @@ class FreeRTOSEventQueueFeature : public Next { * @brief Schedule an event to be run * @param func Function to be run */ - void scheduleEvent(std::function func) { - Event e(std::move(func)); - auto res = xQueueSend(_eventQueue, &e, portMAX_DELAY); - if (res != pdPASS) { - // TODO: handle error - return; - } - e.release(); - } + void scheduleEvent(std::function func) { _scheduleImpl(std::move(func)); } /** * @brief Schedule an event to be run * @param func Function to be run + * @param arg Argument to be passed to function */ - void scheduleEvent(void(*func)(void*), void* arg) { - Event e(func, arg); - auto res = xQueueSend(_eventQueue, &e, portMAX_DELAY); - if (res != pdPASS) { - // TODO: handle error - return; - } - e.release(); - } + void scheduleEvent(void(*func)(void*), void* arg) { _scheduleImpl(func, arg); } + + /** + * @brief Schedule an event to be run + * @param func Function to be run + */ + void scheduleEvent(std::function func) { _scheduleImpl(std::move(func)); } + + /** + * @brief Schedule an event to be run + * @param func Function to be run + * @param arg Argument to be passed to function + */ + void scheduleEvent(void(*func)(void*, TimePoint), void* arg) { _scheduleImpl(func, arg); } /** * @brief Schedule an event to be run from ISR * @param func Function to be run + * @param arg Argument to be passed to function */ - void scheduleEventISR(void(*func)(void*), void* arg) { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - Event e(func, arg); - auto res = xQueueSendFromISR(_eventQueue, &e, &xHigherPriorityTaskWoken); - if (res != pdPASS) { - // TODO: handle error - return; - } - e.release(); - if (xHigherPriorityTaskWoken) { - portYIELD_FROM_ISR(); - } - } + void scheduleEventISR(void(*func)(void*), void* arg) { _scheduleIsrImpl(func, arg); } + + /** + * @brief Schedule an event to be run from ISR + * @param func Function to be run + * @param arg Argument to be passed to function + */ + void scheduleEventISR(void(*func)(void*, TimePoint), void* arg) { _scheduleIsrImpl(func, arg); } /** * @brief Wake up event loop if it is waiting for events From c8f2881c1b4de290bf72903853e8716e3e8a8e4a Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Wed, 6 Dec 2023 16:32:12 +0100 Subject: [PATCH 02/12] move pin accessors to a common base --- main/espFeatures/gpioFeature.h | 32 +++++------------ main/platform/esp32.h | 59 +++++++++++++------------------ main/platform/esp32c3.h | 51 ++++++++++----------------- main/platform/esp32s3.h | 63 ++++++++++++++-------------------- main/platform/espCommon.h | 38 ++++++++++++++++++++ 5 files changed, 113 insertions(+), 130 deletions(-) create mode 100644 main/platform/espCommon.h diff --git a/main/espFeatures/gpioFeature.h b/main/espFeatures/gpioFeature.h index deb203a..8ca8993 100644 --- a/main/espFeatures/gpioFeature.h +++ b/main/espFeatures/gpioFeature.h @@ -42,22 +42,6 @@ struct jac::ConvTraits { template class GpioFeature : public Next { using PinConfig = Next::PlatformInfo::PinConfig; - -public: - static gpio_num_t getDigitalPin(int pin) { - if (PinConfig::DIGITAL_PINS.find(pin) == PinConfig::DIGITAL_PINS.end()) { - throw std::runtime_error("Invalid digital pin"); - } - return static_cast(pin); - } - - static gpio_num_t getInterruptPin(int pin) { - if (PinConfig::INTERRUPT_PINS.find(pin) == PinConfig::INTERRUPT_PINS.end()) { - throw std::runtime_error("Invalid interrupt pin"); - } - return static_cast(pin); - } - private: class Gpio { class InterruptQueue { @@ -149,7 +133,7 @@ class GpioFeature : public Next { Gpio(GpioFeature* feature) : _feature(feature) {} void pinMode(int pinNum, PinMode mode) { - gpio_num_t pin = getDigitalPin(pinNum); + gpio_num_t pin = Next::getDigitalPin(pinNum); switch (mode) { case PinMode::DISABLE: @@ -175,17 +159,17 @@ class GpioFeature : public Next { } void write(int pinNum, int value) { - gpio_num_t pin = getDigitalPin(pinNum); + gpio_num_t pin = Next::getDigitalPin(pinNum); gpio_set_level(pin, value); } int read(int pinNum) { - gpio_num_t pin = getDigitalPin(pinNum); + gpio_num_t pin = Next::getDigitalPin(pinNum); return gpio_get_level(pin); } void attachInterrupt(int pinNum, InterruptMode mode, std::function callback, bool synchronous) { - gpio_num_t pin = getInterruptPin(pinNum); + gpio_num_t pin = Next::getInterruptPin(pinNum); if (_interruptCallbacks.find(pinNum) == _interruptCallbacks.end()) { _interruptCallbacks[pinNum] = std::make_unique(pin, _feature); @@ -244,7 +228,7 @@ class GpioFeature : public Next { } void detachInterrupt(int pinNum, InterruptMode mode) { - gpio_num_t pin = getInterruptPin(pinNum); + gpio_num_t pin = Next::getInterruptPin(pinNum); if (_interruptCallbacks.find(pinNum) == _interruptCallbacks.end() || !(*_interruptCallbacks[pinNum])[mode].first) { throw std::runtime_error("Interrupt not attached"); @@ -300,7 +284,7 @@ class GpioFeature : public Next { gpio.pinMode(pin, PinMode::DISABLE); } for (auto pin : PinConfig::INTERRUPT_PINS) { - gpio_intr_disable(getDigitalPin(pin)); + gpio_intr_disable(Next::getDigitalPin(pin)); } gpio_install_isr_service(0); @@ -331,8 +315,8 @@ class GpioFeature : public Next { ~GpioFeature() { for (auto pin : PinConfig::INTERRUPT_PINS) { - gpio_intr_disable(getDigitalPin(pin)); - gpio_isr_handler_remove(getDigitalPin(pin)); + gpio_intr_disable(Next::getDigitalPin(pin)); + gpio_isr_handler_remove(Next::getDigitalPin(pin)); } gpio_uninstall_isr_service(); diff --git a/main/platform/esp32.h b/main/platform/esp32.h index a9b1311..3c57ae7 100644 --- a/main/platform/esp32.h +++ b/main/platform/esp32.h @@ -1,49 +1,36 @@ #pragma once -#include #include #include #include -#include "hal/adc_types.h" -#include "soc/adc_channel.h" +#include "espCommon.h" -template -class PlatformInfoFeature : public Next { -public: - struct PlatformInfo { - static inline const std::string NAME = "ESP32"; +struct PlatformInfo { + static inline const std::string NAME = "ESP32"; - struct PinConfig { - static inline const std::set DIGITAL_PINS = { - 0, 2, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, - 23, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39 - }; - static inline const std::unordered_map> ANALOG_PINS = { - { 32, { 1, ADC1_GPIO32_CHANNEL } }, - { 33, { 1, ADC1_GPIO33_CHANNEL } }, - { 34, { 1, ADC1_GPIO34_CHANNEL } }, - { 35, { 1, ADC1_GPIO35_CHANNEL } }, - { 36, { 1, ADC1_GPIO36_CHANNEL } }, - { 37, { 1, ADC1_GPIO37_CHANNEL } }, - { 38, { 1, ADC1_GPIO38_CHANNEL } }, - { 39, { 1, ADC1_GPIO39_CHANNEL } } - }; - static inline const std::set INTERRUPT_PINS = DIGITAL_PINS; - static inline const int DEFAULT_I2C_SDA_PIN = 21; - static inline const int DEFAULT_I2C_SCL_PIN = 22; + struct PinConfig { + static inline const std::set DIGITAL_PINS = { + 0, 2, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, + 23, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39 + }; + static inline const std::unordered_map> ANALOG_PINS = { + { 32, { 1, ADC1_GPIO32_CHANNEL } }, + { 33, { 1, ADC1_GPIO33_CHANNEL } }, + { 34, { 1, ADC1_GPIO34_CHANNEL } }, + { 35, { 1, ADC1_GPIO35_CHANNEL } }, + { 36, { 1, ADC1_GPIO36_CHANNEL } }, + { 37, { 1, ADC1_GPIO37_CHANNEL } }, + { 38, { 1, ADC1_GPIO38_CHANNEL } }, + { 39, { 1, ADC1_GPIO39_CHANNEL } } }; + static inline const std::set INTERRUPT_PINS = DIGITAL_PINS; + static inline constexpr int DEFAULT_I2C_SDA_PIN = 21; + static inline constexpr int DEFAULT_I2C_SCL_PIN = 22; }; +}; - void initialize() { - Next::initialize(); - - jac::ContextRef ctx = this->context(); - - jac::Object platformInfo = jac::Object::create(ctx); - platformInfo.defineProperty("name", jac::Value::from(ctx, PlatformInfo::NAME), jac::PropFlags::Enumerable); - ctx.getGlobalObject().defineProperty("PlatformInfo", platformInfo, jac::PropFlags::Enumerable); - } -}; +template +using PlatformInfoFeature = EspCommon; diff --git a/main/platform/esp32c3.h b/main/platform/esp32c3.h index 742dd02..07bb412 100644 --- a/main/platform/esp32c3.h +++ b/main/platform/esp32c3.h @@ -1,45 +1,32 @@ #pragma once -#include #include #include #include -#include "hal/adc_types.h" -#include "soc/adc_channel.h" +#include "espCommon.h" -template -class PlatformInfoFeature : public Next { -public: - struct PlatformInfo { - static inline const std::string NAME = "ESP32-C3"; +struct PlatformInfo { + static inline const std::string NAME = "ESP32-C3"; - struct PinConfig { - static inline const std::set DIGITAL_PINS = { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 21 - }; - static inline const std::unordered_map> ANALOG_PINS = { - { 0, { 1, ADC1_GPIO0_CHANNEL }}, - { 1, { 1, ADC1_GPIO1_CHANNEL }}, - { 2, { 1, ADC1_GPIO2_CHANNEL }}, - { 3, { 1, ADC1_GPIO3_CHANNEL }}, - { 4, { 1, ADC1_GPIO4_CHANNEL }} - }; - static inline const std::set INTERRUPT_PINS = DIGITAL_PINS; - static inline const int DEFAULT_I2C_SDA_PIN = 0; - static inline const int DEFAULT_I2C_SCL_PIN = 1; + struct PinConfig { + static inline const std::set DIGITAL_PINS = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 21 + }; + static inline const std::unordered_map> ANALOG_PINS = { + { 0, { 1, ADC1_GPIO0_CHANNEL }}, + { 1, { 1, ADC1_GPIO1_CHANNEL }}, + { 2, { 1, ADC1_GPIO2_CHANNEL }}, + { 3, { 1, ADC1_GPIO3_CHANNEL }}, + { 4, { 1, ADC1_GPIO4_CHANNEL }} }; + static inline const std::set INTERRUPT_PINS = DIGITAL_PINS; + static inline constexpr int DEFAULT_I2C_SDA_PIN = 0; + static inline constexpr int DEFAULT_I2C_SCL_PIN = 1; }; +}; - void initialize() { - Next::initialize(); - - jac::ContextRef ctx = this->context(); - - jac::Object platformInfo = jac::Object::create(ctx); - platformInfo.defineProperty("name", jac::Value::from(ctx, PlatformInfo::NAME), jac::PropFlags::Enumerable); - ctx.getGlobalObject().defineProperty("PlatformInfo", platformInfo, jac::PropFlags::Enumerable); - } -}; +template +using PlatformInfoFeature = EspCommon; diff --git a/main/platform/esp32s3.h b/main/platform/esp32s3.h index 5f40b7b..97be749 100644 --- a/main/platform/esp32s3.h +++ b/main/platform/esp32s3.h @@ -1,51 +1,38 @@ #pragma once -#include #include #include #include -#include "hal/adc_types.h" -#include "soc/adc_channel.h" +#include "espCommon.h" -template -class PlatformInfoFeature : public Next { -public: - struct PlatformInfo { - static inline const std::string NAME = "ESP32-S3"; +struct PlatformInfo { + static inline constexpr std::string NAME = "ESP32-S3"; - struct PinConfig { - static inline const std::set DIGITAL_PINS = { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, - 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 45, 46, 47, 48 - }; - static inline const std::unordered_map> ANALOG_PINS = { - { 1, { 1, ADC1_GPIO1_CHANNEL } }, - { 2, { 1, ADC1_GPIO2_CHANNEL } }, - { 3, { 1, ADC1_GPIO3_CHANNEL } }, - { 4, { 1, ADC1_GPIO4_CHANNEL } }, - { 5, { 1, ADC1_GPIO5_CHANNEL } }, - { 6, { 1, ADC1_GPIO6_CHANNEL } }, - { 7, { 1, ADC1_GPIO7_CHANNEL } }, - { 8, { 1, ADC1_GPIO8_CHANNEL } }, - { 9, { 1, ADC1_GPIO9_CHANNEL } }, - { 10, { 1, ADC1_GPIO10_CHANNEL } } - }; - static inline const std::set INTERRUPT_PINS = DIGITAL_PINS; - static inline const int DEFAULT_I2C_SDA_PIN = 0; - static inline const int DEFAULT_I2C_SCL_PIN = 1; + struct PinConfig { + static inline const std::set DIGITAL_PINS = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 45, 46, 47, 48 + }; + static inline const std::unordered_map> ANALOG_PINS = { + { 1, { 1, ADC1_GPIO1_CHANNEL } }, + { 2, { 1, ADC1_GPIO2_CHANNEL } }, + { 3, { 1, ADC1_GPIO3_CHANNEL } }, + { 4, { 1, ADC1_GPIO4_CHANNEL } }, + { 5, { 1, ADC1_GPIO5_CHANNEL } }, + { 6, { 1, ADC1_GPIO6_CHANNEL } }, + { 7, { 1, ADC1_GPIO7_CHANNEL } }, + { 8, { 1, ADC1_GPIO8_CHANNEL } }, + { 9, { 1, ADC1_GPIO9_CHANNEL } }, + { 10, { 1, ADC1_GPIO10_CHANNEL } } }; + static inline const std::set INTERRUPT_PINS = DIGITAL_PINS; + static inline constexpr int DEFAULT_I2C_SDA_PIN = 0; + static inline constexpr int DEFAULT_I2C_SCL_PIN = 1; }; +}; - void initialize() { - Next::initialize(); - - jac::ContextRef ctx = this->context(); - - jac::Object platformInfo = jac::Object::create(ctx); - platformInfo.defineProperty("name", jac::Value::from(ctx, PlatformInfo::NAME), jac::PropFlags::Enumerable); - ctx.getGlobalObject().defineProperty("PlatformInfo", platformInfo, jac::PropFlags::Enumerable); - } -}; +template +using PlatformInfoFeature = EspCommon; diff --git a/main/platform/espCommon.h b/main/platform/espCommon.h new file mode 100644 index 0000000..3e1bfdd --- /dev/null +++ b/main/platform/espCommon.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "hal/adc_types.h" +#include "soc/adc_channel.h" + + +template +class EspCommon : public Next { +public: + using PlatformInfo = PlatformInfo_; + + static gpio_num_t getDigitalPin(int pin) { + if (PlatformInfo::PinConfig::DIGITAL_PINS.find(pin) == PlatformInfo::PinConfig::DIGITAL_PINS.end()) { + throw std::runtime_error("Invalid digital pin"); + } + return static_cast(pin); + } + + static gpio_num_t getInterruptPin(int pin) { + if (PlatformInfo::PinConfig::INTERRUPT_PINS.find(pin) == PlatformInfo::PinConfig::INTERRUPT_PINS.end()) { + throw std::runtime_error("Invalid interrupt pin"); + } + return static_cast(pin); + } + + void initialize() { + Next::initialize(); + + jac::ContextRef ctx = this->context(); + + jac::Object platformInfo = jac::Object::create(ctx); + platformInfo.defineProperty("name", jac::Value::from(ctx, PlatformInfo::NAME), jac::PropFlags::Enumerable); + + ctx.getGlobalObject().defineProperty("PlatformInfo", platformInfo, jac::PropFlags::Enumerable); + } +}; From 55c0c64dd13ef1e5c47f63b9155912859e5d775c Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Wed, 6 Dec 2023 18:30:21 +0100 Subject: [PATCH 03/12] WIP replace "gpio" with "Digital" API --- main/espFeatures/digitalFeature.h | 366 ++++++++++++++++++++++++++++++ main/espFeatures/gpioFeature.h | 324 -------------------------- main/main.cpp | 4 +- ts-examples/@types/digital.d.ts | 39 ++++ ts-examples/@types/gpio.d.ts | 45 ---- ts-examples/src/blink.ts | 9 +- ts-examples/src/gomoku.ts | 126 ++++++---- ts-examples/src/radioblink.ts | 73 +++--- ts-examples/src/snake.ts | 61 +++-- 9 files changed, 587 insertions(+), 460 deletions(-) create mode 100644 main/espFeatures/digitalFeature.h delete mode 100644 main/espFeatures/gpioFeature.h create mode 100644 ts-examples/@types/digital.d.ts delete mode 100644 ts-examples/@types/gpio.d.ts diff --git a/main/espFeatures/digitalFeature.h b/main/espFeatures/digitalFeature.h new file mode 100644 index 0000000..d85e47b --- /dev/null +++ b/main/espFeatures/digitalFeature.h @@ -0,0 +1,366 @@ +#pragma once + + +#include +#include +#include +#include +#include +#include +#include "driver/gpio.h" + + +enum class DigitalMode : int { + DISABLE = 0, + OUTPUT = 1, + OUTPUT_OPEN_DRAIN = 2, + INPUT = 3, + INPUT_PULLUP = 4, + INPUT_PULLDOWN = 5, + INPUT_PULLUPDOWN = 6 +}; + +enum class DigitalEdge : int { + DISABLE = 0, + RISING = 1, + FALLING = 2, + CHANGE = 3 +}; + +template<> +struct jac::ConvTraits { + static Value to(ContextRef ctx, DigitalMode val) { + int mode = static_cast(val); + return Value::from(ctx, mode); + } + + static DigitalMode from(ContextRef ctx, ValueWeak val) { + int mode = val.to(); + if (mode < 0 || mode > 6) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "Invalid digital mode"); + } + return static_cast(mode); + } +}; + +template<> +struct jac::ConvTraits { + static Value to(ContextRef ctx, DigitalEdge val) { + int mode = static_cast(val); + return Value::from(ctx, mode); + } + + static DigitalEdge from(ContextRef ctx, ValueWeak val) { + int mode = val.to(); + if (mode < 0 || mode > 3) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "Invalid digital edge"); + } + return static_cast(mode); + } +}; + +namespace detail { + + template + class InterruptQueue { + using ArrayType = std::array; + ArrayType queue; + ArrayType::iterator head = queue.begin(); + ArrayType::iterator tail = queue.begin(); + + auto next(ArrayType::iterator it) { + it++; + return it == queue.end() ? queue.begin() : it; + } + + public: + bool push(DigitalEdge mode) { + if (next(tail) == head) { + return false; + } + *tail = mode; + tail = next(tail); + return true; + } + + DigitalEdge pop() { + if (head == tail) { + return DigitalEdge::DISABLE; + } + DigitalEdge mode = *head; + head = next(head); + return mode; + } + }; + + struct InterruptConf { + const TickType_t _debounceTicks; + InterruptQueue<16> queue; + bool lastRising = false; + TickType_t lastTime = 0; + + bool _isAssigned = false; + bool _synchronous; // TODO: maybe remove asynchronous mode? + std::function)> _callback; + + InterruptConf(int debounceMs = 0) : _debounceTicks(debounceMs / portTICK_PERIOD_MS) {} + + bool updateLast(bool risingEdge) { + // TODO: Use more precise time source + auto now = xTaskGetTickCountFromISR(); + + // two successive interrupts with the same edge within + // 2 ticks are probably caused by some hardware bug + if (lastRising == risingEdge && now - lastTime < 4) { + return false; + } + + lastTime = now; + lastRising = risingEdge; + return true; + } + + void setCallback(std::function)> callback, + bool synchronous) { + _isAssigned = false; + _callback = callback; + _synchronous = synchronous; + _isAssigned = bool(callback); + } + }; + +} // namespace detail + + +template +class Digital { + Feature* const _feature; + const gpio_num_t _pin; + detail::InterruptConf _interruptConf; + + void enableInterrupt(DigitalEdge mode) { + switch (mode) { + case DigitalEdge::DISABLE: + return; + case DigitalEdge::RISING: + gpio_set_intr_type(_pin, GPIO_INTR_POSEDGE); + break; + case DigitalEdge::FALLING: + gpio_set_intr_type(_pin, GPIO_INTR_NEGEDGE); + break; + case DigitalEdge::CHANGE: + gpio_set_intr_type(_pin, GPIO_INTR_ANYEDGE); + break; + } + + static constexpr auto call = +[](void* arg, std::chrono::time_point time) { + auto& self = *static_cast(arg); + switch (self._interruptConf.queue.pop()) { + case DigitalEdge::RISING: + self._interruptConf._callback(true, time); + break; + case DigitalEdge::FALLING: + self._interruptConf._callback(false, time); + break; + default: + break; + } + }; + + gpio_isr_handler_add(_pin, [](void* arg) { + auto& self = *static_cast(arg); + Feature* feature = self._feature; + + if (!self._interruptConf._isAssigned) { + return; + } + + bool risingEdge = gpio_get_level(self._pin) == 1; + if (!self._interruptConf.updateLast(risingEdge)) { + return; + } + + self._interruptConf.queue.push(risingEdge ? DigitalEdge::RISING : DigitalEdge::FALLING); + feature->scheduleEventISR(call, &self); + }, this); + gpio_intr_enable(_pin); + } + + void disableInterrupt() { + if (_interruptConf._isAssigned) { + gpio_intr_disable(_pin); + gpio_isr_handler_remove(_pin); + + _interruptConf._isAssigned = false; + } + } + + void pinMode(DigitalMode mode) { + switch (mode) { + case DigitalMode::DISABLE: + gpio_set_direction(_pin, GPIO_MODE_DISABLE); + break; + case DigitalMode::OUTPUT: + gpio_set_direction(_pin, GPIO_MODE_OUTPUT); + break; + case DigitalMode::INPUT: + gpio_set_direction(_pin, GPIO_MODE_INPUT); + gpio_set_pull_mode(_pin, GPIO_FLOATING); + break; + case DigitalMode::INPUT_PULLUP: + gpio_set_direction(_pin, GPIO_MODE_INPUT); + gpio_set_pull_mode(_pin, GPIO_PULLUP_ONLY); + break; + case DigitalMode::INPUT_PULLDOWN: + gpio_set_direction(_pin, GPIO_MODE_INPUT); + gpio_set_pull_mode(_pin, GPIO_PULLDOWN_ONLY); + break; + case DigitalMode::INPUT_PULLUPDOWN: + gpio_set_direction(_pin, GPIO_MODE_INPUT); + gpio_set_pull_mode(_pin, GPIO_PULLUP_PULLDOWN); + break; + case DigitalMode::OUTPUT_OPEN_DRAIN: + gpio_set_direction(_pin, GPIO_MODE_OUTPUT_OD); + break; + } + } +public: + Digital(Feature* feature, int pin, DigitalMode mode) : + _feature(feature), + _pin(Feature::getDigitalPin(pin)), + _interruptConf(0) + { + pinMode(mode); + } + + Digital(Feature* feature, int pin, DigitalMode mode, DigitalEdge interruptMode, + std::function)> callback, + int debounceMs) : + _feature(feature), + _pin(Feature::getDigitalPin(pin)), + _interruptConf(debounceMs) + { + pinMode(mode); + enableInterrupt(interruptMode); + _interruptConf.setCallback(callback, true); + } + + Digital(const Digital&) = delete; + Digital& operator=(const Digital&) = delete; + Digital(Digital&&) = delete; + Digital& operator=(Digital&&) = delete; + + ~Digital() { + close(); + } + + void write(bool value) { + gpio_set_level(_pin, value); + } + + bool read() { + return gpio_get_level(_pin); + } + + void close() { + disableInterrupt(); + pinMode(DigitalMode::DISABLE); + } +}; + +template +struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { + using Digital_ = Digital; + + static Digital_* constructOpaque(jac::ContextRef ctx, std::vector args) { + // TODO: extend instance lifetime until close or program end + // TODO: check if pin is already in use + + jac::ObjectWeak options = args[0].to(); + + int pin = options.get("pin"); + DigitalMode mode = options.get("mode"); + if (options.hasProperty("onReadable")) { + jac::Function callback = options.get("onReadable"); + DigitalEdge interruptMode = options.get("edge"); + int debounceMs = 0; + if (options.hasProperty("debounce")) { + debounceMs = options.get("debounce"); + } + + return new Digital_(static_cast( + JS_GetContextOpaque(ctx)), pin, mode, interruptMode, + [ctx, callback](bool risingEdge, std::chrono::time_point timestamp) mutable { + jac::Object arg = jac::Object::create(ctx); + arg.set("edge", risingEdge ? DigitalEdge::RISING : DigitalEdge::FALLING); + arg.set("timestamp", std::chrono::duration_cast(timestamp.time_since_epoch()).count()); + callback.call(arg); + }, + debounceMs + ); + } + else { + return new Digital_(static_cast(JS_GetContextOpaque(ctx)), pin, mode); + } + } + + static void addProperties(jac::ContextRef ctx, jac::Object proto) { + jac::FunctionFactory ff(ctx); + + DigitalProtoBuilder::template addMethodMember(ctx, proto, "write", jac::PropFlags::Enumerable); + DigitalProtoBuilder::template addMethodMember(ctx, proto, "read", jac::PropFlags::Enumerable); + DigitalProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); + } +}; + + +template +class DigitalFeature : public Next { + using PinConfig = typename Next::PlatformInfo::PinConfig; + using DigitalClass = jac::Class>; +public: + void initialize() { + Next::initialize(); + + for (auto pin : PinConfig::DIGITAL_PINS) { + gpio_set_direction(this->getDigitalPin(pin), GPIO_MODE_DISABLE); + } + for (auto pin : PinConfig::INTERRUPT_PINS) { + gpio_intr_disable(this->getDigitalPin(pin)); + } + + gpio_install_isr_service(0); + + jac::FunctionFactory ff(this->context()); + auto& module = this->newModule("embedded:io/digital"); + + DigitalClass::init("Digital"); + DigitalClass::initContext(this->context()); + + jac::Object digitalConstructor = DigitalClass::getConstructor(this->context()); + + module.addExport("Digital", digitalConstructor); + + digitalConstructor.set("Input", DigitalMode::INPUT); + digitalConstructor.set("InputPullUp", DigitalMode::INPUT_PULLUP); + digitalConstructor.set("InputPullDown", DigitalMode::INPUT_PULLDOWN); + digitalConstructor.set("InputPullUpDown", DigitalMode::INPUT_PULLUPDOWN); + digitalConstructor.set("Output", DigitalMode::OUTPUT); + digitalConstructor.set("OutputOpenDrain", DigitalMode::OUTPUT_OPEN_DRAIN); + + digitalConstructor.set("None", DigitalEdge::DISABLE); + digitalConstructor.set("Rising", DigitalEdge::RISING); + digitalConstructor.set("Falling", DigitalEdge::FALLING); + } + + DigitalFeature() {} + + ~DigitalFeature() { + for (auto pin : PinConfig::INTERRUPT_PINS) { + gpio_intr_disable(this->getDigitalPin(pin)); + gpio_isr_handler_remove(this->getDigitalPin(pin)); + } + + gpio_uninstall_isr_service(); + } +}; diff --git a/main/espFeatures/gpioFeature.h b/main/espFeatures/gpioFeature.h deleted file mode 100644 index 8ca8993..0000000 --- a/main/espFeatures/gpioFeature.h +++ /dev/null @@ -1,324 +0,0 @@ -#pragma once - - -#include -#include -#include -#include -#include -#include -#include "driver/gpio.h" - - -enum class PinMode { - DISABLE, - OUTPUT, - INPUT, - INPUT_PULLUP, - INPUT_PULLDOWN -}; - -enum class InterruptMode { - DISABLE = static_cast(GPIO_INTR_DISABLE), - RISING = static_cast(GPIO_INTR_POSEDGE), - FALLING = static_cast(GPIO_INTR_NEGEDGE), - CHANGE = static_cast(GPIO_INTR_ANYEDGE), -}; - -template<> -struct jac::ConvTraits { - static Value to(ContextRef ctx, PinMode val) { - int mode = static_cast(val); - return Value::from(ctx, mode); - } - - static PinMode from(ContextRef ctx, ValueWeak val) { - int mode = val.to(); - return static_cast(mode); - } -}; - - -template -class GpioFeature : public Next { - using PinConfig = Next::PlatformInfo::PinConfig; -private: - class Gpio { - class InterruptQueue { - using ArrayType = std::array>, 32>; - ArrayType queue; - ArrayType::iterator head = queue.begin(); - ArrayType::iterator tail = queue.begin(); - - auto next(ArrayType::iterator it) { - it++; - return it == queue.end() ? queue.begin() : it; - } - - public: - bool push(std::shared_ptr> callback) { - if (next(tail) == head) { - return false; - } - *tail = callback; - tail = next(tail); - return true; - } - - std::shared_ptr> pop() { - if (head == tail) { - return nullptr; - } - auto callback = *head; - *head = nullptr; - head = next(head); - return callback; - } - }; - InterruptQueue _interruptQueue; - - class Interrupts { - static constexpr TickType_t DEBOUNCE_TIME = 2; - - std::pair>, bool> rising; - std::pair>, bool> falling; - std::pair>, bool> change; - gpio_num_t pin; - TickType_t lastTime = 0; - bool lastRising = false; - GpioFeature* _feature; - - public: - Interrupts(gpio_num_t pin, GpioFeature* feature) : pin(pin), _feature(feature) {} - - std::pair>, bool>& operator[](InterruptMode mode) { - switch (mode) { - case InterruptMode::RISING: - return rising; - case InterruptMode::FALLING: - return falling; - case InterruptMode::CHANGE: - return change; - default: - throw std::runtime_error("Invalid interrupt mode"); - } - } - - operator bool() const { - return rising.first || falling.first || change.first; - } - - gpio_num_t getPin() const { - return pin; - } - - GpioFeature* getFeature() const { - return _feature; - } - - bool updateLast(bool risingEdge) { - auto now = xTaskGetTickCountFromISR(); - if (lastRising == risingEdge && now - lastTime < DEBOUNCE_TIME) { - return false; - } - lastTime = now; - lastRising = risingEdge; - return true; - } - }; - - std::unordered_map> _interruptCallbacks; - GpioFeature* _feature; - public: - Gpio(GpioFeature* feature) : _feature(feature) {} - - void pinMode(int pinNum, PinMode mode) { - gpio_num_t pin = Next::getDigitalPin(pinNum); - - switch (mode) { - case PinMode::DISABLE: - gpio_set_direction(pin, GPIO_MODE_DISABLE); - break; - case PinMode::OUTPUT: - gpio_set_direction(pin, GPIO_MODE_OUTPUT); - break; - break; - case PinMode::INPUT: - gpio_set_direction(pin, GPIO_MODE_INPUT); - gpio_set_pull_mode(pin, GPIO_FLOATING); - break; - case PinMode::INPUT_PULLUP: - gpio_set_direction(pin, GPIO_MODE_INPUT); - gpio_set_pull_mode(pin, GPIO_PULLUP_ONLY); - break; - case PinMode::INPUT_PULLDOWN: - gpio_set_direction(pin, GPIO_MODE_INPUT); - gpio_set_pull_mode(pin, GPIO_PULLDOWN_ONLY); - break; - } - } - - void write(int pinNum, int value) { - gpio_num_t pin = Next::getDigitalPin(pinNum); - gpio_set_level(pin, value); - } - - int read(int pinNum) { - gpio_num_t pin = Next::getDigitalPin(pinNum); - return gpio_get_level(pin); - } - - void attachInterrupt(int pinNum, InterruptMode mode, std::function callback, bool synchronous) { - gpio_num_t pin = Next::getInterruptPin(pinNum); - - if (_interruptCallbacks.find(pinNum) == _interruptCallbacks.end()) { - _interruptCallbacks[pinNum] = std::make_unique(pin, _feature); - gpio_set_intr_type(pin, GPIO_INTR_ANYEDGE); - gpio_isr_handler_add(pin, [](void* arg) { - auto& callbacks = *static_cast(arg); - auto* feature = callbacks.getFeature(); - auto& interruptQueue = feature->gpio._interruptQueue; - - static void (*call)(void*) = [](void* arg) { - auto& queue = *static_cast(arg); - auto callback = queue.pop(); - if (callback) { - (*callback)(); - } - }; - - bool risingEdge = gpio_get_level(callbacks.getPin()) == 1; - if (!callbacks.updateLast(risingEdge)) { - return; - } - - if (callbacks[InterruptMode::CHANGE].first) { - if (callbacks[InterruptMode::CHANGE].second) { - interruptQueue.push(callbacks[InterruptMode::CHANGE].first); - feature->scheduleEventISR(call, &interruptQueue); - } else { - (*callbacks[InterruptMode::CHANGE].first)(); - } - } - if (callbacks[InterruptMode::RISING].first && risingEdge) { - if (callbacks[InterruptMode::RISING].second) { - interruptQueue.push(callbacks[InterruptMode::RISING].first); - feature->scheduleEventISR(call, &interruptQueue); - } else { - (*callbacks[InterruptMode::RISING].first)(); - } - } - if (callbacks[InterruptMode::FALLING].first && !risingEdge) { - if (callbacks[InterruptMode::FALLING].second) { - interruptQueue.push(callbacks[InterruptMode::FALLING].first); - feature->scheduleEventISR(call, &interruptQueue); - } else { - (*callbacks[InterruptMode::FALLING].first)(); - } - } - }, _interruptCallbacks[pinNum].get()); - gpio_intr_enable(pin); - } - - if ((*_interruptCallbacks[pinNum])[mode].first) { - throw std::runtime_error("Interrupt already attached"); - } - - (*_interruptCallbacks[pinNum])[mode] = std::make_pair(std::make_unique>(std::move(callback)), synchronous); - } - - void detachInterrupt(int pinNum, InterruptMode mode) { - gpio_num_t pin = Next::getInterruptPin(pinNum); - - if (_interruptCallbacks.find(pinNum) == _interruptCallbacks.end() || !(*_interruptCallbacks[pinNum])[mode].first) { - throw std::runtime_error("Interrupt not attached"); - } - - (*_interruptCallbacks[pinNum])[mode].first = nullptr; - - if (!_interruptCallbacks[pinNum]) { - gpio_intr_disable(pin); - gpio_isr_handler_remove(pin); - _interruptCallbacks.erase(pinNum); - } - } - - void on(std::string event, int pinNum, std::function callback) { - if (event == "rising") { - attachInterrupt(pinNum, InterruptMode::RISING, std::move(callback), true); - } - else if (event == "falling") { - attachInterrupt(pinNum, InterruptMode::FALLING, std::move(callback), true); - } - else if (event == "change") { - attachInterrupt(pinNum, InterruptMode::CHANGE, std::move(callback), true); - } - else { - throw std::runtime_error("Invalid event"); - } - } - - void off(std::string event, int pinNum) { - if (event == "rising") { - detachInterrupt(pinNum, InterruptMode::RISING); - } - else if (event == "falling") { - detachInterrupt(pinNum, InterruptMode::FALLING); - } - else if (event == "change") { - detachInterrupt(pinNum, InterruptMode::CHANGE); - } - else { - throw std::runtime_error("Invalid event"); - } - } - }; - -public: - Gpio gpio; - - void initialize() { - Next::initialize(); - - for (auto pin : PinConfig::DIGITAL_PINS) { - gpio.pinMode(pin, PinMode::DISABLE); - } - for (auto pin : PinConfig::INTERRUPT_PINS) { - gpio_intr_disable(Next::getDigitalPin(pin)); - } - - gpio_install_isr_service(0); - - jac::FunctionFactory ff(this->context()); - auto& module = this->newModule("gpio"); - module.addExport("pinMode", ff.newFunction(noal::function(&Gpio::pinMode, &gpio))); - module.addExport("read", ff.newFunction(noal::function(&Gpio::read, &gpio))); - module.addExport("write", ff.newFunction(noal::function(&Gpio::write, &gpio))); - - jac::Object pinModeEnum = jac::Object::create(this->context()); - pinModeEnum.set("DISABLE", static_cast(PinMode::DISABLE)); - pinModeEnum.set("OUTPUT", static_cast(PinMode::OUTPUT)); - pinModeEnum.set("INPUT", static_cast(PinMode::INPUT)); - pinModeEnum.set("INPUT_PULLUP", static_cast(PinMode::INPUT_PULLUP)); - pinModeEnum.set("INPUT_PULLDOWN", static_cast(PinMode::INPUT_PULLDOWN)); - module.addExport("PinMode", pinModeEnum); - - module.addExport("on", ff.newFunction([this](std::string event, int pin, jac::Function callback) { - this->gpio.on(event, pin, [callback = std::move(callback)]() mutable { - callback.call(); - }); - })); - module.addExport("off", ff.newFunction(noal::function(&Gpio::off, &gpio))); - } - - GpioFeature() : gpio(this) {} - - ~GpioFeature() { - for (auto pin : PinConfig::INTERRUPT_PINS) { - gpio_intr_disable(Next::getDigitalPin(pin)); - gpio_isr_handler_remove(Next::getDigitalPin(pin)); - } - - gpio_uninstall_isr_service(); - } -}; diff --git a/main/main.cpp b/main/main.cpp index 5179d83..539c5c9 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -15,7 +15,7 @@ #include #include "espFeatures/smartLedFeature.h" -#include "espFeatures/gpioFeature.h" +#include "espFeatures/digitalFeature.h" #include "espFeatures/freeRTOSEventQueue.h" #include "espFeatures/ledcFeature.h" #include "espFeatures/adcFeature.h" @@ -58,7 +58,7 @@ using Machine = jac::ComposeMachine< jac::FilesystemFeature, jac::ModuleLoaderFeature, jac::EventLoopFeature, - GpioFeature, + DigitalFeature, LedcFeature, AdcFeature, I2CFeature, diff --git a/ts-examples/@types/digital.d.ts b/ts-examples/@types/digital.d.ts new file mode 100644 index 0000000..2f35334 --- /dev/null +++ b/ts-examples/@types/digital.d.ts @@ -0,0 +1,39 @@ +declare module "embedded:io/digital" { + type DigitalMode = number; + type DigitalEdge = number; + + class Digital { + constructor(options: { + pin: number, + mode: DigitalMode, + edge?: DigitalEdge, + debounce?: number, + onReadable?: ({ edge: DigitalEdge, timestamp: number }) => void }, + ); + + /** + * Reads the value of the pin. + * @returns The value of the pin. + */ + read(): number; + + /** + * Writes the value of the pin. + * @param value The value to write. + */ + write(value: number): void; + + close(): void; + + static Input: DigitalMode; + static InputPullUp: DigitalMode; + static InputPullDown: DigitalMode; + static InputPullUpDown: DigitalMode; + static Output: DigitalMode; + static OutputOpenDrain: DigitalMode; + + static None: DigitalEdge; + static Rising: DigitalEdge; + static Falling: DigitalEdge; + } +} diff --git a/ts-examples/@types/gpio.d.ts b/ts-examples/@types/gpio.d.ts deleted file mode 100644 index 9c74419..0000000 --- a/ts-examples/@types/gpio.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -declare module "gpio" { - const PinMode: { - DISABLE: number, - OUTPUT: number, - INPUT: number, - INPUT_PULLUP: number, - INPUT_PULLDOWN: number, - }; - - /** - * Configure the given pin. - * @param pin The pin to configure. - * @param mode The mode to configure the pin in. - */ - function pinMode(pin: number, mode: number): void; - - /** - * Write digital value to the given pin. - * @param pin The pin to write to. - * @param value The value to write. - */ - function write(pin: number, value: number): void; - - /** - * Read digital value from the given pin. - * @param pin The pin to read from. - * @returns The value of the pin (0 or 1). - */ - function read(pin: number): number; - - /** - * Set event handler for the given pin. - * @param event The event to handle. - * @param pin The pin to handle the event for. - * @param callback The callback to call when the event occurs. - */ - function on(event: "rising" | "falling" | "change", pin: number, callback: () => void): void; - - /** - * Remove event handler for the given pin. - * @param event The event to remove. - * @param pin The pin to remove the event handler for. - */ - function off(event: "rising" | "falling" | "change", pin: number): void; -} diff --git a/ts-examples/src/blink.ts b/ts-examples/src/blink.ts index cc83e16..f67961c 100644 --- a/ts-examples/src/blink.ts +++ b/ts-examples/src/blink.ts @@ -1,4 +1,4 @@ -import * as gpio from "gpio"; +import { Digital } from "embedded:io/digital"; /** * This example blinks an LED on pin 45. @@ -6,11 +6,14 @@ import * as gpio from "gpio"; const LED_PIN = 45; -gpio.pinMode(LED_PIN, gpio.PinMode.OUTPUT); +let led = new Digital({ + pin: LED_PIN, + mode: Digital.Output, +}); let state = false; setInterval(() => { - gpio.write(LED_PIN, state ? 1 : 0); + led.write(state ? 1 : 0); state = !state; }, 1000); diff --git a/ts-examples/src/gomoku.ts b/ts-examples/src/gomoku.ts index 4f011f7..049c43c 100644 --- a/ts-examples/src/gomoku.ts +++ b/ts-examples/src/gomoku.ts @@ -1,5 +1,5 @@ import { SmartLed, Rgb, LED_WS2812 } from "smartled"; -import * as gpio from "gpio"; +import { Digital } from "embedded:io/digital"; /** * A simple Gomoku game. @@ -13,29 +13,23 @@ if (PlatformInfo.name == "ESP32") { var LED_PIN = 23; var POWER_PIN = 16; - var UP = 14; - var DOWN = 26; - var LEFT = 32; - var RIGHT = 13; - var MIDDLE = 17; + var UP_PIN = 14; + var DOWN_PIN = 26; + var LEFT_PIN = 32; + var RIGHT_PIN = 13; + var MIDDLE_PIN = 17; } else if (PlatformInfo.name == "ESP32-S3") { var LED_PIN = 45; var POWER_PIN = 0; - var UP = 8; - var DOWN = 14; - var LEFT = 10; - var RIGHT = 4; - var MIDDLE = 9; + var UP_PIN = 8; + var DOWN_PIN = 14; + var LEFT_PIN = 10; + var RIGHT_PIN = 4; + var MIDDLE_PIN = 9; } -gpio.pinMode(POWER_PIN, gpio.PinMode.OUTPUT); -gpio.write(POWER_PIN, 1); - -for (let pin of [UP, DOWN, LEFT, RIGHT, MIDDLE]) { - gpio.pinMode(pin, gpio.PinMode.INPUT_PULLUP); -} let strip = new SmartLed(LED_PIN, 100, LED_WS2812); @@ -118,43 +112,85 @@ function update(oldPos: Pos, newPos: Pos) { pos = newPos; } -gpio.on("falling", UP, () => { - update(pos, { x: pos.x, y: Math.max(0, pos.y - 1) }); -}); -gpio.on("falling", DOWN, () => { - update(pos, { x: pos.x, y: Math.min(9, pos.y + 1) }); +let power = new Digital({ pin: POWER_PIN, mode: Digital.Output }); +power.write(1); + +let up = new Digital({ + pin: UP_PIN, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + update(pos, { x: pos.x, y: Math.max(0, pos.y - 1) }); + } }); -gpio.on("falling", LEFT, () => { - update(pos, { x: Math.max(0, pos.x - 1), y: pos.y }); +let down = new Digital({ + pin: DOWN_PIN, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + update(pos, { x: pos.x, y: Math.min(9, pos.y + 1) }); + } }); -gpio.on("falling", RIGHT, () => { - update(pos, { x: Math.min(9, pos.x + 1), y: pos.y }); +let left = new Digital({ + pin: LEFT_PIN, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + update(pos, { x: Math.max(0, pos.x - 1), y: pos.y }); + } }); -gpio.on("falling", MIDDLE, () => { - if (matrix[pos.x][pos.y] === 0) { - matrix[pos.x][pos.y] = turnRed ? 1 : 2; - set(pos.x, pos.y, colors[matrix[pos.x][pos.y]]); +let right = new Digital({ + pin: RIGHT_PIN, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + update(pos, { x: Math.min(9, pos.x + 1), y: pos.y }); + } +}); - let end = gameEnd(pos); - if (end) { - for (let i = 1; i < end.posCount + 1; i++) { - set(pos.x + i * end.direction[0], pos.y + i * end.direction[1], colors[end.color], 0.5); - } - for (let i = 1; i < end.negCount + 1; i++) { - set(pos.x - i * end.direction[0], pos.y - i * end.direction[1], colors[end.color], 0.5); +let middle = new Digital({ + pin: MIDDLE_PIN, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + if (matrix[pos.x][pos.y] === 0) { + matrix[pos.x][pos.y] = turnRed ? 1 : 2; + set(pos.x, pos.y, colors[matrix[pos.x][pos.y]]); + + let end = gameEnd(pos); + if (end) { + for (let i = 1; i < end.posCount + 1; i++) { + set(pos.x + i * end.direction[0], pos.y + i * end.direction[1], colors[end.color], 0.5); + } + for (let i = 1; i < end.negCount + 1; i++) { + set(pos.x - i * end.direction[0], pos.y - i * end.direction[1], colors[end.color], 0.5); + } + set(pos.x, pos.y, colors[end.color], 0.5); + middle.close(); + up.close(); + down.close(); + left.close(); + right.close(); } - set(pos.x, pos.y, colors[end.color], 0.5); - gpio.off("falling", MIDDLE); - gpio.off("falling", UP); - gpio.off("falling", DOWN); - gpio.off("falling", LEFT); - gpio.off("falling", RIGHT); + turnRed = !turnRed; + strip.show(); } - turnRed = !turnRed; - strip.show(); } }); + + +// hack to prevent the object from being garbage collected +// TODO: remove when fixed +setInterval(() => { + up; + down; + left; + right; + middle; + power; + strip; +}, 100000); diff --git a/ts-examples/src/radioblink.ts b/ts-examples/src/radioblink.ts index f9123d9..dd8edae 100644 --- a/ts-examples/src/radioblink.ts +++ b/ts-examples/src/radioblink.ts @@ -1,7 +1,7 @@ import * as simpleradio from "simpleradio"; -import * as gpio from "gpio"; +import { Digital } from "embedded:io/digital"; -import { stdout, stderr } from "stdio"; +import { stdout } from "stdio"; /** @@ -20,35 +20,47 @@ const LED_GREEN = 17; const LED_YELLOW = 15; const LED_RED = 45; +let ledGreen = new Digital({ + pin: LED_GREEN, + mode: Digital.Output, +}); -gpio.pinMode(BUTTON_A, gpio.PinMode.INPUT); -gpio.pinMode(BUTTON_B, gpio.PinMode.INPUT); -gpio.pinMode(BUTTON_C, gpio.PinMode.INPUT); +let ledYellow = new Digital({ + pin: LED_YELLOW, + mode: Digital.Output, +}); -gpio.pinMode(LED_GREEN, gpio.PinMode.OUTPUT); -gpio.pinMode(LED_YELLOW, gpio.PinMode.OUTPUT); -gpio.pinMode(LED_RED, gpio.PinMode.OUTPUT); +let ledRed = new Digital({ + pin: LED_RED, + mode: Digital.Output, +}); -gpio.on("falling", BUTTON_A, () => { - simpleradio.sendKeyValue("green", 1); -}); -gpio.on("rising", BUTTON_A, () => { - simpleradio.sendKeyValue("green", 0); +let buttonA = new Digital({ + pin: BUTTON_A, + mode: Digital.InputPullUp, + edge: Digital.Falling + Digital.Rising, + onReadable: ( args ) => { + simpleradio.sendKeyValue("green", args.edge === Digital.Falling ? 1 : 0); + } }); -gpio.on("falling", BUTTON_B, () => { - simpleradio.sendKeyValue("yellow", 1); -}); -gpio.on("rising", BUTTON_B, () => { - simpleradio.sendKeyValue("yellow", 0); +let buttonB = new Digital({ + pin: BUTTON_B, + mode: Digital.InputPullUp, + edge: Digital.Falling + Digital.Rising, + onReadable: ( args ) => { + simpleradio.sendKeyValue("yellow", args.edge === Digital.Falling ? 1 : 0); + } }); -gpio.on("falling", BUTTON_C, () => { - simpleradio.sendKeyValue("red", 1); -}); -gpio.on("rising", BUTTON_C, () => { - simpleradio.sendKeyValue("red", 0); +let buttonC = new Digital({ + pin: BUTTON_C, + mode: Digital.InputPullUp, + edge: Digital.Falling + Digital.Rising, + onReadable: ( args ) => { + simpleradio.sendKeyValue("red", args.edge === Digital.Falling ? 1 : 0); + } }); @@ -56,13 +68,22 @@ simpleradio.on("keyvalue", (key, value) => { stdout.write("Received key-value: " + key + "=" + value + "\n"); switch (key) { case "green": - gpio.write(LED_GREEN, value); + ledGreen.write(value); break; case "yellow": - gpio.write(LED_YELLOW, value); + ledYellow.write(value); break; case "red": - gpio.write(LED_RED, value); + ledRed.write(value); break; } }); + + +// hack to prevent the object from being garbage collected +// TODO: remove when fixed +setInterval(() => { + buttonA; + buttonB; + buttonC; +}, 100000); diff --git a/ts-examples/src/snake.ts b/ts-examples/src/snake.ts index 2754a94..e0406c5 100644 --- a/ts-examples/src/snake.ts +++ b/ts-examples/src/snake.ts @@ -1,5 +1,5 @@ import { SmartLed, Rgb, LED_WS2812 } from "smartled"; -import * as gpio from "gpio"; +import { Digital } from "embedded:io/digital"; /** * A simple Snake game. @@ -31,12 +31,8 @@ else if (PlatformInfo.name == "ESP32-S3") { const FOOD_COLOR = { r: 255, g: 0, b: 0 }; const SNAKE_COLOR = { r: 0, g: 255, b: 0 }; -gpio.pinMode(POWER_PIN, gpio.PinMode.OUTPUT); -gpio.write(POWER_PIN, 1); - -for (let pin of [UP, DOWN, LEFT, RIGHT, MIDDLE]) { - gpio.pinMode(pin, gpio.PinMode.INPUT_PULLUP); -} +let power = new Digital({ pin: POWER_PIN, mode: Digital.Output }); +power.write(1); let strip = new SmartLed(LED_PIN, 100, LED_WS2812); function set(x: number, y: number, color: Rgb, brightness: number = 0.2) { @@ -69,17 +65,40 @@ strip.show(); let score = 0; -gpio.on("falling", UP, () => { - direction = { x: 0, y: -1 }; +let up = new Digital({ + pin: UP, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + direction = { x: 0, y: -1 }; + } }); -gpio.on("falling", DOWN, () => { - direction = { x: 0, y: 1 }; + +let down = new Digital({ + pin: DOWN, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + direction = { x: 0, y: 1 }; + } }); -gpio.on("falling", LEFT, () => { - direction = { x: -1, y: 0 }; + +let left = new Digital({ + pin: LEFT, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + direction = { x: -1, y: 0 }; + } }); -gpio.on("falling", RIGHT, () => { - direction = { x: 1, y: 0 }; + +let right = new Digital({ + pin: RIGHT, + mode: Digital.InputPullUp, + edge: Digital.Falling, + onReadable: () => { + direction = { x: 1, y: 0 }; + } }); let blinkState = true; @@ -128,3 +147,15 @@ function step() { } var timer = setInterval(step, 300); + + +// hack to prevent the object from being garbage collected +// TODO: remove when fixed +setInterval(() => { + up; + down; + left; + right; + power; + strip; +}, 100000); From 28e5b959bc7665a68e61d83bfd885842c900f4d7 Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Thu, 7 Dec 2023 01:53:18 +0100 Subject: [PATCH 04/12] add check for number of arguments to constructor --- main/espFeatures/digitalFeature.h | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/main/espFeatures/digitalFeature.h b/main/espFeatures/digitalFeature.h index d85e47b..9e2cc1a 100644 --- a/main/espFeatures/digitalFeature.h +++ b/main/espFeatures/digitalFeature.h @@ -276,6 +276,10 @@ struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, // TODO: extend instance lifetime until close or program end // TODO: check if pin is already in use + if (args.size() < 1) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "Digital constructor requires an options argument"); + } + jac::ObjectWeak options = args[0].to(); int pin = options.get("pin"); @@ -288,8 +292,7 @@ struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, debounceMs = options.get("debounce"); } - return new Digital_(static_cast( - JS_GetContextOpaque(ctx)), pin, mode, interruptMode, + return new Digital_(static_cast(JS_GetContextOpaque(ctx)), pin, mode, interruptMode, [ctx, callback](bool risingEdge, std::chrono::time_point timestamp) mutable { jac::Object arg = jac::Object::create(ctx); arg.set("edge", risingEdge ? DigitalEdge::RISING : DigitalEdge::FALLING); @@ -307,6 +310,8 @@ struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, static void addProperties(jac::ContextRef ctx, jac::Object proto) { jac::FunctionFactory ff(ctx); + // TODO: add required properties + DigitalProtoBuilder::template addMethodMember(ctx, proto, "write", jac::PropFlags::Enumerable); DigitalProtoBuilder::template addMethodMember(ctx, proto, "read", jac::PropFlags::Enumerable); DigitalProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); @@ -317,21 +322,21 @@ struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, template class DigitalFeature : public Next { using PinConfig = typename Next::PlatformInfo::PinConfig; - using DigitalClass = jac::Class>; public: + using DigitalClass = jac::Class>; + void initialize() { Next::initialize(); for (auto pin : PinConfig::DIGITAL_PINS) { - gpio_set_direction(this->getDigitalPin(pin), GPIO_MODE_DISABLE); + gpio_set_direction(Next::getDigitalPin(pin), GPIO_MODE_DISABLE); } for (auto pin : PinConfig::INTERRUPT_PINS) { - gpio_intr_disable(this->getDigitalPin(pin)); + gpio_intr_disable(Next::getDigitalPin(pin)); } gpio_install_isr_service(0); - jac::FunctionFactory ff(this->context()); auto& module = this->newModule("embedded:io/digital"); DigitalClass::init("Digital"); @@ -357,8 +362,8 @@ class DigitalFeature : public Next { ~DigitalFeature() { for (auto pin : PinConfig::INTERRUPT_PINS) { - gpio_intr_disable(this->getDigitalPin(pin)); - gpio_isr_handler_remove(this->getDigitalPin(pin)); + gpio_intr_disable(Next::getDigitalPin(pin)); + gpio_isr_handler_remove(Next::getDigitalPin(pin)); } gpio_uninstall_isr_service(); From 4c89b06309c99261096c2de1b609d972e0ccff74 Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Thu, 7 Dec 2023 01:55:13 +0100 Subject: [PATCH 05/12] WIP replace "adc" with "Analog" API --- main/espFeatures/adcFeature.h | 65 ------------------- main/espFeatures/analogFeature.h | 104 +++++++++++++++++++++++++++++++ main/main.cpp | 4 +- main/platform/esp32s3.h | 2 +- main/platform/espCommon.h | 9 +++ ts-examples/@types/adc.d.ts | 14 ----- ts-examples/@types/analog.d.ts | 16 +++++ ts-examples/src/adc.ts | 12 +++- ts-examples/src/servo.ts | 8 ++- 9 files changed, 147 insertions(+), 87 deletions(-) delete mode 100644 main/espFeatures/adcFeature.h create mode 100644 main/espFeatures/analogFeature.h delete mode 100644 ts-examples/@types/adc.d.ts create mode 100644 ts-examples/@types/analog.d.ts diff --git a/main/espFeatures/adcFeature.h b/main/espFeatures/adcFeature.h deleted file mode 100644 index 1674a21..0000000 --- a/main/espFeatures/adcFeature.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include "driver/adc.h" - - -template -class AdcFeature : public Next { - using PinConfig = Next::PlatformInfo::PinConfig; - - static std::pair getAnalogPin(int pin) { - auto it = PinConfig::ANALOG_PINS.find(pin); - if (it == PinConfig::ANALOG_PINS.end()) { - throw std::runtime_error("Invalid analog pin"); - } - - return it->second; - } - - class Adc { - public: - void configure(int pin) { - int adcNum; - int channel; - std::tie(adcNum, channel) = getAnalogPin(pin); - - esp_err_t err = adc1_config_width(static_cast(ADC_WIDTH_BIT_DEFAULT)); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - - err = adc1_config_channel_atten(static_cast(channel), ADC_ATTEN_DB_11); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - } - - int read(int pin) { - int adcNum; - int channel; - std::tie(adcNum, channel) = getAnalogPin(pin); - - return adc1_get_raw(static_cast(channel)) >> (ADC_WIDTH_BIT_DEFAULT - 10); - } - }; - -public: - Adc adc; - - void initialize() { - Next::initialize(); - - jac::FunctionFactory ff(this->context()); - - jac::Module& adcModule = this->newModule("adc"); - - adcModule.addExport("configure", ff.newFunction(noal::function(&Adc::configure, &adc))); - adcModule.addExport("read", ff.newFunction(noal::function(&Adc::read, &adc))); - } -}; diff --git a/main/espFeatures/analogFeature.h b/main/espFeatures/analogFeature.h new file mode 100644 index 0000000..5031f87 --- /dev/null +++ b/main/espFeatures/analogFeature.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include + +#include +#include + +#include "driver/adc.h" + + +template +class Analog { + Feature* const _feature; + const int _adcNum; + const adc1_channel_t _channel; + const adc_bits_width_t _resolution; + + Analog(Feature* feature, std::pair pinConf, adc_bits_width_t resolution): + _feature(feature), + _adcNum(pinConf.first), + _channel(static_cast(pinConf.second)), + _resolution(resolution) + { + if (_adcNum != 1) { + throw std::runtime_error("Only ADC1 is supported"); + } + + esp_err_t err = adc1_config_width(_resolution); + if (err != ESP_OK) { + throw std::runtime_error(std::string("Error configuring ADC width: ") + esp_err_to_name(err)); + } + + // TODO: make attenuation configurable + err = adc1_config_channel_atten(_channel, ADC_ATTEN_DB_11); + if (err != ESP_OK) { + throw std::runtime_error(std::string("Error configuring ADC attenuation: ") + esp_err_to_name(err)); + } + } +public: + Analog(Feature* feature, int pin, int resolution = ADC_WIDTH_BIT_DEFAULT): + Analog(feature, Feature::getAnalogPin(pin), static_cast(resolution)) + {} + + ~Analog() { + close(); + } + + int read() { + return adc1_get_raw(_channel) >> (ADC_WIDTH_BIT_DEFAULT - _resolution); + } + + void close() { + // do nothing for now + // possibly reconfigure to some default state? + } +}; + +template +struct AnalogProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { + using Analog_ = Analog; + + static Analog_* constructOpaque(jac::ContextRef ctx, std::vector args) { + // TODO: extend instance lifetime until close or program end + // *currently not a problem, as the instance destrutor does nothing* + // TODO: check if pin is already in use + + if (args.size() < 1) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "Analog constructor requires an options argument"); + } + + jac::ObjectWeak options = args[0].to(); + + int pin = options.get("pin"); + if (options.hasProperty("resolution")) { + int resolution = options.get("resolution"); + return new Analog_(static_cast(JS_GetContextOpaque(ctx)), pin, resolution); + } else { + return new Analog_(static_cast(JS_GetContextOpaque(ctx)), pin); + } + } + + static void addProperties(jac::ContextRef ctx, jac::Object proto) { + AnalogProtoBuilder::template addMethodMember(ctx, proto, "read", jac::PropFlags::Enumerable); + AnalogProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); + } +}; + +template +class AnalogFeature : public Next { +public: + using AnalogClass = jac::Class>; + + void initialize() { + Next::initialize(); + + auto& module = this->newModule("embedded:io/analog"); + + AnalogClass::init("Analog"); + AnalogClass::initContext(this->context()); + + module.addExport("Analog", AnalogClass::getConstructor(this->context())); + } +}; diff --git a/main/main.cpp b/main/main.cpp index 539c5c9..448fbfc 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -18,7 +18,7 @@ #include "espFeatures/digitalFeature.h" #include "espFeatures/freeRTOSEventQueue.h" #include "espFeatures/ledcFeature.h" -#include "espFeatures/adcFeature.h" +#include "espFeatures/analogFeature.h" #include "espFeatures/i2cFeature.h" #include "espFeatures/simpleRadioFeature.h" @@ -60,7 +60,7 @@ using Machine = jac::ComposeMachine< jac::EventLoopFeature, DigitalFeature, LedcFeature, - AdcFeature, + AnalogFeature, I2CFeature, SmartLedFeature, jac::TimersFeature, diff --git a/main/platform/esp32s3.h b/main/platform/esp32s3.h index 97be749..563a909 100644 --- a/main/platform/esp32s3.h +++ b/main/platform/esp32s3.h @@ -8,7 +8,7 @@ struct PlatformInfo { - static inline constexpr std::string NAME = "ESP32-S3"; + static inline const std::string NAME = "ESP32-S3"; struct PinConfig { static inline const std::set DIGITAL_PINS = { diff --git a/main/platform/espCommon.h b/main/platform/espCommon.h index 3e1bfdd..d9a4ed0 100644 --- a/main/platform/espCommon.h +++ b/main/platform/espCommon.h @@ -25,6 +25,15 @@ class EspCommon : public Next { return static_cast(pin); } + static std::pair getAnalogPin(int pin) { + auto it = PlatformInfo::PinConfig::ANALOG_PINS.find(pin); + if (it == PlatformInfo::PinConfig::ANALOG_PINS.end()) { + throw std::runtime_error("Invalid analog pin"); + } + + return it->second; + } + void initialize() { Next::initialize(); diff --git a/ts-examples/@types/adc.d.ts b/ts-examples/@types/adc.d.ts deleted file mode 100644 index d77e9d9..0000000 --- a/ts-examples/@types/adc.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module "adc" { - /** - * Enable ADC on the given pin. - * @param pin The pin to enable ADC on. - */ - function configure(pin: number): void; - - /** - * Read the value of the given pin. - * @param pin The pin to read. - * @returns The value of the pin (0-1023) - */ - function read(pin: number): number; -} diff --git a/ts-examples/@types/analog.d.ts b/ts-examples/@types/analog.d.ts new file mode 100644 index 0000000..eb71556 --- /dev/null +++ b/ts-examples/@types/analog.d.ts @@ -0,0 +1,16 @@ +declare module "embedded:io/analog" { + class Analog { + constructor(options: { + pin: number, + resolution?: number + }); + + /** + * Reads the value of the pin. + * @returns The value of the pin. + */ + read(): number; + + close(): void; + } +} diff --git a/ts-examples/src/adc.ts b/ts-examples/src/adc.ts index 1d3f9b4..87b89bd 100644 --- a/ts-examples/src/adc.ts +++ b/ts-examples/src/adc.ts @@ -1,5 +1,5 @@ -import * as adc from "adc"; import * as ledc from "ledc"; +import { Analog } from "embedded:io/analog"; /** * Example showing how to use the ADC to control the brightness of an LED. @@ -11,9 +11,15 @@ const LED_PIN = 45; ledc.configureTimer(0, 1000); ledc.configureChannel(0, LED_PIN, 0, 1023); +const adc = new Analog({ + pin: INPUT_PIN, + resolution: 10 +}); + let power = 3; setInterval(() => { - const value = adc.read(INPUT_PIN); - ledc.setDuty(0, Math.pow(1023 - value, power) / Math.pow(1023, power - 1)); + const value = adc.read(); + console.log(value); + ledc.setDuty(0, Math.pow(value, power) / Math.pow(1023, power - 1)); }, 10); diff --git a/ts-examples/src/servo.ts b/ts-examples/src/servo.ts index adc2e84..5fbcc2f 100644 --- a/ts-examples/src/servo.ts +++ b/ts-examples/src/servo.ts @@ -1,4 +1,4 @@ -import * as adc from "adc"; +import { Analog } from "embedded:io/analog"; import * as ledc from "ledc"; /** @@ -11,9 +11,13 @@ const SERVO_PIN = 17; ledc.configureTimer(0, 50, 12); ledc.configureChannel(0, SERVO_PIN, 0, 1023); +const adc = new Analog({ + pin: INPUT_PIN, + resolution: 10 +}); setInterval(() => { - const value = adc.read(INPUT_PIN); + const value = adc.read(); // map the value from 0-1023 to 1-2ms const ms = (value / 1023) + 1; From 6a99ed731c9d2c2a3913e2251fa3205e789acd25 Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Thu, 7 Dec 2023 14:14:41 +0100 Subject: [PATCH 06/12] WIP replace I2C API --- main/espFeatures/i2cFeature.h | 250 +++++++++++++++++++++------------- ts-examples/@types/i2c.d.ts | 45 +++--- 2 files changed, 180 insertions(+), 115 deletions(-) diff --git a/main/espFeatures/i2cFeature.h b/main/espFeatures/i2cFeature.h index 8ed5efa..5780e98 100644 --- a/main/espFeatures/i2cFeature.h +++ b/main/espFeatures/i2cFeature.h @@ -12,66 +12,100 @@ #include "freertos/FreeRTOS.h" -template +template class I2C { - i2c_port_t port; - bool open = false; + i2c_port_t _port; + uint8_t _address; + bool open = true; // the port is open upon successful construction public: - I2C(int port) : port(static_cast(port)) {} + I2C(int sda, int scl, int bitrate, int address, int port): + _port(static_cast(port)), + _address(static_cast(address & 0x7F)) // 7-bit address + { + if (_address != address) { + throw std::runtime_error("Invalid I2C address"); + } - static std::optional find(int pin) { - return std::nullopt; - } + i2c_config_t conf = { + .mode = I2C_MODE_MASTER, + .sda_io_num = Feature::getDigitalPin(sda), + .scl_io_num = Feature::getDigitalPin(scl), + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master = { + .clk_speed = static_cast(bitrate), + }, + .clk_flags = 0, + }; - std::vector readFrom(uint8_t address, size_t quantity) { - std::vector data(quantity); - esp_err_t err = i2c_master_read_from_device(port, address, data.data(), quantity, 100 / portTICK_PERIOD_MS); + esp_err_t err = i2c_param_config(_port, &conf); if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); + throw std::runtime_error(std::string("Error configuring I2C: ") + esp_err_to_name(err)); } - return data; + err = i2c_driver_install(_port, conf.mode, 0, 0, 0); + if (err != ESP_OK) { + throw std::runtime_error(std::string("Error installing I2C driver: ") + esp_err_to_name(err)); + } } - // not compatible with Espruino - takes variadic data - void writeTo(uint8_t address, std::vector data) { - esp_err_t err = i2c_master_write_to_device(port, address, data.data(), data.size(), 100 / portTICK_PERIOD_MS); + size_t readInto(std::span data, bool stopBit = true) { + if (!open) { + throw std::runtime_error("I2C is closed"); + } + + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (_address << 1) | I2C_MASTER_READ, true); + i2c_master_read(cmd, data.data(), data.size(), stopBit ? I2C_MASTER_LAST_NACK : I2C_MASTER_ACK); + i2c_master_stop(cmd); + esp_err_t err = i2c_master_cmd_begin(_port, cmd, 100 / portTICK_PERIOD_MS); if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); + throw std::runtime_error(std::string("Error reading from I2C: ") + esp_err_to_name(err)); } + + i2c_cmd_link_delete(cmd); + + return data.size(); } - void setup(std::optional scl, std::optional sda, std::optional bitrate) { - if (open) { - throw std::runtime_error("I2C already open"); + std::vector read(size_t quantity, bool stopBit = true) { + std::vector data(quantity); + readInto(data, stopBit); + return data; + } + + void write(std::span data, bool stopBit = true) { + if (!open) { + throw std::runtime_error("I2C is closed"); } - int scl_ = scl.value_or(PlatformInfo::PinConfig::DEFAULT_I2C_SCL_PIN); - int sda_ = sda.value_or(PlatformInfo::PinConfig::DEFAULT_I2C_SDA_PIN); - int bitrate_ = bitrate.value_or(400000); + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (_address << 1) | I2C_MASTER_WRITE, true); + i2c_master_write(cmd, data.data(), data.size(), stopBit); + i2c_master_stop(cmd); - i2c_config_t conf = { - .mode = I2C_MODE_MASTER, - .sda_io_num = sda_, - .scl_io_num = scl_, - .sda_pullup_en = GPIO_PULLUP_ENABLE, - .scl_pullup_en = GPIO_PULLUP_ENABLE, - .master = { - .clk_speed = static_cast(bitrate_), - }, - .clk_flags = 0, - }; + esp_err_t err = i2c_master_cmd_begin(_port, cmd, 100 / portTICK_PERIOD_MS); + if (err != ESP_OK) { + throw std::runtime_error(std::string("Error writing to I2C: ") + esp_err_to_name(err)); + } - i2c_param_config(port, &conf); - i2c_driver_install(port, conf.mode, 0, 0, 0); + i2c_cmd_link_delete(cmd); } void close() { - if (open) { - i2c_driver_delete(port); - open = false; + if (!open) { + return; } + + esp_err_t err = i2c_driver_delete(_port); + if (err != ESP_OK) { + throw std::runtime_error(std::string("Error deleting I2C driver: ") + esp_err_to_name(err)); + } + + open = false; } ~I2C() { @@ -79,75 +113,104 @@ class I2C { } }; -template -struct I2CProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { - using I2C_ = I2C; +template +struct I2CProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { + using I2C_ = I2C; - static void addProperties(JSContext* ctx, jac::Object proto) { - jac::FunctionFactory ff(ctx); + static I2C_* constructOpaque(jac::ContextRef ctx, std::vector args) { + // TODO: extend instance lifetime until close or program end + // *currently not a problem, as the instance destrutor does nothing* + // TODO: check if pins are already in use - // TODO: ugly hack - proto.defineProperty("readFrom", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal, int address, int quantity) { - auto& i2c = *I2CProtoBuilder::getOpaque(ctx, thisVal); - auto data = i2c.readFrom(address, quantity); + if (args.size() < 1) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "Analog constructor requires an options argument"); + } - auto res = jac::ArrayBuffer::create(ctx, std::span(data.data(), data.size())); + jac::ObjectWeak options = args[0].to(); + if (!options.hasProperty("sda")) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "I2C constructor requires an sda pin"); + } + int sda = options.get("sda"); - auto& machine = *reinterpret_cast(JS_GetContextOpaque(ctx)); - jac::Value convertor = machine.eval("(buf) => new Uint8Array(buf)", ""); - return convertor.to().call(res); - })); + if (!options.hasProperty("scl")) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "I2C constructor requires an scl pin"); + } + int scl = options.get("scl"); - proto.defineProperty("writeTo", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal, int address, jac::Value data) { - auto& i2c = *I2CProtoBuilder::getOpaque(ctx, thisVal); - std::vector dataVec; + if (!options.hasProperty("bitrate")) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "I2C constructor requires a bitrate"); + } + int bitrate = options.get("bitrate"); + + if (!options.hasProperty("address")) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "I2C constructor requires an address"); + } + int address = options.get("address"); + + int port = I2C_NUM_0; + if (options.hasProperty("port")) { + port = options.get("port"); + } + + return new I2C_(sda, scl, bitrate, address, port); + } + + static void addProperties(JSContext* ctx, jac::Object proto) { + jac::FunctionFactory ff(ctx); - if (JS_IsString(data.getVal())) { - auto str = data.toString(); - dataVec.resize(str.size()); - std::copy(str.begin(), str.end(), dataVec.begin()); + // TODO: ugly hack + proto.defineProperty("read", ff.newFunctionThisVariadic([](jac::ContextRef ctx, jac::ValueWeak thisVal, std::vector args) { + auto& i2c = *I2CProtoBuilder::getOpaque(ctx, thisVal); + auto& machine = *reinterpret_cast(JS_GetContextOpaque(ctx)); + if (args.size() < 1) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "Read requires at least one argument"); } - else { - auto& machine = *reinterpret_cast(JS_GetContextOpaque(ctx)); - jac::Value toArrayBuffer = machine.eval( + + jac::Value checkBufferType = machine.eval( R"--( (data) => { - if (data instanceof ArrayBuffer) return data; - if (ArrayBuffer.isView(data)) return data.buffer; - if (typeof data === 'number') return new Int8Array([data]).buffer; - if (Array.isArray(data)) return new Int8Array(data).buffer; + if (data instanceof ArrayBuffer) return true; + if (typeof data === 'number') return false; throw new Error('Invalid data type'); } -)--", ""); +)--", ""); + + bool isBuffer = checkBufferType.to().call(args[0]); - auto res = toArrayBuffer.to().call(data); - auto dataView = res.typedView(); - dataVec.resize(dataView.size()); - std::copy(dataView.begin(), dataView.end(), dataVec.begin()); + bool stopBit = true; + if (args.size() > 1) { + stopBit = args[1].to(); } - i2c.writeTo(address, std::move(dataVec)); - })); + if (isBuffer) { + auto data = args[0].to(); + auto dataView = data.typedView(); + return jac::Value::from(ctx, static_cast(i2c.readInto(dataView, stopBit))); + } + else { + int quantity = args[0].to(); + auto data = i2c.read(quantity, stopBit); + return jac::ArrayBuffer::create(ctx, std::span(data.data(), data.size())).template to(); + } + }), jac::PropFlags::Enumerable); - proto.defineProperty("setup", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal, jac::Object options) { + proto.defineProperty("write", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal, std::vector args) { auto& i2c = *I2CProtoBuilder::getOpaque(ctx, thisVal); - - std::optional scl; - std::optional sda; - std::optional bitrate; - - if (options.hasProperty("scl")) { - scl = options.get("scl"); - } - if (options.hasProperty("sda")) { - sda = options.get("sda"); + if (args.size() < 1) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "Write requires at least one argument"); } - if (options.hasProperty("bitrate")) { - bitrate = options.get("bitrate"); + + jac::ArrayBufferWeak buffer = args[0].to(); + bool stopBit = true; + if (args.size() > 1) { + stopBit = args[1].to(); } - i2c.setup(scl, sda, bitrate); - })); + auto dataView = buffer.typedView(); + i2c.write(dataView, stopBit); + }), jac::PropFlags::Enumerable); + + I2CProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); } }; @@ -164,9 +227,12 @@ class I2CFeature : public Next { void initialize() { Next::initialize(); - jac::Module& mod = this->newModule("i2c"); - for (int i = 0; i < SOC_I2C_NUM; ++i) { - mod.addExport("I2C" + std::to_string(i), I2CClass::createInstance(this->context(), new I2C(i))); - } + jac::Module& mod = this->newModule("embedded:io/i2c"); + + I2CClass::initContext(this->context()); + + jac::Object i2cConstructor = I2CClass::getConstructor(this->context()); + + mod.addExport("I2C", i2cConstructor); } }; diff --git a/ts-examples/@types/i2c.d.ts b/ts-examples/@types/i2c.d.ts index fd6685a..6df5fe1 100644 --- a/ts-examples/@types/i2c.d.ts +++ b/ts-examples/@types/i2c.d.ts @@ -1,34 +1,33 @@ -declare module "i2c" { - interface I2C { - /** - * Find an I2C interface by its pin. - * @param pin The pin the I2C interface is connected to. - * @returns The I2C interface, or undefined if not found. - */ - find(pin: number): I2C | undefined; +declare module "embedded:io/i2c" { + class I2C { + constructor(options: { + data: number, + clock: number, + hz: number, + address: number, + port?: number + }); /** - * Read from the given address. - * @param address The address to read from. - * @param quantity The number of bytes to read. - * @returns The bytes read. + * Reads data from the I2C bus. + * @param buffer The buffer to read data into. + * @param stop Whether to send a stop bit after reading, default: true. */ - readFrom(address: number, quantity: number): Uint8Array; + read(buffer: ArrayBuffer, stop?: boolean): number; /** - * Write to the given address. - * @param address The address to write to. - * @param buffer The data to write. + * Reads data from the I2C bus. + * @param byteLength The number of bytes to read. + * @param stop Whether to send a stop bit after reading, default: true. + * @returns The data read from the bus. */ - writeTo(address: number, buffer: ArrayBuffer | Uint8Array | number[] | string | number): void; + read(byteLength: number, stop?: boolean): ArrayBuffer; /** - * Setup the I2C interface. - * @param options The options to use when setting up the I2C interface. + * Writes data to the I2C bus. + * @param buffer The buffer to write data from. + * @param stop Whether to send a stop bit after writing, default: true. */ - setup(options: { scl?: number, sda?: number, bitrate?: number }): void; + write(buffer: ArrayBuffer, stop?: boolean): void; } - - const I2C1: I2C; - const I2C2: I2C | undefined; } From d30ecbab17c89db4cc9428bdf23b87259dffe8ec Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Thu, 7 Dec 2023 14:26:49 +0100 Subject: [PATCH 07/12] move js class initialization to constructor --- main/espFeatures/analogFeature.h | 5 ++++- main/espFeatures/digitalFeature.h | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/main/espFeatures/analogFeature.h b/main/espFeatures/analogFeature.h index 5031f87..48abac3 100644 --- a/main/espFeatures/analogFeature.h +++ b/main/espFeatures/analogFeature.h @@ -91,12 +91,15 @@ class AnalogFeature : public Next { public: using AnalogClass = jac::Class>; + AnalogFeature() { + AnalogClass::init("Analog"); + } + void initialize() { Next::initialize(); auto& module = this->newModule("embedded:io/analog"); - AnalogClass::init("Analog"); AnalogClass::initContext(this->context()); module.addExport("Analog", AnalogClass::getConstructor(this->context())); diff --git a/main/espFeatures/digitalFeature.h b/main/espFeatures/digitalFeature.h index 9e2cc1a..2cd414d 100644 --- a/main/espFeatures/digitalFeature.h +++ b/main/espFeatures/digitalFeature.h @@ -325,6 +325,10 @@ class DigitalFeature : public Next { public: using DigitalClass = jac::Class>; + DigitalFeature() { + DigitalClass::init("Digital"); + } + void initialize() { Next::initialize(); @@ -339,7 +343,6 @@ class DigitalFeature : public Next { auto& module = this->newModule("embedded:io/digital"); - DigitalClass::init("Digital"); DigitalClass::initContext(this->context()); jac::Object digitalConstructor = DigitalClass::getConstructor(this->context()); @@ -358,8 +361,6 @@ class DigitalFeature : public Next { digitalConstructor.set("Falling", DigitalEdge::FALLING); } - DigitalFeature() {} - ~DigitalFeature() { for (auto pin : PinConfig::INTERRUPT_PINS) { gpio_intr_disable(Next::getDigitalPin(pin)); From 1922e48b8721dc59b78dbecbda173253c0fbf597 Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Tue, 12 Dec 2023 02:24:00 +0100 Subject: [PATCH 08/12] implement naive debouncing of gpio --- main/espFeatures/digitalFeature.h | 33 ++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/main/espFeatures/digitalFeature.h b/main/espFeatures/digitalFeature.h index 2cd414d..3b47dce 100644 --- a/main/espFeatures/digitalFeature.h +++ b/main/espFeatures/digitalFeature.h @@ -4,9 +4,7 @@ #include #include #include -#include -#include -#include + #include "driver/gpio.h" @@ -99,6 +97,7 @@ namespace detail { bool lastRising = false; TickType_t lastTime = 0; + DigitalEdge _mode = DigitalEdge::DISABLE; bool _isAssigned = false; bool _synchronous; // TODO: maybe remove asynchronous mode? std::function)> _callback; @@ -106,12 +105,16 @@ namespace detail { InterruptConf(int debounceMs = 0) : _debounceTicks(debounceMs / portTICK_PERIOD_MS) {} bool updateLast(bool risingEdge) { - // TODO: Use more precise time source auto now = xTaskGetTickCountFromISR(); - // two successive interrupts with the same edge within - // 2 ticks are probably caused by some hardware bug - if (lastRising == risingEdge && now - lastTime < 4) { + // TODO: replace naive debounce with a better solution + if (now - lastTime < _debounceTicks) { + return false; + } + if (_mode == DigitalEdge::FALLING && risingEdge) { + return false; + } + if (_mode == DigitalEdge::RISING && !risingEdge) { return false; } @@ -169,21 +172,22 @@ class Digital { gpio_isr_handler_add(_pin, [](void* arg) { auto& self = *static_cast(arg); - Feature* feature = self._feature; + bool risingEdge = gpio_get_level(self._pin) == 1; if (!self._interruptConf._isAssigned) { return; } - bool risingEdge = gpio_get_level(self._pin) == 1; if (!self._interruptConf.updateLast(risingEdge)) { return; } self._interruptConf.queue.push(risingEdge ? DigitalEdge::RISING : DigitalEdge::FALLING); - feature->scheduleEventISR(call, &self); + self._feature->scheduleEventISR(call, &self); }, this); gpio_intr_enable(_pin); + + _interruptConf._mode = mode; } void disableInterrupt() { @@ -192,6 +196,11 @@ class Digital { gpio_isr_handler_remove(_pin); _interruptConf._isAssigned = false; + + // FIXME: callback may still be scheduled in the event queue/being executed + // so the callback can't be safely destroyed (by delaying the destruction + // the chance of a crash is reduced but not eliminated) + // _interruptConf._callback = nullptr; } } @@ -235,7 +244,7 @@ class Digital { Digital(Feature* feature, int pin, DigitalMode mode, DigitalEdge interruptMode, std::function)> callback, - int debounceMs) : + int debounceMs): _feature(feature), _pin(Feature::getDigitalPin(pin)), _interruptConf(debounceMs) @@ -287,7 +296,7 @@ struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, if (options.hasProperty("onReadable")) { jac::Function callback = options.get("onReadable"); DigitalEdge interruptMode = options.get("edge"); - int debounceMs = 0; + int debounceMs = 10; if (options.hasProperty("debounce")) { debounceMs = options.get("debounce"); } From aba76f4b2509535fff4148a60397759d912823e0 Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Tue, 12 Dec 2023 02:28:42 +0100 Subject: [PATCH 09/12] remove remains of asynchronous interrupt mode --- main/espFeatures/digitalFeature.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/main/espFeatures/digitalFeature.h b/main/espFeatures/digitalFeature.h index 3b47dce..8336869 100644 --- a/main/espFeatures/digitalFeature.h +++ b/main/espFeatures/digitalFeature.h @@ -99,7 +99,6 @@ namespace detail { DigitalEdge _mode = DigitalEdge::DISABLE; bool _isAssigned = false; - bool _synchronous; // TODO: maybe remove asynchronous mode? std::function)> _callback; InterruptConf(int debounceMs = 0) : _debounceTicks(debounceMs / portTICK_PERIOD_MS) {} @@ -123,11 +122,9 @@ namespace detail { return true; } - void setCallback(std::function)> callback, - bool synchronous) { + void setCallback(std::function)> callback) { _isAssigned = false; _callback = callback; - _synchronous = synchronous; _isAssigned = bool(callback); } }; @@ -251,7 +248,7 @@ class Digital { { pinMode(mode); enableInterrupt(interruptMode); - _interruptConf.setCallback(callback, true); + _interruptConf.setCallback(callback); } Digital(const Digital&) = delete; From 37679d9e59ad668950a6e94bef2f174bc75d5b79 Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Sun, 17 Dec 2023 14:57:14 +0100 Subject: [PATCH 10/12] WIP replace ledc with "PWM" API --- main/espFeatures/ledcFeature.h | 173 -------------------- main/espFeatures/pwmFeature.h | 239 ++++++++++++++++++++++++++++ main/main.cpp | 4 +- ts-examples/@types/ledc.d.ts | 44 ----- ts-examples/@types/pwm.d.ts | 27 ++++ ts-examples/src/adc.ts | 12 +- ts-examples/src/piezo.ts | 26 --- ts-examples/src/{ledc.ts => pwm.ts} | 11 +- ts-examples/src/servo.ts | 15 +- 9 files changed, 291 insertions(+), 260 deletions(-) delete mode 100644 main/espFeatures/ledcFeature.h create mode 100644 main/espFeatures/pwmFeature.h delete mode 100644 ts-examples/@types/ledc.d.ts create mode 100644 ts-examples/@types/pwm.d.ts delete mode 100644 ts-examples/src/piezo.ts rename ts-examples/src/{ledc.ts => pwm.ts} (64%) diff --git a/main/espFeatures/ledcFeature.h b/main/espFeatures/ledcFeature.h deleted file mode 100644 index 08921f0..0000000 --- a/main/espFeatures/ledcFeature.h +++ /dev/null @@ -1,173 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include "driver/ledc.h" - - -template -class LedcFeature : public Next { - class Ledc { - std::unordered_map _usedTimers; - std::unordered_map _usedChannels; - public: - void configureTimer(int timerNum, int frequency, int resolution) { - if (resolution < 1 || resolution >= LEDC_TIMER_BIT_MAX) { - throw std::runtime_error("Resolution must be between 1 and " + std::to_string(LEDC_TIMER_BIT_MAX - 1)); - } - if (frequency < 1) { - throw std::runtime_error("Frequency must be greater than 0"); - } - - ledc_timer_config_t ledc_timer = { - .speed_mode = LEDC_LOW_SPEED_MODE, - .duty_resolution = static_cast(resolution), - .timer_num = static_cast(timerNum), - .freq_hz = static_cast(frequency), - .clk_cfg = LEDC_AUTO_CLK - }; - esp_err_t err = ledc_timer_config(&ledc_timer); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - - _usedTimers.insert({ timerNum, resolution }); - } - - void configureChannel(int channelNum, int gpioNum, int timerNum, int duty) { - auto timer = _usedTimers.find(timerNum); - if (timer == _usedTimers.end()) { - throw std::runtime_error("Timer not configured"); - } - if (duty < 0 || duty > 1023) { - throw std::runtime_error("Duty must be between 0 and 1023"); - } - - duty = (1 << timer->second) * duty / 1023; - - ledc_channel_config_t ledc_channel = { - .gpio_num = Next::getDigitalPin(gpioNum), - .speed_mode = LEDC_LOW_SPEED_MODE, - .channel = static_cast(channelNum), - .intr_type = LEDC_INTR_DISABLE, - .timer_sel = static_cast(timerNum), - .duty = static_cast(duty), - .hpoint = 0, - .flags = { 0 } - }; - esp_err_t err = ledc_channel_config(&ledc_channel); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - - _usedChannels.insert({ channelNum, timerNum }); - } - - void setFrequency(int timerNum, int frequency) { - if (_usedTimers.find(timerNum) == _usedTimers.end()) { - throw std::runtime_error("Timer not in use"); - } - if (frequency < 1) { - throw std::runtime_error("Frequency must be greater than 0"); - } - - esp_err_t err = ledc_set_freq(LEDC_LOW_SPEED_MODE, static_cast(timerNum), frequency); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - } - - void setDuty(int channelNum, int duty) { - auto channel = _usedChannels.find(channelNum); - if (channel == _usedChannels.end()) { - throw std::runtime_error("Channel not in use"); - } - auto timer = _usedTimers.find(channel->second); - if (duty < 0 || duty > 1023) { - throw std::runtime_error("Duty must be between 0 and 1023"); - } - - duty = (1 << timer->second) * duty / 1023; - - esp_err_t err = ledc_set_duty(LEDC_LOW_SPEED_MODE, static_cast(channelNum), duty); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - - err = ledc_update_duty(LEDC_LOW_SPEED_MODE, static_cast(channelNum)); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - } - - void stopTimer(int timerNum) { - bool channelInUse = false; - for (auto channel : _usedChannels) { - if (channel.second == timerNum) { - channelInUse = true; - break; - } - } - if (channelInUse) { - throw std::runtime_error("Timer still in use by channel"); - } - - esp_err_t err = ledc_timer_rst(LEDC_LOW_SPEED_MODE, static_cast(timerNum)); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - - _usedTimers.erase(timerNum); - } - - void stopChannel(int channelNum) { - esp_err_t err = ledc_stop(LEDC_LOW_SPEED_MODE, static_cast(channelNum), 0); - if (err != ESP_OK) { - throw std::runtime_error(esp_err_to_name(err)); - } - - _usedChannels.erase(channelNum); - } - - ~Ledc() { - for (auto channel : _usedChannels) { - ledc_stop(LEDC_LOW_SPEED_MODE, static_cast(channel.first), 0); - } - for (auto timer : _usedTimers) { - ledc_timer_rst(LEDC_LOW_SPEED_MODE, static_cast(timer.first)); - } - } - }; -public: - Ledc ledc; - - void initialize() { - Next::initialize(); - - jac::FunctionFactory ff(this->context()); - - jac::Module& ledcModule = this->newModule("ledc"); - ledcModule.addExport("configureTimer", ff.newFunctionVariadic([this](std::vector args) { - if (args.size() < 2) { - throw std::runtime_error("Expected at least 2 arguments"); - } - int timerNum = args[0].to(); - int frequency = args[1].to(); - int resolution = 10; - if (args.size() == 3) { - resolution = args[2].to(); - } - - this->ledc.configureTimer(timerNum, frequency, resolution); - })); - ledcModule.addExport("configureChannel", ff.newFunction(noal::function(&Ledc::configureChannel, &ledc))); - ledcModule.addExport("setFrequency", ff.newFunction(noal::function(&Ledc::setFrequency, &ledc))); - ledcModule.addExport("setDuty", ff.newFunction(noal::function(&Ledc::setDuty, &ledc))); - ledcModule.addExport("stopTimer", ff.newFunction(noal::function(&Ledc::stopTimer, &ledc))); - ledcModule.addExport("stopChannel", ff.newFunction(noal::function(&Ledc::stopChannel, &ledc))); - } -}; diff --git a/main/espFeatures/pwmFeature.h b/main/espFeatures/pwmFeature.h new file mode 100644 index 0000000..10f4c4a --- /dev/null +++ b/main/espFeatures/pwmFeature.h @@ -0,0 +1,239 @@ +#pragma once + +#include +#include + +#include +#include + +#include "driver/ledc.h" + + +struct Timer { + int _freq; + int _resolution; + int num; + mutable std::atomic _refs; + + Timer(int freq, int resolution, int num): + _freq(freq), + _resolution(resolution), + num(num), + _refs(0) + { + ledc_timer_config_t timerConfig = { + .speed_mode = LEDC_LOW_SPEED_MODE, + .duty_resolution = static_cast(resolution), + .timer_num = static_cast(num), + .freq_hz = static_cast(freq), + .clk_cfg = LEDC_AUTO_CLK, + }; + + esp_err_t err = ledc_timer_config(&timerConfig); + if (err != ESP_OK) { + throw std::runtime_error(esp_err_to_name(err)); + } + } + + void incRefs() const { + _refs++; + } + + bool decRefs() const { + _refs--; + return _refs == 0; + } + + auto operator<=>(const std::pair& p) const { + return std::pair(_freq, _resolution) <=> p; + } + + auto operator<=>(const Timer& other) const { + return std::tuple(_freq, _resolution, num) <=> std::tuple(other._freq, other._resolution, other.num); + } +}; + +template +class PWM { + Feature* const _feature; +public: + // TODO: hide these (make view const) + gpio_num_t _pin; + int _hz; + int _duty; + const Timer* _timer; + int _channel; + +private: + PWM(Feature* feature, int pin, double hz, const Timer* timer, int channel): + _feature(feature), + _pin(Feature::getDigitalPin(pin)), + _hz(hz), + _duty(0), + _timer(timer), + _channel(channel) + { + ledc_channel_config_t ledc_channel = { + .gpio_num = _pin, + .speed_mode = LEDC_LOW_SPEED_MODE, + .channel = static_cast(_channel), + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = static_cast(timer->num), + .duty = 0, + .hpoint = 0, + .flags = { 0 } + }; + esp_err_t err = ledc_channel_config(&ledc_channel); + if (err != ESP_OK) { + throw std::runtime_error(esp_err_to_name(err)); + } + } +public: + + PWM(Feature* feature, int pin, double hz, int resolution): + PWM(feature, pin, hz, + feature->takeTimer(hz, resolution), + feature->takeChannel() + ) + {} + + ~PWM() { + close(); + } + + void write(int duty) { + if (!_timer) { + throw std::runtime_error("PWM is closed"); + } + if (duty < 0 || duty > (1 << _timer->_resolution)) { + throw std::runtime_error("Duty must be between 0 and 2^resolution"); + } + + esp_err_t err = ledc_set_duty(LEDC_LOW_SPEED_MODE, static_cast(_channel), duty); + if (err != ESP_OK) { + throw std::runtime_error(esp_err_to_name(err)); + } + + err = ledc_update_duty(LEDC_LOW_SPEED_MODE, static_cast(_channel)); + if (err != ESP_OK) { + throw std::runtime_error(esp_err_to_name(err)); + } + _duty = duty; + } + + void close() { + if (!_timer) { + return; + } + ledc_stop(LEDC_LOW_SPEED_MODE, static_cast(_channel), 0); + _feature->releaseTimer(_timer); + _timer = nullptr; + } +}; + +template +struct PWMProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { + using PWM_ = PWM; + + static PWM_* constructOpaque(jac::ContextRef ctx, std::vector args) { + // TODO: extend instance lifetime until close or program end + // TODO: check if pin is already in use + + if (args.size() < 1) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "PWM constructor requires an options argument"); + } + + jac::ObjectWeak options = args[0].to(); + + int pin = options.get("pin"); + double hz = options.get("hz"); + int resolution = options.get("resolution"); + + return new PWM_(static_cast(JS_GetContextOpaque(ctx)), pin, hz, resolution); + } + + static void addProperties(jac::ContextRef ctx, jac::Object proto) { + jac::FunctionFactory ff(ctx); + + // TODO: add required properties + PWMProtoBuilder::template addMethodMember(ctx, proto, "write", jac::PropFlags::Enumerable); + PWMProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); + + PWMProtoBuilder::template addPropMember(ctx, proto, "hz", jac::PropFlags::Enumerable); + + proto.defineProperty("timer", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal) { + PWM_& pwm = *PWMProtoBuilder::getOpaque(ctx, thisVal); + return pwm._timer->num; + }), jac::PropFlags::Enumerable); + } +}; + +template +class PwmFeature : public Next { + std::set> _timers; + std::set _usedTimers; + std::set _usedChannels; +public: + int findMin(std::set& set) { + int last = 0; + for (int item : set) { + if (item - last > 1) { + break; + } + last = item; + } + return last + 1; + } + + const Timer* takeTimer(int freq, int resolution) { + auto it = _timers.find(std::make_pair(freq, resolution)); + if (it == _timers.end()) { + int num = findMin(_usedTimers); + if (num == LEDC_TIMER_MAX) { + throw std::runtime_error("No timers available"); + } + it = _timers.emplace(freq, resolution, _timers.size()).first; + _usedTimers.insert(num); + } + it->incRefs(); + return &*it; + } + + void releaseTimer(const Timer* timer) { + if (timer->decRefs()) { + _timers.erase(*timer); + _usedTimers.erase(timer->num); + } + } + + int takeChannel() { + int num = findMin(_usedChannels); + + if (num == LEDC_CHANNEL_MAX) { + throw std::runtime_error("No channels available"); + } + + _usedChannels.insert(num); + return num; + } + + void releaseChannel(int channel) { + _usedChannels.erase(channel); + } + + using PWMClass = jac::Class>; + + PwmFeature() { + PWMClass::init("PWM"); + } + + void initialize() { + Next::initialize(); + + jac::Module& pwmModule = this->newModule("embedded:io/pwm"); + + PWMClass::initContext(this->context()); + + pwmModule.addExport("PWM", PWMClass::getConstructor(this->context())); + } +}; diff --git a/main/main.cpp b/main/main.cpp index 448fbfc..45fa441 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -17,7 +17,7 @@ #include "espFeatures/smartLedFeature.h" #include "espFeatures/digitalFeature.h" #include "espFeatures/freeRTOSEventQueue.h" -#include "espFeatures/ledcFeature.h" +#include "espFeatures/pwmFeature.h" #include "espFeatures/analogFeature.h" #include "espFeatures/i2cFeature.h" #include "espFeatures/simpleRadioFeature.h" @@ -59,7 +59,7 @@ using Machine = jac::ComposeMachine< jac::ModuleLoaderFeature, jac::EventLoopFeature, DigitalFeature, - LedcFeature, + PwmFeature, AnalogFeature, I2CFeature, SmartLedFeature, diff --git a/ts-examples/@types/ledc.d.ts b/ts-examples/@types/ledc.d.ts deleted file mode 100644 index 49b016e..0000000 --- a/ts-examples/@types/ledc.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -declare module "ledc" { - /** - * Configure the given timer. - * @param timer The timer to configure. - * @param frequency The frequency to configure the timer to. - * @param resolution The resolution to configure the timer to (default 10 bits, changes frequency range) - */ - function configureTimer(timer: number, frequency: number, resolution?: number): void; - - /** - * Configure the given LEDC channel. - * @param channel The channel to configure. - * @param pin The pin to configure the channel to. - * @param timer The timer to configure the channel to. - * @param duty The duty to configure the channel to (0-1023). - */ - function configureChannel(channel: number, pin: number, timer: number, duty: number): void; - - /** - * Set the frequency of the given timer. - * @param timer The timer to set the frequency of. - * @param frequency The frequency to set the timer to. - */ - function setFrequency(timer: number, frequency: number): void; - - /** - * Set the duty of the given channel. - * @param channel The channel to set the duty of. - * @param duty The duty to set the channel to (0-1023). - */ - function setDuty(channel: number, duty: number): void; - - /** - * Stop the given timer. - * @param timer The timer to stop. - */ - function stopTimer(timer: number): void; - - /** - * Stop the given channel. - * @param channel The channel to stop. - */ - function stopChannel(channel: number): void; -} diff --git a/ts-examples/@types/pwm.d.ts b/ts-examples/@types/pwm.d.ts new file mode 100644 index 0000000..37f9e61 --- /dev/null +++ b/ts-examples/@types/pwm.d.ts @@ -0,0 +1,27 @@ +declare module "embedded:io/pwm" { + class PWM{ + constructor(options: { + pin: number, + hz: number, + resolution: number + }); + + hz: number; + + /** + * Returns number of the timer used by the PWM. + */ + timer(): number; + + /** + * Sets the PWM duty cycle. + * @param duty The duty cycle to set. + */ + write(duty: number): void; + + /** + * Stops the PWM. + */ + close(): void; + } +} diff --git a/ts-examples/src/adc.ts b/ts-examples/src/adc.ts index 87b89bd..55f2848 100644 --- a/ts-examples/src/adc.ts +++ b/ts-examples/src/adc.ts @@ -1,5 +1,5 @@ -import * as ledc from "ledc"; import { Analog } from "embedded:io/analog"; +import { PWM } from "embedded:io/pwm"; /** * Example showing how to use the ADC to control the brightness of an LED. @@ -8,8 +8,11 @@ import { Analog } from "embedded:io/analog"; const INPUT_PIN = 1; const LED_PIN = 45; -ledc.configureTimer(0, 1000); -ledc.configureChannel(0, LED_PIN, 0, 1023); +const ledPWM = new PWM({ + pin: LED_PIN, + hz: 1000, + resolution: 10 +}); const adc = new Analog({ pin: INPUT_PIN, @@ -20,6 +23,5 @@ let power = 3; setInterval(() => { const value = adc.read(); - console.log(value); - ledc.setDuty(0, Math.pow(value, power) / Math.pow(1023, power - 1)); + ledPWM.write(Math.pow(value, power) / Math.pow(1023, power - 1)); }, 10); diff --git a/ts-examples/src/piezo.ts b/ts-examples/src/piezo.ts deleted file mode 100644 index a2bab8e..0000000 --- a/ts-examples/src/piezo.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as ledc from "ledc"; - -/** - * Example showing how to use the LEDC to control the frequency of a piezo. - */ - -const PIEZO_PIN = 18; - -ledc.configureTimer(0, 1000); -ledc.configureChannel(0, PIEZO_PIN, 0, 512); - -let frequency = 1000; -let step = 10; - -setInterval(() => { - frequency += step; - if (frequency >= 2000) { - frequency = 2000; - step *= -1; - } - if (frequency <= 1000) { - frequency = 1000; - step *= -1; - } - ledc.setFrequency(0, frequency); -}, 10); diff --git a/ts-examples/src/ledc.ts b/ts-examples/src/pwm.ts similarity index 64% rename from ts-examples/src/ledc.ts rename to ts-examples/src/pwm.ts index c269393..9b28cdc 100644 --- a/ts-examples/src/ledc.ts +++ b/ts-examples/src/pwm.ts @@ -1,4 +1,4 @@ -import * as ledc from "ledc"; +import { PWM } from "embedded:io/pwm"; /** * Example showing how to use the LEDC to control the brightness of an LED. @@ -6,8 +6,11 @@ import * as ledc from "ledc"; const LED_PIN = 45; -ledc.configureTimer(0, 1000); -ledc.configureChannel(0, LED_PIN, 0, 1023); +const ledPWM = new PWM({ + pin: LED_PIN, + hz: 1000, + resolution: 10 +}); let duty = 0; let step = 10; @@ -24,5 +27,5 @@ setInterval(() => { duty = 0; step *= -1; } - ledc.setDuty(0, Math.pow(duty, power) / Math.pow(1023, power - 1)); + ledPWM.write(Math.pow(duty, power) / Math.pow(1023, power - 1)); }, 10); diff --git a/ts-examples/src/servo.ts b/ts-examples/src/servo.ts index 5fbcc2f..1d4f7ba 100644 --- a/ts-examples/src/servo.ts +++ b/ts-examples/src/servo.ts @@ -1,15 +1,18 @@ import { Analog } from "embedded:io/analog"; -import * as ledc from "ledc"; +import { PWM } from "embedded:io/pwm"; /** * Example showing how to control servos using the LEDC peripheral. */ const INPUT_PIN = 1; -const SERVO_PIN = 17; +const SERVO_PIN = 35; -ledc.configureTimer(0, 50, 12); -ledc.configureChannel(0, SERVO_PIN, 0, 1023); +const servoPWM = new PWM({ + pin: SERVO_PIN, + hz: 50, + resolution: 10 +}); const adc = new Analog({ pin: INPUT_PIN, @@ -25,5 +28,5 @@ setInterval(() => { // convert to a duty cycle const duty = (ms / 20) * 1023; - ledc.setDuty(0, duty); -}, 20); + servoPWM.write(duty); +}, 5); From b0cf04b07004567287c5e11e7d8a559e194ce26c Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Wed, 3 Jan 2024 00:25:28 +0100 Subject: [PATCH 11/12] extend io instance lifetimes until close/restart --- main/espFeatures/analogFeature.h | 25 ++++++++++++------ main/espFeatures/digitalFeature.h | 21 ++++++++++++---- main/espFeatures/extendLifetimeFeature.h | 32 ++++++++++++++++++++++++ main/espFeatures/i2cFeature.h | 22 +++++++++------- main/espFeatures/pwmFeature.h | 17 ++++++++++--- main/main.cpp | 2 ++ ts-examples/@types/i2c.d.ts | 5 ++++ ts-examples/src/gomoku.ts | 13 ---------- ts-examples/src/index.ts | 2 +- ts-examples/src/radioblink.ts | 9 ------- ts-examples/src/snake.ts | 12 --------- 11 files changed, 100 insertions(+), 60 deletions(-) create mode 100644 main/espFeatures/extendLifetimeFeature.h diff --git a/main/espFeatures/analogFeature.h b/main/espFeatures/analogFeature.h index 48abac3..011c9a7 100644 --- a/main/espFeatures/analogFeature.h +++ b/main/espFeatures/analogFeature.h @@ -4,14 +4,15 @@ #include #include -#include #include "driver/adc.h" template class Analog { +public: Feature* const _feature; +private: const int _adcNum; const adc1_channel_t _channel; const adc_bits_width_t _resolution; @@ -31,7 +32,7 @@ class Analog { throw std::runtime_error(std::string("Error configuring ADC width: ") + esp_err_to_name(err)); } - // TODO: make attenuation configurable + // TODO: make attenuation configurable? err = adc1_config_channel_atten(_channel, ADC_ATTEN_DB_11); if (err != ESP_OK) { throw std::runtime_error(std::string("Error configuring ADC attenuation: ") + esp_err_to_name(err)); @@ -52,17 +53,15 @@ class Analog { void close() { // do nothing for now - // possibly reconfigure to some default state? + // TODO: possibly reconfigure back to some default state? } }; template -struct AnalogProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { +struct AnalogProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties, public jac::ProtoBuilder::LifetimeHandles { using Analog_ = Analog; static Analog_* constructOpaque(jac::ContextRef ctx, std::vector args) { - // TODO: extend instance lifetime until close or program end - // *currently not a problem, as the instance destrutor does nothing* // TODO: check if pin is already in use if (args.size() < 1) { @@ -80,9 +79,21 @@ struct AnalogProtoBuilder : public jac::ProtoBuilder::Opaque>, p } } + static void postConstruction(jac::ContextRef ctx, jac::Object thisVal, std::vector args) { + Analog_& self = *AnalogProtoBuilder::getOpaque(ctx, thisVal); + self._feature->extendLifetime(thisVal); + } + static void addProperties(jac::ContextRef ctx, jac::Object proto) { + jac::FunctionFactory ff(ctx); + AnalogProtoBuilder::template addMethodMember(ctx, proto, "read", jac::PropFlags::Enumerable); - AnalogProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); + + proto.defineProperty("close", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal) { + Analog_& self = *AnalogProtoBuilder::getOpaque(ctx, thisVal); + self.close(); + self._feature->releaseLifetime(thisVal); + }), jac::PropFlags::Enumerable); } }; diff --git a/main/espFeatures/digitalFeature.h b/main/espFeatures/digitalFeature.h index 8336869..5a61bcc 100644 --- a/main/espFeatures/digitalFeature.h +++ b/main/espFeatures/digitalFeature.h @@ -5,6 +5,8 @@ #include #include +#include + #include "driver/gpio.h" @@ -134,8 +136,10 @@ namespace detail { template class Digital { +public: Feature* const _feature; const gpio_num_t _pin; +private: detail::InterruptConf _interruptConf; void enableInterrupt(DigitalEdge mode) { @@ -275,11 +279,10 @@ class Digital { }; template -struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { +struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties, public jac::ProtoBuilder::LifetimeHandles { using Digital_ = Digital; static Digital_* constructOpaque(jac::ContextRef ctx, std::vector args) { - // TODO: extend instance lifetime until close or program end // TODO: check if pin is already in use if (args.size() < 1) { @@ -313,14 +316,22 @@ struct DigitalProtoBuilder : public jac::ProtoBuilder::Opaque>, } } + static void postConstruction(jac::ContextRef ctx, jac::Object thisVal, std::vector args) { + Digital_& self = *DigitalProtoBuilder::getOpaque(ctx, thisVal); + self._feature->extendLifetime(thisVal); + } + static void addProperties(jac::ContextRef ctx, jac::Object proto) { jac::FunctionFactory ff(ctx); - // TODO: add required properties - DigitalProtoBuilder::template addMethodMember(ctx, proto, "write", jac::PropFlags::Enumerable); DigitalProtoBuilder::template addMethodMember(ctx, proto, "read", jac::PropFlags::Enumerable); - DigitalProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); + + proto.defineProperty("close", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal) { + Digital_& self = *DigitalProtoBuilder::getOpaque(ctx, thisVal); + self.close(); + self._feature->releaseLifetime(thisVal); + }), jac::PropFlags::Enumerable); } }; diff --git a/main/espFeatures/extendLifetimeFeature.h b/main/espFeatures/extendLifetimeFeature.h new file mode 100644 index 0000000..9ea21eb --- /dev/null +++ b/main/espFeatures/extendLifetimeFeature.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include + +template +class ExtendLifetimeFeature : public Next { + std::unordered_set _objects; +public: + void extendLifetime(jac::Object obj) { + auto [ctx, val] = obj.loot(); + _objects.insert(val); + } + + void releaseLifetime(jac::ValueWeak obj) { + auto it = _objects.find(obj.getVal()); + if (it == _objects.end()) { + return; + } + + JS_FreeValue(this->context(), *it); + _objects.erase(it); + } + + ~ExtendLifetimeFeature() { + for (auto val : _objects) { + JS_FreeValue(this->context(), val); + } + } +}; diff --git a/main/espFeatures/i2cFeature.h b/main/espFeatures/i2cFeature.h index 5780e98..6338c1c 100644 --- a/main/espFeatures/i2cFeature.h +++ b/main/espFeatures/i2cFeature.h @@ -2,14 +2,10 @@ #include #include -#include -#include #include -#include #include "driver/i2c.h" -#include "freertos/FreeRTOS.h" template @@ -114,12 +110,10 @@ class I2C { }; template -struct I2CProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { +struct I2CProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties, public jac::ProtoBuilder::LifetimeHandles { using I2C_ = I2C; static I2C_* constructOpaque(jac::ContextRef ctx, std::vector args) { - // TODO: extend instance lifetime until close or program end - // *currently not a problem, as the instance destrutor does nothing* // TODO: check if pins are already in use if (args.size() < 1) { @@ -155,10 +149,15 @@ struct I2CProtoBuilder : public jac::ProtoBuilder::Opaque>, public return new I2C_(sda, scl, bitrate, address, port); } + static void postConstruction(jac::ContextRef ctx, jac::Object thisVal, std::vector args) { + auto& machine = *reinterpret_cast(JS_GetContextOpaque(ctx)); + machine.extendLifetime(thisVal); + } + static void addProperties(JSContext* ctx, jac::Object proto) { jac::FunctionFactory ff(ctx); - // TODO: ugly hack + // XXX: ugly hack with inline javascript proto.defineProperty("read", ff.newFunctionThisVariadic([](jac::ContextRef ctx, jac::ValueWeak thisVal, std::vector args) { auto& i2c = *I2CProtoBuilder::getOpaque(ctx, thisVal); auto& machine = *reinterpret_cast(JS_GetContextOpaque(ctx)); @@ -210,7 +209,12 @@ R"--( i2c.write(dataView, stopBit); }), jac::PropFlags::Enumerable); - I2CProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); + proto.defineProperty("close", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal) { + I2C_& self = *I2CProtoBuilder::getOpaque(ctx, thisVal); + self.close(); + auto& machine = *reinterpret_cast(JS_GetContextOpaque(ctx)); + machine.releaseLifetime(thisVal); + }), jac::PropFlags::Enumerable); } }; diff --git a/main/espFeatures/pwmFeature.h b/main/espFeatures/pwmFeature.h index 10f4c4a..12696ef 100644 --- a/main/espFeatures/pwmFeature.h +++ b/main/espFeatures/pwmFeature.h @@ -55,9 +55,9 @@ struct Timer { template class PWM { - Feature* const _feature; public: // TODO: hide these (make view const) + Feature* const _feature; gpio_num_t _pin; int _hz; int _duty; @@ -132,11 +132,10 @@ class PWM { }; template -struct PWMProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { +struct PWMProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties, public jac::ProtoBuilder::LifetimeHandles { using PWM_ = PWM; static PWM_* constructOpaque(jac::ContextRef ctx, std::vector args) { - // TODO: extend instance lifetime until close or program end // TODO: check if pin is already in use if (args.size() < 1) { @@ -152,12 +151,16 @@ struct PWMProtoBuilder : public jac::ProtoBuilder::Opaque>, public return new PWM_(static_cast(JS_GetContextOpaque(ctx)), pin, hz, resolution); } + static void postConstruction(jac::ContextRef ctx, jac::Object thisVal, std::vector args) { + PWM_& self = *PWMProtoBuilder::getOpaque(ctx, thisVal); + self._feature->extendLifetime(thisVal); + } + static void addProperties(jac::ContextRef ctx, jac::Object proto) { jac::FunctionFactory ff(ctx); // TODO: add required properties PWMProtoBuilder::template addMethodMember(ctx, proto, "write", jac::PropFlags::Enumerable); - PWMProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); PWMProtoBuilder::template addPropMember(ctx, proto, "hz", jac::PropFlags::Enumerable); @@ -165,6 +168,12 @@ struct PWMProtoBuilder : public jac::ProtoBuilder::Opaque>, public PWM_& pwm = *PWMProtoBuilder::getOpaque(ctx, thisVal); return pwm._timer->num; }), jac::PropFlags::Enumerable); + + proto.defineProperty("close", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisVal) { + PWM_& self = *PWMProtoBuilder::getOpaque(ctx, thisVal); + self.close(); + self._feature->releaseLifetime(thisVal); + }), jac::PropFlags::Enumerable); } }; diff --git a/main/main.cpp b/main/main.cpp index 45fa441..f276c7f 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -21,6 +21,7 @@ #include "espFeatures/analogFeature.h" #include "espFeatures/i2cFeature.h" #include "espFeatures/simpleRadioFeature.h" +#include "espFeatures/extendLifetimeFeature.h" #include "util/uartStream.h" @@ -58,6 +59,7 @@ using Machine = jac::ComposeMachine< jac::FilesystemFeature, jac::ModuleLoaderFeature, jac::EventLoopFeature, + ExtendLifetimeFeature, DigitalFeature, PwmFeature, AnalogFeature, diff --git a/ts-examples/@types/i2c.d.ts b/ts-examples/@types/i2c.d.ts index 6df5fe1..e106925 100644 --- a/ts-examples/@types/i2c.d.ts +++ b/ts-examples/@types/i2c.d.ts @@ -29,5 +29,10 @@ declare module "embedded:io/i2c" { * @param stop Whether to send a stop bit after writing, default: true. */ write(buffer: ArrayBuffer, stop?: boolean): void; + + /** + * Closes the I2C connection. + */ + close(): void; } } diff --git a/ts-examples/src/gomoku.ts b/ts-examples/src/gomoku.ts index 049c43c..8294fff 100644 --- a/ts-examples/src/gomoku.ts +++ b/ts-examples/src/gomoku.ts @@ -181,16 +181,3 @@ let middle = new Digital({ } } }); - - -// hack to prevent the object from being garbage collected -// TODO: remove when fixed -setInterval(() => { - up; - down; - left; - right; - middle; - power; - strip; -}, 100000); diff --git a/ts-examples/src/index.ts b/ts-examples/src/index.ts index 9e6d36a..7672e18 100644 --- a/ts-examples/src/index.ts +++ b/ts-examples/src/index.ts @@ -1,4 +1,4 @@ /** * Select the example by changing the import path. */ -import "./blink.js"; +import "./button.js"; diff --git a/ts-examples/src/radioblink.ts b/ts-examples/src/radioblink.ts index dd8edae..41445c2 100644 --- a/ts-examples/src/radioblink.ts +++ b/ts-examples/src/radioblink.ts @@ -78,12 +78,3 @@ simpleradio.on("keyvalue", (key, value) => { break; } }); - - -// hack to prevent the object from being garbage collected -// TODO: remove when fixed -setInterval(() => { - buttonA; - buttonB; - buttonC; -}, 100000); diff --git a/ts-examples/src/snake.ts b/ts-examples/src/snake.ts index e0406c5..4c057de 100644 --- a/ts-examples/src/snake.ts +++ b/ts-examples/src/snake.ts @@ -147,15 +147,3 @@ function step() { } var timer = setInterval(step, 300); - - -// hack to prevent the object from being garbage collected -// TODO: remove when fixed -setInterval(() => { - up; - down; - left; - right; - power; - strip; -}, 100000); From 99b0326b5c2951466ed8d526b129a1229ac14c2a Mon Sep 17 00:00:00 2001 From: Petr Kubica Date: Thu, 11 Jan 2024 15:57:17 +0100 Subject: [PATCH 12/12] rewrite SmartLed to use Display class pattern --- main/espFeatures/smartLedFeature.h | 173 +++++++++++++++++------------ ts-examples/@types/smartled.d.ts | 41 +++---- ts-examples/src/blinksmart.ts | 18 ++- ts-examples/src/gomoku.ts | 21 +++- ts-examples/src/snake.ts | 23 +++- 5 files changed, 159 insertions(+), 117 deletions(-) diff --git a/main/espFeatures/smartLedFeature.h b/main/espFeatures/smartLedFeature.h index 9beb951..9f4068a 100644 --- a/main/espFeatures/smartLedFeature.h +++ b/main/espFeatures/smartLedFeature.h @@ -28,7 +28,6 @@ struct jac::ConvTraits { } }; - template<> struct jac::ConvTraits { static Value to(ContextRef ctx, LedType val) { @@ -47,85 +46,113 @@ struct jac::ConvTraits { } }; -template -class SmartLedFeature : public Next { - static inline std::set _usedRmtChannels; +template +class SmartLedWrapper { + std::optional _led; + +public: + SmartLedWrapper(LedType type, int count, int pin) { + gpio_num_t gpio = Feature::getDigitalPin(pin); - struct SmartLedProtoBuilder : public jac::ProtoBuilder::Opaque, public jac::ProtoBuilder::Properties { - static SmartLed* constructOpaque(JSContext* ctx, std::vector args) { - if (args.size() < 2) { - throw std::runtime_error("Invalid number of arguments"); - } - int pin = args[0].to(); - int count = args[1].to(); - - LedType type = LED_WS2812; - if (args.size() > 2) { - type = args[2].to(); - } - - if (Next::PlatformInfo::PinConfig::DIGITAL_PINS.find(pin) == Next::PlatformInfo::PinConfig::DIGITAL_PINS.end()) { - throw std::runtime_error("Invalid pin number"); - } - - int channel = 0; - while (_usedRmtChannels.find(channel) != _usedRmtChannels.end()) { - channel++; - } - if (channel >= 4) { - throw std::runtime_error("No available RMT channels"); - } - _usedRmtChannels.insert(channel); - - return new SmartLed(type, count, pin, channel, SingleBuffer); + int channel = 0; + while (Feature::_usedRmtChannels.find(channel) != Feature::_usedRmtChannels.end()) { + channel++; + } + if (channel >= 4) { + throw std::runtime_error("No available RMT channels"); + } + Feature::_usedRmtChannels.insert(channel); + + _led.emplace(type, count, gpio, channel, SingleBuffer); + + _led->show(); + } + + void close() { + if (_led) { + int channel = _led->channel(); + _led->wait(); + _led = std::nullopt; + + Feature::_usedRmtChannels.erase(channel); + } + } + + void begin(int x, int y, int width, int height) { + if (!_led) { + throw std::runtime_error("SmartLed is closed"); + } + if (x != 0 || y != 0 || width != _led->size() || height != 1) { + throw std::runtime_error("SmartLed only supports full width"); + } + } + + void send(std::span data) { + if (!_led) { + throw std::runtime_error("SmartLed is closed"); + } + if (data.size() != _led->size()) { + throw std::runtime_error("SmartLed only supports full width"); } - static void destroyOpaque(JSRuntime* rt, SmartLed* ptr) noexcept { - if (!ptr) { - return; - } + std::copy(data.begin(), data.end(), _led->begin()); + _led->show(); + } + + ~SmartLedWrapper() { + close(); + } +}; + +template +struct SmartLedProtoBuilder : public jac::ProtoBuilder::Opaque>, public jac::ProtoBuilder::Properties { + using SmartLed_ = SmartLedWrapper; - ptr->wait(); - int channel = ptr->channel(); - delete ptr; - _usedRmtChannels.erase(channel); + static SmartLed_* constructOpaque(JSContext* ctx, std::vector args) { + if (args.size() < 1) { + throw jac::Exception::create(jac::Exception::Type::TypeError, "SmartLed constructor requires an options argument"); } - static void addProperties(JSContext* ctx, jac::Object proto) { - jac::FunctionFactory ff(ctx); - - proto.defineProperty("show", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisValue) { - SmartLed& led = *getOpaque(ctx, thisValue); - led.wait(); - led.show(); - }), jac::PropFlags::Enumerable); - - proto.defineProperty("set", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisValue, int idx, Rgb color) { - SmartLed& strip = *getOpaque(ctx, thisValue); - if (idx < 0 || idx >= strip.size()) { - throw std::runtime_error("Invalid index"); - } - strip[idx] = color; - }), jac::PropFlags::Enumerable); - - proto.defineProperty("get", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisValue, int idx) { - SmartLed& strip = *getOpaque(ctx, thisValue); - if (idx < 0 || idx >= strip.size()) { - throw std::runtime_error("Invalid index"); - } - return strip[idx]; - }), jac::PropFlags::Enumerable); - - proto.defineProperty("clear", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisValue) { - SmartLed& strip = *getOpaque(ctx, thisValue); - for (int i = 0; i < strip.size(); i++) { - strip[i] = Rgb(0, 0, 0); - } - }), jac::PropFlags::Enumerable); + jac::ObjectWeak options = args[0].to(); + + int pin = options.get("pin"); + int count = options.get("count"); + + LedType type = LED_WS2812; + if (options.hasProperty("type")) { + type = options.get("type"); } - }; + + return new SmartLed_(type, count, pin); + } + + static void addProperties(JSContext* ctx, jac::Object proto) { + jac::FunctionFactory ff(ctx); + + proto.defineProperty("begin", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisValue, jac::ObjectWeak options) { + SmartLed_& self = *SmartLedProtoBuilder::getOpaque(ctx, thisValue); + int x = options.get("x"); + int y = options.get("y"); + int width = options.get("width"); + int height = options.get("height"); + self.begin(x, y, width, height); + }), jac::PropFlags::Enumerable); + + proto.defineProperty("send", ff.newFunctionThis([](jac::ContextRef ctx, jac::ValueWeak thisValue, jac::ArrayBufferWeak data) { + SmartLed_& self = *SmartLedProtoBuilder::getOpaque(ctx, thisValue); + self.send(data.typedView()); + }), jac::PropFlags::Enumerable); + + SmartLedProtoBuilder::template addMethodMember(ctx, proto, "close", jac::PropFlags::Enumerable); + } +}; + + +template +class SmartLedFeature : public Next { + static inline std::set _usedRmtChannels; public: - using SmartLedClass = jac::Class; + using SmartLedClass = jac::Class>; SmartLedFeature() { SmartLedClass::init("SmartLed"); @@ -143,4 +170,6 @@ class SmartLedFeature : public Next { mod.addExport("LED_WS2813", jac::Value::from(this->context(), LED_WS2813)); mod.addExport("LED_SK6812", jac::Value::from(this->context(), LED_SK6812)); } + + friend SmartLedWrapper; }; diff --git a/ts-examples/@types/smartled.d.ts b/ts-examples/@types/smartled.d.ts index a9c51e8..68f4c71 100644 --- a/ts-examples/@types/smartled.d.ts +++ b/ts-examples/@types/smartled.d.ts @@ -14,37 +14,22 @@ declare module "smartled" { } class SmartLed { - /** - * Create a new Smart LED strip. - * @param pin The pin the strip is connected to. - * @param count The number of LEDs in the strip. - * @param type The type of LED strip. - */ - constructor(pin: number, count: number, type?: LedType); + constructor(options: { + pin: number, + count: number, + type?: LedType + }); - /** - * Show the current buffer on the strip. - */ - public show(): void; + begin(options: { + x: number, + y: number, + width: number, + height: number + }); - /** - * Set the color of the given LED. - * @param index The index of the LED to set. - * @param rgb The color to set the LED to. - */ - public set(index: number, rgb: Rgb): void; + send(data: ArrayBuffer): void; - /** - * Get the color of the given LED. - * @param index The index of the LED to get. - * @returns The color of the LED. - */ - public get(index: number): Rgb; - - /** - * Clear the buffer. - */ - public clear(): void; + close(): void; } const LED_WS2812: LedType; diff --git a/ts-examples/src/blinksmart.ts b/ts-examples/src/blinksmart.ts index 76ba2d8..3503743 100644 --- a/ts-examples/src/blinksmart.ts +++ b/ts-examples/src/blinksmart.ts @@ -1,4 +1,4 @@ -import { SmartLed } from "smartled"; +import { LED_WS2812B, SmartLed } from "smartled"; /** * This example blinks using a smart LED on pin 48. @@ -6,18 +6,26 @@ import { SmartLed } from "smartled"; const LED_PIN = 48; -let strip = new SmartLed(LED_PIN, 1); +let strip = new SmartLed({ + pin: LED_PIN, + count: 1, + type: LED_WS2812B +}); + +let buffer = new ArrayBuffer(4); +let view = new Uint32Array(buffer); let state = false; setInterval(() => { if (state) { - strip.set(0, { r: 0, g: 0, b: 0 }); + view[0] = 0x000000; } else { - strip.set(0, { r: 20, g: 0, b: 0 }); + // BRG + view[0] = 0x002000; } - strip.show(); + strip.send(buffer); state = !state; }, 1000); diff --git a/ts-examples/src/gomoku.ts b/ts-examples/src/gomoku.ts index 8294fff..5437ad9 100644 --- a/ts-examples/src/gomoku.ts +++ b/ts-examples/src/gomoku.ts @@ -30,8 +30,14 @@ else if (PlatformInfo.name == "ESP32-S3") { var MIDDLE_PIN = 9; } +let strip = new SmartLed({ + pin: LED_PIN, + count: 100, + type: LED_WS2812 +}); -let strip = new SmartLed(LED_PIN, 100, LED_WS2812); +let buffer = new ArrayBuffer(400); +let view = new Uint32Array(buffer); interface Pos { x: number; @@ -48,9 +54,12 @@ for (let i = 0; i < 10; i++) { } } -function set(x: number, y: number, color: Rgb, brightness: number = 0.1) { +function set(x: number, y: number, color: Rgb, brightness: number = 0.2) { brightness /= 4; - strip.set(x + y * 10, { r: color.r * brightness, g: color.g * brightness, b: color.b * brightness }); + view[x + y * 10] = + (Math.floor(color.b * brightness) << 16) | + (Math.floor(color.r * brightness) << 8) | + (Math.floor(color.g * brightness)); } let colors: Rgb[] = [ @@ -60,7 +69,7 @@ let colors: Rgb[] = [ ]; set(0, 0, { r: 0, g: 255, b: 0 }); -strip.show(); +strip.send(buffer); let turnRed = true; @@ -108,7 +117,7 @@ function gameEnd(pos: Pos): { color: number, direction: number[], posCount: numb function update(oldPos: Pos, newPos: Pos) { set(oldPos.x, oldPos.y, colors[matrix[oldPos.x][oldPos.y]]); set(newPos.x, newPos.y, { r: 0, g: 255, b: 0 }); - strip.show(); + strip.send(buffer); pos = newPos; } @@ -177,7 +186,7 @@ let middle = new Digital({ right.close(); } turnRed = !turnRed; - strip.show(); + strip.send(buffer); } } }); diff --git a/ts-examples/src/snake.ts b/ts-examples/src/snake.ts index 4c057de..7fbf6fc 100644 --- a/ts-examples/src/snake.ts +++ b/ts-examples/src/snake.ts @@ -1,4 +1,4 @@ -import { SmartLed, Rgb, LED_WS2812 } from "smartled"; +import { SmartLed, LED_WS2812, Rgb } from "smartled"; import { Digital } from "embedded:io/digital"; /** @@ -34,9 +34,20 @@ const SNAKE_COLOR = { r: 0, g: 255, b: 0 }; let power = new Digital({ pin: POWER_PIN, mode: Digital.Output }); power.write(1); -let strip = new SmartLed(LED_PIN, 100, LED_WS2812); +let strip = new SmartLed({ + pin: LED_PIN, + count: 100, + type: LED_WS2812 +}); + +let buffer = new ArrayBuffer(400); +let view = new Uint32Array(buffer); + function set(x: number, y: number, color: Rgb, brightness: number = 0.2) { - strip.set(x + y * 10, { r: color.r * brightness, g: color.g * brightness, b: color.b * brightness }); + view[x + y * 10] = + (Math.floor(color.b * brightness) << 16) | + (Math.floor(color.r * brightness) << 8) | + (Math.floor(color.g * brightness)); } let snake = [ @@ -61,7 +72,7 @@ function newFood() { return { x, y }; } let food = newFood(); -strip.show(); +strip.send(buffer); let score = 0; @@ -106,7 +117,7 @@ function blinkSnake() { for (let pos of snake) { set(pos.x, pos.y, blinkState ? SNAKE_COLOR : { r: 0, g: 0, b: 0 }); } - strip.show(); + strip.send(buffer); blinkState = !blinkState; } @@ -143,7 +154,7 @@ function step() { set(last.x, last.y, { r: 0, g: 0, b: 0 }); } set(next.x, next.y, SNAKE_COLOR); - strip.show(); + strip.send(buffer); } var timer = setInterval(step, 300);