MotionModule is an open-source Raspberry Pi robot controller for eight brushed DC motors, a PCA9685 servo board with sixteen outputs, and a Pi-connected IMU. The idea is to bring the controller and expansion functions of a REV Robotics Control Hub and Expansion Hub together in one buildable, open-source project. Anyone can build it using the supplied code, wiring guide, bill of materials and enclosure CAD.
The reusable runtime owns GPIO, I2C, safety, networking, diagnostics, and the browser dashboard. Each robot is one separate Python folder containing its own behavior and an optional hardware map, so the same installation can run a Mecanum, tank, walking, or other robot.
Build it, wire it correctly, and run it from main. The main branch is the
ready-to-use version for the documented hardware. Changes are developed and
tested on testing before they are merged into main; testing is the
development branch and can contain unfinished changes. The branches may match
immediately after a merge, then diverge as development continues.
Keep a physical motor-power cutoff within reach and raise the wheels when checking a newly assembled robot. Follow the wiring and power guide.
The complete reference wiring below shows the Pi, all four motor drivers, the PCA9685 servo board, the MPU6500 IMU and the power rails. Labels use physical Pi header pin numbers. Wire colors match Debug → Wiring → Follow the signal in the dashboard, which also includes this diagram below Names you can use in code.
Open the full-size diagram or use the scalable SVG. See the pinout guide for the connection tables and power details.
These photos show MotionModule in use on the owner's personal Mecanum testing robot. The electronics are reusable; this four-wheel drivetrain is one example of what the controller can run.
| Installed motion module | Controller with the cover removed |
|---|---|
![]() |
![]() |
The open-module photo shows the build without the IMU plugged in. The current build adds a Pi-connected MPU6500, supported by the included sensor code for heading assist and autonomous turns. The labelled cover keeps the motor-driver terminals and servo channels accessible.
| Battery module on the robot | Switch and battery lead |
|---|---|
![]() |
![]() |
The electronics box accommodates both the REV Slim and goBILDA 12 V NiMH batteries listed in the bill of materials. One part holds the battery, rocker switch and outgoing battery wire. The other part is an empty, hollow enclosure for your 12 V wiring. In this robot, the buck converters and power distribution feeding all four motor drivers are hidden inside that compartment to keep the wiring cleaner.
| STEP model | What it contains |
|---|---|
| Motion module | Controller enclosure and mounting assembly |
| Electronics box | Battery/switch section and hollow power-wiring compartment |
The dashboard also offers both files under Debug → Parts list, with direct Download STEP CAD links that work on the robot hotspot without internet. Import these STEP files into your CAD application. See the CAD notes for the assembly descriptions and the photos above for the installed examples.
To show the dashboard off, or to work on it, run it on any computer. The demo is the real dashboard and Driver Station connected to a simulated robot: arm Drive and hold W and the motor bars move, the simulated cameras and IMU run, the sensors change, and the example autonomous routine can be enabled. Nothing touches hardware or the computer's network.
Windows, in PowerShell:
irm https://raw.githubusercontent.com/AloeVeraZ/MotionModule/main/demo.ps1 | iexmacOS or Linux:
curl -fsSL https://raw.githubusercontent.com/AloeVeraZ/MotionModule/main/demo.sh | bashThe command downloads MotionModule, sets up its own Python environment (it
needs Python 3.11 or newer; on Windows it offers to install Python with winget
when there is none), and opens http://127.0.0.1:8080 in the browser. Press
Ctrl+C to stop it. Running the command again fetches the latest version; with
no internet it reuses the last download.
In a clone of the repository, run .\demo.ps1 or ./demo.sh instead. That
copy is used as it is, so changes to the dashboard appear the next time the
demo starts.
To open the demo from a phone or tablet on the same Wi-Fi, set
MOTIONMODULE_DEMO_HOST to 0.0.0.0 before the command
($env:MOTIONMODULE_DEMO_HOST = '0.0.0.0' in PowerShell, or
export MOTIONMODULE_DEMO_HOST=0.0.0.0 in a Unix shell), allow Python through
the firewall if asked, and browse to the computer's IP address on port 8080.
MOTIONMODULE_DEMO_BRANCH picks another branch and MOTIONMODULE_DEMO_PORT
another port.
See the root-level bill of materials for the reference parts:
- Raspberry Pi 5 with a 40-pin header;
- four dual H-bridge boards for eight brushed-motor outputs;
- one PCA9685 I2C board, giving 16 servo channels;
- one 12 V battery for the whole robot, stepped down to 5 V USB-C for the Pi and to a separate regulated rail for the servos;
- Wago 221 lever connectors for the 12 V joins and ordinary jumper wires for the Pi's control signals; and
- a MPU6500 IMU on the Pi's independent I2C bus for heading; the robot can also drive without it. Optional USB GPIO expansion is separate from this setup.
Read the complete pinout and power boundaries before wiring. Never connect motor battery positive or the PCA9685 servo V+ rail to a Pi header power pin.
The controller boards and the power module are what the robot actually needs; motors, servos and wire below them are recommendations. Both batteries ship already fused, so there is no separate breaker to buy. The enclosure STEP files are included in cad/. Debug → Parts list shows the whole reference BOM, marked required or recommended, and works inside the app with no internet connection.
In Raspberry Pi Imager, install current Raspberry Pi OS, create a normal sudo-capable user, enable SSH for initial administration, and enter the Wi-Fi the robot should prefer. Boot the Pi, connect once, and run:
curl -fsSL https://raw.githubusercontent.com/AloeVeraZ/MotionModule/main/install.sh | bashDo not put sudo before that command. The installer:
- installs all OS and Python dependencies;
- creates a versioned runtime and persistent robot workspace;
- configures the dashboard, GPIO/I2C access, mDNS, and Wi-Fi fallback;
- runs the non-moving MotionModule Doctor automatically;
- prints the GitHub pinout as its final message; and
- reboots the Pi.
Fresh installs select Mecanum with teleop and the IMU autonomous routine ready to use. Autonomous turns in place through 90°, 180°, 270°, and back to the starting direction; it only moves after you Enable and press Start. Debug Overview reports IMU activity as 1/1 or 0/1 responding. The Pi 5 fan uses approximately 75% PWM at 47 °C and 100% at 50 °C after installation and reboot.
The first install uses hostname motionmodule. Give multiple robots unique
names with --hostname motionmodule-01. Updates are always explicit: the Pi
checks GitHub for a newer version and says so under Debug -> Checks & logs,
but nothing is ever installed until you press Update now.
Put the computer on the same Wi-Fi as the Pi and open this in Chrome or Edge:
http://motionmodule.local
The Pi's numeric IP also works. If no saved Wi-Fi connects within 30 seconds, the robot creates its fallback hotspot:
Network: MotionModule
Password: motionrobot
Website: http://10.42.0.1
The same Driver Station and deployment flow works through normal Wi-Fi, Ethernet, or the robot hotspot. Debug shows the current hostname and every IP, can scan and join another network, can start the hotspot for the current boot, and can rename the robot. A reboot always tries saved Wi-Fi first.
Three workspace pages plus the separate Driver Station:
- Overview — live motor power labelled with your own names, servo commands and I2C responses, watchdog state, temperature, memory, disk, uptime, network status, and a three-step guide for a first-time build.
- Debug — Wiring (colour-coded 40-pin header map, driver and servo
wiring, separate 16-output servo-board diagrams, the names you can use in
code, and USB inventory), Parts (complete reference BOM and enclosure CAD),
Tests (guarded raised-wheel motor and servo tests, chosen by name),
Drive Test (Mecanum by default; customize motor/servo mappings in
test.py), Checks & logs (Doctor, service log, command reference), and Network (Wi-Fi, hostname, hotspot). Drive Test keeps fixed W/S, A/D, Q/E, and Space inputs. The sample ships withtest.py; without one it uses the same built-in Mecanum mixer. See custom Drive Test code. - Code — Deploy a local Python folder and open the time-limited Terminal.
- Open Driver Station — launches the operator console, which runs the
deployed project's own
drive()and shows cameras, IMU, Pi inputs, USB sensor controllers, and the project's declared controls. A computer drives with the keys or a game controller; a phone or tablet drives with two on-screen sticks and shows only robot control, the sticks and the cameras, upright or on its side.
For the Mecanum project, Use confirmed Mecanum mixer starts checked in
the Driver Station. Manual movement then uses exactly the same mixer as
the default Debug → Drive Test, even if the Pi has older custom robot code. Uncheck
it to run the project's own drive() instead; changing it stops and disables
drive. Sensors, extra controls, and autonomous routines remain project-owned.
Other project names default to their own code. No saved project file is replaced.
Custom test.py mappings affect only Debug, not the full Driver Station.
Updates preserve NetworkManager's saved Wi-Fi profiles and MotionModule's preferred-network setting. Updating while on the hotspot or disconnected does not forget the saved Wi-Fi. The hotspot remains a fallback if reconnection fails; an unavailable saved network cannot guarantee connectivity.
The dashboard and Driver Station share one look: condensed Barlow Condensed headings over Inter text on a dark blueprint grid, with green, yellow, and red status lights. A light theme is one click away in the top bar. The fonts ship inside MotionModule, so the pages look the same on the robot hotspot with no internet. The pages use no background animation or blur effects, so they stay responsive when opened on a Raspberry Pi 3 with 1 GB of memory.
No editor plugin or remote coding connection is required. Code the project in any local editor, then:
- Open Code → Deploy in the robot dashboard.
- Press Choose your robot folder.
- Select the whole folder containing
robot.py. - Review the files, accept the stop/restart confirmation, and press Deploy and run.
- Wait for the dashboard to reconnect after the service restarts.
The Pi accepts Python and project documentation only, checks every Python file,
parses any hardware.py without executing it, validates the pins and safety
limits, stops all outputs, backs up an older project with the same name,
atomically installs the new folder, makes it active, and restarts MotionModule.
A validation error leaves the working project in place.
Press Download Mecanum sample on that page for a complete starting folder. Unzip it, rename the folder, edit it locally, and deploy the renamed folder.
MotionModule ships with one editable hardware.py containing every motor's
name, BCM pin pair, inversion setting and driver/output explanation, plus
servo names, board addresses and timing settings. The installed copy is
~/.config/motionmodule/hardware.py. Debug and Code offer a download of the
configuration currently in use.
Your smallest browser project needs only robot.py: it uses the installed
hardware map. Add a hardware.py next to it when that robot needs different
names or wiring. MotionModule uses the project copy first, then the installed
map, then the default shipped with the runtime. Existing TOML configurations
remain supported for compatibility.
Use the names directly in your robot code:
module.motor("driver_1a").set(0.25)
module.servo("servo_0").set_angle(90)
module.stop_all()The Mecanum sample's own hardware file names its four wheels front_left,
rear_left, front_right and rear_right, preserving the tested pinout and
inversions. For your own robot, change a name in the hardware file and use
that name in code. No second pin-definition or driver-wrapper file is needed.
Every project is self-contained, and only the first file is required:
MyRobot/
├── robot.py # required browser-control entry point
├── hardware.py # optional: your own names, pins, and inversions
├── sensors.py # the MPU6500 wired directly to the Pi
├── autonomous.py # optional: the routine the robot runs by itself
├── dashboard.py # optional: Driver Station cameras, sensors, keys, sticks
├── drivetrain.py # optional Python modules
├── mechanisms.py
└── README.md # optional project notes
A project folder does not need this file. Include it only when this robot needs its own names or wiring; without it, the robot uses the installed hardware map described above.
hardware.py contains exactly one literal HARDWARE dictionary. It cannot
contain imports, function calls, calculations, or executable setup code. This
lets MotionModule validate an uploaded pinout without running student code.
HARDWARE = {
"module": {
"pwm_hz": 1000,
"deadtime_ms": 15,
"watchdog_ms": 500,
},
"motors": {
1: {
"name": "left_drive",
"forward_gpio": 26, # physical pin 37
"reverse_gpio": 19, # physical pin 35, right next to it
"inverted": False,
},
2: {
"name": "right_drive",
"forward_gpio": 13, # physical pin 33
"reverse_gpio": 6, # physical pin 31
"inverted": False,
},
},
"servos": {
"enabled": True,
"i2c_bus": 1,
"frequency_hz": 50,
"addresses": [0x40],
"minimum_pulse_us": 500,
"maximum_pulse_us": 2500,
},
}Use BCM GPIO numbers in this file. Debug converts them to physical header pins
and lists every name it defines. The shipped hardware.py contains the complete
eight-motor reference map with a comment showing each output's driver, header
pin, and GPIO, so the easiest way to start is to download it from Debug or Code
and rename the outputs you actually use.
Every name must be unique across motors and servos, and must start with a letter and use only letters, numbers, and underscores.
robot.py must define create_drive(module). Return an object with
drive(forward, strafe, rotate, speed) and stop() methods. Do not move
hardware or start a permanent loop at import time, because the dashboard loads
this file during startup.
An optional sibling dashboard.py can define
create_dashboard(module, drive) to supply two camera feeds, one gyro/IMU,
Raspberry Pi digital inputs, and USB-controller analog/digital inputs to the
separate full Driver Station at /driver-station, and to lay out its keys,
game-controller sticks, touch sticks and phone panels. It is discovered
automatically and is not required for drivetrain debugging or robot control.
The complete contract and copyable example are in
docs/CODING.md.
The Mecanum robot's motors, PCA9685 servo controller and MPU6500 IMU connect
directly to the Raspberry Pi. sensors.py reads one MPU6500 on the independent
i2c-gpio bus: SDA on physical pin 11, SCL on 12, VCC on 17, GND on 6, and
AD0 on 20. Use the single IMU wiring plan.
The Pi installer enables this bus for its next reboot. No extra sensors are declared.
The robot can still drive without an IMU; heading stays unavailable, the
driving assist switches off, and the sample autonomous routine does not move.
With the IMU, the Driver Station holds the robot's heading while it drives,
Z / C (or the bumpers) snap-turn exactly 90°, and the sample autonomous drives
an IMU-guided square. The IMU is zeroed only by its Zero IMU button.
An Arduino GIGA R1 WiFi can optionally connect by USB for additional GPIO inputs. The repository includes USB auto-detection, bridge firmware and a Python pin API. This is an experimental extra, not part of the Mecanum setup; operation on your board and sensors needs verification. Uno and Mega boards are not supported by the bundled GIGA firmware. See USB GPIO expansion for the opt-in code and setup for flashing.
This is a complete two-sided drive example:
LEFT = ("driver_1a", "driver_1b")
RIGHT = ("driver_2a", "driver_2b")
def clamp(value):
return max(-1.0, min(1.0, value))
class TankDrive:
def __init__(self, module):
self.module = module
def drive(self, forward, strafe, rotate, speed=0.5):
# rotate +1 turns left (counter-clockwise): left side back, right forward
left = forward - rotate
right = forward + rotate
scale = max(1.0, abs(left), abs(right))
outputs = {name: clamp(left / scale * speed) for name in LEFT}
outputs.update({name: clamp(right / scale * speed) for name in RIGHT})
self.module.set_motors(outputs)
return {"outputs": outputs}
def stop(self):
self.module.set_motors({name: 0 for name in LEFT + RIGHT})
def create_drive(module):
return TankDrive(module)Those four names come from the shipped hardware.py. Rename them there to
left_front, right_rear, or whatever matches your machine, and use the new
names here.
The Driver Station supplies values from -1.0 to 1.0 for forward,
strafe, and rotate; speed is its 0.0 to 1.0 limit. The returned
dictionary must contain JSON-compatible data.
Use a name from the active hardware.py, or a motor channel from 1–8:
intake = module.motor("driver_3a") # or module.motor(5)
intake.set(0.30)
intake.stop()Update a drivetrain together, by name or by channel:
module.set_motors({"driver_1a": 0.4, "driver_1b": 0.4, "driver_2a": 0.4, "driver_2b": 0.4})Values are clamped to -1.0 through 1.0. MotionModule applies the project
inversion map, inserts coast time before a direction reversal, and refreshes
the watchdog. Keep nonzero commands arriving faster than the configured
watchdog timeout and call module.stop_all() for a whole-robot stop.
Use a servo name, or an explicit board/channel pair. PCA9685 boards count from 0, and each has channels 0–15:
claw = module.servo("servo_0") # or module.servo(channel=0, board=0)
claw.set_angle(30)
claw.set_angle(110)
claw.release()For a calibrated positional or continuous-rotation servo, use a verified pulse inside the configured range:
claw.set_pulse_us(1500)The Debug servo tool includes generic 180°/360° position profiles and goBILDA
position, five-turn, and continuous-rotation profiles. release() disables
the PWM signal; it does not first move a mechanism to a safe pose.
- A PCA9685 can acknowledge its I2C address, so Debug reports detected or no response for each configured board.
- USB devices identify themselves. Debug lists their product, vendor/product ID, Pi port, Linux driver, device file, and whether the service user has access. This is live inventory, not firmware management.
- The reference H-bridge inputs and ordinary servos have no return data. The Pi cannot prove that a board, motor, or servo is plugged into those output-only wires. Debug labels those outputs as configured but unverified; use the guarded bench tests with the robot raised.
Chrome / Edge on robot network
│ HTTP
▼
Nginx :80
│ local proxy
▼
MotionModule dashboard + active Python project
├── GPIO PWM → four H-bridges → eight motor outputs
├── I2C → PCA9685 board(s) → servo channels
├── sysfs → read-only USB inventory
└── watchdog → stops stale motor commands
The service loads ~/MotionModule/active/robot.py. active points to one
folder under ~/MotionModule/robots; browser uploads preserve previous copies
under ~/MotionModule/backups. Runtime releases live separately, so installing
or rolling back MotionModule does not overwrite the work in a robot project. A
folder whose files are all copies MotionModule shipped holds no such work, so
an install gives it that release's sample and keeps the replaced folder under
~/MotionModule/backups.
The network service tries saved Wi-Fi for 30 seconds and creates the fallback hotspot only when none connects. Nginx provides the same port-80 page in either mode.
Use this order:
- Open Debug and inspect warnings, the active pinout, USB/I2C devices, network addresses, and service log.
- Run
motionmodule doctor; it does not intentionally move hardware. - Run
motionmodule pinoutand compare every wire before applying power. - Raise the robot and use the guarded Motor Bench Test at half power.
- Select the correct board, channel, and behavior in Servo Pulse Test.
- Check
motionmodule logsafter a failed project start.
The web terminal at the bottom of Code is a real, unprivileged Bash shell. For security it needs a short-lived code created during an admin SSH session:
motionmodule terminal enable # valid for 15 minutes
motionmodule terminal enable 30 # choose 1–120 minutes
motionmodule terminal disableEnter the printed code in the webpage. The grant expires automatically, is invalid after reboot, and an idle shell closes after five minutes. The robot UI uses HTTP, so never put reusable passwords or tokens in this terminal and never expose it to the public internet.
Useful commands are also explained inside Debug:
| Command | Purpose |
|---|---|
motionmodule status |
Show the service state |
motionmodule doctor |
Run non-moving checks |
motionmodule pinout |
Print the physical wiring map |
motionmodule restart |
Stop outputs and reload the active project |
motionmodule logs |
Follow Python and service output |
motionmodule project list |
List installed robot folders |
motionmodule project NAME |
Select another installed folder |
motionmodule versions |
List installed runtime versions |
motionmodule rollback |
Return to the earlier release of the same branch |
motionmodule install main |
Replace MotionModule with a branch, tag, or commit |
motionmodule giga flash |
Install the sensor firmware on the plugged-in Arduino GIGA |
motionmodule giga status |
Show the GIGA, the bundled firmware, and dfu-util |
Open Debug -> Checks & logs in the dashboard. A Pi with internet access
asks GitHub what each branch is on and shows a line per branch: green when
this robot is running the newest version, red with Update now when one is
waiting. A Pi on testing also sees the main line, so it can go back; a Pi
on main sees only main. Pressing the button stops the motors, runs the
update as root in the background, and automatically reboots the whole Pi after
a successful install; the log appears under the card while it works. The
dashboard waits for the Pi to come back. If sudo on the Pi asks for a
password, a popup asks for it first, and the update uses it only while it runs.
Nothing installs on its own.
The same thing over SSH:
motionmodule install main
motionmodule versions
motionmodule rollbackInstalling replaces the MotionModule software rather than stacking versions,
so the main and testing branches are interchangeable: a Pi on either one
can install the other, in either direction. Robot projects and their backups,
the active project, hardware.py pin names, and Wi-Fi settings are always
kept. motionmodule rollback returns to the earlier release of the same
branch when one is kept; to change branches, install the other one. The
installer notes list exactly what is removed.
To test the repository without robot hardware:
python -m venv .venv
python -m pip install -e .
python -m unittest discover -s tests -v
python -m motion_module doctorSet MOTIONMODULE_MOCK=1 on a Pi to avoid claiming GPIO and I2C hardware.
Detailed references are in Setup, Coding,
Pinout, and Architecture.
MotionModule/
├── core/motion_module/ # controller, safety, dashboard, deploy, USB, network
│ ├── hardware.py # the shipped pin and name definitions
│ └── hardware_guide.py # offline parts list and wiring reference
├── installer/ # Pi install, services, Wi-Fi, versions, rollback
├── cad/ # motion-module and electronics-box STEP models
├── docs/ # guides, wiring diagram, and reference-build photos
├── examples/
│ └── Mecanum/ # complete downloadable Python robot folder
│ ├── robot.py
│ └── hardware.py
├── tests/ # hardware-independent automated tests
├── BOM.md
├── install.sh
├── requirements.txt
└── pyproject.toml





