slopping up a platformio library for people to make rover peripherals

This commit is contained in:
legop3
2026-09-09 18:43:39 -04:00
parent 8895ed6bd8
commit 9ca039229a
13 changed files with 1005 additions and 501 deletions
@@ -0,0 +1,78 @@
#include "RoverPeripheral.h"
#include "internal/RoverPeripheralFirmata.h"
RoverPeripheral::RoverPeripheral()
: implementation_(new RoverPeripheralFirmata("Rover peripheral")) {}
RoverPeripheral::~RoverPeripheral() {
delete implementation_;
}
void RoverPeripheral::name(const String& peripheralName) {
implementation_->setName(peripheralName);
}
void RoverPeripheral::addCameraServo(const RoverCameraServoConfig& config) {
implementation_->addRoverCameraServo(config);
}
void RoverPeripheral::addHeadlight(const RoverDigitalOutputConfig& config) {
implementation_->addRoverHeadlight(config);
}
void RoverPeripheral::addLaser(const RoverDigitalOutputConfig& config) {
implementation_->addRoverLaser(config);
}
void RoverPeripheral::addSlider(const SliderControlConfig& config, const ServoOutput& output) {
implementation_->addServoSlider(config, output);
}
void RoverPeripheral::addSlider(const SliderControlConfig& config, const PwmOutput& output) {
implementation_->addPwmSlider(config, output);
}
void RoverPeripheral::addButton(const ButtonControlConfig& config, const DigitalOutput& output) {
implementation_->addDigitalButton(config, output);
}
void RoverPeripheral::addSlider(const SliderControlConfig& config, SliderCallback callback) {
implementation_->addSlider(config, callback);
}
void RoverPeripheral::addButton(const ButtonControlConfig& config, ButtonCallback callback) {
implementation_->addButton(config, callback);
}
void RoverPeripheral::addButton(const ButtonControlConfig& config, ActionCallback callback) {
// One-shot callbacks apply only to momentary buttons. A toggle requires the
// bool callback overload because application code must receive its new state.
if (config.mode != ButtonMode::Momentary) {
abort();
}
implementation_->addButton(
config,
[callback](bool pressed) {
if (pressed && callback) {
callback();
}
}
);
}
void RoverPeripheral::addNumber(const NumberControlConfig& config, NumberCallback callback) {
implementation_->addNumber(config, callback);
}
void RoverPeripheral::addText(const TextControlConfig& config, TextCallback callback) {
implementation_->addText(config, callback);
}
void RoverPeripheral::begin(FirmataExt& extension) {
implementation_->begin(extension);
}
void RoverPeripheral::update() {
implementation_->update();
}
@@ -0,0 +1,156 @@
#pragma once
#include <Arduino.h>
#include <functional>
/** Describes whether a logical on value drives an output pin high or low. */
enum class OutputPolarity {
ActiveHigh,
ActiveLow,
};
/** Selects whether a button retains its state or is active only while held. */
enum class ButtonMode {
Toggle,
Momentary,
};
/** Configuration for the rover's existing camera-tilt control. */
struct RoverCameraServoConfig {
uint8_t pin = 0;
float minimumAngleDegrees = -15;
float maximumAngleDegrees = 30;
float homeAngleDegrees = 0;
float nudgeDegrees = 2;
uint16_t minimumPulseMicroseconds = 900;
uint16_t maximumPulseMicroseconds = 2100;
bool allowRawPulse = false;
bool inverted = false;
};
/** Configuration for the rover's existing headlight or laser control. */
struct RoverDigitalOutputConfig {
uint8_t pin = 0;
OutputPolarity polarity = OutputPolarity::ActiveHigh;
bool initiallyOn = false;
};
/** Shared display and range settings for a slider control. */
struct SliderControlConfig {
String name;
int minimum = 0;
int maximum = 100;
};
/** Shared display and interaction settings for a button control. */
struct ButtonControlConfig {
String name;
ButtonMode mode = ButtonMode::Momentary;
};
/** Shared display and range settings for a number input. */
struct NumberControlConfig {
String name;
int minimum = 0;
int maximum = 100;
};
/** Shared display and length settings for a text input. */
struct TextControlConfig {
String name;
size_t maximumLength = 32;
};
/** Selects a standard Firmata servo as the destination for a slider. */
struct ServoOutput {
uint8_t pin = 0;
};
/** Selects an ESP32 PWM pin as the destination for a slider. */
struct PwmOutput {
uint8_t pin = 0;
};
/** Selects an ESP32 digital pin as the destination for a button. */
struct DigitalOutput {
uint8_t pin = 0;
OutputPolarity polarity = OutputPolarity::ActiveHigh;
};
using SliderCallback = std::function<void(int)>;
using ButtonCallback = std::function<void(bool)>;
using ActionCallback = std::function<void()>;
using NumberCallback = std::function<void(int)>;
using TextCallback = std::function<void(const String&)>;
class FirmataExt;
class RoverPeripheralFirmata;
/**
* Registration API for a self-describing rover peripheral.
*
* A sketch constructs each configuration one field at a time and registers it
* in configureRoverPeripheral(). Serial and protocol setup stay in the library.
*/
class RoverPeripheral {
public:
RoverPeripheral();
~RoverPeripheral();
RoverPeripheral(const RoverPeripheral&) = delete;
RoverPeripheral& operator=(const RoverPeripheral&) = delete;
/** Sets the peripheral name shown above its accessory controls. */
void name(const String& peripheralName);
/** Registers the rover's existing camera-tilt control. */
void addCameraServo(const RoverCameraServoConfig& config);
/** Registers the rover's existing headlight control. */
void addHeadlight(const RoverDigitalOutputConfig& config);
/** Registers the rover's existing laser control. */
void addLaser(const RoverDigitalOutputConfig& config);
/** Registers a slider backed by a standard Firmata servo output. */
void addSlider(const SliderControlConfig& config, const ServoOutput& output);
/** Registers a slider backed by an ESP32 PWM output. */
void addSlider(const SliderControlConfig& config, const PwmOutput& output);
/** Registers a button backed by an ESP32 digital output. */
void addButton(const ButtonControlConfig& config, const DigitalOutput& output);
/** Registers a slider handled by application code. */
void addSlider(const SliderControlConfig& config, SliderCallback callback);
/** Registers a button whose callback receives its logical state. */
void addButton(const ButtonControlConfig& config, ButtonCallback callback);
/** Registers a momentary button whose callback runs only on press. */
void addButton(const ButtonControlConfig& config, ActionCallback callback);
/** Registers a number input handled by application code. */
void addNumber(const NumberControlConfig& config, NumberCallback callback);
/** Registers a text input handled by application code. */
void addText(const TextControlConfig& config, TextCallback callback);
private:
// The implementation is opaque so importing this header does not expose any
// Firmata types or require firmware authors to understand the wire protocol.
RoverPeripheralFirmata* implementation_;
void begin(FirmataExt& extension);
void update();
friend void setup();
friend void loop();
};
/** Called once by the library after Arduino and Serial initialization. */
void configureRoverPeripheral(RoverPeripheral& peripheral);
/** Optional non-blocking hook for recurring application work. */
void updateRoverPeripheral();
@@ -0,0 +1,46 @@
#include "RoverPeripheral.h"
#include <ConfigurableFirmata.h>
#include <FirmataExt.h>
namespace {
FirmataExt firmataExtension;
RoverPeripheral peripheral;
} // namespace
// A weak no-op preserves the zero-boilerplate case while allowing a sketch to
// define the same function when animations or state machines need regular work.
void __attribute__((weak)) updateRoverPeripheral() {}
void setup() {
// The public configuration hook runs after Arduino initialization, allowing
// peripheral code to safely use pinMode() and initialize third-party devices.
Serial.begin(115200);
configureRoverPeripheral(peripheral);
// ConfigurableFirmata batches reads on ESP32-class boards. Arduino's default
// one-second Stream timeout would delay short commands while waiting for the
// batch buffer to fill, so consume only bytes that have already arrived.
Serial.setTimeout(0);
Firmata.begin(Serial);
peripheral.begin(firmataExtension);
// Applying a normal Firmata reset after registration establishes every
// declared initial output and makes the first host connection deterministic.
Firmata.parse(SYSTEM_RESET);
}
void loop() {
// ConfigurableFirmata retains partial parser state between iterations. Stop
// after each complete message so user update work cannot be starved by a
// sustained burst, while ordinary short commands are still drained at once.
while (Firmata.available()) {
Firmata.processInput();
if (!Firmata.isParsingMessage()) {
break;
}
}
peripheral.update();
updateRoverPeripheral();
}
@@ -28,31 +28,41 @@ RoverPeripheralFirmata* RoverPeripheralFirmata::instance_ = nullptr;
RoverPeripheralFirmata::RoverPeripheralFirmata(const String& name) : name_(name) {}
void RoverPeripheralFirmata::validateControlIdentity(const String& id, const String& name) const {
if (id.length() == 0 || name.length() == 0) {
void RoverPeripheralFirmata::setName(const String& name) {
if (name.length() == 0) {
// A blank heading makes multiple attached peripherals impossible to
// distinguish. Treat it as a firmware-authoring error at startup rather
// than advertising ambiguous controls to the rover.
abort();
}
name_ = name;
}
void RoverPeripheralFirmata::validateControlName(const String& name) const {
if (name.length() == 0) {
// Registration errors are programmer errors discovered during setup. A
// hard stop is preferable to advertising a partially usable device whose
// behavior depends on which malformed control the driver touches first.
abort();
}
for (const ControlRegistration& existing : controls_) {
if (existing.id == id) {
if (existing.id == name) {
abort();
}
}
}
void RoverPeripheralFirmata::validateRange(const String& id, int minimum, int maximum) const {
if (id.length() == 0 || minimum > maximum) {
void RoverPeripheralFirmata::validateRange(const String& name, int minimum, int maximum) const {
if (name.length() == 0 || minimum > maximum) {
abort();
}
}
void RoverPeripheralFirmata::addServoSlider(const SliderControlConfig& config, const FirmataServoOutput& output) {
validateControlIdentity(config.id, config.name);
validateRange(config.id, config.minimum, config.maximum);
void RoverPeripheralFirmata::addServoSlider(const SliderControlConfig& config, const ServoOutput& output) {
validateControlName(config.name);
validateRange(config.name, config.minimum, config.maximum);
ControlRegistration control;
control.id = config.id;
control.id = config.name;
control.name = config.name;
control.type = ControlType::Slider;
control.output = OutputType::Servo;
@@ -62,11 +72,11 @@ void RoverPeripheralFirmata::addServoSlider(const SliderControlConfig& config, c
controls_.push_back(control);
}
void RoverPeripheralFirmata::addPwmSlider(const SliderControlConfig& config, const FirmataPwmOutput& output) {
validateControlIdentity(config.id, config.name);
validateRange(config.id, config.minimum, config.maximum);
void RoverPeripheralFirmata::addPwmSlider(const SliderControlConfig& config, const PwmOutput& output) {
validateControlName(config.name);
validateRange(config.name, config.minimum, config.maximum);
ControlRegistration control;
control.id = config.id;
control.id = config.name;
control.name = config.name;
control.type = ControlType::Slider;
control.output = OutputType::Pwm;
@@ -76,10 +86,10 @@ void RoverPeripheralFirmata::addPwmSlider(const SliderControlConfig& config, con
controls_.push_back(control);
}
void RoverPeripheralFirmata::addDigitalButton(const ButtonControlConfig& config, const FirmataDigitalOutput& output) {
validateControlIdentity(config.id, config.name);
void RoverPeripheralFirmata::addDigitalButton(const ButtonControlConfig& config, const DigitalOutput& output) {
validateControlName(config.name);
ControlRegistration control;
control.id = config.id;
control.id = config.name;
control.name = config.name;
control.type = ControlType::Button;
control.output = OutputType::Digital;
@@ -90,10 +100,10 @@ void RoverPeripheralFirmata::addDigitalButton(const ButtonControlConfig& config,
}
void RoverPeripheralFirmata::addSlider(const SliderControlConfig& config, SliderCallback callback) {
validateControlIdentity(config.id, config.name);
validateRange(config.id, config.minimum, config.maximum);
validateControlName(config.name);
validateRange(config.name, config.minimum, config.maximum);
ControlRegistration control;
control.id = config.id;
control.id = config.name;
control.name = config.name;
control.type = ControlType::Slider;
control.output = OutputType::Custom;
@@ -104,9 +114,9 @@ void RoverPeripheralFirmata::addSlider(const SliderControlConfig& config, Slider
}
void RoverPeripheralFirmata::addButton(const ButtonControlConfig& config, ButtonCallback callback) {
validateControlIdentity(config.id, config.name);
validateControlName(config.name);
ControlRegistration control;
control.id = config.id;
control.id = config.name;
control.name = config.name;
control.type = ControlType::Button;
control.output = OutputType::Custom;
@@ -116,10 +126,10 @@ void RoverPeripheralFirmata::addButton(const ButtonControlConfig& config, Button
}
void RoverPeripheralFirmata::addNumber(const NumberControlConfig& config, NumberCallback callback) {
validateControlIdentity(config.id, config.name);
validateRange(config.id, config.minimum, config.maximum);
validateControlName(config.name);
validateRange(config.name, config.minimum, config.maximum);
ControlRegistration control;
control.id = config.id;
control.id = config.name;
control.name = config.name;
control.type = ControlType::Number;
control.output = OutputType::Custom;
@@ -130,12 +140,12 @@ void RoverPeripheralFirmata::addNumber(const NumberControlConfig& config, Number
}
void RoverPeripheralFirmata::addText(const TextControlConfig& config, TextCallback callback) {
validateControlIdentity(config.id, config.name);
validateControlName(config.name);
if (config.maximumLength == 0) {
abort();
}
ControlRegistration control;
control.id = config.id;
control.id = config.name;
control.name = config.name;
control.type = ControlType::Text;
control.output = OutputType::Custom;
@@ -5,95 +5,25 @@
#include <ConfigurableFirmata.h>
#include <ESP32Servo.h>
#include <FirmataExt.h>
#include <RoverPeripheral.h>
#include <functional>
#include <vector>
enum class OutputPolarity {
ActiveHigh,
ActiveLow,
};
enum class ButtonMode {
Toggle,
Momentary,
};
struct FirmataServoOutput {
uint8_t pin = 0;
};
struct FirmataPwmOutput {
uint8_t pin = 0;
};
struct FirmataDigitalOutput {
uint8_t pin = 0;
OutputPolarity polarity = OutputPolarity::ActiveHigh;
};
struct RoverCameraServoConfig {
uint8_t pin = 0;
float minimumAngleDegrees = -15;
float maximumAngleDegrees = 30;
float homeAngleDegrees = 0;
float nudgeDegrees = 2;
uint16_t minimumPulseMicroseconds = 900;
uint16_t maximumPulseMicroseconds = 2100;
bool allowRawPulse = false;
bool inverted = false;
};
struct RoverDigitalOutputConfig {
uint8_t pin = 0;
OutputPolarity polarity = OutputPolarity::ActiveHigh;
bool initiallyOn = false;
};
struct SliderControlConfig {
String id;
String name;
int minimum = 0;
int maximum = 100;
};
struct ButtonControlConfig {
String id;
String name;
ButtonMode mode = ButtonMode::Momentary;
};
struct NumberControlConfig {
String id;
String name;
int minimum = 0;
int maximum = 100;
};
struct TextControlConfig {
String id;
String name;
size_t maximumLength = 32;
};
using SliderCallback = std::function<void(int)>;
using ButtonCallback = std::function<void(bool)>;
using NumberCallback = std::function<void(int)>;
using TextCallback = std::function<void(const String&)>;
/*
* RoverPeripheralFirmata is both the sketch-facing registration API and one
* ConfigurableFirmata feature. Keeping those responsibilities together gives a
* peripheral author one object to configure while still allowing ordinary
* Firmata tooling to use digital, PWM, and servo commands on the same stream.
* RoverPeripheralFirmata is the protocol-facing implementation behind the
* small RoverPeripheral public facade. Keeping this class private prevents
* peripheral sketches from depending on Firmata types while ordinary Firmata
* tooling can still use digital, PWM, and servo commands on the same stream.
*/
class RoverPeripheralFirmata : public FirmataFeature {
public:
explicit RoverPeripheralFirmata(const String& name);
void addServoSlider(const SliderControlConfig& config, const FirmataServoOutput& output);
void addPwmSlider(const SliderControlConfig& config, const FirmataPwmOutput& output);
void addDigitalButton(const ButtonControlConfig& config, const FirmataDigitalOutput& output);
void setName(const String& name);
void addServoSlider(const SliderControlConfig& config, const ServoOutput& output);
void addPwmSlider(const SliderControlConfig& config, const PwmOutput& output);
void addDigitalButton(const ButtonControlConfig& config, const DigitalOutput& output);
void addSlider(const SliderControlConfig& config, SliderCallback callback);
void addButton(const ButtonControlConfig& config, ButtonCallback callback);
void addNumber(const NumberControlConfig& config, NumberCallback callback);
@@ -155,8 +85,8 @@ class RoverPeripheralFirmata : public FirmataFeature {
RoverDigitalOutputConfig laser_;
Servo* servos_[TOTAL_PINS] = {};
void validateControlIdentity(const String& id, const String& name) const;
void validateRange(const String& id, int minimum, int maximum) const;
void validateControlName(const String& name) const;
void validateRange(const String& name, int minimum, int maximum) const;
void buildAndSendDescription();
void dispatchCustomControl(byte argc, byte* argv);
void writeDigitalPin(byte pin, bool enabled);
@@ -167,4 +97,3 @@ class RoverPeripheralFirmata : public FirmataFeature {
static void digitalPinValueCallback(byte pin, int value);
static void systemResetCallback();
};