browser → nginx :80 → versioned dashboard :8080
│
├── browser project deployment
active robot folder ───────┤
├── GPIO PWM → four dual H-bridges → eight motors
├── independent Pi I2C → MPU6500 IMU
├── I2C → PCA9685 board(s) → servos
├── USB serial → optional GIGA GPIO inputs (experimental)
├── dfu-util → GIGA bootloader (firmware installs)
└── time-limited PTY Bash terminal
All motor writes share a lock. A logical sign reversal coasts all motors for the configured deadtime, and a watchdog stops all output after the configured period without a control heartbeat. GPIO starts low and is forced low on normal shutdown and handled Python exceptions. Software safety does not replace a fused power system and physical cutoff.
~/.local/share/motionmodule/
├── current -> releases/main-...
├── previous -> releases/main-... # only an earlier release of the same branch
└── releases/
~/MotionModule/
├── active -> robots/Mecanum/
├── robots/
│ ├── Mecanum/
│ │ ├── robot.py
│ │ ├── hardware.py
│ │ ├── autonomous.py
│ │ └── dashboard.py
│ └── AnotherRobot/
└── backups/
~/.config/motionmodule/hardware.py # installed pin and name definitions
~/.config/motionmodule/config.toml # pre-hardware.py installs only
~/.config/motionmodule/terminal-access.json
Installing a tag, branch, or commit builds and tests a new release before the
current link changes. It does not overwrite the work in a robot project: a
folder still holding an untouched copy of a sample takes the sample the new
release ships, with the replaced folder kept under backups/, and a folder
with one file of its own is left as it is. Once the new release is running,
the install replaces the old MotionModule software: other
releases and stale MotionModule system files are removed, so main and
testing can replace each other in either direction. Only an earlier release
of the same branch is kept, for rollback. Rollback switches the runtime links,
not the student folders. See installer/README.md for
exactly what is removed and what is kept.
The service follows ~/MotionModule/active/robot.py and auto-loads an optional
sibling dashboard.py for the separate full Driver Station's camera, IMU, Pi
input, and USB-controller telemetry. GPIO and servo
configuration is resolved in one order: the active project's data-only
hardware.py, then the installed ~/.config/motionmodule/hardware.py, then the
copy shipped inside the runtime. Installs predating that file keep their TOML
configuration, which is still read for compatibility.
The Code page sends a browser-selected directory as multipart files to the local dashboard. The backend:
- requires the unguessable per-page dashboard token;
- accepts a single safe root folder with Python, Arduino sketches, and text documentation only;
- enforces count, individual-file, and total-size limits;
- rejects path traversal, links, binary data, caches, and build output;
- compiles every
.py, verifiescreate_drive(module), verifiescreate_dashboard(module, drive)whendashboard.pyexists, and, when the folder ships one, parseshardware.pywithast.literal_evalwithout importing it; - stops outputs before writing project state;
- moves an existing target to
backups, atomically installs the staged folder, and atomically updatesactive; and - exits so systemd restarts the dashboard on the new project.
The service uses Restart=always, so a deliberate clean exit after deployment
returns through the same launcher. Nginx stays the stable port-80 front door.
boot
│
├── saved Wi-Fi connects within 30 seconds → hostname.local + DHCP IP
│
└── no saved connection → MotionModule hotspot → http://10.42.0.1
Ethernet also reaches the same dashboard. A root-owned NetworkManager helper exposes only fixed status, scan, connect, preferred-network, hotspot, and hostname operations to the unprivileged service. A manual hotspot is for the current boot; the next boot tries saved client Wi-Fi again.
PCA9685 boards acknowledge on I2C. USB devices expose descriptors in Linux sysfs, so the dashboard can list identity, topology, driver binding, device node, and permission status. The Arduino GIGA R1 WiFi is matched by its official USB VID/PID: 2341:0266 running a sketch, 2341:0366 in its bootloader. USB discovery alone cannot identify which physical sensor is wired to a pin.
MotionModule always owns one built-in MPU6500 reader, exposed as the read-only
module.imu property. It finds the independent i2c-gpio adapter and detects
the chip at 0x68 or 0x69, retrying discovery or missing hardware in the background.
The dashboard service retains the same reader across program switches and closes
it at service shutdown. Simulation and missing hardware keep an offline sensor
object with unavailable readings. Both telemetry endpoints use the core reading,
independently of a program's optional dashboard file. The wiring is the single plan in
Debug → Wiring and PINOUT.md. Motors and the PCA9685 also connect to the Pi.
No additional sensors are declared by the sample.
firmware/giga_sensor_bridge/giga_sensor_bridge.ino the firmware, generic
firmware/giga_sensor_bridge.bin prebuilt, flashed by the Pi
firmware/giga_sensor_bridge.json version and checksums
firmware/build.py rebuilds the binary (arduino-cli)
The firmware only moves bytes: it reads the pins and I2C registers it is told
to, at the interval it is told, and answers one-off reads, writes and bus
scans. An opt-in project extension declares additional GPIO inputs;
module.giga() creates one GigaR1Bridge for the process (never opened in simulation), and
its reader thread sends MM3 CONFIG <id> <ms> <pins> <streams> after every
connection. The GIGA answers with one JSON line of readings per interval,
tagged with that id, so readings meant for another configuration are never
used. The original MM1 CONFIG pins-only protocol is still understood in both
directions, so older MotionModule software and the original sketch keep
working; firmware whose protocol this version cannot use is reported as
needing a flash rather than guessed at.
This experimental extension targets GIGA R1 WiFi, not Uno or Mega. Hardware
operation needs verification; it is not required by the reference robot.
The Python bridge configures GPIO inputs only. Generic I2C commands remain
in the Arduino firmware for custom extensions, but no bundled sensor-specific
IMU drivers run over USB. The built-in MPU6500 is read only by LocalIMU on
the Pi's independent I2C bus. Its driver measures gyro bias while still,
tracks tilt with the accelerometer, and integrates relative yaw about the
measured up direction. Positive yaw is counter-clockwise, matching rotate.
motionmodule giga flash and Debug's Install firmware button run
motion_module.giga_firmware: the reader releases the port, a 1200-baud touch
restarts the board into its bootloader, dfu-util writes the bundled binary at
0x08040000, and the board's version is checked once it restarts. The installer
provides dfu-util, the dialout and plugdev groups, and a udev rule for the
GIGA's USB IDs. tests/firmware/harness.cpp runs the real firmware against
simulated I2C chips, and tests/fake_giga.py tests GPIO input transport against a
simulated board, so both sides are tested without hardware.
The reference GPIO H-bridges and PWM servo signal have no return channel. MotionModule can validate their configured pins and safely pulse an output, but it cannot electronically prove a driver, motor, or servo is attached.
The terminal API requires the per-page dashboard token and a second temporary
access code created by motionmodule terminal enable. The code is stored with
mode 0600, expires, and is tied to the current Linux boot ID. The backend owns
one unprivileged PTY at a time, caps retained output and input, and closes idle
sessions.