# ExoMy CUNO — Software `CUNO` steht für `Celestian Unified Navigation and Observation`. ExoMy Mars-Rover auf Basis eines Raspberry Pi mit ROS1 Melodic, betrieben vollständig in einem Docker-Container. --- Die Software liegt in diesem Repository direkt an der Wurzel. Nicht zur Software gehörende CAD-, Datenblatt- und Anleitungsbestände wurden bewusst entfernt. ## Zugang zum Raspberry Pi | | | |---|---| | **Modell** | `Raspberry Pi 4 Model B Rev 1.4` | | **Hostname** | `cuno` | | **Benutzer** | `pi` | | **SSH-Passwort** | `Sonneberg` | | **LAN-IP (`eskimue.de`)** | `192.168.1.83` | | **WLAN-IP (`eskimue.de`)** | `192.168.1.9` | | **WLAN-IP (`4pi`, SSH)** | `10.42.44.150` | | **Fallback-AP** | SSID `CUNO`, IP `192.168.50.1`, Passwort `astr0cun042` | ```bash # im WLAN "eskimue.de" ssh pi@192.168.1.9 # im WLAN "4pi" ssh pi@10.42.44.150 ``` --- ## Architektur ``` Web-GUI (Port 8000) │ WebSocket (Port 9090) ▼ rosbridge_websocket ──► /joy ──► joystick_parser_node ──► /rover_command │ f710_joy_node ──────────────────────────────────────────────────┘ (Logitech F710, /dev/input/js0) /rover_command ──► robot_node ──► /motor_commands ──► motor_node ──► PCA9685 PWM ──► Motoren ``` Zusätzlich ist ein BNO085-IMU-Sensor auf I2C-Bus `1` mit Adresse `0x4A` angebunden. Er läuft als eigener Python-3-Service auf dem Pi (`exomy-imu-ros.service`), publiziert über `rosbridge` nach ROS und wird von der Admin-API wieder aus ROS gelesen. **Software auf dem Pi:** `/home/pi/ExoMy_Software/` **ROS-Workspace im Container:** `/root/exomy_ws/src/exomy/` --- ## Systemd-Services auf dem Pi | Service | Beschreibung | |---|---| | `exomy-admin-api.service` | Admin-API (`exomy_admin_api.py`) | | `exomy-camera-stream.service` | MJPEG Kamera-Stream | | `exomy-imu-ros.service` | Liest den BNO085 per Python 3 auf dem Pi und publiziert IMU-Daten nach ROS | | `exomy-wifi-bootstrap.service` | Wählt beim Start `4pi` oder `eskimue.de`, sonst eigener Access Point | ### WLAN-Startlogik Beim Booten sucht der Rover genau einmal nach den bekannten WLANs `4pi` und `eskimue.de`. - Wenn `4pi` sichtbar ist, verbindet er sich mit `4pi`. - Sonst, wenn `eskimue.de` sichtbar ist, verbindet er sich mit `eskimue.de`. - Wenn keines von beiden sichtbar ist, spannt er den eigenen Access Point `CUNO` auf. Nach dieser Entscheidung bleibt die gewählte Verbindung bestehen. Es gibt kein späteres Nachscannen und kein automatisches Umschalten auf ein anderes WLAN oder auf den Access Point. Erst nach einem Neustart wird erneut gesucht. ```bash # Status prüfen systemctl status exomy-admin-api.service ``` --- ## I2C-Konfiguration Beide I2C-Geräte hängen am Bus `i2c-1` (SDA = GPIO2 / Pin 3, SCL = GPIO3 / Pin 5). | Adresse | Gerät | Rolle | |---|---|---| | `0x40` | PCA9685 (Servo HAT) | PWM / Servo-Controller | | `0x4A` | BNO085 | IMU / Lagesensor | | `0x70` | PCA9685 All-Call | Broadcast-Adresse (normal) | **I2C-Geschwindigkeit: 400 kHz** (seit 2026-05-28 gesetzt, Standard war 100 kHz). Adafruit gibt 400 kHz als empfohlene Geschwindigkeit für den BNO085 an — der Sensor läuft auf dem Raspberry Pi mit dieser Einstellung zuverlässiger. Der PCA9685 unterstützt 400 kHz ebenfalls. Eintrag in `/boot/firmware/config.txt`: ``` dtparam=i2c_arm=on dtparam=i2c_arm_baudrate=400000 ``` Änderung erfordert `sudo reboot`. Danach Geschwindigkeit prüfen: ```bash cat /sys/bus/i2c/devices/i2c-1/of_node/clock-frequency | od -An -N4 -tu4 # big-endian uint32 → 2149189120 entspricht 400000 Hz ``` Beide Geräte sichtbar machen: ```bash sudo i2cdetect -y 1 # 0x40 = PCA9685, 0x4A = BNO085 ``` Verkabelung BNO085 (STEMMA QT / Qwiic): | Kabel | Signal | Raspberry Pi | |---|---|---| | Rot | VIN (3,3 V) | Pin 1 — **nicht** Servo V+ | | Schwarz | GND | Pin 6 | | Blau | SDA | Pin 3 (GPIO2) | | Gelb | SCL | Pin 5 (GPIO3) | > **errno 5 (I2C-Fehler):** Zuerst `sudo i2cdetect -y 1` prüfen. Rotes Kabel auf 3,3 V (nicht Servo V+), I2C-Kabel kurz halten, Baudrate auf 400 kHz gesetzt? --- ## Docker-Container Der Container `exomy_autostart` (Image: `exomy`) startet automatisch beim Booten. ```bash # Status docker ps # Logs docker logs exomy_autostart --tail=50 # Shell im Container docker exec -it exomy_autostart bash ``` ### ROS-Nodes im Container | Node | Beschreibung | |---|---| | `f710_joy_node.py` | Liest Logitech F710 von `/dev/input/js0`, publiziert auf `/joy` mit 20 Hz — **auch wenn der Joystick in Ruhe liegt** | | `joystick_parser_node.py` | Konvertiert `/joy` → `/rover_command`; priorisiert Web-GUI über physischen Controller | | `gps_node.py` | Liest den GPS-Empfänger, publiziert Fix- und Diagnosedaten und kann über die Admin-Seite gezielt neu gestartet oder per Kaltstart zur Neuinitialisierung gezwungen werden | | `robot_node.py` | Berechnet Lenkwinkel und Geschwindigkeiten aus `/rover_command` | | `motor_node.py` | Setzt PWM-Werte über PCA9685; Watchdog stoppt Motoren nach 5 s ohne Befehl | | `rosbridge_websocket` | WebSocket-Bridge für die Web-GUI (Port 9090) | | `rosapi_node` | ROS-API für die Web-GUI | ### IMU: BNO085 - Sensor: Adafruit BNO085 Breakout - Bus: I2C `1` - Adresse: `0x4A` - Standardrate: `15 Hz` - Der IMU-Sensor läuft nicht im ROS-Melodic-Container, sondern als eigener Python-3-Systemd-Service auf dem Pi. - Grund: Der restliche ROS-Stack im Container nutzt noch überwiegend Python 2, der BNO085-Treiber benötigt aber Python 3. - Der Service `exomy-imu-ros.service` liest den Sensor und publiziert über `rosbridge` nach ROS. ### IMU-Einbaulage und Achsen-Remapping Der BNO085 ist um 90° um die Z-Achse verdreht eingebaut. Dadurch wären die X- und Y-Achsen des Sensors gegenüber dem Rover-Koordinatensystem vertauscht — Roll und Pitch kämen vertauscht an. `imu_node.py` korrigiert das direkt beim Publizieren: X- und Y-Komponenten werden für Quaternion, Gyro, Beschleunigung und Magnetfeld getauscht. Alle anderen Teile des Systems (GUI, Admin-API) sehen bereits korrekte Daten und müssen nichts kompensieren. Wenn der Sensor jemals neu ausgerichtet eingebaut wird, muss das Remapping in `publish_measurements()` in `imu_node.py` entsprechend angepasst oder entfernt werden. ### IMU-Topics | Topic | Typ | Inhalt | |---|---|---| | `/imu/data` | `sensor_msgs/Imu` | Quaternion, Gyro und lineare Beschleunigung | | `/imu/mag` | `sensor_msgs/MagneticField` | Magnetfeld | | `/imu/status` | `std_msgs/String` | Statusmeldung des IMU-Dienstes | ### Admin-Funktionen - Admin-Seite: `http://:8000/admin.html` - Kamera-Tab nur mit `4:3`-Profilen: - `640 x 480` - `1024 x 768` - `1296 x 972` - Systemstatus mit zusätzlicher WLAN-Signalstärke in Prozent - GPS-Aktionen: - `GPS neu starten` - `GPS-Kaltstart` für den GlobalSat `BU-353N5` IMU in der Admin-Seite: - Status - Adresse - Beschleunigung - Gyro - Magnetfeld - Quaternion - `Roll / Pitch / Yaw` ### Ports | Port | Verwendung | |---|---| | `8000` | Web-GUI | | `9090` | ROSBridge WebSocket | | `8082` | Admin-API | --- ## Deploy-Workflow Dateien lokal bearbeiten, dann auf den Pi und in den laufenden Container übertragen: ```bash # Datei auf den Pi kopieren scp src/meine_datei.py pi@192.168.1.9:/home/pi/ExoMy_Software/src/ # In den Container kopieren ssh pi@192.168.1.9 "docker cp /home/pi/ExoMy_Software/src/meine_datei.py exomy_autostart:/root/exomy_ws/src/exomy/src/" # Container neu starten ssh pi@192.168.1.9 "docker restart exomy_autostart" ``` Im WLAN `4pi` dieselben Befehle jeweils mit `10.42.44.150` statt `192.168.1.9` verwenden. GUI-Dateien: ```bash scp gui/index.html pi@192.168.1.9:/home/pi/ExoMy_Software/gui/ ssh pi@192.168.1.9 "docker cp /home/pi/ExoMy_Software/gui/index.html exomy_autostart:/root/exomy_ws/src/exomy/gui/" # Kein Neustart nötig — Browser-Reload genügt ``` IMU-/Admin-API-Änderungen: ```bash # Python-Datei auf den Pi kopieren scp src/imu_node.py pi@192.168.1.9:/home/pi/ExoMy_Software/src/ scp scripts/exomy_admin_api.py pi@192.168.1.9:/home/pi/ExoMy_Software/scripts/ scp scripts/exomy-imu-ros.service pi@192.168.1.9:/home/pi/ExoMy_Software/scripts/ # Admin-API neu starten ssh pi@192.168.1.9 "sudo systemctl restart exomy-admin-api.service" # IMU-ROS-Service aktualisieren/neu starten ssh pi@192.168.1.9 "sudo cp /home/pi/ExoMy_Software/scripts/exomy-imu-ros.service /etc/systemd/system/exomy-imu-ros.service && sudo systemctl daemon-reload && sudo systemctl restart exomy-imu-ros.service" ``` Wichtige Checks: ```bash # IMU-Service auf dem Pi systemctl status exomy-imu-ros.service # IMU-Topics im Container docker exec exomy_autostart bash -lc "source /opt/ros/melodic/setup.bash && rostopic list | grep ^/imu" # Beispielstatus docker exec exomy_autostart bash -lc "source /opt/ros/melodic/setup.bash && rostopic echo -n 1 /imu/status" ``` --- ## Bekannte Probleme & Fixes ### Web-GUI: Ruckartige Bewegung bei angeschlossenem Controller **Problem:** Der `f710_joy_node` sendet permanent mit 20 Hz auf `/joy`, auch wenn der Logitech F710 in Ruhe liegt (Nullwerte). Wenn die Web-GUI gleichzeitig steuert, wechseln sich Fahr- und Stopp-Befehle im 50-ms-Takt ab — der Rover bewegt sich ruckartig. **Fix:** `joystick_parser_node.py` ignoriert Nachrichten des physischen Controllers für 2 Sekunden, sobald eine Web-GUI-Nachricht eintrifft (erkennbar an `frame_id == "webgui"`). Danach übernimmt der physische Controller automatisch wieder. ### SyntaxError in rover.py (Point-Turn-Modus) **Problem:** Überzählige schließende Klammer in `rover.py` Zeile 157 ließ `robot_node.py` beim Start abstürzen — der Rover war nicht steuerbar. ```python # Falsch: math.atan((self.wheel_rx + self.wheel_fx) / self.wheel_ry))) # Richtig: math.atan((self.wheel_rx + self.wheel_fx) / self.wheel_ry)) ``` --- ## Auto-Rotate (automatisches Drehen zu einem Ziel-Heading) Die Admin-Seite (`http://:8000/admin.html`) enthält im IMU-Bereich eine Funktion, mit der der Rover automatisch zu einem eingegebenen Ziel-Heading dreht. ### Bedienung 1. Zielwinkel eingeben (0–359°, wobei 0 = Nord, 90 = Ost, 180 = Süd, 270 = West — **nach Nord-Kalibrierung**) 2. „Zu Heading drehen" drücken 3. Der Rover dreht auf der Stelle und stoppt automatisch bei ±1° Genauigkeit 4. „Drehung abbrechen" bricht den Vorgang vorzeitig ab ### Technischer Ablauf ``` Admin-API → startet auto_rotate_ros.py via docker exec im Container → setzt rosparam /auto_rotate_active = True → joystick_parser_node ignoriert ALLE Eingaben (Web-GUI, D-Pad, F710) → auto_rotate_ros.py dreht mit Point-Turn-Modus bei fester Geschwindigkeit (~20 %) → bei Ziel erreicht (oder Abbruch): → rosparam /auto_rotate_active = False ← erst jetzt wird Steuerung freigegeben → letzter Fahrmodus (vor der Drehung) wird wiederhergestellt ``` ### Wichtige Hinweise - **Geschwindigkeit:** Fest auf ~20 % (entspricht `vel=4` durch die Expo-Kurve), unabhängig vom eingestellten Speed-Limit in der Admin-Seite. - **Nord-Kalibrierung muss gesetzt sein:** Der Zielwinkel bezieht sich auf den kalibrierten Norden. Ohne Kalibrierung dreht der Rover zum magnetischen Roh-Heading. - **Kein manuelles Eingreifen während der Drehung:** Joystick, D-Pad und F710 sind über `rosparam /auto_rotate_active` vollständig blockiert. Erst nach Abschluss oder Abbruch wird die Steuerung freigegeben. - **Fahrmodus:** Der Rover wechselt intern in den Point-Turn-Modus und kehrt danach automatisch in den zuvor aktiven Modus zurück (Ackermann, Point Turn oder Crabbing). - **Neigungskalibrierung:** Wenn die IMU-Neigung genullt wurde, gilt das nur für die Anzeige — die Rotation selbst nutzt nur den Yaw-Winkel. ### Dateien | Datei | Beschreibung | |---|---| | `scripts/auto_rotate_ros.py` | Läuft im Docker-Container, nativer rospy-Node | | `scripts/imu_rotate.py` | Steuerung des docker-exec-Prozesses, Status-Tracking | ### Deploy nach Änderungen ```bash # auto_rotate_ros.py oder joystick_parser_node.py geändert: scp scripts/auto_rotate_ros.py pi@192.168.1.9:/home/pi/ExoMy_Software/scripts/ scp src/joystick_parser_node.py pi@192.168.1.9:/home/pi/ExoMy_Software/src/ ssh pi@192.168.1.9 "sudo docker restart exomy_autostart" # imu_rotate.py oder exomy_admin_api.py geändert: scp scripts/imu_rotate.py scripts/exomy_admin_api.py pi@192.168.1.9:/home/pi/ExoMy_Software/scripts/ ssh pi@192.168.1.9 "sudo systemctl restart exomy-admin-api.service" ``` --- ## Fahrmodi | Modus | Taste (Controller) | Taste (Web-GUI) | |---|---|---| | Ackermann | A | Schaltfläche „Ackermann" | | Point Turn (Drehen auf der Stelle) | X | Schaltfläche „Turn on Point" | | Crab (Seitwärtsfahrt) | Y | Schaltfläche „Crab" | | Motoren ein/aus | START | Schaltfläche „Motoren" | --- ## Latenz-Simulation Die Latenz-Simulation wird über die Admin-Seite eingestellt und wirkt gleichzeitig auf Steuerung und Kamerabild. - Bereich: Admin-Seite `http://:8000/admin.html` - Einstellbereich: `0` bis `10` Sekunden - Steuerbefehle: werden im ROS-Node `delay_node` gepuffert - Kamerabild: läuft über den Delay-Proxy auf Port `8083` - Rohstream: Port `8081` bleibt ohne Verzögerung für Debug und interne Vorschau - Hauptseite: nutzt bewusst `8083`, damit die eingestellte Verzögerung auch im Fahrbild sichtbar ist Signalweg: ```text Kamera -> 8081 Rohstream -> 8083 Delay-Proxy -> Web-GUI Joystick/Websteuerung -> /joy -> /delay_node -> /joy_delayed -> Rover-Steuerung ``` Damit lässt sich eine künstliche Laufzeit simulieren, ohne den eigentlichen Kameradienst umzubauen. --- ## Originales ESA-Projekt - [Wiki](https://github.com/esa-prl/ExoMy/wiki) — Bauanleitung und Dokumentation - [Website](https://esa-prl.github.io/ExoMy/) - [Dokumentations-Repository](https://github.com/esa-prl/ExoMy)