View Overview
RIFT is the centralized command-and-control backbone of the DREAM robotics ecosystem. Built with Python and Flask, it serves as a high-speed telemetry hub that bridges the gap between various hardware platformsβlike WHIP, NORA, and KIDAβand the user interface. By utilizing a unified communication protocol, RIFT allows for seamless fleet management and synchronized multi-agent operations.
Core Features
- Fleet Dashboard: Real-time monitoring and control of multiple robots from a single interface.
- Protocol Bridging: Seamlessly translates commands between PC, Android, and diverse microcontroller platforms.
- Autoconnect Hub: Logic-based routing for local and remote instances (localhost:5000 through 5006).
- Telemetry Visualization: Live data streaming from IMU (MPU6050) and Ultrasonic sensors across the fleet.
This system uses NORA-Robot-v00 as a central hub, while human devices like phones and PCs act as control interfaces. DREAM can also assist with verbal communication between users and robots.
- Microcontrollers: ESP32, Arduino IDE
- PC & Mobile Apps: Android Studio, MinGW (Windows/Linux)
- Operating Systems: Raspberry Pi OS, Ubuntu, Windows, Android
RIFT: :5000
DREAM: :5001 (her own site - DREAM on your phone)
DREAM: :5009 (DREAM's dashboard and API, HTTPS)
NORA: :5002
KIDA-00: :5003
KIDA-01: :5004
WHIP: :5005
MILA: :5010
NIKA: :5013
ARM: :5011run.bat starts the Python hub (app.py): it sets up venv\ and installs requirements.txt the first time (again only when that file changes). That's the one with the conversations and the mission log. run_server.bat starts the native C++ hub instead (and the Go, Rust, Julia and F# hubs have their own run_server_*.bat); they share the same dashboard but only have the fleet registry, so the Conversations and Mission Log sections stay hidden there.
RIFT's own WiFi network, "RIFT", is where every robot lives. A router broadcasts it, and the machine running RIFT is its hub at 192.168.4.1: it hands out the robots' addresses, shares its internet with them if it has any, and runs the fleet registry on port 5000. That's the address every robot already registers with, so they find RIFT without knowing anything else. NORA used to be the hub. She's now a normal robot, and hosts her own "NORA" network only as a fallback when RIFT isn't around.
internet (WiFi dongle, or another port) ββΊ hub: RIFT at 192.168.4.1:5000
β Ethernet
βΌ
Mercusys MW302R, Access Point mode, WiFi "RIFT" (2.4 GHz, WPA2)
β² β² β² β² β² β²
NORA MILA WHIP KIDA00 KIDA01 ARM's controller
The hub can be Windows, Kubuntu or a Raspberry Pi 3 (Raspberry Pi OS Bookworm): anything with an Ethernet port. Your home network stays as it is. The robots can reach out to the internet through the hub, but nothing on your home network can reach the robots, except RIFT's dashboard.
Set it up
1. The router (Mercusys MW302R, official guide)
- Plug the hub's Ethernet port into the router's WAN port.
- Join the router's WiFi (the name and password on its sticker) and open http://mwlogin.net.
- Change Mode (top right) β Access Point Mode. It reboots.
- WiFi name RIFT, security WPA2-PSK (AES), password
mmvbkbak(the fleet's: it's in every robot'ssecrets.h). The ESP robots can't do WPA3. - Change the router's admin password, and turn WPS off if it's offered.
2. The hub. From the RIFT folder:
| Hub | Command |
|---|---|
| Kubuntu / Raspberry Pi | ./hub/setup_hub.sh --dry-run to look, then ./hub/setup_hub.sh --service (RIFT also starts at boot) |
| Windows | in PowerShell as Administrator: .\hub\setup_hub.ps1 -DryRun to look, then .\hub\setup_hub.ps1 |
Both scripts say what they'll change and ask first. They're safe to run again, and --undo / -Undo takes it all back.
- With internet (a WiFi dongle or another port), the hub shares it with the robots.
- Without internet, the robots still get addresses and everything in the fleet works, just offline. On Windows with no uplink, put the router in its normal Router mode instead, with its own address handing (DHCP) on.
3. The robots join "RIFT" and register with 192.168.4.1:5000. If RIFT isn't there they use NORA's fallback network, and they rejoin RIFT when it comes back.
- ESP robots (NORA, MILA, WHIP): their
secrets.hnext to the sketch has RIFT's password (mmvbkbak), NORA's fallback password (mmvbkbak, the same on every robot) and the robot's PIN (1234); then flash. Without asecrets.hthe sketch still builds, withsecrets.example.h's placeholders that can't join RIFT. - Pis (KIDA00, KIDA01):
sudo nmcli dev wifi connect RIFT password 'mmvbkbak'andsudo nmcli dev wifi connect NORA password 'mmvbkbak', thensudo nmcli con modify RIFT connection.autoconnect-priority 20andsudo nmcli con modify NORA connection.autoconnect-priority 10, so they prefer RIFT. - PC programs (DREAM, TINA, NINA, ARM's controller): their
RiftHostsetting is127.0.0.1on the hub itself,192.168.4.1from any other machine.
Setting up the hub with Claude Code
Open Claude Code in the RIFT folder on the hub machine and paste this:
This machine is going to be the RIFT fleet hub. Read README.md, section "The fleet network: RIFT is the hub",
then help me set it up:
1. git pull first. Check which OS this is (Windows / Kubuntu / Raspberry Pi OS) and which network adapters
there are: the Ethernet port that goes to the router, and the internet uplink (WiFi dongle or other port).
2. Run the hub setup script in dry-run mode and show me what it would change. Wait for my OK, then run it.
3. Start RIFT (run.bat / run.sh, or the rift service on Linux) and check that http://192.168.4.1:5000/robots
answers from this machine.
4. Once I've switched a robot on, check that it shows up in /robots, and help me find out why if it doesn't.
Don't flash any robot without asking me.
RIFT has the robots chat: every 25β60 s it picks two that can talk and has the first say a phrase and the second answer. 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, in its own voice.
- Phrasebook:
Fleet/brainfuck_talk.pybuilds the shortest program it can for each phrase (0β6: hello, how are you, happy, curious, sleepy, let's play, bye) and reply (7β13), checks each with an interpreter, and writes the sametalk_bf.hinto every robot's sketch plusFleet/talk_bf.json. EditPHRASESand rerun it to change what they say. - Who talks: robots that register a
talk:<port>capability (with RIFT or NORA) and NORA herself; RIFT asks them withGET /chirp?u=<0-13>. - Personalities and moods: each robot has its own chattiness and favourite phrases (
Fleet/conversations.py), and a mood that drifts with the time of day and how its chats go - bright robots suggest playing, sleepy ones yawn. - With context: each pair of robots has a visit. They say hello first (again after half an hour apart), usually ask how the other is next, don't repeat what was just said, and only say goodbye after a few exchanges - then leave each other alone for 10 minutes.
- Startup sounds: KIDA00, KIDA01 and NORA play a sound when their programs start, so nobody talks to them until they've been online 12 s (
STARTUP_QUIET_S). A robot that drops out and comes back waits again. - Busy is busy: a robot that answers
/chirpwith 409 (TINA while she's working, or just after you used her) is noted as busy and left out for a minute - not logged as "no answer", and nobody's mood drops. - Real conversations: robots with a mind (DREAM, KIDA-01, TINA register
peer) play rock paper scissors, and the winner picks words or Brainfuck (peer_talk.py); RIFT relays the turns. - Log:
GET /talkreturns the shared conversation log and moods (NORA's IR chats with IDA are merged in);POST /talkstarts one now. The dashboard's Conversations section shows who said what, the program and what it prints.
RIFT conducts: POST /music {"action": "play", "track": 100} and every player starts the same song at the same moment. RIFT picks a start 4 s ahead on its own clock and sends each player POST /music {"action", "track", "at"}. Each player sets its clock by RIFT's (GET /clock), loads the song, and starts exactly then, so a slow network doesn't put them out of step. pause, resume and stop work the same way, and GET /music lists the players and the last command.
The dashboard's Wheel locks section locks every robot's wheels (WHIP's legs) at once, like an e-stop, and unlocks them all with the fleet PIN (RIFT_FLEET_PIN, 1234 by default). POST /locks {"action": "lock"} needs nothing; {"action": "unlock", "pin": "1234"} checks the PIN first and passes it on to each robot as hers. NORA, KIDA-00, MILA, NIKA and WHIP get /motorlockOff?pw=, KIDA-01 her /action motor_lock_off, and IDA hears it over IR through NORA's /link (or RIFT's own board when NORA's away), one way, so she can't say whether it arrived. MILA, NIKA and WHIP get it over IR too when they're offline or their firmware is older than their WiFi lock. ARM has no wheel lock. Unlocking stays a person at the dashboard: /talk's relay still never sends one (Fleet/locks.py). Only the Python hub has it; the section hides on the others.
- Players register the capability
music:<port>. TINA plays along now (started 0.4 ms after the set moment in the first test); KIDA00, KIDA01 and NORA are next. - Tracks are numbered the same everywhere: the number in the file name, as on NORA's SD card (
NORA-Robot-v00/music:Track000.mp3,Track100.mp3, ...). - Code:
Fleet/music.py.
- Fleet: the dashboard lists every robot online - RIFT's own registry, NORA's (where most robots register) and NORA herself - each with an Open button for its own web page. The address comes from a
web:<port>capability if the robot registers one, else its usual port (NORA 5002, KIDA00 5003, KIDA01 5004, WHIP 5005, DREAM 5009 over HTTPS, MILA 5010, ARM 5011).GET /fleetreturns the same list as JSON. - Mission log: RIFT keeps the fleet's diary (
Fleet/mission.py): who came online or went quiet, who said what to whom, whose mood changed, counted in mission days from the first entry. It's saved tomission_log.json, so the mission carries on across restarts.GET /missionreturns it; a robot can write its own entry withPOST /mission {"who": "DREAM", "text": "..."}.
An Arduino UNO on the hub PC (arduino/rift_link) gives RIFT what NORA has: an IR transmitter to drive and greet IDA, MILA, WHIP and KIDA-01 over the fleet IR link, an IR receiver that reports every frame it hears (NORA's "I am here" beacon, the robots' phrases, any remote), and a buzzer that speaks the Brainfuck phrases in the hub's own low voice.
| Part | UNO pin |
|---|---|
| IR LED (940 nm), via 100 Ξ© (or an NPN transistor for range) | 3 |
| IR receiver OUT (VS1838B / TSOP38238, 5 V) | 2 |
| Passive buzzer | 8 |
RIFT finds the board by the fleet handshake (WHO β I am Rift), 25 s after it starts and only on ports nobody else has open, so it never steals ARM's or DREAM's board. Without the board everything below just answers 503.
GET /board: connected or not, its port, the last IR frames heardGET /board/link?r=ida&c=fw: drive a robot (fw bw left right stop auto manual speed, andlock/unlockits wheels)GET /board/say?r=mila&p=0: beep a phrase (0-6), then send it to that robot over IRGET /board/talk?u=7: beep a Brainfuck utterance (0-13)
View How to Run
sudo snap install android-studio --classic
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txtRuns the fleet registry, mDNS discovery (including browsing for DREAM), and the RIFT/NORA integration threads (fleet-authority heartbeat, internet share) all in one Python process β no compiling required.
python app.pyThis is a full alternative to the native fleet server below: both speak the
same /robots + /register protocol on port 5000, so run one or the other,
not both, on a given machine.
network-discovery.cpp is RIFT's native fleet-registry server: it publishes
itself on the LAN via mDNS/Avahi (_rift._tcp), browses for other RIFT
instances, and serves the /robots + /register endpoints that
registration.cpp, PC App/PyGame/registration.py, and Fleet/register.py
all talk to. registration.cpp is the subnet-scanning client that queries
/robots.
./build.shThis installs the required apt dev packages (libcurl4-openssl-dev,
libavahi-client-dev, nlohmann-json3-dev, libcpp-httplib-dev) if
missing β you'll be asked for your sudo password β then builds both
binaries into PC App/App/bin/.
To start the fleet server once built:
./run_server.shOr build manually with the Makefile:
cd "PC App/App"
makeg++ registration.cpp -o registration.exe -lcurlapp.py is the reference fleet hub. PC App/Go, PC App/Rust, PC App/FSharp (.NET 8) and PC App/Julia are complete, cross-platform re-implementations of it - the same protocol on the same port 5000, so run one hub per machine, whichever language you prefer. Each one has:
- the fleet registry:
POST /register(form body, or the JSON thatAndroid App/examples/Registration.ktsends),GET /robots(entries expire after 20 s without a heartbeat),GET /ping GET /peers, plus mDNS publish/browse of_rift._tcpand DREAM's_flask-link._tcp, so hubs (and DREAM) find each otherGET/POST /mode: the WiFi/Bluetooth connection mode, and the NORA fleet-authority heartbeat that goes with it (HTTP to192.168.4.1:5000, orH<name>:<caps>over a Bluetooth serial port)- the same dashboard:
templates/index.htmlandstatic/are served as they are, no template engine needed - the NetworkManager internet share for NORA's AP (Linux only, like
Fleet/internet_share.py) --scan, the subnet scanner (whatregistration.cpp/registration.pydo): finds every hub or robot answering/robotson this machine's /24
| Language | Launch | Needs |
|---|---|---|
| Go | run_server_go.bat / ./run_server_go.sh |
Go |
| Rust | run_server_rust.bat / ./run_server_rust.sh |
Rust |
| F# | run_server_fsharp.bat / ./run_server_fsharp.sh |
.NET 8 SDK |
| Julia | run_server_julia.bat / ./run_server_julia.sh |
Julia (installs its packages on first run) |
./run_server_go.sh # start the hub on :5000
./run_server_rust.sh --scan # scan this subnet for hubs/robots
./run_server_fsharp.sh --name RIFT2 --port 5010 --no-internet-share # a second hub for testingOptions (all four): --name, --port, --nora-host, --nora-port, --heartbeat-secs, --ttl-secs, --no-heartbeat, --no-mdns, --no-internet-share, --root, --scan. On Linux, Bluetooth mode needs the serial port bound to the NORA pairing (e.g. sudo rfcomm bind 0 <NORA_MAC> β /dev/rfcomm0), and the internet share needs NetworkManager (nmcli); like app.py, the hub tries to join NORA's AP whenever it starts unless --no-internet-share is given.
All four were checked against the same protocol test suite and against each other and the original app.py: each hub lists the others (and app.py) as mDNS peers, and every robot heartbeat - including ARM's real controllers - lands in /robots.
The same protocol (/ping, /register, /robots, /peers, /mode, dashboard, /static, NORA heartbeat) is also implemented in:
-
PC App/App(C++) - Windows + Linux, no Avahi/curl/nlohmann needed. Build withbuild.sh/build.bat(Windows links statically so the exe runs from any shell). -
Unity App/RIFT- the hub as an app (Unity 6000.6.2f1). It starts by itself, with a panel showing a card per robot (its page, and STOP for robots that takedo:stop), the other hubs, the log, and settings kept between runs (name, port, NORA's address, heartbeat / mDNS / internet share). Build it from the editor's RIFT menu (Build Windows / Android / Linux), or without opening it:build_unity.bat(Windows, orbuild_unity.bat Android) /./build_unity.sh(Linux); builds land inUnity App/RIFT/Builds. A build carries its own copy oftemplates/,static/and the colours (packed into StreamingAssets at build time), so it runs anywhere. The hub code is plain C# inAssets/Rift. Bluetooth mode needs the project's Api Compatibility Level set to .NET Framework. -
Android App/- the RIFT app for Android 8+ (Kotlin, an Android Studio / Gradle project). The phone runs the hub as a foreground service; the app has an on/off switch, the hub's address (tap to copy), the fleet list, a settings button (name, port) and RIFT's dashboard underneath, served by the phone itself. Besides the protocol above it serves/fleet,/clock,/colour_scheme.css,/camerasand the dashboard's Ask & tell (POST /talkwithask/do, withFleet/relay.py's safety tiers; it relays to robots that registerdo:<action>). Conversations, music, the mission log and/nearstay on RIFT's PC: the dashboard hides those sections. The hub code (RiftCore.kt,RiftRelay.kt,RiftMdns.kt,RiftAuthority.kt,NetworkDiscovery.ktinapp/src/main/java/com/cursedentertainment/rift/) is plain JVM;RiftAndroid.ktadds the service, the screen and the Bluetooth link (in Bluetooth modebt_portis NORA's paired MAC address). Every build copiestemplates/,static/andcolour_scheme.xmlinto the app, and makes its colours and icon fromcolour_scheme.xmlandimages/rift_avatar.jpg, so nothing is copied by hand.Build and install: open
Android Appin Android Studio and press Run, or from a terminal (JDK 17):cd "Android App" gradlew assembleDebug # ./gradlew on Linux/macOS adb install -r app/build/outputs/apk/debug/app-debug.apk gradlew test # the hub core's 28 protocol tests, on the PC
Android Studio writes
local.properties(the SDK path); from a terminal create it yourself withsdk.dir=C:/Users/<you>/AppData/Local/Android/Sdk, or setANDROID_HOME.
Android App/examples/Registration.kt (an example robot registering itself) posts JSON, which every hub above accepts (only the original app.py is form-only).
Verified: the C++, Unity (C# on a desktop runtime with Unity stubs) and Android (core on a JVM) hubs pass the same 32-check protocol suite and interoperate over mDNS/heartbeat with the Go/Rust/F#/Julia hubs. Not run on real hardware: Linux-only code paths, Bluetooth, internet sharing, and the Android app on a phone (it builds against API 34 and its hub core passes gradlew test, but hasn't been installed on a device yet).
Every robot sends a small "I'm here" beacon twice a second and listens for the others'. From the signal strength it knows roughly how close each one is, and from how that changes over time whether it's coming closer, steady or leaving:
| Signal | Zone |
|---|---|
| above β45 dBm | very close |
| β45 to β60 dBm | near |
| β60 to β75 dBm | medium |
| below β75 dBm | far |
It's coarse (walls, bodies and antenna angle all change it): for "who's around", not distance. Precise collision avoidance stays with the ultrasonic and ToF sensors.
They tell each other what they're doing, because a rising signal looks the same from both sides even when only one robot moves. Over ESP-NOW they say it in Brainfuck, like the fleet's conversations: each beacon carries a program that prints park, go, wait or hand (a human is driving), and the receiver runs it. Bluetooth adverts are too small for a program, so BLE carries the same state as one byte.
Who makes way, in self-driving modes only:
| The other robot... | So this one... |
|---|---|
| is parked | is the one closing in: steers away |
| is yielding | carries on, carefully |
| is driven by a human | makes way (it's unpredictable) |
| drives itself | follows the alphabet: KIDA00, KIDA01, NORA, WHIP; everyone makes way for MILA and NIKA, who can't hear the others |
| is leaving | carries on |
Making way = stop for 2 s, turn away, then drive on (and not yield again for 5 s, so two robots that stay close don't take turns forever). While another robot is near, or coming closer, it drives slower with wider margins.
RIFT gathers every robot's /near into one picture at GET /near and the dashboard's Who's near whom section, and decodes the Brainfuck each robot heard over ESP-NOW (with Fleet/brainfuck_talk.py's interpreter, step-limited so a garbled program can't hang it). On a Pi, RIFT also beacons over Bluetooth LE as RIFT (always parked) and hears the robots itself; on a PC it only listens. Code: Fleet/near_map.py, Fleet/fleet_near.py.
Fleet dashboard. Captured without a robot connected, so live values show their offline state.
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
