mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
slopping up a platformio library for people to make rover peripherals
This commit is contained in:
@@ -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();
|
||||
}
|
||||
+36
-26
@@ -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;
|
||||
+12
-83
@@ -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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user