mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
slopping up a platformio library for people to make rover peripherals
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Daniel Roberts
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,218 @@
|
||||
# RoverPeripheral
|
||||
|
||||
RoverPeripheral is an ESP32 Arduino library for MultiRoombaRover peripherals.
|
||||
The ESP32 reports its built-in rover roles and accessory controls to `roverd`
|
||||
over USB serial.
|
||||
|
||||
## PlatformIO installation
|
||||
|
||||
Classic ESP32 DevKitC-style board:
|
||||
|
||||
```ini
|
||||
[env:esp32dev]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
|
||||
lib_deps =
|
||||
legop3/RoverPeripheral @ ^2.0.0
|
||||
```
|
||||
|
||||
Native-USB ESP32-S3 DevKitC:
|
||||
|
||||
```ini
|
||||
[env:esp32-s3-devkitc-1]
|
||||
platform = espressif32
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
build_flags =
|
||||
-D ARDUINO_USB_MODE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
lib_deps =
|
||||
legop3/RoverPeripheral @ ^2.0.0
|
||||
```
|
||||
|
||||
## Program structure
|
||||
|
||||
Include `RoverPeripheral.h` and define `configureRoverPeripheral()`:
|
||||
|
||||
```cpp
|
||||
#include <RoverPeripheral.h>
|
||||
|
||||
void configureRoverPeripheral(RoverPeripheral& peripheral) {
|
||||
peripheral.name("Headlight controller");
|
||||
|
||||
RoverDigitalOutputConfig headlight;
|
||||
headlight.pin = 18;
|
||||
headlight.polarity = OutputPolarity::ActiveHigh;
|
||||
headlight.initiallyOn = false;
|
||||
peripheral.addHeadlight(headlight);
|
||||
}
|
||||
```
|
||||
|
||||
The library provides `setup()` and `loop()`. Do not define them in the
|
||||
peripheral program.
|
||||
|
||||
## Built-in rover roles
|
||||
|
||||
Camera tilt:
|
||||
|
||||
```cpp
|
||||
RoverCameraServoConfig cameraServo;
|
||||
cameraServo.pin = 14;
|
||||
cameraServo.minimumAngleDegrees = -15;
|
||||
cameraServo.maximumAngleDegrees = 30;
|
||||
cameraServo.homeAngleDegrees = 0;
|
||||
cameraServo.nudgeDegrees = 2;
|
||||
cameraServo.minimumPulseMicroseconds = 900;
|
||||
cameraServo.maximumPulseMicroseconds = 2100;
|
||||
cameraServo.allowRawPulse = false;
|
||||
cameraServo.inverted = false;
|
||||
peripheral.addCameraServo(cameraServo);
|
||||
```
|
||||
|
||||
Headlight or laser:
|
||||
|
||||
```cpp
|
||||
RoverDigitalOutputConfig headlight;
|
||||
headlight.pin = 18;
|
||||
headlight.polarity = OutputPolarity::ActiveHigh;
|
||||
headlight.initiallyOn = false;
|
||||
peripheral.addHeadlight(headlight);
|
||||
|
||||
RoverDigitalOutputConfig laser;
|
||||
laser.pin = 16;
|
||||
laser.polarity = OutputPolarity::ActiveHigh;
|
||||
laser.initiallyOn = false;
|
||||
peripheral.addLaser(laser);
|
||||
```
|
||||
|
||||
These registrations use the existing camera, headlight, and laser controls in
|
||||
the rover UI. They do not create accessory controls.
|
||||
|
||||
## Accessory controls
|
||||
|
||||
Controls appear in registration order. Each control name must be unique within
|
||||
the peripheral. The name is also used as the control identifier.
|
||||
|
||||
### Servo slider
|
||||
|
||||
```cpp
|
||||
SliderControlConfig position;
|
||||
position.name = "Arm position";
|
||||
position.minimum = 0;
|
||||
position.maximum = 180;
|
||||
|
||||
ServoOutput servo;
|
||||
servo.pin = 13;
|
||||
|
||||
peripheral.addSlider(position, servo);
|
||||
```
|
||||
|
||||
### PWM slider
|
||||
|
||||
```cpp
|
||||
SliderControlConfig brightness;
|
||||
brightness.name = "Light brightness";
|
||||
brightness.minimum = 0;
|
||||
brightness.maximum = 255;
|
||||
|
||||
PwmOutput light;
|
||||
light.pin = 17;
|
||||
|
||||
peripheral.addSlider(brightness, light);
|
||||
```
|
||||
|
||||
### Digital button
|
||||
|
||||
```cpp
|
||||
ButtonControlConfig workLight;
|
||||
workLight.name = "Work light";
|
||||
workLight.mode = ButtonMode::Toggle;
|
||||
|
||||
DigitalOutput light;
|
||||
light.pin = 21;
|
||||
light.polarity = OutputPolarity::ActiveHigh;
|
||||
|
||||
peripheral.addButton(workLight, light);
|
||||
```
|
||||
|
||||
### Custom slider
|
||||
|
||||
```cpp
|
||||
void setMotorSpeed(int value) {
|
||||
// Apply value to the device.
|
||||
}
|
||||
|
||||
SliderControlConfig speed;
|
||||
speed.name = "Motor speed";
|
||||
speed.minimum = 0;
|
||||
speed.maximum = 100;
|
||||
|
||||
peripheral.addSlider(speed, setMotorSpeed);
|
||||
```
|
||||
|
||||
### Custom button
|
||||
|
||||
```cpp
|
||||
void setMotorRunning(bool running) {
|
||||
// Start or stop the device.
|
||||
}
|
||||
|
||||
ButtonControlConfig motor;
|
||||
motor.name = "Motor";
|
||||
motor.mode = ButtonMode::Momentary;
|
||||
|
||||
peripheral.addButton(motor, setMotorRunning);
|
||||
```
|
||||
|
||||
A momentary bool callback receives `true` on press and `false` on release. A
|
||||
zero-argument callback can be used for a one-shot momentary action.
|
||||
|
||||
### Number input
|
||||
|
||||
```cpp
|
||||
void setRepeatCount(int value) {
|
||||
// Store or apply value.
|
||||
}
|
||||
|
||||
NumberControlConfig repeats;
|
||||
repeats.name = "Repeat count";
|
||||
repeats.minimum = 1;
|
||||
repeats.maximum = 20;
|
||||
|
||||
peripheral.addNumber(repeats, setRepeatCount);
|
||||
```
|
||||
|
||||
### Text input
|
||||
|
||||
```cpp
|
||||
void setDisplayMessage(const String& value) {
|
||||
// Store or display value.
|
||||
}
|
||||
|
||||
TextControlConfig message;
|
||||
message.name = "Display message";
|
||||
message.maximumLength = 64;
|
||||
|
||||
peripheral.addText(message, setDisplayMessage);
|
||||
```
|
||||
|
||||
## Recurring work
|
||||
|
||||
Define `updateRoverPeripheral()` when the program needs recurring non-blocking
|
||||
work:
|
||||
|
||||
```cpp
|
||||
void updateRoverPeripheral() {
|
||||
// Update a state machine or device.
|
||||
}
|
||||
```
|
||||
|
||||
Callbacks and `updateRoverPeripheral()` must not block serial processing.
|
||||
`Serial` is reserved for Firmata and must not be used for debug output.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
#include <RoverPeripheral.h>
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t kActionPin = 21;
|
||||
|
||||
int repeatCount = 1;
|
||||
String displayMessage;
|
||||
|
||||
void setActionActive(bool pressed) {
|
||||
digitalWrite(kActionPin, pressed ? HIGH : LOW);
|
||||
}
|
||||
|
||||
void setRepeatCount(int value) {
|
||||
repeatCount = value;
|
||||
}
|
||||
|
||||
void setDisplayMessage(const String& value) {
|
||||
displayMessage = value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void configureRoverPeripheral(RoverPeripheral& io) {
|
||||
io.name("Complete rover peripheral");
|
||||
|
||||
RoverCameraServoConfig cameraServo;
|
||||
cameraServo.pin = 14;
|
||||
cameraServo.minimumAngleDegrees = -15;
|
||||
cameraServo.maximumAngleDegrees = 30;
|
||||
cameraServo.homeAngleDegrees = 0;
|
||||
cameraServo.nudgeDegrees = 2;
|
||||
cameraServo.minimumPulseMicroseconds = 900;
|
||||
cameraServo.maximumPulseMicroseconds = 2100;
|
||||
cameraServo.allowRawPulse = false;
|
||||
cameraServo.inverted = false;
|
||||
io.addCameraServo(cameraServo);
|
||||
|
||||
RoverDigitalOutputConfig headlight;
|
||||
headlight.pin = 18;
|
||||
headlight.polarity = OutputPolarity::ActiveHigh;
|
||||
headlight.initiallyOn = false;
|
||||
io.addHeadlight(headlight);
|
||||
|
||||
RoverDigitalOutputConfig laser;
|
||||
laser.pin = 16;
|
||||
laser.polarity = OutputPolarity::ActiveHigh;
|
||||
laser.initiallyOn = false;
|
||||
io.addLaser(laser);
|
||||
|
||||
pinMode(kActionPin, OUTPUT);
|
||||
digitalWrite(kActionPin, LOW);
|
||||
|
||||
SliderControlConfig brightness;
|
||||
brightness.name = "Light brightness";
|
||||
brightness.minimum = 0;
|
||||
brightness.maximum = 255;
|
||||
|
||||
PwmOutput brightnessOutput;
|
||||
brightnessOutput.pin = 17;
|
||||
io.addSlider(brightness, brightnessOutput);
|
||||
|
||||
ButtonControlConfig action;
|
||||
action.name = "Special action";
|
||||
action.mode = ButtonMode::Momentary;
|
||||
io.addButton(action, setActionActive);
|
||||
|
||||
NumberControlConfig repeats;
|
||||
repeats.name = "Repeat count";
|
||||
repeats.minimum = 1;
|
||||
repeats.maximum = 20;
|
||||
io.addNumber(repeats, setRepeatCount);
|
||||
|
||||
TextControlConfig message;
|
||||
message.name = "Display message";
|
||||
message.maximumLength = 64;
|
||||
io.addText(message, setDisplayMessage);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#include <RoverPeripheral.h>
|
||||
|
||||
void configureRoverPeripheral(RoverPeripheral& io) {
|
||||
io.name("Headlight controller");
|
||||
|
||||
RoverDigitalOutputConfig headlight;
|
||||
headlight.pin = 18;
|
||||
headlight.polarity = OutputPolarity::ActiveHigh;
|
||||
headlight.initiallyOn = false;
|
||||
io.addHeadlight(headlight);
|
||||
}
|
||||
@@ -1,10 +1,25 @@
|
||||
{
|
||||
"name": "RoverPeripheralFirmata",
|
||||
"version": "0.1.0",
|
||||
"description": "Self-describing Firmata controls for MultiRoombaRover ESP32 peripherals",
|
||||
"$schema": "https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/schema/library.json",
|
||||
"name": "RoverPeripheral",
|
||||
"version": "2.0.1",
|
||||
"description": "Create self-describing ESP32 hardware controls for MultiRoombaRover",
|
||||
"keywords": [
|
||||
"esp32",
|
||||
"firmata",
|
||||
"robotics",
|
||||
"rover"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/legop3/MultiRoombaRover.git"
|
||||
},
|
||||
"homepage": "https://github.com/legop3/MultiRoombaRover/tree/main/esp32/libraries/RoverPeripheralFirmata",
|
||||
"license": "MIT",
|
||||
"frameworks": "arduino",
|
||||
"platforms": "espressif32",
|
||||
"headers": "RoverPeripheral.h",
|
||||
"dependencies": {
|
||||
"ConfigurableFirmata": "https://github.com/firmata/ConfigurableFirmata.git#3.2.0",
|
||||
"bblanchon/ArduinoJson": "^7.4.2",
|
||||
"madhephaestus/ESP32Servo": "^3.0.8"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -5,14 +5,11 @@ default_envs = esp32dev
|
||||
platform = espressif32
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
lib_extra_dirs = ../libraries
|
||||
lib_deps =
|
||||
; 3.2.0 targets the Arduino 2.x core shipped by PlatformIO's stable ESP32
|
||||
; platform. ConfigurableFirmata 3.4.0 switched its bundled PWM source to the
|
||||
; Arduino 3.x LEDC API even when that unused source is compiled as a dependency.
|
||||
https://github.com/firmata/ConfigurableFirmata.git#3.2.0
|
||||
bblanchon/ArduinoJson@^7.4.2
|
||||
madhephaestus/ESP32Servo@^3.0.8
|
||||
; Install the local package through PlatformIO's dependency manager so this
|
||||
; reference project exercises the same transitive dependency behavior as an
|
||||
; external project using the published Registry package.
|
||||
RoverPeripheral=file://../libraries/RoverPeripheralFirmata
|
||||
|
||||
; This is the generic ESP32-WROOM-32/DevKitC target used by boards carrying a
|
||||
; CH340 or CP210x USB-to-UART bridge. Linux normally exposes it as ttyUSB*.
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
#include <Arduino.h>
|
||||
#include <ConfigurableFirmata.h>
|
||||
#include <FirmataExt.h>
|
||||
#include <RoverPeripheralFirmata.h>
|
||||
|
||||
FirmataExt firmataExtension;
|
||||
RoverPeripheralFirmata peripheral("Rover GPIO");
|
||||
#include <RoverPeripheral.h>
|
||||
|
||||
namespace {
|
||||
// Every example pin is present on both the classic ESP32 DevKitC and the
|
||||
@@ -12,18 +6,33 @@ namespace {
|
||||
// S3 boards use them for USB D- and D+.
|
||||
constexpr uint8_t kSpecialActionPin = 21;
|
||||
|
||||
void runSpecialAction() {
|
||||
// This intentionally represents arbitrary device behavior rather than a raw
|
||||
// pin mapping. Replace it with a motor sequence, LED animation, actuator
|
||||
// routine, or any other application-specific function the accessory needs.
|
||||
digitalWrite(kSpecialActionPin, HIGH);
|
||||
delay(80);
|
||||
digitalWrite(kSpecialActionPin, LOW);
|
||||
int repeatCount = 1;
|
||||
String displayMessage;
|
||||
|
||||
void runSpecialAction(bool pressed) {
|
||||
// Receiving both button edges lets application hardware remain active only
|
||||
// while the driver holds the momentary control.
|
||||
digitalWrite(kSpecialActionPin, pressed ? HIGH : LOW);
|
||||
}
|
||||
|
||||
void registerBuiltInRoverControls() {
|
||||
// These three roles replace physical GPIO backends while preserving the
|
||||
// existing camera, headlight, and laser commands and HUD controls.
|
||||
void setRepeatCount(int value) {
|
||||
// A real device can use this value when it starts its next animation or
|
||||
// actuator sequence. Storing it keeps this reference callback non-blocking.
|
||||
repeatCount = value;
|
||||
}
|
||||
|
||||
void setDisplayMessage(const String& value) {
|
||||
// Display hardware can render the stored value from updateRoverPeripheral().
|
||||
// Avoiding Serial output is important because Serial belongs to Firmata.
|
||||
displayMessage = value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void configureRoverPeripheral(RoverPeripheral& io) {
|
||||
io.name("Rover GPIO");
|
||||
|
||||
// Standard roles retain the rover's existing HUD controls while moving the
|
||||
// electrical outputs to this ESP32 on either a Pi or laptop rover host.
|
||||
RoverCameraServoConfig cameraServo;
|
||||
cameraServo.pin = 14;
|
||||
cameraServo.minimumAngleDegrees = -15;
|
||||
@@ -34,89 +43,55 @@ void registerBuiltInRoverControls() {
|
||||
cameraServo.maximumPulseMicroseconds = 2100;
|
||||
cameraServo.allowRawPulse = false;
|
||||
cameraServo.inverted = false;
|
||||
peripheral.addRoverCameraServo(cameraServo);
|
||||
io.addCameraServo(cameraServo);
|
||||
|
||||
RoverDigitalOutputConfig headlight;
|
||||
headlight.pin = 18;
|
||||
headlight.polarity = OutputPolarity::ActiveHigh;
|
||||
headlight.initiallyOn = false;
|
||||
peripheral.addRoverHeadlight(headlight);
|
||||
io.addHeadlight(headlight);
|
||||
|
||||
RoverDigitalOutputConfig laser;
|
||||
laser.pin = 16;
|
||||
laser.polarity = OutputPolarity::ActiveHigh;
|
||||
laser.initiallyOn = false;
|
||||
peripheral.addRoverLaser(laser);
|
||||
}
|
||||
io.addLaser(laser);
|
||||
|
||||
void registerGenericControls() {
|
||||
// Registration order is UI order. This servo slider is handled entirely by
|
||||
// standard Firmata SET_PIN_MODE and EXTENDED_ANALOG messages from roverd.
|
||||
pinMode(kSpecialActionPin, OUTPUT);
|
||||
digitalWrite(kSpecialActionPin, LOW);
|
||||
|
||||
// Accessory controls render in precisely this registration order.
|
||||
SliderControlConfig servoPosition;
|
||||
servoPosition.id = "servoPosition";
|
||||
servoPosition.name = "Servo position";
|
||||
servoPosition.minimum = 0;
|
||||
servoPosition.maximum = 180;
|
||||
|
||||
FirmataServoOutput servoOutput;
|
||||
ServoOutput servoOutput;
|
||||
servoOutput.pin = 13;
|
||||
peripheral.addServoSlider(servoPosition, servoOutput);
|
||||
io.addSlider(servoPosition, servoOutput);
|
||||
|
||||
// PWM brightness is another standard Firmata output. No sketch callback is
|
||||
// involved when the driver moves this slider.
|
||||
SliderControlConfig lightBrightness;
|
||||
lightBrightness.id = "lightBrightness";
|
||||
lightBrightness.name = "Light brightness";
|
||||
lightBrightness.minimum = 0;
|
||||
lightBrightness.maximum = 255;
|
||||
|
||||
FirmataPwmOutput lightOutput;
|
||||
PwmOutput lightOutput;
|
||||
lightOutput.pin = 17;
|
||||
peripheral.addPwmSlider(lightBrightness, lightOutput);
|
||||
io.addSlider(lightBrightness, lightOutput);
|
||||
|
||||
// A custom momentary control receives both press and release. This example
|
||||
// runs a one-shot action only on press, but a motor could use both values to
|
||||
// start while held and stop on release.
|
||||
ButtonControlConfig specialAction;
|
||||
specialAction.id = "specialAction";
|
||||
specialAction.name = "Run special action";
|
||||
specialAction.name = "Special action";
|
||||
specialAction.mode = ButtonMode::Momentary;
|
||||
peripheral.addButton(specialAction, [](bool pressed) {
|
||||
if (pressed) {
|
||||
runSpecialAction();
|
||||
}
|
||||
});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
pinMode(kSpecialActionPin, OUTPUT);
|
||||
digitalWrite(kSpecialActionPin, LOW);
|
||||
|
||||
registerBuiltInRoverControls();
|
||||
registerGenericControls();
|
||||
|
||||
// Supplying Serial as a Stream keeps all protocol code identical between a
|
||||
// CH340/CP210x UART bridge and native ESP32-S3 USB CDC. Only PlatformIO's S3
|
||||
// build flags differ.
|
||||
Serial.begin(115200);
|
||||
Firmata.begin(Serial);
|
||||
peripheral.begin(firmataExtension);
|
||||
|
||||
// A Firmata system reset establishes declared initial output states and also
|
||||
// proves that all callbacks were installed before normal traffic begins.
|
||||
Firmata.parse(SYSTEM_RESET);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Processing one complete parser unit at a time prevents a long serial burst
|
||||
// from starving application work while still draining ordinary USB traffic
|
||||
// quickly on both supported transports.
|
||||
while (Firmata.available()) {
|
||||
Firmata.processInput();
|
||||
if (!Firmata.isParsingMessage()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
peripheral.update();
|
||||
io.addButton(specialAction, runSpecialAction);
|
||||
|
||||
NumberControlConfig repeats;
|
||||
repeats.name = "Repeat count";
|
||||
repeats.minimum = 1;
|
||||
repeats.maximum = 20;
|
||||
io.addNumber(repeats, setRepeatCount);
|
||||
|
||||
TextControlConfig message;
|
||||
message.name = "Display message";
|
||||
message.maximumLength = 64;
|
||||
io.addText(message, setDisplayMessage);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user