- Robot Type: 6 DOF Arm
- Arduino IDE:
scripts/armandscripts/center_first_motorare for the Arduino UNO and need the Adafruit PWM Servo Driver Library (Library Manager)
| Component | Details |
|---|---|
| Microcontroller | Arduino Uno (scripts/arm/arm.ino, 115200 baud) |
| Servo driver | PCA9685 16-channel PWM driver, I2C address 0x40, 50 Hz |
| Servos | 6× 270° servos (claw limited to 40–145°) |
| Servo power | External 5–6 V supply to the PCA9685's V+ terminal (not the Uno's 5 V pin) |
| Controllers | USB joystick, keyboard, phone / browser, RIFT |
| Camera (Mind mode) | Any USB webcam pointed at the arm and the desk around it (--camera N) |
| Signal | Connection |
|---|---|
| PCA9685 SDA / SCL | Uno A4 / A5 |
| PCA9685 VCC / GND | Uno 5 V / GND |
| PCA9685 V+ | External servo supply (share GND with the Uno) |
| Buzzer + / − | Uno D8 / GND (passive buzzer - her voice in the fleet's conversations) |
| Motor 1 base | PCA9685 channel 0 |
| Motor 2 shoulder | channel 1 |
| Motor 3 elbow | channel 2 |
| Motor 4 wrist pitch | channel 3 |
| Motor 5 wrist roll | channel 4 |
| Motor 6 claw | channel 5 |
Channels, limits, rest angles and directions live in config.json.
Note
The arm's link lengths in config.json (geometry) are still unmeasured placeholders, so IK mode can't be accurate yet. Measure them in millimetres and fill them in.
The Mind button (Python controller) hands the arm to her LLM mind: she looks, thinks out loud on screen, says what she's doing in her own voice (patient, dry, Zira slow and low), moves a joint, and remembers. Only on an x86 PC with 8 GB+ RAM and a GTX 1050 or better (mind.py checks), with Ollama's qwen3 (sized to the GPU) and a vision model (gemma3).
- She watches herself through a webcam pointed at the arm (
--camera N, default 0). The vision model lists what's in the picture and she decides from that list whether her arm is there. (Asked "is there a robot arm?", a small vision model says yes to a dark frame or an empty desk, so it's never asked that.) - When she can't see herself she's very hesitant: a dark, covered or blank picture, no camera, or no arm in what the model sees, and every move becomes one 3° nudge, whatever she planned, and she says why. Seeing herself, she moves in 10° steps.
- Pick something up: choose what in the Mind panel: a colour button (red, orange, yellow, green, blue, purple), click anything in the live camera preview to take its colour, or type what it is in the text box (Enter). A colour is found with OpenCV in every frame and outlined in the preview; a typed thing is found in the vision model's list. Her mind gets where it is in the picture and how big, and a how-to (centre it with the base, open the claw, reach until it's big and low, close, lift), and works it out one move at a time.
- Picking is new and needs tuning on the real arm: seeing the colour is reliable; getting the claw around it is the 4B model's job, and it's slow and clumsy at it.
Requires opencv-python (in requirements.txt) and a USB webcam.
| Motor | 1 base | 2 shoulder | 3 elbow | 4 wrist pitch | 5 wrist roll | 6 claw |
|---|---|---|---|---|---|---|
| Rest angle | 135 | 55 | 100 | 230 | 135 | 40 (open) |
The arm goes to this pose:
- When the Arduino starts up
- When no PC is controlling it. Every controller resends its angles at least every 0.5 s. If
arm.inohears nothing for 2 s (controller closed or crashed, cable pulled), the arm eases back to rest. - When you let go of the stick, if Hold position is unticked. The stick motors ease back to rest one at a time, wrist roll first, then the shoulder, then the base. Ticked (the default), letting go leaves her where she is.
- When you press Home, or Reset on the web page
The values live in two places, so change both: rest in config.json (the controllers) and REST_ANGLE in scripts/arm/arm.ino (the Arduino, reflash after changing).
The same list opens in its own window: F1 in any of the controllers (Python, C++, C#, Rust, Go and Julia), or python scripts/keymap.py. They all take the keyboard, the joystick and a gamepad the same way, and notice a joystick or gamepad plugged in or out while they run. The C++ controller reads gamepads through XInput (Xbox pads, a Logitech F710 switched to X); the others go through SDL2, so PlayStation and Switch pads work there too. The other controllers' F1 window reads scripts/keymap.json: after changing the list in keymap.py, write it again with python scripts/keymap.py --json scripts/keymap.json.
Joystick (Joystick mode)
| Key | Does |
|---|---|
| Stick left / right (axis 0) | motor 1, base |
| Stick forward / back (axis 1) | motor 2, shoulder |
| Twist (axis 2) | motor 5, wrist roll |
| Hat up / down | motor 3, elbow: 5 degrees a press |
| Button 3 / button 2 | motor 4, wrist pitch: 5 degrees a press |
| Trigger (button 0) | motor 6, claw: a click closes / opens it (Hold position), or held = closed |
| Hold position (tick box) | ticked: the stick sets speed and letting go holds her where she is; unticked: the stick sets position and letting go sends her back to rest one joint at a time, wrist roll first (Python and C++ controllers; the others: centred = rest) |
| a held hat direction or button is one press; motors 3 and 4 stay where you stepped them | |
| every motor stays inside its min / max from config.json; invert flips its direction |
Gamepad (Joystick mode, when no joystick is plugged in)
| Key | Does |
|---|---|
| Left stick across / up-down | motor 1, base / motor 2, shoulder |
| Right stick across | motor 5, wrist roll |
| D-pad up / down | motor 3, elbow: 5 degrees a press |
| Y (3) / X (2) | motor 4, wrist pitch: 5 degrees a press |
| A (0) | motor 6, claw: like the trigger |
| a joystick always wins over a gamepad; plug either in any time |
Keyboard (every mode except IK; click the window first)
| Key | Does |
|---|---|
| A / D | motor 1, base: -5 / +5 degrees |
| W / S | motor 2, shoulder: up / down 5 degrees |
| R / F | motor 3, elbow: up / down 5 degrees |
| T / G | motor 4, wrist pitch: up / down 5 degrees |
| Q / E | motor 5, wrist roll: -5 / +5 degrees |
| Space | motor 6, claw: held = closed, released = open |
| Shift + any of those | 1 degree steps, for fine positioning |
| Home | every motor back to the rest pose |
| Mind (button) | her LLM mind picks things up: a colour button, a click on the camera, or type what (Python controller) |
| 1 - 9 | pick a macro |
| F1 | this window |
| Esc | quit |
| holding a key repeats it; in Joystick mode a key moves a stick motor until you move that stick again |
- Keyboard controls in the C++, C#, Go, Rust and Julia controllers
- Gamepad (Xbox/PlayStation) mapping: two sticks for the arm, triggers for the claw, bumpers for wrist roll
- Pick-and-place challenge: a timer in the window for moving an object from A to B, best run saved as a macro
- Preset macros (wave, bow) on single keys
- Phone tilt control on the web page: tilt to steer the base and shoulder
- Mirror mode: one controller driving two arms through
--connect
- Her arm, drawn: a panel beside the controls draws her from her own geometry (
config.json) and joint maths (IK_controller.py): from the side (base, shoulder, elbow, wrist, claw, her reach as a circle) and from above (the base turning), following every command. With no Arduino plugged in it's her emulator: everything still works, and you see what she would do. UntilkinematicZerois calibrated she draws her rest pose as level. - THEME: DEFAULT, LIGHT or CUSTOM, all from
colour_scheme.xml; CUSTOM puts its colours inscripts/controller_settings.jsonfor you to change. - VOICE: she talks through this PC outside MIND too (Zira, slow and low, dry): modes, recording, playback,
the claw. Her MIND log:
~/.robot_minds/logs/ARM-<date>.log. - Every launcher comes in
.bat,.ps1and.sh(the C++ controller is a Windows program: its.shsays so).
Nothing here needs a hard-coded COM port. scripts/arm/arm.ino answers a one-word question, so a controller can find the arm by asking:
PC: WHO
COM6: I am Arm
Every controller does this when you leave out --port: it opens each port (the compiled controllers only try ports that look like Arduino adapters) at 115200 baud, waits about 2 seconds for the board to reset, sends WHO, and uses the port that answers I am Arm. If none answers it falls back to the old guess (the first port whose description looks like an Arduino, CH340, CP210x or FTDI adapter). --port COM6 still overrides all of it, and --list-ports lists what's there.
- You must reflash the board with the current
arm.inobefore it can answer. - Sharing one PC with DREAM. Her sensor board (
dream_sensors.ino, 9600 baud) answersWHOwithI am Dreamthe same way, so both projects can run from one computer and each finds its own board, whatever COM numbers Windows hands out. Only one program can hold a serial port, so run one controller per arm. - Python:
scripts/board_id.pydoes the asking (python scripts/board_id.pylists every port and who answered). C++, Go, Rust and Julia ask only the ports that look like Arduino adapters, so they skip Bluetooth ports that can take a long time to open. C# can't read USB descriptions, so it asks every port at once. - Asking a port takes about 3 seconds, so
--portis faster when you know it.
Every UI takes its colours from one file, colour_scheme.xml in the repo root. Edit a value and restart the controller. The compiled controllers read it at start-up too, so colour changes need no rebuild (build once first to get the code that reads it).
<colour name="background" value="#33292F"/>
<colour name="button_active" value="#9C0060"/>Write each colour on its own line as name="..." value="#RRGGBB" (name first; the C++, Rust and Julia loaders read it line by line). A missing file, or a missing or malformed colour, falls back to the built-in default for that one colour.
| Role | Default | Used for |
|---|---|---|
background |
#33292F |
Window and page background |
panel, border |
#331F2B, #361529 |
Rows, list boxes, outlines |
text, text_dim |
#FFFFFF, #C9B6C1 |
Main text, status lines |
button, button_hover, button_active, button_disabled |
#361529, #691548, #9C0060, #331F2B |
Buttons (active = the selected mode) |
accent, accent_hover |
#9C0060, #C21F82 |
Slider knobs, highlights, values |
track, selected |
#691548, #691548 |
Slider track, the chosen macro |
danger, danger_hover, warn |
#963232, #AD3A3A, #F08C3C |
Record and reset, warnings |
How each front end applies it:
| UI | How |
|---|---|
Python window (controller.py) |
scripts/colour_scheme.py supplies the colours |
| Web page | The controller serves /colour_scheme.xml; app.js applies it over style.css's defaults |
| Julia | Read into the drawing constants at start-up |
| Rust (egui), Go (Fyne) | Applied as the toolkit's theme |
| C# (Avalonia) | Overrides the Fluent theme's button, slider and list colours |
| C++ (Win32) | Window, labels and list box, with owner-drawn buttons. Buttons don't show a hover colour, and slider thumbs keep the Windows look |
The Go, Rust, C# and Julia versions were not built or run when this was added, so the first build is worth a look.
scripts/cpp_controller/ is a native Win32 rewrite of controller.py with the same four modes (Joystick/Sliders/IK/Fleet) and the same macro recording/playback, for anyone who'd rather run a standalone .exe than install Python/pygame. It reads the same config.json, records to the same scripts/macros/*.json, and its Fleet mode serves the same scripts/web/ control page and HTTP API - the two controllers are drop-in equivalents of each other.
build_cpp_controller.bat
cpp_controller.bat --list-ports
cpp_controller.bat --port COM6 --mode fleetbuild_cpp_controller.bat compiles it with g++ (MinGW-w64; MSYS2's C:\msys64\ucrt64 comes first if it's installed, since an old GCC 6 MinGW can't build C++17) if found on PATH, falling back to CMake + MSVC otherwise, producing scripts/cpp_controller/build/controller.exe. It's linked statically, so it runs without MinGW's DLLs (picking up a mismatched libstdc++-6.dll from PATH, such as Git's, made it crash on start). It only depends on the Windows SDK (Win32, Winsock, winmm, XInput, SetupAPI) - no other libraries to install.
scripts/julia_controller/ is a Julia rewrite of controller.py, for anyone who'd rather use Julia than Python or C++. Same four modes (Joystick/Sliders/IK/Fleet), same macro recording/playback, same config.json/scripts/macros/*.json/scripts/web/ - a drop-in equivalent of the other two controllers. It draws its window with SDL2 (via SimpleDirectMediaLayer.jl, the same library pygame itself is built on) and talks to the Arduino over LibSerialPort.jl; Fleet mode's HTTP bridge is HTTP.jl.
julia_controller.bat --list-ports
julia_controller.bat --port COM6
julia_controller.bat --port COM6 --mode fleetjulia_controller.bat installs the Julia package dependencies on first run (Pkg.instantiate(), using scripts/julia_controller/Project.toml) and then launches controller.jl - no separate build step, same as the Python controller. Requires Julia on PATH.
scripts/csharp_controller/ is a C# / .NET 8 rewrite of controller.py that runs on both Windows and Linux. Same four modes (Joystick/Sliders/IK/Fleet), same macro recording/playback, same config.json/scripts/macros/*.json/scripts/web/ as the other controllers. The window is Avalonia (cross-platform, built in code rather than XAML), joystick input goes through SDL2 via Silk.NET.SDL (which bundles the native SDL2 library), serial is System.IO.Ports, and Fleet mode's HTTP bridge is Kestrel (ASP.NET Core), so it binds to the network without needing admin rights.
csharp_controller.bat --list-ports
csharp_controller.bat --port COM6
csharp_controller.bat --port COM6 --mode fleetchmod +x csharp_controller.sh
./csharp_controller.sh --list-ports
./csharp_controller.sh --port /dev/ttyACM0Requires the .NET 8 SDK; the launchers run it with dotnet run, which restores NuGet packages on first run.
Notes:
System.IO.Portscan't read USB device descriptions like pyserial does, so it asks every portWHOat once and uses the one that answersI am Arm(Finding the arm's port). If none answers, it falls back to a simpler guess: on Linux the first/dev/ttyACM*//dev/ttyUSB*, on Windows only when there's exactly one COM port. Otherwise pass--port.- Linux permissions: add yourself to the
dialoutgroup for serial access (sudo usermod -aG dialout $USER) andinputfor joystick access, then log out and back in. - If SDL2 fails to load on Linux, install it from your package manager (e.g.
sudo apt install libsdl2-2.0-0).
scripts/rust_controller/ is a Rust rewrite of controller.py for Windows and Linux: same four modes, macros, live sync and --connect client mode as the others, in one native executable. The window is egui (via eframe), serial is the serialport crate (USB descriptions are read, so --port auto-detects an Arduino/FTDI/CH340 like the Python version, preferring the port that answers I am Arm), and Fleet mode's HTTP server is tiny_http. There is no C library to build against: SDL2 is loaded at runtime just for the joystick, so if it isn't found the controller still runs with Joystick mode disabled.
rust_controller.bat --list-ports
rust_controller.bat --port COM6 --servechmod +x rust_controller.sh
./rust_controller.sh --port /dev/ttyACM0 --serveRequires Rust (the first build downloads and compiles the dependencies, which takes a few minutes). Joystick support needs the SDL2 shared library: on Windows put SDL2.dll (from the SDL2 releases, SDL2-2.x.x-win32-x64.zip) next to the built arm_controller.exe or anywhere on PATH; on Linux sudo apt install libsdl2-2.0-0. Linux serial/joystick permissions are the same as for the C# controller (dialout and input groups).
scripts/go_controller/ is a Go rewrite of controller.py for Windows and Linux with the same four modes, macros, live sync and --connect client mode as the others. The window is Fyne, serial is go.bug.st/serial (USB descriptions are read, so --port auto-detects an Arduino/FTDI/CH340, preferring the port that answers I am Arm), and Fleet mode's HTTP server is the standard library's net/http. SDL2 is loaded at runtime (via purego, no cgo for that part) just for the joystick, so a missing SDL2 only disables Joystick mode.
go_controller.bat --list-ports
go_controller.bat --port COM6 --servechmod +x go_controller.sh
./go_controller.sh --port /dev/ttyACM0 --serveRequires Go and, because Fyne uses cgo, a C compiler (Windows: winget install BrechtSanders.WinLibs.POSIX.UCRT; Linux: sudo apt install gcc libgl1-mesa-dev xorg-dev). The first build downloads and compiles Fyne, which takes a few minutes. Joystick support needs the SDL2 shared library, same as the Rust controller (Windows: SDL2.dll next to the executable or on PATH; Linux: sudo apt install libsdl2-2.0-0).
This arm can join the RIFT fleet dashboard, the same way MILA/WHIP/NORA/KIDA do. Both controllers' Fleet mode bridges RIFT's HTTP protocol to the Arduino's USB serial connection: it serves /status, /cmd?motor=&angle=, and /reset, and heartbeats a /register call to RIFT/NORA so this arm ("ARM", port 5011) shows up in the dashboard.
The same HTTP server also serves a small web control page (scripts/web/index.html/style.css/app.js) at / - open http://<this machine's IP>:5011/ in any browser on the network for a per-motor slider UI, no RIFT required.
run.bat is the entry point: the first time it creates venv\ and installs requirements.txt (again only when that file changes), then starts the controller with whatever arguments you give it. controller.bat and fleet_server.bat go through it too.
run.bat --list-ports :: find the Arduino's port
run.bat --port COM6 --mode fleetWhichever controller owns the Arduino's serial port keeps one shared set of motor angles. Its own window (sliders, joystick, IK), the browser page on your phone, and RIFT all read and write that same set, so setting Motor 1 to 140 anywhere shows up everywhere: the desktop sliders move, the web page's sliders move, and the arm moves. The rule is "last writer wins": whichever input changed most recently sets the angle, and inputs you aren't touching just follow it.
The Python and C++ controllers start the HTTP server and join RIFT whenever they run, in any mode (Sliders, Joystick, IK), like every other robot - so ARM always shows up in RIFT and the phone stays in sync with the desktop window. Add --no-serve to keep it to yourself (the Go/Rust/Julia/C# controllers still need --serve):
python scripts/controller.py --port COM6 --serve
cpp_controller.bat --port COM6 --serveThe window's status line then shows the address to open on the phone. Only one controller can hold the serial port, so the phone/browser connects to that one; the web page polls four times a second. In Joystick mode an axis only overrides the shared value while you're actually moving it, so untouched axes follow the phone.
Any controller can also run as a client of another one, with no serial port of its own: --connect http://<hub>:5011 mirrors the hub's angles (10 times a second) and sends this window's changes back, so a Python window on a laptop, the C++ window on the PC and a phone browser all move together. The hub is whichever controller has the Arduino plugged in and --serve (or Fleet mode) on. Connecting never moves the arm - the client adopts the hub's angles first - and it works from every controller, including different ones (a C++ client on a Python hub, etc.).
# PC with the Arduino (the hub)
cpp_controller.bat --port COM6 --serve
# laptop, phone-independent second window
python scripts/controller.py --connect http://192.168.0.10:5011--connect can't be combined with --serve or --mode fleet.
The robots talk in Brainfuck: every phrase is a Brainfuck program that prints its words (hello is ++++++++[>+++++++++++++<-]>.+. → hi), and a robot says it by beeping the program on its buzzer, one tone per symbol, pitched to its own voice. The phrasebook (talk_bf.h) is generated by RIFT's Fleet/brainfuck_talk.py: phrases 0–6 (hello, how are you, happy, curious, sleepy, let's play, bye) and their replies 7–13. RIFT runs the conversations between robots over WiFi and logs them on its dashboard; it asks ARM to speak with GET /chirp?u=<0-13>. controller.py (Fleet mode) and the C++ controller queue it for the Arduino as TALK:<u>, beeped on the new buzzer on D8 a little lower than the others. They register talk:<fleet port> so RIFT can find her.
ARM is the YFROBOT / Makerfabs 6-DOF disc-arm frame (the Makerfabs PICO arm uses the same one). The link lengths in config.json's "geometry" come from the kit's dimension drawing (about ±3 mm): baseHeight 72 (table to shoulder pivot), upperArmLength 104, forearmLength 143, wristLength 173 (wrist pivot to closed fingertips).
IK measures each joint from an optional per-motor "kinematicZero": the raw angle with the base pointing forward, the upper arm level and the forearm and wrist in line. Until it's set, IK falls back to "rest" (a folded pose, so most targets read as unreachable). "kinematicSign" (1 or -1) says whether a bigger raw angle bends that joint up. The Python and C++ controllers use kinematicZero; the C#, Go, Rust and Julia ones still measure from rest. To calibrate: put the arm in that straight pose in Sliders mode, read motors 1-4, and add them to config.json.
Per-motor servo channel, angle range, resting angle, and invert flag all come from config.json (see scripts/motor_config.py) - the same config every controller uses. .bat launchers (controller.bat, fleet_server.bat, build_cpp_controller.bat, cpp_controller.bat, julia_controller.bat, csharp_controller.bat/.sh) live at the repo root alongside config.json; the Python, C++, Julia, and C# sources themselves stay under scripts/.
Python controller, C++ controller, Web control page. Captured without the arm plugged in, so the controllers are in display-only mode at the rest pose.
Minds — DREAM companion · TINA engineer · NINA caretaker · RIFT fleet hub · LYCEA training school
Robots — NORA · MILA · WHIP · KIDA-00 · KIDA-01 · IDA · ARM · NIKA
Remotes — RITA handheld remote


