diff --git a/examples/Wippersnapper_NoFS/Wippersnapper_NoFS.ino b/examples/Wippersnapper_NoFS/Wippersnapper_NoFS.ino index aa17f8133..dabf68632 100644 --- a/examples/Wippersnapper_NoFS/Wippersnapper_NoFS.ino +++ b/examples/Wippersnapper_NoFS/Wippersnapper_NoFS.ino @@ -1,36 +1,31 @@ -// Adafruit IO WipperSnapper Beta -// -// -// NOTE: This software is a BETA release and in active development. -// Please report bugs or errors to -// https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/issues +// Adafruit IO WipperSnapper // // This sketch is for devices which lack USB-MSD or LittleFS support such -// as the Arduino MKR WiFi 1010, Arduino Nano 33 IoT. +// as the ESP32Dev for Wokwi Simulator // // Adafruit invests time and resources providing this open source code. // Please support Adafruit and open source hardware by purchasing // products from Adafruit! // -// Brent Rubell for Adafruit Industries, 2021 +// Brent Rubell for Adafruit Industries, 2025 // // All text above must be included in any redistribution. -#include "Wippersnapper_Networking.h" - +#include "ws_adapters.h" +#define WS_DEBUG // Enable debug output /************************ Adafruit IO Config *******************************/ // Visit io.adafruit.com if you need to create an account, // or if you need your Adafruit IO key. #define IO_USERNAME "YOUR_AIO_USERNAME" #define IO_KEY "YOUR_AIO_KEY" - +#define IO_URL "io.adafruit.com" +#define IO_PORT 8883 /**************************** WiFi Config ***********************************/ #define WIFI_SSID "YOUR_WIFI_SSID" #define WIFI_PASS "YOUR_WIFI_PASSWORD" -#include "Wippersnapper_Networking.h" -ws_adapter_wifi wipper(IO_USERNAME, IO_KEY, WIFI_SSID, WIFI_PASS); +ws_adapter_wifi wipper(IO_USERNAME, IO_KEY, WIFI_SSID, WIFI_PASS, IO_URL, IO_PORT); void setup() { // Provisioning must occur prior to serial init. diff --git a/src/Wippersnapper_V2.cpp b/src/Wippersnapper_V2.cpp index 732405b23..209f7464f 100644 --- a/src/Wippersnapper_V2.cpp +++ b/src/Wippersnapper_V2.cpp @@ -55,6 +55,8 @@ Wippersnapper_V2::Wippersnapper_V2() { WsV2._ds18x20_controller = new DS18X20Controller(); WsV2._i2c_controller = new I2cController(); WsV2._pixels_controller = new PixelsController(); + WsV2._pwm_controller = new PWMController(); + WsV2._servo_controller = new ServoController(); }; /**************************************************************************/ @@ -335,7 +337,6 @@ bool cbDecodeBrokerToDevice(pb_istream_t *stream, const pb_field_t *field, WS_DEBUG_PRINTLN("-> Checkin Response Message Type"); WS_DEBUG_PRINT("Handling Checkin Response..."); if (!handleCheckinResponse(stream)) { - WS_DEBUG_PRINTLN("Failure handling Checkin Response!"); return false; } WS_DEBUG_PRINTLN("Handled!"); @@ -343,91 +344,120 @@ bool cbDecodeBrokerToDevice(pb_istream_t *stream, const pb_field_t *field, case wippersnapper_signal_BrokerToDevice_digitalio_add_tag: WS_DEBUG_PRINTLN("-> DigitalIO Add Message Type"); if (!WsV2.digital_io_controller->Handle_DigitalIO_Add(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to add digitalio pin!"); return false; } break; case wippersnapper_signal_BrokerToDevice_digitalio_remove_tag: WS_DEBUG_PRINTLN("-> DigitalIO Remove Message Type"); if (!WsV2.digital_io_controller->Handle_DigitalIO_Remove(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to remove digitalio pin!"); return false; } break; case wippersnapper_signal_BrokerToDevice_digitalio_write_tag: WS_DEBUG_PRINTLN("-> DigitalIO Write Message Type"); if (!WsV2.digital_io_controller->Handle_DigitalIO_Write(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to write to digitalio pin!"); return false; } break; case wippersnapper_signal_BrokerToDevice_analogio_add_tag: WS_DEBUG_PRINTLN("-> AnalogIO Add Message Type"); if (!WsV2.analogio_controller->Handle_AnalogIOAdd(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to add analogio pin!"); return false; } break; case wippersnapper_signal_BrokerToDevice_analogio_remove_tag: WS_DEBUG_PRINTLN("-> AnalogIO Remove Message Type"); if (!WsV2.analogio_controller->Handle_AnalogIORemove(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to remove analogio pin!"); return false; } break; case wippersnapper_signal_BrokerToDevice_ds18x20_add_tag: WS_DEBUG_PRINTLN("-> DS18X20 Add Message Type"); if (!WsV2._ds18x20_controller->Handle_Ds18x20Add(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to add DS18X20 sensor!"); return false; } break; case wippersnapper_signal_BrokerToDevice_ds18x20_remove_tag: WS_DEBUG_PRINTLN("-> DS18X20 Remove Message Type"); if (!WsV2._ds18x20_controller->Handle_Ds18x20Remove(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to remove DS18X20 sensor!"); return false; } break; case wippersnapper_signal_BrokerToDevice_i2c_device_add_replace_tag: WS_DEBUG_PRINTLN("-> I2C Device Add/Replace Message Type"); if (!WsV2._i2c_controller->Handle_I2cDeviceAddOrReplace(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to add/replace I2C device!"); return false; } break; case wippersnapper_signal_BrokerToDevice_i2c_bus_scan_tag: WS_DEBUG_PRINTLN("-> I2C Bus Scan Message Type"); if (!WsV2._i2c_controller->Handle_I2cBusScan(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to add/replace I2C device!"); return false; } break; case wippersnapper_signal_BrokerToDevice_i2c_device_remove_tag: WS_DEBUG_PRINTLN("-> I2C Device Remove Message Type"); if (!WsV2._i2c_controller->Handle_I2cDeviceRemove(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to remove I2C device!"); return false; } break; case wippersnapper_signal_BrokerToDevice_pixels_add_tag: WS_DEBUG_PRINTLN("-> Pixels Add Message Type"); if (!WsV2._pixels_controller->Handle_Pixels_Add(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to add pixels strand!"); return false; } break; case wippersnapper_signal_BrokerToDevice_pixels_remove_tag: WS_DEBUG_PRINTLN("-> Pixels Remove Message Type"); if (!WsV2._pixels_controller->Handle_Pixels_Remove(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to remove pixels strand!"); return false; } break; case wippersnapper_signal_BrokerToDevice_pixels_write_tag: WS_DEBUG_PRINTLN("-> Pixels Write Message Type"); if (!WsV2._pixels_controller->Handle_Pixels_Write(stream)) { - WS_DEBUG_PRINTLN("ERROR: Unable to write to pixels strand!"); + return false; + } + break; + case wippersnapper_signal_BrokerToDevice_pwm_add_tag: + WS_DEBUG_PRINTLN("-> PWM Add Message Type"); + if (!WsV2._pwm_controller->Handle_PWM_Add(stream)) { + return false; + } + break; + case wippersnapper_signal_BrokerToDevice_pwm_write_duty_tag: + WS_DEBUG_PRINTLN("-> PWM Write Duty Cycle Message Type"); + if (!WsV2._pwm_controller->Handle_PWM_Write_DutyCycle(stream)) { + return false; + } + break; + case wippersnapper_signal_BrokerToDevice_pwm_write_freq_tag: + WS_DEBUG_PRINTLN("-> PWM Write Frequency Message Type"); + if (!WsV2._pwm_controller->Handle_PWM_Write_Frequency(stream)) { + return false; + } + break; + case wippersnapper_signal_BrokerToDevice_pwm_remove_tag: + WS_DEBUG_PRINTLN("-> PWM Remove Message Type"); + if (!WsV2._pwm_controller->Handle_PWM_Remove(stream)) { + return false; + } + break; + case wippersnapper_signal_BrokerToDevice_servo_add_tag: + WS_DEBUG_PRINTLN("-> Servo Add Message Type"); + if (!WsV2._servo_controller->Handle_Servo_Add(stream)) { + return false; + } + break; + case wippersnapper_signal_BrokerToDevice_servo_write_tag: + WS_DEBUG_PRINTLN("-> Servo Write Message Type"); + if (!WsV2._servo_controller->Handle_Servo_Write(stream)) { + return false; + } + break; + case wippersnapper_signal_BrokerToDevice_servo_remove_tag: + WS_DEBUG_PRINTLN("-> Servo Remove Message Type"); + if (!WsV2._servo_controller->Handle_Servo_Remove(stream)) { return false; } break; @@ -1005,6 +1035,17 @@ bool Wippersnapper_V2::PublishSignal(pb_size_t which_payload, void *payload) { MsgSignal.payload.pixels_added = *(wippersnapper_pixels_PixelsAdded *)payload; break; + case wippersnapper_signal_DeviceToBroker_pwm_added_tag: + WS_DEBUG_PRINTLN("PWMAdded"); + MsgSignal.which_payload = wippersnapper_signal_DeviceToBroker_pwm_added_tag; + MsgSignal.payload.pwm_added = *(wippersnapper_pwm_PWMAdded *)payload; + break; + case wippersnapper_signal_DeviceToBroker_servo_added_tag: + WS_DEBUG_PRINTLN("ServoAdded"); + MsgSignal.which_payload = + wippersnapper_signal_DeviceToBroker_servo_added_tag; + MsgSignal.payload.servo_added = *(wippersnapper_servo_ServoAdded *)payload; + break; default: WS_DEBUG_PRINTLN("ERROR: Invalid signal payload type, bailing out!"); return false; diff --git a/src/Wippersnapper_V2.h b/src/Wippersnapper_V2.h index c3fee0e51..bb8bebfb8 100644 --- a/src/Wippersnapper_V2.h +++ b/src/Wippersnapper_V2.h @@ -76,19 +76,12 @@ #include #include -// Nanopb dependencies +// Nanopb messages and dependencies +#include "protos/signal.pb.h" #include #include #include #include -#include - -// Include Signal Proto -#include "protos/checkin.pb.h" -#include "protos/digitalio.pb.h" -#include "protos/ds18x20.pb.h" -#include "protos/pixels.pb.h" -#include "protos/signal.pb.h" // External libraries #include "Adafruit_MQTT.h" // MQTT Client @@ -112,7 +105,9 @@ #include "components/ds18x20/controller.h" #include "components/i2c/controller.h" #include "components/pixels/controller.h" +#include "components/pwm/controller.h" #include "components/sensor/model.h" +#include "components/servo/controller.h" // Display #ifdef USE_DISPLAY @@ -153,6 +148,8 @@ class AnalogIOController; class DS18X20Controller; class I2cController; class PixelsController; +class PWMController; +class ServoController; /**************************************************************************/ /*! @@ -255,7 +252,10 @@ class Wippersnapper_V2 { nullptr; ///< Instance of DS18X20 controller I2cController *_i2c_controller = nullptr; ///< Instance of I2C controller PixelsController *_pixels_controller = - nullptr; ///< Instance of Pixels controller + nullptr; ///< Instance of Pixels controller + PWMController *_pwm_controller = nullptr; ///< Instance of PWM controller + ServoController *_servo_controller = + nullptr; ///< Instance of Servo controller // TODO: does this really need to be global? uint8_t _macAddrV2[6]; /*!< Unique network iface identifier */ diff --git a/src/components/ds18x20/model.cpp b/src/components/ds18x20/model.cpp index 0cd8b078a..00ff6a700 100644 --- a/src/components/ds18x20/model.cpp +++ b/src/components/ds18x20/model.cpp @@ -24,7 +24,6 @@ DS18X20Model::DS18X20Model() { memset(&_msg_DS18x20Added, 0, sizeof(_msg_DS18x20Added)); memset(&_msg_DS18x20Remove, 0, sizeof(_msg_DS18x20Remove)); memset(&_msg_DS18x20Event, 0, sizeof(_msg_DS18x20Event)); - // no-op } /***********************************************************************/ diff --git a/src/components/pwm/controller.cpp b/src/components/pwm/controller.cpp new file mode 100644 index 000000000..9b76e0b3d --- /dev/null +++ b/src/components/pwm/controller.cpp @@ -0,0 +1,205 @@ +/*! + * @file src/components/pwm/controller.cpp + * + * Controller for the pwm API + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#include "controller.h" + +/**************************************************************************/ +/*! + @brief Ctor for PWMController. +*/ +/**************************************************************************/ +PWMController::PWMController() { + _pwm_model = new PWMModel(); + _active_pwm_pins = 0; +} + +/**************************************************************************/ +/*! + @brief Dtor for PWMController. +*/ +/**************************************************************************/ +PWMController::~PWMController() { delete _pwm_model; } + +/**************************************************************************/ +/*! + @brief Handles the PWM_Add message. + @param stream The stream containing the message data. + @return True if the message was handled successfully, false otherwise. +*/ +/**************************************************************************/ +bool PWMController::Handle_PWM_Add(pb_istream_t *stream) { + bool did_attach; + if (!_pwm_model->DecodePWMAdd(stream)) { + WS_DEBUG_PRINTLN("[pwm] Failed to decode PWMAdd message!"); + return false; + } + wippersnapper_pwm_PWMAdd msg_add = *_pwm_model->GetPWMAddMsg(); + uint8_t pin = atoi(msg_add.pin + 1); + _pwm_hardware[_active_pwm_pins] = new PWMHardware(); + + WS_DEBUG_PRINT("[pwm] Attaching pin: "); + WS_DEBUG_PRINT(msg_add.pin); + did_attach = _pwm_hardware[_active_pwm_pins]->AttachPin( + pin, (uint32_t)msg_add.frequency, (uint32_t)msg_add.resolution); + if (!did_attach) { + WS_DEBUG_PRINTLN("[pwm] Failed to attach pin!"); + delete _pwm_hardware[_active_pwm_pins]; + } else { + _active_pwm_pins++; + } + + // Publish PixelsAdded message to the broker + if (!_pwm_model->EncodePWMAdded(msg_add.pin, did_attach)) { + WS_DEBUG_PRINTLN("[pwm]: Failed to encode PWMAdded message!"); + return false; + } + if (!WsV2.PublishSignal(wippersnapper_signal_DeviceToBroker_pwm_added_tag, + _pwm_model->GetPWMAddedMsg())) { + WS_DEBUG_PRINTLN("[PWM]: Unable to publish PWMAdded message!"); + return false; + } + WS_DEBUG_PRINTLN("...attached!"); + return true; +} + +/**************************************************************************/ +/*! + @brief Handles the PWM_Remove message. + @param stream The stream containing the message data. + @return True if the message was handled successfully, false otherwise. +*/ +/**************************************************************************/ +bool PWMController::Handle_PWM_Remove(pb_istream_t *stream) { + if (!_pwm_model->DecodePWMRemove(stream)) { + WS_DEBUG_PRINTLN("[pwm] Error: Failed to decode PWMRemove message!"); + return false; + } + wippersnapper_pwm_PWMRemove msg_remove = *_pwm_model->GetPWMRemoveMsg(); + uint8_t pin = atoi(msg_remove.pin + 1); + int pin_idx = GetPWMHardwareIdx(pin); + if (pin_idx == -1) { + WS_DEBUG_PRINTLN("[pwm] Error: pin not found!"); + return false; + } + + // Detach and free the pin for other uses + WS_DEBUG_PRINT("[pwm] Detaching pin: "); + WS_DEBUG_PRINT(msg_remove.pin); + if (_pwm_hardware[pin_idx] != nullptr) { + bool detach_result = _pwm_hardware[pin_idx]->DetachPin(); + if (!detach_result) { + WS_DEBUG_PRINTLN("[pwm] Error: Failed to detach pin."); + } + delete _pwm_hardware[pin_idx]; + _pwm_hardware[pin_idx] = nullptr; + } else { + WS_DEBUG_PRINTLN("[pwm] Error: Pin not attached!"); + } + + // Reorganize _active_pwm_pins + _active_pwm_pins--; + for (int i = pin_idx; i < _active_pwm_pins; i++) { + _pwm_hardware[i] = _pwm_hardware[i + 1]; + } + _pwm_hardware[_active_pwm_pins] = nullptr; + WS_DEBUG_PRINTLN("...detached!"); + return true; +} + +/**************************************************************************/ +/*! + @brief Returns the index of the PWM hardware object that corresponds + to the given pin. + @param pin The pin number to search for. + @return The index of the PWM hardware object, or -1 if not found. +*/ +/**************************************************************************/ +int PWMController::GetPWMHardwareIdx(uint8_t pin) { + for (int i = 0; i < _active_pwm_pins; i++) { + if (_pwm_hardware[i]->GetPin() == pin) { + return i; + } + } + return -1; +} + +/**************************************************************************/ +/*! + @brief Handles the PWM_Write_DutyCycle message. + @param stream The stream containing the message data. + @return True if the message was handled successfully, false otherwise. +*/ +/**************************************************************************/ +bool PWMController::Handle_PWM_Write_DutyCycle(pb_istream_t *stream) { + if (!_pwm_model->DecodePWMWriteDutyCycle(stream)) { + WS_DEBUG_PRINTLN( + "[pwm] Error: Failed to decode PWMWriteDutyCycle message!"); + return false; + } + + wippersnapper_pwm_PWMWriteDutyCycle msg_write_duty_cycle = + *_pwm_model->GetPWMWriteDutyCycleMsg(); + uint8_t pin = atoi(msg_write_duty_cycle.pin + 1); + int pin_idx = GetPWMHardwareIdx(pin); + if (pin_idx == -1) { + WS_DEBUG_PRINTLN("[pwm] Error: pin not found!"); + return false; + } + + // Write the duty cycle to the pin + if (!_pwm_hardware[pin_idx]->WriteDutyCycle( + msg_write_duty_cycle.duty_cycle)) { + WS_DEBUG_PRINTLN("[pwm] Error: Failed to write duty cycle!"); + return false; + } + WS_DEBUG_PRINTLN("[pwm] Wrote duty cycle: "); + WS_DEBUG_PRINT(msg_write_duty_cycle.duty_cycle); + WS_DEBUG_PRINTLN(" to pin: "); + WS_DEBUG_PRINT(msg_write_duty_cycle.pin); + return true; +} + +/**************************************************************************/ +/*! + @brief Handles the PWM_Write_Frequency message. + @param stream The stream containing the message data. + @return True if the message was handled successfully, false otherwise. +*/ +/**************************************************************************/ +bool PWMController::Handle_PWM_Write_Frequency(pb_istream_t *stream) { + if (!_pwm_model->DecodePWMWriteFrequency(stream)) { + WS_DEBUG_PRINTLN( + "[pwm] Error: Failed to decode PWMWriteFrequency message!"); + return false; + } + wippersnapper_pwm_PWMWriteFrequency msg_write_frequency = + *_pwm_model->GetPWMWriteFrequencyMsg(); + uint8_t pin = atoi(msg_write_frequency.pin + 1); + int pin_idx = GetPWMHardwareIdx(pin); + if (pin_idx == -1) { + WS_DEBUG_PRINTLN("[pwm] Error: pin not found!"); + return false; + } + + if (_pwm_hardware[pin_idx]->WriteTone(msg_write_frequency.frequency) != + msg_write_frequency.frequency) { + WS_DEBUG_PRINTLN("[pwm] Error: Failed to write frequency!"); + return false; + } + WS_DEBUG_PRINTLN("[pwm] Wrote frequency: "); + WS_DEBUG_PRINT(msg_write_frequency.frequency); + WS_DEBUG_PRINTLN(" to pin: "); + WS_DEBUG_PRINT(msg_write_frequency.pin); + return true; +} \ No newline at end of file diff --git a/src/components/pwm/controller.h b/src/components/pwm/controller.h new file mode 100644 index 000000000..a81d3d0a8 --- /dev/null +++ b/src/components/pwm/controller.h @@ -0,0 +1,49 @@ +/*! + * @file src/components/pwm/controller.h + * + * Controller for the pwm API + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#ifndef WS_PWM_CONTROLLER_H +#define WS_PWM_CONTROLLER_H +#include "Wippersnapper_V2.h" +#include "hardware.h" +#include "model.h" +#define MAX_PWM_PINS 25 ///< Maximum number of PWM pins supported + +class Wippersnapper_V2; // Forward declaration +class PWMModel; // Forward declaration +class PWMHardware; // Forward declaration + +/**************************************************************************/ +/*! + @brief Routes messages using the pwm.proto API to the + appropriate hardware and model classes, controls and tracks + the state of the hardware's PWM pins. +*/ +/**************************************************************************/ +class PWMController { +public: + PWMController(); + ~PWMController(); + bool Handle_PWM_Add(pb_istream_t *stream); + bool Handle_PWM_Write_DutyCycle(pb_istream_t *stream); + bool Handle_PWM_Write_Frequency(pb_istream_t *stream); + bool Handle_PWM_Remove(pb_istream_t *stream); + int GetPWMHardwareIdx(uint8_t pin); + +private: + PWMModel *_pwm_model; + PWMHardware *_pwm_hardware[MAX_PWM_PINS] = {nullptr}; + int _active_pwm_pins; +}; +extern Wippersnapper_V2 WsV2; ///< Wippersnapper V2 instance +#endif // WS_PWM_CONTROLLER_H \ No newline at end of file diff --git a/src/components/pwm/hardware.cpp b/src/components/pwm/hardware.cpp new file mode 100644 index 000000000..bb03f9379 --- /dev/null +++ b/src/components/pwm/hardware.cpp @@ -0,0 +1,171 @@ +/*! + * @file src/components/pwm/hardware.cpp + * + * Hardware for the pwm.proto message. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#include "hardware.h" + +/**************************************************************************/ +/*! + @brief Ctor for PWMHardware +*/ +/**************************************************************************/ +PWMHardware::PWMHardware() { _is_attached = false; } + +/**************************************************************************/ +/*! + @brief Dtor for PWMHardware +*/ +/**************************************************************************/ +PWMHardware::~PWMHardware() { + if (_is_attached) + DetachPin(); +} + +/**************************************************************************/ +/*! + @brief Attach a pin to the PWM hardware + @param pin The pin to attach + @param frequency The frequency of the PWM signal + @param resolution The resolution of the PWM signal + @return true if the pin was successfully attached, false otherwise +*/ +/**************************************************************************/ +bool PWMHardware::AttachPin(uint8_t pin, uint32_t frequency, + uint32_t resolution) { +#ifdef ARDUINO_ARCH_ESP32 + _is_attached = ledcAttach(pin, frequency, resolution); +#else + _is_attached = true; +#endif + + if (_is_attached) { + _pin = pin; + _frequency = frequency; + _resolution = resolution; + _duty_cycle = 0; + } + + return _is_attached; +} + +/**************************************************************************/ +/*! + @brief Detaches a PWM pin and frees it for use. + @return true if the PWM pin was successfully detached, false otherwise. +*/ +/**************************************************************************/ +bool PWMHardware::DetachPin() { + if (!_is_attached) { + WS_DEBUG_PRINTLN("[pwm] Pin not attached!"); + return false; + } + bool did_detach = false; +#ifdef ARDUINO_ARCH_ESP32 + did_detach = ledcDetach(_pin); +#else + digitalWrite(_pin, LOW); // "Disable" the pin's output + did_detach = true; +#endif + + _is_attached = false; // always mark as false, for tracking + return did_detach; +} + +/**************************************************************************/ +/*! + @brief Writes a duty cycle to a PWM pin with a fixed frequency + of 5kHz and 8-bit resolution. + @param duty The desired duty cycle to write to the pin. + @return true if the duty cycle was successfully written, false otherwise +*/ +/**************************************************************************/ +bool PWMHardware::WriteDutyCycle(uint32_t duty) { + if (!_is_attached) { + WS_DEBUG_PRINTLN("[pwm] Pin not attached!"); + return false; + } + bool did_write = false; +#if defined(ARDUINO_ESP8266_ADAFRUIT_HUZZAH) && defined(STATUS_LED_PIN) + // Adafruit Feather ESP8266's analogWrite() gets inverted since the builtin + // LED is reverse-wired + _duty_cycle = 255 - duty; +#else + _duty_cycle = duty; +#endif + +#ifdef ARDUINO_ARCH_ESP32 + did_write = analogWrite(_duty_cycle); +#else + analogWrite(_pin, duty); + did_write = true; +#endif + + return true; +} + +/**************************************************************************/ +/*! + @brief Writes a frequency to a PWM pin with a fixed duty cycle. + @param freq The desired frequency to write to the pin. + @return The frequency that was written to the pin. +*/ +/**************************************************************************/ +uint32_t PWMHardware::WriteTone(uint32_t freq) { + if (!_is_attached) { + WS_DEBUG_PRINTLN("[pwm] Pin not attached!"); + return false; + } + + uint32_t rc = 0; +#ifdef ARDUINO_ARCH_ESP32 + rc = ledcWriteTone(_pin, freq); +#else + tone(_pin, freq); + rc = freq; +#endif + + return rc; +} + +/**************************************************************************/ +/*! + @brief Returns the pin number of the PWM pin + @return The logical pin number of the PWM pin +*/ +/**************************************************************************/ +uint8_t PWMHardware::GetPin() { return _pin; } + +// LEDC API Wrappers +#ifdef ARDUINO_ARCH_ESP32 +/**************************************************************************/ +/*! + @brief Mocks the Arduino analogWrite() function for the Arduino-ESP32 + LEDC API + @param value The value to write (0-255) + @return true if the value was successfully written, false otherwise +*/ +/**************************************************************************/ +bool PWMHardware::analogWrite(uint32_t value) { + // clamp + if (value > 255 || value < 0) { + WS_DEBUG_PRINTLN("[pwm] Value out of range!"); + return false; + } + + // Calculate duty cycle for the `value` + uint32_t dutyCycle = + (uint32_t)(((double)_resolution / 255.0) * min(value, (uint32_t)255)); + return ledcWrite(_pin, dutyCycle); +} + +#endif // ARDUINO_ARCH_ESP32 \ No newline at end of file diff --git a/src/components/pwm/hardware.h b/src/components/pwm/hardware.h new file mode 100644 index 000000000..f4a743e36 --- /dev/null +++ b/src/components/pwm/hardware.h @@ -0,0 +1,49 @@ +/*! + * @file src/components/pwm/hardware.h + * + * Hardware for the pwm.proto message. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#ifndef WS_PWM_HARDWARE_H +#define WS_PWM_HARDWARE_H +#include "Wippersnapper_V2.h" +#ifdef ARDUINO_ARCH_ESP32 +#include "esp32-hal-ledc.h" +#include "esp_err.h" +#endif + +/**************************************************************************/ +/*! + @brief Interface for interacting with hardware's PWM API. +*/ +/**************************************************************************/ +class PWMHardware { +public: + PWMHardware(); + ~PWMHardware(); + bool AttachPin(uint8_t pin, uint32_t frequency, uint32_t resolution); + bool DetachPin(); + bool WriteDutyCycle(uint32_t duty); + uint32_t WriteTone(uint32_t freq); + uint8_t GetPin(); + +// Abstractions for LEDC API +#ifdef ARDUINO_ARCH_ESP32 + bool analogWrite(uint32_t value); +#endif +private: + bool _is_attached; + uint8_t _pin; + uint32_t _frequency; + uint32_t _resolution; + uint32_t _duty_cycle; +}; +#endif // WS_PWM_HARDWARE_H diff --git a/src/components/pwm/model.cpp b/src/components/pwm/model.cpp new file mode 100644 index 000000000..aff8e2bd3 --- /dev/null +++ b/src/components/pwm/model.cpp @@ -0,0 +1,166 @@ +/*! + * @file src/components/pwm/model.cpp + * + * Model for the pwm.proto message. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#include "model.h" + +/**************************************************************************/ +/*! + @brief Ctor for PWMModel. +*/ +/**************************************************************************/ +PWMModel::PWMModel() { + memset(&_msg_pwm_add, 0, sizeof(_msg_pwm_add)); + memset(&_msg_pwm_added, 0, sizeof(_msg_pwm_added)); + memset(&_msg_pwm_remove, 0, sizeof(_msg_pwm_remove)); + memset(&_msg_pwm_write_duty_cycle, 0, sizeof(_msg_pwm_write_duty_cycle)); + memset(&_msg_pwm_write_frequency, 0, sizeof(_msg_pwm_write_frequency)); +} + +/**************************************************************************/ +/*! + @brief Dtor for PWMModel. +*/ +/**************************************************************************/ +PWMModel::~PWMModel() { + memset(&_msg_pwm_add, 0, sizeof(_msg_pwm_add)); + memset(&_msg_pwm_added, 0, sizeof(_msg_pwm_added)); + memset(&_msg_pwm_remove, 0, sizeof(_msg_pwm_remove)); + memset(&_msg_pwm_write_duty_cycle, 0, sizeof(_msg_pwm_write_duty_cycle)); + memset(&_msg_pwm_write_frequency, 0, sizeof(_msg_pwm_write_frequency)); +} + +/**************************************************************************/ +/*! + @brief Decodes a PWMAdd message from an input stream. + @param stream The stream to decode from. + @return true if successful, false otherwise. +*/ +/**************************************************************************/ +bool PWMModel::DecodePWMAdd(pb_istream_t *stream) { + memset(&_msg_pwm_add, 0, sizeof(_msg_pwm_add)); + return pb_decode(stream, wippersnapper_pwm_PWMAdd_fields, &_msg_pwm_add); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the PWMAdd message. + @return Pointer to the PWMAdd message. +*/ +/**************************************************************************/ +wippersnapper_pwm_PWMAdd *PWMModel::GetPWMAddMsg() { return &_msg_pwm_add; } + +/**************************************************************************/ +/*! + @brief Encodes a PWMAdded message with the given pin name and attach + status. + @param pin_name The name of the pin. + @param did_attach True if the pin was successfully attached, false + otherwise. + @return true if successful, false otherwise. +*/ +/**************************************************************************/ +bool PWMModel::EncodePWMAdded(char *pin_name, bool did_attach) { + // Fill the message + memset(&_msg_pwm_added, 0, sizeof(_msg_pwm_added)); + _msg_pwm_added.did_attach = did_attach; + strncpy(_msg_pwm_added.pin, pin_name, sizeof(_msg_pwm_added.pin)); + // Encode it! + size_t sz_msg; + if (!pb_get_encoded_size(&sz_msg, wippersnapper_pwm_PWMAdded_fields, + &_msg_pwm_added)) + return false; + uint8_t buf[sz_msg]; + pb_ostream_t msg_stream = pb_ostream_from_buffer(buf, sizeof(buf)); + return pb_encode(&msg_stream, wippersnapper_pwm_PWMAdded_fields, + &_msg_pwm_added); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the PWMAdded message. + @return Pointer to the PWMAdded message. +*/ +/**************************************************************************/ +wippersnapper_pwm_PWMAdded *PWMModel::GetPWMAddedMsg() { + return &_msg_pwm_added; +} + +/**************************************************************************/ +/*! + @brief Decodes a PWMRemove message from an input stream. + @param stream The stream to decode from. + @return true if successful, false otherwise. +*/ +/**************************************************************************/ +bool PWMModel::DecodePWMRemove(pb_istream_t *stream) { + memset(&_msg_pwm_remove, 0, sizeof(_msg_pwm_remove)); + return pb_decode(stream, wippersnapper_pwm_PWMRemove_fields, + &_msg_pwm_remove); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the PWMRemove message. + @return Pointer to the PWMRemove message. +*/ +/**************************************************************************/ +wippersnapper_pwm_PWMRemove *PWMModel::GetPWMRemoveMsg() { + return &_msg_pwm_remove; +} + +/**************************************************************************/ +/*! + @brief Decodes a PWMWriteDutyCycle message from an input stream. + @param stream The stream to decode from. + @return true if successful, false otherwise. +*/ +/**************************************************************************/ +bool PWMModel::DecodePWMWriteDutyCycle(pb_istream_t *stream) { + memset(&_msg_pwm_write_duty_cycle, 0, sizeof(_msg_pwm_write_duty_cycle)); + return pb_decode(stream, wippersnapper_pwm_PWMWriteDutyCycle_fields, + &_msg_pwm_write_duty_cycle); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the PWMWriteDutyCycle message. + @return Pointer to the PWMWriteDutyCycle message. +*/ +/**************************************************************************/ +wippersnapper_pwm_PWMWriteDutyCycle *PWMModel::GetPWMWriteDutyCycleMsg() { + return &_msg_pwm_write_duty_cycle; +} + +/**************************************************************************/ +/*! + @brief Decodes a PWMWriteFrequency message from an input stream. + @param stream The stream to decode from. + @return true if successful, false otherwise. +*/ +/**************************************************************************/ +bool PWMModel::DecodePWMWriteFrequency(pb_istream_t *stream) { + memset(&_msg_pwm_write_frequency, 0, sizeof(_msg_pwm_write_frequency)); + return pb_decode(stream, wippersnapper_pwm_PWMWriteFrequency_fields, + &_msg_pwm_write_frequency); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the PWMWriteFrequency message. + @return Pointer to the PWMWriteFrequency message. +*/ +/**************************************************************************/ +wippersnapper_pwm_PWMWriteFrequency *PWMModel::GetPWMWriteFrequencyMsg() { + return &_msg_pwm_write_frequency; +} diff --git a/src/components/pwm/model.h b/src/components/pwm/model.h new file mode 100644 index 000000000..89bcd8a91 --- /dev/null +++ b/src/components/pwm/model.h @@ -0,0 +1,49 @@ +/*! + * @file src/components/pwm/model.h + * + * Model for the pwm.proto message. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#ifndef WS_PWM_MODEL_H +#define WS_PWM_MODEL_H +#include "Wippersnapper_V2.h" + +/**************************************************************************/ +/*! + @brief Provides an interface for creating, encoding, and parsing + messages from pwm.proto. +*/ +/**************************************************************************/ +class PWMModel { +public: + PWMModel(); + ~PWMModel(); + bool DecodePWMAdd(pb_istream_t *stream); + wippersnapper_pwm_PWMAdd *GetPWMAddMsg(); + bool EncodePWMAdded(char *pin_name, bool did_attach); + wippersnapper_pwm_PWMAdded *GetPWMAddedMsg(); + bool DecodePWMRemove(pb_istream_t *stream); + wippersnapper_pwm_PWMRemove *GetPWMRemoveMsg(); + bool DecodePWMWriteDutyCycle(pb_istream_t *stream); + wippersnapper_pwm_PWMWriteDutyCycle *GetPWMWriteDutyCycleMsg(); + bool DecodePWMWriteFrequency(pb_istream_t *stream); + wippersnapper_pwm_PWMWriteFrequency *GetPWMWriteFrequencyMsg(); + +private: + wippersnapper_pwm_PWMAdd _msg_pwm_add; ///< PWMAdd message object + wippersnapper_pwm_PWMAdded _msg_pwm_added; ///< PWMAdded message object + wippersnapper_pwm_PWMRemove _msg_pwm_remove; ///< PWMRemove message object + wippersnapper_pwm_PWMWriteDutyCycle + _msg_pwm_write_duty_cycle; ///< PWMWriteDutyCycle message object + wippersnapper_pwm_PWMWriteFrequency + _msg_pwm_write_frequency; ///< PWMWriteFrequency message object +}; +#endif // WS_PWM_MODEL_H \ No newline at end of file diff --git a/src/components/servo/controller.cpp b/src/components/servo/controller.cpp new file mode 100644 index 000000000..fd49833d5 --- /dev/null +++ b/src/components/servo/controller.cpp @@ -0,0 +1,195 @@ +/*! + * @file src/components/servo/controller.cpp + * + * Controller for the servo API + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#include "controller.h" + +/**************************************************************************/ +/*! + @brief Constructor +*/ +/**************************************************************************/ +ServoController::ServoController() { + _servo_model = new ServoModel(); + _active_servo_pins = 0; +} + +/**************************************************************************/ +/*! + @brief Destructor +*/ +/**************************************************************************/ +ServoController::~ServoController() { + // De-initialize all servos + for (int i = 0; i < _active_servo_pins; i++) { + if (_servo_hardware[i] != nullptr) { + delete _servo_hardware[i]; + _servo_hardware[i] = nullptr; + } + } + + if (_servo_model != nullptr) { + delete _servo_model; + _servo_model = nullptr; + } +} + +/**************************************************************************/ +/*! + @brief Publishes a ServoAdded message to the broker + @param servo_pin + Pin number of the servo + @param did_attach + True if the servo was attached successfully, False otherwise + @returns True if successful, False otherwise +*/ +/**************************************************************************/ +bool ServoController::PublishServoAddedMsg( + const char *servo_pin, bool did_attach, + wippersnapper_servo_ServoAdd *msg_add) { + _servo_model->EncodeServoAdded(msg_add->servo_pin, did_attach); + if (!WsV2.PublishSignal(wippersnapper_signal_DeviceToBroker_servo_added_tag, + _servo_model->GetServoAddedMsg())) { + WS_DEBUG_PRINTLN("[servo] Error: Failed publishing a ServoAdded message!"); + return false; + } + return true; +} + +/**************************************************************************/ +/*! + @brief Handles a ServoAdd message + @param stream + pb_istream_t to decode from + @returns True if successful, False otherwise +*/ +/**************************************************************************/ +bool ServoController::Handle_Servo_Add(pb_istream_t *stream) { + bool did_attach; + if (_active_servo_pins >= MAX_SERVOS) { + WS_DEBUG_PRINTLN("[servo] Error: Maximum number of servos reached!"); + PublishServoAddedMsg("PIN_UNKNOWN", false, _servo_model->GetServoAddMsg()); + return false; + } + + if (!_servo_model->DecodeServoAdd(stream)) { + WS_DEBUG_PRINTLN("[servo] Error: Failed to decode ServoAdd message!"); + PublishServoAddedMsg("PIN_UNKNOWN", false, _servo_model->GetServoAddMsg()); + return false; + } + + wippersnapper_servo_ServoAdd *msg_add = _servo_model->GetServoAddMsg(); + uint8_t pin = atoi(msg_add->servo_pin + 1); + _servo_hardware[_active_servo_pins] = new ServoHardware( + pin, (int)msg_add->min_pulse_width, (int)msg_add->max_pulse_width, + (int)msg_add->servo_freq); + // Attempt to attach the servo to the pin + did_attach = _servo_hardware[_active_servo_pins]->ServoAttach(); + + // Write the pulse width to the servo + if (did_attach) { + _servo_hardware[_active_servo_pins]->ServoWrite( + (int)msg_add->min_pulse_width); + WS_DEBUG_PRINT("[servo] Servo attached to pin: "); + WS_DEBUG_PRINTLN(msg_add->servo_pin); + _active_servo_pins++; + } else { + WS_DEBUG_PRINT("[servo] Error: Failed to attach servo to pin !"); + WS_DEBUG_PRINT(msg_add->servo_pin); + delete _servo_hardware[_active_servo_pins]; + _servo_hardware[_active_servo_pins] = nullptr; + } + + // Publish ServoAdded message to IO + if (!PublishServoAddedMsg("", false, _servo_model->GetServoAddMsg())) + return false; + return true; +} + +/**************************************************************************/ +/*! + @brief Handles a ServoWrite message + @param stream + pb_istream_t to decode from + @returns True if successful, False otherwise +*/ +/**************************************************************************/ +bool ServoController::Handle_Servo_Write(pb_istream_t *stream) { + if (!_servo_model->DecodeServoWrite(stream)) { + WS_DEBUG_PRINTLN("[servo] Error: Failed to decode ServoWrite message!"); + return false; + } + wippersnapper_servo_ServoWrite *msg_write = _servo_model->GetServoWriteMsg(); + uint8_t pin = atoi(msg_write->servo_pin + 1); + int servo_idx = GetServoIndex(pin); + if (servo_idx == -1) { + WS_DEBUG_PRINTLN("[servo] Error: Servo pin not found!"); + return false; + } + // Write the pulse width to the servo + _servo_hardware[servo_idx]->ServoWrite(msg_write->pulse_width); + return true; +} + +/**************************************************************************/ +/*! + @brief Handles a ServoRemove message + @param stream + pb_istream_t to decode from + @returns True if successful, False otherwise +*/ +/**************************************************************************/ +bool ServoController::Handle_Servo_Remove(pb_istream_t *stream) { + if (_active_servo_pins <= 0) { + WS_DEBUG_PRINTLN("[servo] Error: No active servos!"); + return false; + } + + if (!_servo_model->DecodeServoRemove(stream)) { + WS_DEBUG_PRINTLN("[servo] Error: Failed to decode ServoRemove message!"); + return false; + } + wippersnapper_servo_ServoRemove *msg_remove = + _servo_model->GetServoRemoveMsg(); + uint8_t pin = atoi(msg_remove->servo_pin + 1); + int servo_idx = GetServoIndex(pin); + if (servo_idx == -1) { + WS_DEBUG_PRINTLN("[servo] Error: Servo pin not found!"); + return false; + } + + // The destructor of ServoHardware will handle proper detachment + delete _servo_hardware[servo_idx]; + _servo_hardware[servo_idx] = nullptr; + + // Shift _active_servo_pins down + for (int i = servo_idx; i < _active_servo_pins - 1; i++) { + _servo_hardware[i] = _servo_hardware[i + 1]; + _servo_hardware[i + 1] = nullptr; + } + _servo_hardware[_active_servo_pins - 1] = nullptr; + _active_servo_pins--; + + WS_DEBUG_PRINT("[servo] Servo removed from pin: "); + WS_DEBUG_PRINTLN(msg_remove->servo_pin); + return true; +} + +int ServoController::GetServoIndex(uint8_t pin) { + for (int i = 0; i < _active_servo_pins; i++) { + if (_servo_hardware[i]->GetPin() == pin) { + return i; + } + } + return -1; +} \ No newline at end of file diff --git a/src/components/servo/controller.h b/src/components/servo/controller.h new file mode 100644 index 000000000..f4bea36e6 --- /dev/null +++ b/src/components/servo/controller.h @@ -0,0 +1,57 @@ +/*! + * @file src/components/servo/controller.h + * + * Controller for the servo API + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#ifndef WS_SERVO_CONTROLLER_H +#define WS_SERVO_CONTROLLER_H +#include "Wippersnapper_V2.h" +#include "hardware.h" +#include "model.h" + +#ifdef ARDUINO_ARCH_RP2040 +#define MAX_SERVOS \ + 8 ///< Maximum number of servo objects for RP2040, + ///< https://arduino-pico.readthedocs.io/en/latest/servo.html +#else +#define MAX_SERVOS 16 ///< Maximum number of servo objects +#endif + +class Wippersnapper_V2; // Forward declaration +class ServoModel; // Forward declaration +class ServoHardware; // Forward declaration + +/**************************************************************************/ +/*! + @brief Routes messages using the servo.proto API to the + appropriate hardware and model classes, controls and tracks + the state of the hardware's Servo pins. +*/ +/**************************************************************************/ +class ServoController { +public: + ServoController(); + ~ServoController(); + bool Handle_Servo_Add(pb_istream_t *stream); + bool Handle_Servo_Write(pb_istream_t *stream); + bool Handle_Servo_Remove(pb_istream_t *stream); + +private: + int GetServoIndex(uint8_t pin); + bool PublishServoAddedMsg(const char *servo_pin, bool did_attach, + wippersnapper_servo_ServoAdd *msg_add); + ServoModel *_servo_model; + ServoHardware *_servo_hardware[MAX_SERVOS] = {nullptr}; + int _active_servo_pins; ///< Number of active servo pins +}; +extern Wippersnapper_V2 WsV2; ///< Wippersnapper V2 instance +#endif // WS_SERVO_CONTROLLER_H \ No newline at end of file diff --git a/src/components/servo/hardware.cpp b/src/components/servo/hardware.cpp new file mode 100644 index 000000000..ef234beb0 --- /dev/null +++ b/src/components/servo/hardware.cpp @@ -0,0 +1,218 @@ +/*! + * @file src/components/servo/hardware.cpp + * + * Hardware for the servo.proto message. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#include "hardware.h" + +/**************************************************************************/ +/*! + @brief Constructs a ServoHardware object + @param pin + The GPIO pin to attach the servo to + @param min_pulse_width + The minimum pulse width, in microseconds + @param max_pulse_width + The maximum pulse width, in microseconds + @param frequency + The frequency of the PWM signal, in Hz (50Hz is sent from IO) +*/ +/**************************************************************************/ +ServoHardware::ServoHardware(int pin, int min_pulse_width, int max_pulse_width, + int frequency) { + _pin = pin; + _min_pulse_width = min_pulse_width; + _max_pulse_width = max_pulse_width; + _frequency = frequency; + +#ifndef ARDUINO_ARCH_ESP32 + _servo = new Servo(); +#else + _is_attached = false; +#endif +} + +/**************************************************************************/ +/*! + @brief Detaches the servo from the pin and frees the pin for + other uses. +*/ +/**************************************************************************/ +ServoHardware::~ServoHardware() { + if (!ServoDetach()) { + WS_DEBUG_PRINTLN("[servo] Error: Failed to detach servo!"); + } +} + +/**************************************************************************/ +/*! + @brief Detaches the servo from the pin and frees the pin for + other uses. + @returns true if successful, false otherwise +*/ +/**************************************************************************/ +bool ServoHardware::ServoDetach() { +#ifdef ARDUINO_ARCH_ESP32 + if (!attached()) { + WS_DEBUG_PRINTLN("[servo] Detach Error: Servo not attached!"); + return false; + } + detach(); +#else + if (_servo == nullptr || !_servo->attached()) { + WS_DEBUG_PRINTLN("[servo] Detach Error: Servo not attached!"); + return false; + } + _servo->detach(); + delete _servo; + _servo = nullptr; +#endif + return true; +} + +/**************************************************************************/ +/*! + @brief Attempts to attach a servo to a GPIO pin. + @returns true if successful, false otherwise. +*/ +/**************************************************************************/ +bool ServoHardware::ServoAttach() { + uint16_t rc; + +// Attach the servo to the pin +#ifdef ARDUINO_ARCH_ESP32 + if (!ledcAttach(_pin, _frequency, LEDC_TIMER_WIDTH)) { + rc = ERROR_SERVO_ATTACH; + } else { + WS_DEBUG_PRINTLN("[servo:hw:L99] Servo attached to pin"); + rc = 1; + _is_attached = true; + } +#else + if (_servo == nullptr) { + WS_DEBUG_PRINTLN("[servo] Attach Error: Servo not initialized!"); + return false; + } + rc = _servo->attach(_pin, _min_pulse_width, _max_pulse_width); +#endif + + if (rc == ERROR_SERVO_ATTACH) { + WS_DEBUG_PRINT("[servo] Error: Failed to attach servo to pin: "); + WS_DEBUG_PRINTLN(_pin); + return false; + } + + return true; +} + +/**************************************************************************/ +/*! + @brief Returns the logical pin number of the servo + @returns The logical pin number of the servo +*/ +/**************************************************************************/ +uint8_t ServoHardware::GetPin() { return _pin; } + +/**************************************************************************/ +/*! + @brief Clamps the pulse width to the min/max range + @param value + The value to clamp + @returns The clamped value +*/ +/**************************************************************************/ +int ServoHardware::ClampPulseWidth(int value) { + if (value < _min_pulse_width) { + value = _min_pulse_width; + } + if (value > _max_pulse_width) { + value = _max_pulse_width; + } + return value; +} + +/**************************************************************************/ +/*! + @brief Writes a value to the servo pin + @param value + The value to write to the servo pin +*/ +/**************************************************************************/ +void ServoHardware::ServoWrite(int value) { +#ifdef ARDUINO_ARCH_ESP32 + if (!attached()) { + WS_DEBUG_PRINTLN("[servo] Error: Servo not attached!"); + return; + } + writeMicroseconds(value); +#else + if (_servo == nullptr || !_servo->attached()) { + WS_DEBUG_PRINTLN("[servo] Error: Servo not attached!"); + return; + } + value = ClampPulseWidth(value); + _servo->writeMicroseconds(value); + WS_DEBUG_PRINT("[servo] Set Pulse Width: "); + WS_DEBUG_PRINT(value); + WS_DEBUG_PRINT(" µs on pin: "); + WS_DEBUG_PRINT(_pin); +#endif +} + +#ifdef ARDUINO_ARCH_ESP32 +/**************************************************************************/ +/*! + @brief Mocks writeMicroseconds() call in arduino/servo api for + ESP32x's LEDC manager. + @param value + The value to write to the servo pin. +*/ +/**************************************************************************/ +void ServoHardware::writeMicroseconds(int value) { + // Clamp the value to the min/max range + value = ClampPulseWidth(value); + + // Formula from ESP32Servo library + // https://github.com/madhephaestus/ESP32Servo/blob/master/src/ESP32Servo.cpp + // count = (pulse_high_width / (pulse_period/2**timer_width)) + // 50Hz servo = 20ms pulse_period + uint32_t count = + ((double)value / ((double)20000 / (double)pow(2, LEDC_TIMER_WIDTH))); + if (!ledcWrite(_pin, count)) + WS_DEBUG_PRINTLN("[servo] Error: Failed to write to servo pin!"); + + WS_DEBUG_PRINT("[servo] Set Pulse Width: "); + WS_DEBUG_PRINT(value); + WS_DEBUG_PRINT(" uS on pin: "); + WS_DEBUG_PRINT(_pin); +} + +/**************************************************************************/ +/*! + @brief Detaches the servo from the LEDC manager and frees the pin for + other uses. +*/ +/**************************************************************************/ +void ServoHardware::detach() { + if (!attached()) + return; + _is_attached = ledcDetach(_pin); +} + +/**************************************************************************/ +/*! + @brief Returns true if the servo is attached to the pin + @returns true if the servo is attached to the pin, false otherwise +*/ +/**************************************************************************/ +bool ServoHardware::attached() { return _is_attached; } +#endif \ No newline at end of file diff --git a/src/components/servo/hardware.h b/src/components/servo/hardware.h new file mode 100644 index 000000000..ae33142f0 --- /dev/null +++ b/src/components/servo/hardware.h @@ -0,0 +1,64 @@ +/*! + * @file src/components/servo/hardware.h + * + * Hardware for the servo.proto message. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#ifndef WS_SERVO_HARDWARE_H +#define WS_SERVO_HARDWARE_H +#include "Wippersnapper_V2.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include "esp32-hal-ledc.h" +#include "esp_err.h" +#define LEDC_TIMER_WIDTH \ + SOC_LEDC_TIMER_BIT_WIDTH ///< Dynamically scale bit width for each ESP32-x + ///< Arch. +#else +#include +#endif + +#define ERROR_SERVO_ATTACH 255 ///< Error code for servo attach failure + +/**************************************************************************/ +/*! + @brief Interface for interacting with hardware's Servo API. +*/ +/**************************************************************************/ +class ServoHardware { +public: + ServoHardware(int pin, int min_pulse_width, int max_pulse_width, + int frequency); + ~ServoHardware(); + bool ServoAttach(); + void ServoWrite(int value); + uint8_t GetPin(); + +private: + bool ServoDetach(); + int ClampPulseWidth(int value); +#ifdef ARDUINO_ARCH_ESP32 + // Mocks Servo library API for ESP32x's LEDC manager + // https://github.com/arduino-libraries/Servo/blob/master/src/Servo.h + bool attached(); + void detach(); + void writeMicroseconds(int value); + bool _is_attached; +#endif +#ifndef ARDUINO_ARCH_ESP32 + Servo *_servo = nullptr; +#endif + uint8_t _pin; + int _max_pulse_width; + int _min_pulse_width; + int _frequency; +}; +#endif // WS_SERVO_HARDWARE_H \ No newline at end of file diff --git a/src/components/servo/model.cpp b/src/components/servo/model.cpp new file mode 100644 index 000000000..0645219ff --- /dev/null +++ b/src/components/servo/model.cpp @@ -0,0 +1,149 @@ +/*! + * @file src/components/servo/model.cpp + * + * Model for the servo.proto message. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#include "model.h" + +/**************************************************************************/ +/*! + @brief Constructor +*/ +/**************************************************************************/ +ServoModel::ServoModel() { + memset(&_msg_servo_add, 0, sizeof(_msg_servo_add)); + memset(&_msg_servo_added, 0, sizeof(_msg_servo_added)); + memset(&_msg_servo_remove, 0, sizeof(_msg_servo_remove)); + memset(&_msg_servo_write, 0, sizeof(_msg_servo_write)); +} + +/**************************************************************************/ +/*! + @brief Destructor +*/ +/**************************************************************************/ +ServoModel::~ServoModel() { + memset(&_msg_servo_add, 0, sizeof(_msg_servo_add)); + memset(&_msg_servo_added, 0, sizeof(_msg_servo_added)); + memset(&_msg_servo_remove, 0, sizeof(_msg_servo_remove)); + memset(&_msg_servo_write, 0, sizeof(_msg_servo_write)); +} + +/**************************************************************************/ +/*! + @brief Decodes a ServoAdd message from a pb_istream_t + @param stream + pb_istream_t to decode from + @returns True if successful, False otherwise +*/ +/**************************************************************************/ +bool ServoModel::DecodeServoAdd(pb_istream_t *stream) { + memset(&_msg_servo_add, 0, sizeof(_msg_servo_add)); + return pb_decode(stream, wippersnapper_servo_ServoAdd_fields, + &_msg_servo_add); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the ServoAdd message + @returns Pointer to ServoAdd message +*/ +/**************************************************************************/ +wippersnapper_servo_ServoAdd *ServoModel::GetServoAddMsg() { + return &_msg_servo_add; +} + +/**************************************************************************/ +/*! + @brief Encodes a ServoAdded message + @param pin_name + Name of the pin + @param did_attach + True if a servo was attached to the pin successfully, + False otherwise + @returns True if successful, False otherwise +*/ +/**************************************************************************/ +bool ServoModel::EncodeServoAdded(char *pin_name, bool did_attach) { + // Fill the message + memset(&_msg_servo_added, 0, sizeof(_msg_servo_added)); + _msg_servo_added.attach_success = did_attach; + strncpy(_msg_servo_added.servo_pin, pin_name, + sizeof(_msg_servo_added.servo_pin) - 1); + // Encode it! + size_t sz_msg; + if (!pb_get_encoded_size(&sz_msg, wippersnapper_servo_ServoAdded_fields, + &_msg_servo_added)) + return false; + uint8_t buf[sz_msg]; + pb_ostream_t msg_stream = pb_ostream_from_buffer(buf, sizeof(buf)); + return pb_encode(&msg_stream, wippersnapper_servo_ServoAdded_fields, + &_msg_servo_added); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the ServoAdded message + @returns Pointer to ServoAdded message +*/ +/**************************************************************************/ +wippersnapper_servo_ServoAdded *ServoModel::GetServoAddedMsg() { + return &_msg_servo_added; +} + +/**************************************************************************/ +/*! + @brief Decodes a ServoRemove message from a pb_istream_t + @param stream + pb_istream_t to decode from + @returns True if successful, False otherwise +*/ +/**************************************************************************/ +bool ServoModel::DecodeServoRemove(pb_istream_t *stream) { + memset(&_msg_servo_remove, 0, sizeof(_msg_servo_remove)); + return pb_decode(stream, wippersnapper_servo_ServoRemove_fields, + &_msg_servo_remove); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the ServoRemove message + @returns Pointer to ServoRemove message +*/ +/**************************************************************************/ +wippersnapper_servo_ServoRemove *ServoModel::GetServoRemoveMsg() { + return &_msg_servo_remove; +} + +/**************************************************************************/ +/*! + @brief Decodes a ServoWrite message from a pb_istream_t + @param stream + pb_istream_t to decode from + @returns True if successful, False otherwise +*/ +/**************************************************************************/ +bool ServoModel::DecodeServoWrite(pb_istream_t *stream) { + memset(&_msg_servo_write, 0, sizeof(_msg_servo_write)); + return pb_decode(stream, wippersnapper_servo_ServoWrite_fields, + &_msg_servo_write); +} + +/**************************************************************************/ +/*! + @brief Returns a pointer to the ServoWrite message + @returns Pointer to ServoWrite message +*/ +/**************************************************************************/ +wippersnapper_servo_ServoWrite *ServoModel::GetServoWriteMsg() { + return &_msg_servo_write; +} \ No newline at end of file diff --git a/src/components/servo/model.h b/src/components/servo/model.h new file mode 100644 index 000000000..c0bdd27cc --- /dev/null +++ b/src/components/servo/model.h @@ -0,0 +1,47 @@ +/*! + * @file src/components/servo/model.h + * + * Model for the servo.proto message. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Copyright (c) Brent Rubell 2025 for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * + */ +#ifndef WS_SERVO_MODEL_H +#define WS_SERVO_MODEL_H +#include "Wippersnapper_V2.h" + +/**************************************************************************/ +/*! + @brief Provides an interface for creating, encoding, and parsing + messages from servo.proto. +*/ +/**************************************************************************/ +class ServoModel { +public: + ServoModel(); + ~ServoModel(); + bool DecodeServoAdd(pb_istream_t *stream); + wippersnapper_servo_ServoAdd *GetServoAddMsg(); + bool EncodeServoAdded(char *pin_name, bool did_attach); + wippersnapper_servo_ServoAdded *GetServoAddedMsg(); + bool DecodeServoRemove(pb_istream_t *stream); + wippersnapper_servo_ServoRemove *GetServoRemoveMsg(); + bool DecodeServoWrite(pb_istream_t *stream); + wippersnapper_servo_ServoWrite *GetServoWriteMsg(); + +private: + wippersnapper_servo_ServoAdd _msg_servo_add; ///< ServoAdd message object + wippersnapper_servo_ServoAdded + _msg_servo_added; ///< ServoAdded message object + wippersnapper_servo_ServoRemove + _msg_servo_remove; ///< ServoRemove message object + wippersnapper_servo_ServoWrite + _msg_servo_write; ///< ServoWrite message object +}; +#endif // WS_SERVO_MODEL_H \ No newline at end of file