This project builds two MADS plugin binaries from src/. Both link
ArduinoDriver::arduino_driver, which requires cxx_std_20; CMake
propagates that requirement to every target that links it, so both plugins
are actually built as C++20 even though CMakeLists.txt sets
CMAKE_CXX_STANDARD 17 as the project-wide default.
Framework context — the plugin lifecycle, the order in which the host agent
calls each method, what every return value does, how settings reach the plugin,
topics, blobs, testing, deployment and protocol migration — is in the
mads-plugin skill under .claude/skills/mads-plugin/.
Read SKILL.md before changing plugin code, and follow its reference/
files rather than inferring host behaviour from the template comments.
Regenerate that skill for a newer MADS with mads plugin --update.
Both plugins share one mixin, src/usb_driver.hpp
(class USBDriver): it owns the ArduinoDriver::Context/Device, opens the
device (open(serial), called from set_params(), never from the
constructor — MADS constructs the plugin object before any settings exist),
and centralizes pin-mode handling (read_pin_modes() validates and records
into _pin_modes; apply_pin_modes() sends PIN_MODE for each of them).
- Source:
src/arduinousb.cpp - Behavior: source, class
UsbsourcePlugin— polls the configured pins once per MADS tick over a USB control transfer each (a few hundred Hz at most). - Behavior: sink, class
UsbsinkPlugin— writesdigital/pwm/dacvalues from the input frame to pins configured in the matching mode. - Both classes are registered under the CMake target name
arduinousb(MADS_REGISTER_PLUGINS(UsbsourcePlugin, UsbsinkPlugin), driver name =PLUGIN_NAME="arduinousb"). Note:kind()on both classes currently returns a hardcoded literal ("usbsource"/"usbsink") instead ofPLUGIN_NAME; in protocol P8 this only produces a startup warning (kind()no longer selects the settings section — the agent name does), but it is a latent inconsistency worth fixing ifarduinousb.cppis touched again. - INI section: named after the agent (
-n), e.g.[arduinousb_source],[arduinousb_sink]inmads.ini.
| Key | Type | Default | Meaning |
|---|---|---|---|
serial |
string | "" |
USB serial number to open; empty = first device found. |
pin_modes |
table | source: {"1"="PULLDOWN"}; sink: {} |
Pin → mode. Source allows INPUT, PULLUP, PULLDOWN, ANALOG; sink allows OUTPUT, PWM, DAC. |
Source output frame: {"digital": {"<pin>": 0|1, ...}, "analog": {"<pin>": <volts>, ...}, "agent_id": "..."}
— one scalar reading per configured pin, once per tick. Sink input frame:
{"digital": {"<pin>": true|false}, "pwm": {"<pin>": <0..1>}, "dac": {"<pin>": <volts>}}.
- Source:
src/arduinostream.cpp - Behavior: source, class
UsbstreamPlugin, registered alone (MADS_REGISTER_PLUGINS(UsbstreamPlugin);kind()correctly returnsPLUGIN_NAME, i.e. the CMake target namearduinostream). - On boards that expose
USBIO_FLAG_STREAMING(e.g. Portenta H7), samples the configured pins on the device at a fixed rate (up to 10 kHz) over the bulk IN endpoint (ArduinoDriver::Device::start_stream()/ArduinoDriver::Stream), buffers decoded samples on the host without blocking (Stream::read(span, 0ms)drained inget_output()), and publishes them in chunks with QoS metadata (loss counters, achieved rate, latency). See the README'sarduinostreamsection for the full settings table, an example frame and the QoS field meanings — keep the two in sync. - INI section:
[arduinostream](or whatever-nselects). - Key internal pieces:
TimeUnwrapper(turns the device's wrapping 32-bitmicros()into a monotonic 64-bit timestamp, seeded from aDeviceTimeanchor taken before each stream starts —read_time()throwsDeviceBusyonce aStreamis running);SessionTimeline(puts every stream session on onet_ustimeline that never goes backwards, bridging a board reset with host time);_staging/_staging_t, flat buffers drained from theStreamqueue every tick, each record timestamped as it is drained, and shifted left after each published chunk. - Restarts (
restart,max_restarts): when theStreamstops,get_output()drains what it had decoded, thenlaunch_restart()runsstart_session()(close, reopen by serial, pin modes, anchor,start_stream()) throughstd::async. While that future is valid the restart thread owns_devand_stream: the main thread must not touch either, and only publishes already-staged records.finish_restart()collects the result, schedules backoff on failure (returnserror), and counters of ended sessions are summed into_base. - Caveat: starting a stream marks the
Device"busy", but that guard is per-process (inside this plugin's ownDeviceobject) — a second MADS agent is a separate process with its ownDeviceand does not getDeviceBusyfor free. In practice it either fails to open the same board at all (the USB interface is already claimed) or, if it does open it, itsPIN_MODE/RESETon the board stops the stream on the device. Since ArduinoDriver v0.3.1 theStreamworker notices (itsGET_STREAM_STATUSpoll reportsrunning == 0) and ends with an error, so this plugin restarts the stream and the two agents keep undoing each other. Do not runarduinousb_source/arduinousb_sinkandarduinostreamagainst the same board at the same time (see the comment indirector.toml).
See each plugin's subsection above, and the README for arduinostream's
full frame example (t_us, analog/digital, qos, time_ref).
- C++20 in practice (see the note at the top of this file), built with CMake/Ninja; LLVM formatting, two-space indent.
CamelCasefor classes and namespaces,snake_casefor methods and variables,_leading_underscorefor private members, declared last.- Do not add third-party dependencies without asking.
nlohmann/jsonandArduinoDriver::arduino_driverare already available viaFetchContent, and the plugin base classes also provide aSerialPorthelper inserialport.hpp(unused by these two plugins). - All plugin logic must be reachable from the test
main()at the bottom of each source file, and that test must be deterministic and self-checking: assert on both the returned status and the payload, and exit non-zero on failure.arduinostream.cpp'smain()also accepts--offline, which runs only its hardware-freeTimeUnwrapper/SessionTimelinechecks and skips anything that opens a USB device — use that flag whenever a real board must not be touched — and--soak SECONDS, which streams pins 15 and 16 at 10 kHz and checks restarts keept_usincreasing andqos.totalsnon-decreasing (useful on a failing link, or while unplugging and replugging the board). - An exception must never escape a plugin method: catch it, set
_errorand returnreturn_type::error(orcriticalinset_params()'s effect — seereference/return-types.md). - Never block, sleep or busy-wait inside a plugin method.
arduinostream'sget_output()drains its stream with an explicit0mstimeout for this reason.
# Once ArduinoDriver v0.2.2+ is tagged and published:
cmake -Bbuild -DCMAKE_INSTALL_PREFIX="$(mads -p)"
# Until then, against a local ArduinoDriver checkout:
cmake -Bbuild -DFETCHCONTENT_SOURCE_DIR_ARDUINODRIVER=/path/to/ArduinoDriver
cmake --build build -j4
./build/arduinousb.plugin # standalone test driver (opens a real board)
./build/arduinostream.plugin --offline # standalone test driver, no hardware
./build/arduinostream.plugin # same, plus hardware checks if a streaming board is attached
./build/arduinostream.plugin --soak 180 # 3 min at 10 kHz, checks behaviour across stream restarts
mads inspect_plugin build/arduinousb.plugin
mads inspect_plugin build/arduinostream.plugin
mads source build/arduinousb.plugin -n arduinousb_source
mads source build/arduinostream.pluginOn macOS both .plugin files are also directly-executable test drivers (see
add_plugin() in CMakeLists.txt); on Linux/Windows the test driver is a
separate <name> / <name>.exe binary built alongside the .plugin
shared library.