Alles verbessert

This commit is contained in:
2026-05-21 19:30:33 +02:00
parent 5dccc8985e
commit 33b71416ff
19 changed files with 855 additions and 229 deletions
+146 -26
View File
@@ -1,34 +1,154 @@
# ExoMy - Software Repository # ExoMy CUNO — Software
This repository contains the software to run Exomy. The [wiki](https://github.com/esa-prl/ExoMy/wiki) explains you how to use it.
![ExoMy image](https://github.com/esa-prl/ExoMy/wiki/images/renderings/2020_02_25.JPG) ExoMy Mars-Rover auf Basis eines Raspberry Pi mit ROS1 Melodic, betrieben vollständig in einem Docker-Container.
# ExoMy Project Structure ---
### [Website](https://esa-prl.github.io/ExoMy/) ## Zugang zum Raspberry Pi
There is a website about ExoMy. It does not help you build it, but is still nice to look at.
### [Wiki](https://github.com/esa-prl/ExoMy/wiki) | | |
The [wiki](https://github.com/esa-prl/ExoMy/wiki) contains step by step instructions on how the 3D-printed rover ExoMy can be built, controlled and customized. |---|---|
| **Hostname** | `cuno` |
| **Benutzer** | `pi` |
| **LAN-IP** | `192.168.1.83` |
| **WLAN-IP** | `192.168.1.9` (bevorzugt) |
| **Fallback-AP** | SSID `CUNO`, IP `192.168.50.1`, Passwort `astr0cun042` |
### [Documentation Repository](https://github.com/esa-prl/ExoMy) ```bash
This repository contains all the files of the documentation of ExoMy. Just click on [*Releases*](https://github.com/esa-prl/ExoMy/releases) and download the files of the latest release. They are explained further in the [wiki](https://github.com/esa-prl/ExoMy/wiki). ssh pi@192.168.1.9
```
### Social Media ---
<!-- Add icon library -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.1/css/all.css">
<!-- Add font awesome icons --> ## Architektur
<p>
<img src="https://github.com/esa-prl/ExoMy/wiki/images/social_media_icons/discord-brands.svg" width="20px">
<a href="https://discord.gg/gZk62gg"> Join the Community!</a>
</p>
<p>
<img src="https://github.com/esa-prl/ExoMy/wiki/images/social_media_icons/twitter-square-brands.svg" width="20px">
<a href="https://twitter.com/exomy_rover"> @ExoMy_Rover</a>
</p>
<p>
<img src="https://github.com/esa-prl/ExoMy/wiki/images/social_media_icons/instagram-square-brands.svg" width="20px">
<a href="https://www.instagram.com/exomy_rover/"> @ExoMy_Rover</a>
</p>
```
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
```
**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-wifi-fallback.service` | WLAN-Fallback auf eigenen Access Point |
```bash
# Status prüfen
systemctl status exomy-admin-api.service
```
---
## 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 |
| `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 |
### 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 ExoMy_Software-master/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"
```
GUI-Dateien:
```bash
scp ExoMy_Software-master/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
```
---
## 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))
```
---
## 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" |
---
## 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)
@@ -38,8 +38,13 @@ steer_pwm_neutral_rr: 295
# PWW range around steering neutral position # PWW range around steering neutral position
steer_pwm_range: 250 steer_pwm_range: 250
# PWM value for the stop position of driving motors # PWM values for the stop position of driving motors
drive_pwm_neutral: 307 drive_pwm_neutral_fl: 307
drive_pwm_neutral_fr: 307
drive_pwm_neutral_cl: 307
drive_pwm_neutral_cr: 307
drive_pwm_neutral_rl: 307
drive_pwm_neutral_rr: 307
# PWW range around driving neutral position # PWW range around driving neutral position
drive_pwm_range: 100 drive_pwm_range: 100
@@ -38,8 +38,13 @@ steer_pwm_neutral_rr: 300
# PWW range around steering neutral position # PWW range around steering neutral position
steer_pwm_range: 250 steer_pwm_range: 250
# PWM value for the stop position of driving motors # PWM values for the stop position of driving motors
drive_pwm_neutral: 307 drive_pwm_neutral_fl: 307
drive_pwm_neutral_fr: 307
drive_pwm_neutral_cl: 307
drive_pwm_neutral_cr: 307
drive_pwm_neutral_rl: 307
drive_pwm_neutral_rr: 307
# PWW range around driving neutral position # PWW range around driving neutral position
drive_pwm_range: 100 drive_pwm_range: 100
+5 -7
View File
@@ -10,15 +10,12 @@ RUN apt-get update && \
# Install additional ros packages # Install additional ros packages
RUN apt-get update && apt-get install ros-melodic-rosbridge-server ros-melodic-joy -y RUN apt-get update && apt-get install ros-melodic-rosbridge-server ros-melodic-joy -y
RUN pip install adafruit-pca9685 RUN apt-get update && apt-get install python-smbus -y
RUN pip install adafruit-pureio
RUN pip install Adafruit-GPIO==1.0.3 --no-deps
RUN pip install Adafruit-PCA9685==1.0.1 --no-deps
# Install packages for web application
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
RUN apt-get update && \
apt-get install nodejs -y
RUN npm install http-server -g
# Install packages for camera use # Install packages for camera use
RUN apt-get update && \ RUN apt-get update && \
apt-get install ros-melodic-web-video-server ros-melodic-usb-cam -y apt-get install ros-melodic-web-video-server ros-melodic-usb-cam -y
@@ -33,4 +30,5 @@ RUN mkdir -p $EXOMY_WS/src
WORKDIR /root WORKDIR /root
COPY ./entrypoint.sh / COPY ./entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
+40 -5
View File
@@ -1,19 +1,55 @@
#!/bin/bash #!/bin/bash
set -eo pipefail
cleanup() {
for pid_var in HTTP_PID ROSMASTER_PID ROSBRIDGE_PID ROSAPI_PID ROBOT_PID MOTOR_PID JOYSTICK_PID JOY_PID; do
if [[ -n "${!pid_var:-}" ]]; then
kill "${!pid_var}" 2>/dev/null || true
fi
done
wait || true
}
if [[ $1 == "config" ]] if [[ $1 == "config" ]]
then then
cd /root/exomy_ws/src/exomy/scripts cd /root/exomy_ws/src/exomy/scripts
bash bash
elif [[ $1 == "autostart" ]] elif [[ $1 == "autostart" ]]
then then
trap cleanup EXIT INT TERM
source /opt/ros/melodic/setup.bash source /opt/ros/melodic/setup.bash
cd /root/exomy_ws cd /root/exomy_ws
catkin_make catkin_make
http-server src/exomy/gui -p 8000 & export ROS_MASTER_URI=http://localhost:11311
source devel/setup.bash source devel/setup.bash
roslaunch exomy exomy.launch
bash cd src/exomy/gui
python -m SimpleHTTPServer 8000 &
HTTP_PID=$!
cd /root/exomy_ws
rosmaster --core -p 11311 -w 3 > /tmp/rosmaster.log 2>&1 &
ROSMASTER_PID=$!
sleep 3
rosparam load /root/exomy_ws/src/exomy/config/exomy.yaml
rosparam set /controller logitech-F710
/opt/ros/melodic/lib/rosbridge_server/rosbridge_websocket > /tmp/rosbridge.log 2>&1 &
ROSBRIDGE_PID=$!
/opt/ros/melodic/lib/rosapi/rosapi_node > /tmp/rosapi.log 2>&1 &
ROSAPI_PID=$!
python /root/exomy_ws/src/exomy/src/f710_joy_node.py > /tmp/joy_node.log 2>&1 &
JOY_PID=$!
python /root/exomy_ws/src/exomy/src/robot_node.py > /tmp/robot_node.log 2>&1 &
ROBOT_PID=$!
python /root/exomy_ws/src/exomy/src/motor_node.py > /tmp/motor_node.log 2>&1 &
MOTOR_PID=$!
python /root/exomy_ws/src/exomy/src/joystick_parser_node.py > /tmp/joystick_parser.log 2>&1 &
JOYSTICK_PID=$!
wait -n "$HTTP_PID" "$ROSMASTER_PID" "$ROSBRIDGE_PID" "$ROSAPI_PID" "$ROBOT_PID" "$MOTOR_PID" "$JOYSTICK_PID" "$JOY_PID"
exit 1
elif [[ $1 == "devel" ]] elif [[ $1 == "devel" ]]
then then
cd /root/exomy_ws cd /root/exomy_ws
@@ -24,4 +60,3 @@ then
else else
bash bash
fi fi
+84 -65
View File
@@ -138,7 +138,7 @@
</div> </div>
<p class="admin_copy motor_test_neutral_copy"> <p class="admin_copy motor_test_neutral_copy">
Das entspricht [config_drive_motor_neutral.py]: ein gemeinsamer `drive_pwm_neutral`-Wert für alle Fahrmotoren. Mit `-` und `+` sendest du den Stillstands-PWM direkt an alle Antriebe, bis wirklich alles steht. Mit `-` und `+` stellst du den Stillstands-PWM jetzt pro Fahrmotor einzeln ein. `Speichern` schreibt alle sechs Werte dauerhaft in die `exomy.yaml`.
</p> </p>
<div class="motor_test_neutral_toolbar"> <div class="motor_test_neutral_toolbar">
@@ -146,22 +146,7 @@
<button id="drive_neutral_save_button" class="button buttonA" onclick="saveDriveNeutralValue()">Speichern</button> <button id="drive_neutral_save_button" class="button buttonA" onclick="saveDriveNeutralValue()">Speichern</button>
</div> </div>
<article class="drive_neutral_card" id="drive_neutral_card"> <div id="drive_neutral_grid" class="steering_neutral_grid"></div>
<div class="steering_neutral_card_header">
<span class="info_label">Drive PWM Neutral</span>
<strong>Alle Fahrmotoren</strong>
</div>
<div class="steering_neutral_value_row drive_neutral_value_row">
<button class="button buttonWarm steering_neutral_adjust" onclick="adjustDriveNeutralValue(-5)">-</button>
<strong id="drive_neutral_value" class="steering_neutral_value">-</strong>
<button class="button buttonA steering_neutral_adjust" onclick="adjustDriveNeutralValue(5)">+</button>
</div>
<div class="steering_neutral_meta">
<span id="drive_neutral_pins">Pins: -</span>
</div>
</article>
</div> </div>
</section> </section>
</div> </div>
@@ -188,9 +173,8 @@
var activeMotorTestTab = 'impulse'; var activeMotorTestTab = 'impulse';
var steeringNeutralValues = {}; var steeringNeutralValues = {};
var steeringNeutralSavedValues = {}; var steeringNeutralSavedValues = {};
var driveNeutralValue = null; var driveNeutralValues = {};
var driveNeutralSavedValue = null; var driveNeutralSavedValues = {};
var driveNeutralPins = {};
var STEERING_NEUTRAL_STEP = 5; var STEERING_NEUTRAL_STEP = 5;
var WHEEL_ORDER = ['fl', 'fr', 'cl', 'cr', 'rl', 'rr']; var WHEEL_ORDER = ['fl', 'fr', 'cl', 'cr', 'rl', 'rr'];
@@ -288,7 +272,7 @@
loadSteeringNeutralValues(); loadSteeringNeutralValues();
} }
if (isDriveNeutral && driveNeutralValue === null) { if (isDriveNeutral && !Object.keys(driveNeutralValues).length) {
loadDriveNeutralValue(); loadDriveNeutralValue();
} }
} }
@@ -348,38 +332,59 @@
button.disabled = isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || !hasUnsavedNeutralChanges(); button.disabled = isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || !hasUnsavedNeutralChanges();
} }
function renderDriveNeutralCard() { function renderDriveNeutralCards() {
var valueNode = document.getElementById('drive_neutral_value'); var grid = document.getElementById('drive_neutral_grid');
var pinsNode = document.getElementById('drive_neutral_pins'); if (!grid) {
var card = document.getElementById('drive_neutral_card'); return;
if (valueNode) {
valueNode.textContent = driveNeutralValue === null ? '-' : String(driveNeutralValue);
} }
if (pinsNode) { var html = [];
var pinValues = WHEEL_ORDER WHEEL_ORDER.forEach(function (wheel) {
.filter(function (wheel) { return Object.prototype.hasOwnProperty.call(driveNeutralPins, wheel); }) var entry = driveNeutralValues[wheel];
.map(function (wheel) { return wheel.toUpperCase() + ':' + driveNeutralPins[wheel]; }); if (!entry) {
pinsNode.textContent = pinValues.length ? 'Pins: ' + pinValues.join(' ') : 'Pins: -'; return;
} }
if (card) { var savedEntry = driveNeutralSavedValues[wheel];
var isDirty = driveNeutralValue !== null && driveNeutralSavedValue !== null && Number(driveNeutralValue) !== Number(driveNeutralSavedValue); var isDirty = !savedEntry || Number(savedEntry.value) !== Number(entry.value);
card.classList.toggle('is_dirty', isDirty); html.push(
} '<article class="steering_neutral_card steering_neutral_card_compact' + (isDirty ? ' is_dirty' : '') + '">' +
'<div class="steering_neutral_compact_row">' +
'<div class="steering_neutral_card_header steering_neutral_card_header_compact">' +
'<strong class="steering_neutral_title">' + entry.wheel_label + '</strong>' +
'</div>' +
'<div class="steering_neutral_value_row steering_neutral_value_row_compact">' +
'<button class="button buttonWarm steering_neutral_adjust" onclick="adjustDriveNeutralValue(\'' + wheel + '\', -' + STEERING_NEUTRAL_STEP + ')">-</button>' +
'<strong class="steering_neutral_value">' + entry.value + '</strong>' +
'<button class="button buttonA steering_neutral_adjust" onclick="adjustDriveNeutralValue(\'' + wheel + '\', ' + STEERING_NEUTRAL_STEP + ')">+</button>' +
'</div>' +
'</div>' +
'</article>'
);
});
grid.innerHTML = html.join('');
updateDriveNeutralSaveButton(); updateDriveNeutralSaveButton();
} }
function hasUnsavedDriveNeutralChanges() {
return WHEEL_ORDER.some(function (wheel) {
var currentEntry = driveNeutralValues[wheel];
var savedEntry = driveNeutralSavedValues[wheel];
if (!currentEntry || !savedEntry) {
return false;
}
return Number(currentEntry.value) !== Number(savedEntry.value);
});
}
function updateDriveNeutralSaveButton() { function updateDriveNeutralSaveButton() {
var button = document.getElementById('drive_neutral_save_button'); var button = document.getElementById('drive_neutral_save_button');
if (!button) { if (!button) {
return; return;
} }
var hasChanges = driveNeutralValue !== null && driveNeutralSavedValue !== null && Number(driveNeutralValue) !== Number(driveNeutralSavedValue); button.disabled = isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving || !hasUnsavedDriveNeutralChanges();
button.disabled = isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving || !hasChanges;
} }
async function fetchMotorTestStatus() { async function fetchMotorTestStatus() {
@@ -606,7 +611,7 @@
isDriveNeutralLoading = true; isDriveNeutralLoading = true;
updateDriveNeutralSaveButton(); updateDriveNeutralSaveButton();
setMotorTestStatus('Lade Fahr-Neutralwert...'); setMotorTestStatus('Lade Fahr-Neutralwerte...');
try { try {
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral'); var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral');
@@ -616,37 +621,44 @@
throw new Error(data.status || ('status ' + response.status)); throw new Error(data.status || ('status ' + response.status));
} }
driveNeutralValue = Number(data.drive_neutral && data.drive_neutral.value); driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : {};
driveNeutralSavedValue = driveNeutralValue; driveNeutralSavedValues = JSON.parse(JSON.stringify(driveNeutralValues));
driveNeutralPins = data.drive_neutral && data.drive_neutral.pins ? data.drive_neutral.pins : {}; renderDriveNeutralCards();
renderDriveNeutralCard();
renderContainerStatus(data); renderContainerStatus(data);
setMotorTestStatus(data.status || 'Fahr-Neutralwert geladen'); setMotorTestStatus(data.status || 'Fahr-Neutralwerte geladen');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setMotorTestStatus('Fehler'); setMotorTestStatus('Fehler');
alert(error.message || 'Der Fahr-Neutralwert konnte nicht geladen werden.'); alert(error.message || 'Die Fahr-Neutralwerte konnten nicht geladen werden.');
} finally { } finally {
isDriveNeutralLoading = false; isDriveNeutralLoading = false;
updateDriveNeutralSaveButton(); updateDriveNeutralSaveButton();
} }
} }
async function adjustDriveNeutralValue(delta) { async function adjustDriveNeutralValue(wheel, delta) {
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) { if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) {
return; return;
} }
if (driveNeutralValue === null) { var entry = driveNeutralValues[wheel];
if (!entry) {
return; return;
} }
driveNeutralValue = Math.max(0, Math.min(4095, Number(driveNeutralValue) + Number(delta))); entry.value = Math.max(0, Math.min(4095, Number(entry.value) + Number(delta)));
renderDriveNeutralCard(); renderDriveNeutralCards();
isDriveNeutralPreviewRunning = true; isDriveNeutralPreviewRunning = true;
updateDriveNeutralSaveButton(); updateDriveNeutralSaveButton();
setMotorTestStatus('Fahre Fahr-Neutralwert an...'); setMotorTestStatus('Fahre Fahr-Neutralwerte an...');
var values = {};
WHEEL_ORDER.forEach(function (wheelKey) {
if (driveNeutralValues[wheelKey]) {
values[wheelKey] = Number(driveNeutralValues[wheelKey].value);
}
});
try { try {
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral/preview', { var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral/preview', {
@@ -655,7 +667,7 @@
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
value: driveNeutralValue values: values
}) })
}); });
var data = await response.json(); var data = await response.json();
@@ -664,10 +676,10 @@
throw new Error(data.status || ('status ' + response.status)); throw new Error(data.status || ('status ' + response.status));
} }
driveNeutralPins = data.drive_neutral && data.drive_neutral.pins ? data.drive_neutral.pins : driveNeutralPins; driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : driveNeutralValues;
renderDriveNeutralCard(); renderDriveNeutralCards();
renderContainerStatus(data); renderContainerStatus(data);
setMotorTestStatus(data.status || 'Fahr-Neutralwert angefahren'); setMotorTestStatus(data.status || 'Fahr-Neutralwerte angefahren');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setMotorTestStatus('Fehler'); setMotorTestStatus('Fehler');
@@ -683,13 +695,21 @@
return; return;
} }
if (driveNeutralValue === null || driveNeutralSavedValue === null || Number(driveNeutralValue) === Number(driveNeutralSavedValue)) { if (!hasUnsavedDriveNeutralChanges()) {
return; return;
} }
isDriveNeutralSaving = true; isDriveNeutralSaving = true;
updateDriveNeutralSaveButton(); updateDriveNeutralSaveButton();
setMotorTestStatus('Speichere Fahr-Neutralwert...'); setMotorTestStatus('Speichere Fahr-Neutralwerte...');
var values = {};
WHEEL_ORDER.forEach(function (wheel) {
var entry = driveNeutralValues[wheel];
if (entry) {
values[entry.key] = Number(entry.value);
}
});
try { try {
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral/save', { var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral/save', {
@@ -698,7 +718,7 @@
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
value: driveNeutralValue values: values
}) })
}); });
var data = await response.json(); var data = await response.json();
@@ -707,16 +727,15 @@
throw new Error(data.status || ('status ' + response.status)); throw new Error(data.status || ('status ' + response.status));
} }
driveNeutralValue = Number(data.drive_neutral && data.drive_neutral.value); driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : driveNeutralValues;
driveNeutralSavedValue = driveNeutralValue; driveNeutralSavedValues = JSON.parse(JSON.stringify(driveNeutralValues));
driveNeutralPins = data.drive_neutral && data.drive_neutral.pins ? data.drive_neutral.pins : driveNeutralPins; renderDriveNeutralCards();
renderDriveNeutralCard();
renderContainerStatus(data); renderContainerStatus(data);
setMotorTestStatus(data.status || 'Fahr-Neutralwert gespeichert'); setMotorTestStatus(data.status || 'Fahr-Neutralwerte gespeichert');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setMotorTestStatus('Fehler'); setMotorTestStatus('Fehler');
alert(error.message || 'Der Fahr-Neutralwert konnte nicht gespeichert werden.'); alert(error.message || 'Die Fahr-Neutralwerte konnten nicht gespeichert werden.');
} finally { } finally {
isDriveNeutralSaving = false; isDriveNeutralSaving = false;
updateDriveNeutralSaveButton(); updateDriveNeutralSaveButton();
+39
View File
@@ -89,6 +89,10 @@
<span class="info_label">CPU-Temperatur</span> <span class="info_label">CPU-Temperatur</span>
<strong id="info_temp">-</strong> <strong id="info_temp">-</strong>
</div> </div>
<div class="info_card">
<span class="info_label">Unterspannung</span>
<strong id="info_undervoltage" class="state_unknown">-</strong>
</div>
<div class="info_card"> <div class="info_card">
<span class="info_label">Uptime</span> <span class="info_label">Uptime</span>
<strong id="info_uptime">-</strong> <strong id="info_uptime">-</strong>
@@ -145,12 +149,47 @@
node.classList.add('state_unknown'); node.classList.add('state_unknown');
} }
function setUndervoltageState(id, text, state) {
var node = document.getElementById(id);
if (!node) {
return;
}
node.textContent = text || '-';
node.classList.remove('state_active', 'state_inactive', 'state_unknown');
if (state === 'active') {
node.classList.add('state_inactive');
node.style.animation = 'statusBlink 0.9s step-end infinite';
return;
}
node.style.animation = '';
if (state === 'past') {
node.classList.add('state_unknown');
return;
}
if (state === 'clear') {
node.classList.add('state_active');
return;
}
node.classList.add('state_unknown');
}
function renderStatus(data) { function renderStatus(data) {
setAdminStatus(data.status || 'Bereit'); setAdminStatus(data.status || 'Bereit');
setText('info_ips', data.system && data.system.ips); setText('info_ips', data.system && data.system.ips);
setText('info_wifi', data.system && data.system.wifi); setText('info_wifi', data.system && data.system.wifi);
setText('info_temp', data.system && data.system.cpu_temperature); setText('info_temp', data.system && data.system.cpu_temperature);
setUndervoltageState(
'info_undervoltage',
data.system && data.system.undervoltage,
data.system && data.system.undervoltage_state
);
setText('info_uptime', data.system && data.system.uptime); setText('info_uptime', data.system && data.system.uptime);
setText('info_disk', data.system && data.system.disk_free); setText('info_disk', data.system && data.system.disk_free);
+155 -57
View File
@@ -187,6 +187,12 @@ header {
color: #fff; color: #fff;
box-shadow: 0 0 24px rgba(0,136,255,.45), inset 0 0 16px rgba(255,255,255,.1); box-shadow: 0 0 24px rgba(0,136,255,.45), inset 0 0 16px rgba(255,255,255,.1);
} }
@keyframes mode-flash {
0% { box-shadow: 0 0 0px rgba(0,255,145,0), background-color: var(--pri); }
30% { box-shadow: 0 0 28px rgba(0,255,145,.9), background-color: #00ff91; }
100% { box-shadow: 0 0 24px rgba(0,136,255,.45), background-color: var(--pri); }
}
.dbtn.flash { animation: mode-flash .5s ease-out forwards; }
#cam-panel { flex: 1; display: flex; flex-direction: column; min-height: 0; } #cam-panel { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.cam-port { .cam-port {
@@ -205,19 +211,6 @@ header {
background: #05070d; background: #05070d;
z-index: 1; z-index: 1;
} }
.c-scanlines {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 5;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 3px,
rgba(0,0,0,.09) 3px,
rgba(0,0,0,.09) 4px
);
}
.c-vignette { .c-vignette {
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -230,42 +223,19 @@ header {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 50px; width: 180px;
height: 50px; height: 180px;
z-index: 10; z-index: 10;
pointer-events: none;
} }
.c-xhair::before { @keyframes xh-spin { to { transform: rotate(360deg); } }
content: ""; @keyframes xh-pulse {
position: absolute; 0% { transform: scale(1); opacity: .7; }
top: 50%; 100% { transform: scale(2.8); opacity: 0; }
left: 0;
right: 0;
height: 1px;
background: rgba(0,255,145,.45);
transform: translateY(-50%);
} }
.c-xhair::after { .xh-rot { animation: xh-spin 14s linear infinite; transform-box: fill-box; transform-origin: center; }
content: ""; .xh-rot-rev { animation: xh-spin 8s linear infinite reverse; transform-box: fill-box; transform-origin: center; }
position: absolute; .xh-pulse { animation: xh-pulse 2.5s ease-out infinite; transform-box: fill-box; transform-origin: center; }
left: 50%;
top: 0;
bottom: 0;
width: 1px;
background: rgba(0,255,145,.45);
transform: translateX(-50%);
}
.xc {
position: absolute;
width: 9px;
height: 9px;
border-color: rgba(0,255,145,.7);
border-style: solid;
border-width: 0;
}
.xc.tl { top: 0; left: 0; border-top-width: 1.5px; border-left-width: 1.5px; }
.xc.tr { top: 0; right: 0; border-top-width: 1.5px; border-right-width: 1.5px; }
.xc.bl { bottom: 0; left: 0; border-bottom-width: 1.5px; border-left-width: 1.5px; }
.xc.br { bottom: 0; right: 0; border-bottom-width: 1.5px; border-right-width: 1.5px; }
.c-rec { .c-rec {
position: absolute; position: absolute;
top: 13px; top: 13px;
@@ -328,11 +298,20 @@ header {
.sval.g { color: var(--ok); } .sval.g { color: var(--ok); }
.sval.o { color: var(--sec); } .sval.o { color: var(--sec); }
.sval.b { color: var(--pri); } .sval.b { color: var(--pri); }
.sval.alert {
color: #D50000;
animation: uv-blink 0.9s step-end infinite;
}
.sval.warntext { color: var(--sec); }
.sval-conn { display: flex; align-items: center; gap: 5px; color: var(--ok); font-size: 10px; } .sval-conn { display: flex; align-items: center; gap: 5px; color: var(--ok); font-size: 10px; }
.sbar-wrap { display: flex; align-items: center; gap: 8px; } .sbar-wrap { display: flex; align-items: center; gap: 8px; }
.sbar { width: 52px; height: 3px; background: rgba(0,136,255,.12); border-radius: 1px; overflow: hidden; } .sbar { width: 52px; height: 3px; background: rgba(0,136,255,.12); border-radius: 1px; overflow: hidden; }
.sbar-i { height: 100%; border-radius: 1px; background: var(--pri); } .sbar-i { height: 100%; border-radius: 1px; background: var(--pri); }
.sbar-i.w { background: var(--sec); } .sbar-i.w { background: var(--sec); }
@keyframes uv-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
}
#joy-panel .p-hdr { padding: 14px 12px; } #joy-panel .p-hdr { padding: 14px 12px; }
.mode-toggle { display: flex; gap: 2px; margin-left: auto; } .mode-toggle { display: flex; gap: 2px; margin-left: auto; }
@@ -563,10 +542,41 @@ header {
STREAM: 8081 STREAM: 8081
</div> </div>
<div class="c-xhair"> <div class="c-xhair">
<div class="xc tl"></div><div class="xc tr"></div> <svg viewBox="-90 -90 180 180" xmlns="http://www.w3.org/2000/svg" width="180" height="180">
<div class="xc bl"></div><div class="xc br"></div> <!-- äußerer rotierender Strichelring -->
<circle class="xh-rot" cx="0" cy="0" r="82" fill="none" stroke="rgba(0,255,145,.2)" stroke-width="1" stroke-dasharray="4 10"/>
<!-- mittlerer Ring, Gegenrichtung -->
<circle class="xh-rot-rev" cx="0" cy="0" r="60" fill="none" stroke="rgba(0,255,145,.15)" stroke-width=".75" stroke-dasharray="2 16"/>
<!-- innerer fester Ring -->
<circle cx="0" cy="0" r="16" fill="none" stroke="rgba(0,255,145,.3)" stroke-width=".75"/>
<!-- Kreuzlinien mit Lücke in der Mitte -->
<line x1="-86" y1="0" x2="-19" y2="0" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
<line x1="19" y1="0" x2="86" y2="0" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
<line x1="0" y1="-86" x2="0" y2="-19" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
<line x1="0" y1="19" x2="0" y2="86" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
<!-- Skalenstriche bei r=60 -->
<line x1="-60" y1="-6" x2="-60" y2="6" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
<line x1="60" y1="-6" x2="60" y2="6" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
<line x1="-6" y1="-60" x2="6" y2="-60" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
<line x1="-6" y1="60" x2="6" y2="60" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
<!-- Skalenstriche bei r=38 -->
<line x1="-38" y1="-3.5" x2="-38" y2="3.5" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
<line x1="38" y1="-3.5" x2="38" y2="3.5" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
<line x1="-3.5" y1="-38" x2="3.5" y2="-38" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
<line x1="-3.5" y1="38" x2="3.5" y2="38" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
<!-- Eckklammern -->
<path d="M-74,-58 L-74,-74 L-58,-74" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
<path d="M58,-74 L74,-74 L74,-58" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
<path d="M-74,58 L-74,74 L-58,74" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
<path d="M58,74 L74,74 L74,58" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
<!-- pulsierender Ring -->
<circle class="xh-pulse" cx="0" cy="0" r="6" fill="none" stroke="rgba(0,255,145,.7)" stroke-width="1"/>
<!-- Mittelpunkt -->
<circle cx="0" cy="0" r="2.5" fill="rgba(0,255,145,.9)"/>
<circle cx="0" cy="0" r="1" fill="rgba(255,255,255,.7)"/>
</svg>
</div> </div>
<div class="c-scanlines"></div>
<div class="c-vignette"></div> <div class="c-vignette"></div>
</div> </div>
</div> </div>
@@ -633,6 +643,10 @@ header {
<span class="slbl">IP Adressen</span> <span class="slbl">IP Adressen</span>
<span class="sval b" id="stat-ips">--</span> <span class="sval b" id="stat-ips">--</span>
</div> </div>
<div class="srow warn">
<span class="slbl">Unterspannung</span>
<span class="sval b" id="stat-undervoltage">--</span>
</div>
<div class="srow info"> <div class="srow info">
<span class="slbl">Uptime Pi</span> <span class="slbl">Uptime Pi</span>
<span class="sval b" id="stat-uptime">--</span> <span class="sval b" id="stat-uptime">--</span>
@@ -704,6 +718,7 @@ header {
var hostUrl = window.location.hostname; var hostUrl = window.location.hostname;
var ros = null; var ros = null;
var joyListener = null; var joyListener = null;
var roverCommandListener = null;
var publishTimer = null; var publishTimer = null;
var statusTimer = null; var statusTimer = null;
var axes = [0, 0, 0, 0, 0, 0]; var axes = [0, 0, 0, 0, 0, 0];
@@ -771,6 +786,45 @@ function updateModeDisplay() {
setText("stat-mode", label); setText("stat-mode", label);
} }
function applyDriveMode(mode, flash) {
var changed = mode !== driveMode;
driveMode = mode;
document.querySelectorAll(".dbtn[data-mode]").forEach(function (btn) {
var isActive = btn.dataset.mode === mode;
btn.classList.toggle("active", isActive);
if (isActive && changed && flash) {
btn.classList.remove("flash");
void btn.offsetWidth;
btn.classList.add("flash");
}
});
updateModeDisplay();
updateControlLocks();
}
function mirrorJoyThumb(x, y) {
if (dragging || inputMode !== "joystick") return;
var data = joyMetrics();
var dx = x * data.maxD;
var dy = -y * data.maxD;
var thumb = document.getElementById("jthumb");
thumb.style.transition = "left .05s, top .05s";
thumb.style.left = "calc(50% + " + dx + "px)";
thumb.style.top = "calc(50% + " + dy + "px)";
thumb.style.transform = "translate(-50%,-50%)";
updateAxesDisplay(x, y);
}
function locomotionModeToUi(modeValue) {
if (modeValue === 2) {
return "turn-on-point";
}
if (modeValue === 3) {
return "crab";
}
return "ackermann";
}
function updateMotorDisplay() { function updateMotorDisplay() {
setText("stat-motors", motorsEnabled ? "Aktiv" : "Aus"); setText("stat-motors", motorsEnabled ? "Aktiv" : "Aus");
} }
@@ -787,11 +841,35 @@ function setRosStatus(text) {
setText("mission-state", text === "Verbunden" ? "Nominal" : text); setText("mission-state", text === "Verbunden" ? "Nominal" : text);
} }
function updateUndervoltageDisplay(text, state) {
var el = document.getElementById("stat-undervoltage");
if (!el) {
return;
}
el.textContent = text || "--";
el.classList.remove("b", "g", "o", "alert", "warntext");
if (state === "active") {
el.classList.add("alert");
return;
}
if (state === "past") {
el.classList.add("warntext");
return;
}
if (state === "clear") {
el.classList.add("g");
return;
}
el.classList.add("b");
}
function updateServiceState(payload) { function updateServiceState(payload) {
var system = payload.system || {}; var system = payload.system || {};
var services = payload.services || {}; var services = payload.services || {};
setText("stat-wifi", system.wifi || "unbekannt"); setText("stat-wifi", system.wifi || "unbekannt");
setText("stat-ips", system.ips || "unbekannt"); setText("stat-ips", system.ips || "unbekannt");
updateUndervoltageDisplay(system.undervoltage || "unbekannt", system.undervoltage_state || "unknown");
setText("stat-uptime", system.uptime || "unbekannt"); setText("stat-uptime", system.uptime || "unbekannt");
setText("stat-disk", system.disk_free || "unbekannt"); setText("stat-disk", system.disk_free || "unbekannt");
setText("val-cpu", system.cpu_usage || "--"); setText("val-cpu", system.cpu_usage || "--");
@@ -832,10 +910,10 @@ function publishJoy() {
} }
function startPublishing() { function startPublishing() {
publishJoy();
if (publishTimer) { if (publishTimer) {
clearInterval(publishTimer); return;
} }
publishJoy();
publishTimer = window.setInterval(publishJoy, 50); publishTimer = window.setInterval(publishJoy, 50);
} }
@@ -868,12 +946,7 @@ function pulseButton(buttonIndex) {
} }
function selectDriveMode(mode, buttonIndex) { function selectDriveMode(mode, buttonIndex) {
driveMode = mode; applyDriveMode(mode);
document.querySelectorAll(".dbtn[data-mode]").forEach(function (btn) {
btn.classList.toggle("active", btn.dataset.mode === mode);
});
updateModeDisplay();
updateControlLocks();
pulseButton(buttonIndex); pulseButton(buttonIndex);
} }
@@ -940,6 +1013,7 @@ function moveThumb(clientX, clientY) {
var nx = dx / data.maxD; var nx = dx / data.maxD;
var ny = -dy / data.maxD; var ny = -dy / data.maxD;
setAxes(nx, ny); setAxes(nx, ny);
publishJoy();
startPublishing(); startPublishing();
} }
@@ -1013,6 +1087,30 @@ window.addEventListener("load", function () {
messageType: "sensor_msgs/Joy" messageType: "sensor_msgs/Joy"
}); });
roverCommandListener = new ROSLIB.Topic({
ros: ros,
name: "/rover_command",
messageType: "exomy/RoverCommand"
});
roverCommandListener.subscribe(function (message) {
applyDriveMode(locomotionModeToUi(message.locomotion_mode), true);
motorsEnabled = !!message.motors_enabled;
updateMotorDisplay();
});
var joySubscriber = new ROSLIB.Topic({
ros: ros,
name: "/joy",
messageType: "sensor_msgs/Joy"
});
joySubscriber.subscribe(function (message) {
if (message.header.frame_id === "webgui") return;
var x = (message.axes[0] || 0);
var y = -(message.axes[1] || 0);
mirrorJoyThumb(x, y);
});
document.querySelectorAll(".dbtn[data-mode]").forEach(function (button) { document.querySelectorAll(".dbtn[data-mode]").forEach(function (button) {
button.addEventListener("click", function () { button.addEventListener("click", function () {
selectDriveMode(button.dataset.mode, parseInt(button.dataset.button, 10)); selectDriveMode(button.dataset.mode, parseInt(button.dataset.button, 10));
+9
View File
@@ -455,6 +455,15 @@ h2 {
color: #8a6b35; color: #8a6b35;
} }
@keyframes statusBlink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.25;
}
}
.admin-page { .admin-page {
color: #1b2730; color: #1b2730;
overflow-y: auto; overflow-y: auto;
@@ -54,6 +54,15 @@ DRIVE_PIN_KEYS = {
'rr': 'pin_drive_rr', 'rr': 'pin_drive_rr',
} }
DRIVE_NEUTRAL_KEYS = {
'fl': 'drive_pwm_neutral_fl',
'fr': 'drive_pwm_neutral_fr',
'cl': 'drive_pwm_neutral_cl',
'cr': 'drive_pwm_neutral_cr',
'rl': 'drive_pwm_neutral_rl',
'rr': 'drive_pwm_neutral_rr',
}
ACTION_SETTINGS = { ACTION_SETTINGS = {
'steer_left': { 'steer_left': {
'kind': 'steer', 'kind': 'steer',
@@ -94,7 +103,10 @@ class PCA9685Direct:
try: try:
import smbus import smbus
except ImportError as exc: except ImportError as exc:
raise RuntimeError('smbus ist nicht installiert.') from exc try:
import smbus2 as smbus
except ImportError as inner_exc:
raise RuntimeError('smbus ist nicht installiert.') from inner_exc
self.address = address self.address = address
self.bus = smbus.SMBus(busnum) self.bus = smbus.SMBus(busnum)
@@ -176,7 +188,10 @@ class MotorTester:
self.config = _load_config(self.config_path) self.config = _load_config(self.config_path)
self.pwm.set_pwm_freq(50) self.pwm.set_pwm_freq(50)
self.steer_pwm_range = int(self.config['steer_pwm_range']) self.steer_pwm_range = int(self.config['steer_pwm_range'])
self.drive_pwm_neutral = int(self.config['drive_pwm_neutral']) self.drive_pwm_neutral = {
wheel: int(self.config[DRIVE_NEUTRAL_KEYS[wheel]])
for wheel in WHEELS
}
self.drive_pwm_range = int(self.config['drive_pwm_range']) self.drive_pwm_range = int(self.config['drive_pwm_range'])
def _steer_pin(self, wheel): def _steer_pin(self, wheel):
@@ -188,10 +203,12 @@ class MotorTester:
def _drive_pin(self, wheel): def _drive_pin(self, wheel):
return int(self.config[DRIVE_PIN_KEYS[wheel]]) return int(self.config[DRIVE_PIN_KEYS[wheel]])
def set_drive_neutral_pwm(self, pwm_value): def _drive_neutral(self, wheel):
duty_cycle = int(pwm_value) return int(self.drive_pwm_neutral[wheel])
def set_drive_neutral_pwm(self, values):
for wheel in WHEELS: for wheel in WHEELS:
self.pwm.set_pwm(self._drive_pin(wheel), 0, duty_cycle) self.pwm.set_pwm(self._drive_pin(wheel), 0, int(values[wheel]))
time.sleep(0.03) time.sleep(0.03)
def center_all_steering(self): def center_all_steering(self):
@@ -201,7 +218,7 @@ class MotorTester:
def stop_all_driving(self): def stop_all_driving(self):
for wheel in WHEELS: for wheel in WHEELS:
self.pwm.set_pwm(self._drive_pin(wheel), 0, self.drive_pwm_neutral) self.pwm.set_pwm(self._drive_pin(wheel), 0, self._drive_neutral(wheel))
time.sleep(0.03) time.sleep(0.03)
def set_steering_angle(self, wheel, angle): def set_steering_angle(self, wheel, angle):
@@ -237,12 +254,12 @@ class MotorTester:
def test_driving(self, wheel, speed, duration): def test_driving(self, wheel, speed, duration):
self.pwm.set_pwm(self._steer_pin(wheel), 0, self._steer_neutral(wheel)) self.pwm.set_pwm(self._steer_pin(wheel), 0, self._steer_neutral(wheel))
duty_cycle = int( duty_cycle = int(
self.drive_pwm_neutral + self._drive_neutral(wheel) +
speed / 100.0 * self.drive_pwm_range * WHEEL_DIRECTIONS[wheel] speed / 100.0 * self.drive_pwm_range * WHEEL_DIRECTIONS[wheel]
) )
self.pwm.set_pwm(self._drive_pin(wheel), 0, duty_cycle) self.pwm.set_pwm(self._drive_pin(wheel), 0, duty_cycle)
time.sleep(duration) time.sleep(duration)
self.pwm.set_pwm(self._drive_pin(wheel), 0, self.drive_pwm_neutral) self.pwm.set_pwm(self._drive_pin(wheel), 0, self._drive_neutral(wheel))
def execute(self, wheel, action): def execute(self, wheel, action):
if wheel not in WHEELS: if wheel not in WHEELS:
@@ -418,30 +435,61 @@ def save_steering_neutral_values(values, config_path=None):
def get_drive_neutral_value(config_path=None): def get_drive_neutral_value(config_path=None):
tester = MotorTester(config_path=config_path) tester = MotorTester(config_path=config_path)
values = {}
for wheel in WHEELS:
key = DRIVE_NEUTRAL_KEYS[wheel]
values[wheel] = {
'wheel': wheel,
'wheel_label': WHEEL_LABELS[wheel],
'key': key,
'value': int(tester.config[key]),
'pin': tester._drive_pin(wheel),
}
return { return {
'config_path': tester.config_path, 'config_path': tester.config_path,
'value': int(tester.config['drive_pwm_neutral']), 'values': values,
'pins': {wheel: tester._drive_pin(wheel) for wheel in WHEELS},
} }
def preview_drive_neutral_value(pwm_value, config_path=None): def preview_drive_neutral_value(values, config_path=None):
tester = MotorTester(config_path=config_path) tester = MotorTester(config_path=config_path)
value = int(pwm_value) preview_values = {}
for wheel in WHEELS:
if wheel not in values:
raise ValueError(f'Fahr-Neutralwert fehlt: {DRIVE_NEUTRAL_KEYS[wheel]}')
preview_values[wheel] = int(values[wheel])
tester.center_all_steering() tester.center_all_steering()
tester.set_drive_neutral_pwm(value) tester.set_drive_neutral_pwm(preview_values)
return { return {
'value': value, 'values': {
'pins': {wheel: tester._drive_pin(wheel) for wheel in WHEELS}, wheel: {
'wheel': wheel,
'wheel_label': WHEEL_LABELS[wheel],
'key': DRIVE_NEUTRAL_KEYS[wheel],
'value': preview_values[wheel],
'pin': tester._drive_pin(wheel),
}
for wheel in WHEELS
},
} }
def save_drive_neutral_value(pwm_value, config_path=None): def save_drive_neutral_value(values, config_path=None):
tester = MotorTester(config_path=config_path) tester = MotorTester(config_path=config_path)
value = int(pwm_value) updates = {}
_update_config_values(tester.config_path, {'drive_pwm_neutral': value}) for wheel in WHEELS:
key = DRIVE_NEUTRAL_KEYS[wheel]
if key not in values:
raise ValueError(f'Fahr-Neutralwert fehlt: {key}')
updates[key] = int(values[key])
_update_config_values(tester.config_path, updates)
tester.config = _load_config(tester.config_path) tester.config = _load_config(tester.config_path)
tester.drive_pwm_neutral = int(tester.config['drive_pwm_neutral']) tester.drive_pwm_neutral = {
wheel: int(tester.config[DRIVE_NEUTRAL_KEYS[wheel]])
for wheel in WHEELS
}
tester.center_all_steering() tester.center_all_steering()
tester.set_drive_neutral_pwm(tester.drive_pwm_neutral) tester.set_drive_neutral_pwm(tester.drive_pwm_neutral)
return get_drive_neutral_value(config_path=tester.config_path) return get_drive_neutral_value(config_path=tester.config_path)
@@ -4,31 +4,29 @@ import time
import os import os
config_filename = '../config/exomy.yaml' config_filename = '../config/exomy.yaml'
WHEELS = ('fl', 'fr', 'cl', 'cr', 'rl', 'rr')
def get_driving_pins(): def get_driving_pins():
pin_list = []
with open(config_filename, 'r') as file: with open(config_filename, 'r') as file:
param_dict = yaml.load(file) param_dict = yaml.load(file)
for key, value in param_dict.items(): return [param_dict['pin_drive_' + wheel] for wheel in WHEELS]
if('pin_drive_' in str(key)):
pin_list.append(value)
return pin_list
def get_drive_pwm_neutral(): def get_drive_pwm_neutral_values():
with open(config_filename, 'r') as file: with open(config_filename, 'r') as file:
param_dict = yaml.load(file) param_dict = yaml.load(file)
for key, value in param_dict.items(): values = {}
if('drive_pwm_neutral' in str(key)): for wheel in WHEELS:
return value key = 'drive_pwm_neutral_' + wheel
if key not in param_dict:
default_value = 300 print('The parameter ' + key + ' could not be found in the exomy.yaml \n')
print('The parameter drive_pwm_neutral could not be found in the exomy.yaml \n') print('It was set to the default value: 300\n')
print('It was set to the default value: '+ default_value + '\n') values[wheel] = 300
return default_value else:
values[wheel] = param_dict[key]
return values
if __name__ == "__main__": if __name__ == "__main__":
print( print(
@@ -62,7 +60,7 @@ On each motor you have to turn the correction screw until the motor really stand
pwm = Adafruit_PCA9685.PCA9685() pwm = Adafruit_PCA9685.PCA9685()
''' '''
The drive_pwm_neutral value is determined from the exomy.yaml file. The drive_pwm_neutral values are determined from the exomy.yaml file.
But it can be also calculated from the values of the PWM board and motors, But it can be also calculated from the values of the PWM board and motors,
like shown in the following calculation: like shown in the following calculation:
@@ -83,11 +81,11 @@ On each motor you have to turn the correction screw until the motor really stand
value = int(duty_cycle*4096.0) # 307 value = int(duty_cycle*4096.0) # 307
''' '''
value = get_drive_pwm_neutral() value_dict = get_drive_pwm_neutral_values()
pin_list = get_driving_pins() pin_list = get_driving_pins()
for pin in pin_list: for index, pin in enumerate(pin_list):
pwm.set_pwm(pin, 0, value) pwm.set_pwm(pin, 0, value_dict[WHEELS[index]])
time.sleep(0.1) time.sleep(0.1)
raw_input('Press any button if you are done to complete configuration\n') raw_input('Press any button if you are done to complete configuration\n')
@@ -0,0 +1,13 @@
[Unit]
Description=ExoMy WLAN Fallback Manager
After=NetworkManager.service
Wants=NetworkManager.service
[Service]
Type=simple
ExecStart=/usr/local/bin/exomy-wifi-fallback-manager.sh
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
@@ -72,6 +72,41 @@ def read_memory_usage():
return 'unbekannt' return 'unbekannt'
def read_undervoltage_status():
stdout, _, returncode = run_command(['vcgencmd', 'get_throttled'])
if returncode != 0 or not stdout or '=' not in stdout:
return {
'text': 'unbekannt',
'state': 'unknown',
}
try:
throttled_value = int(stdout.split('=', 1)[1].strip(), 16)
except ValueError:
return {
'text': 'unbekannt',
'state': 'unknown',
}
undervoltage_now = bool(throttled_value & 0x1)
undervoltage_occurred = bool(throttled_value & 0x10000)
if undervoltage_now:
return {
'text': 'Ja, aktuell',
'state': 'active',
}
if undervoltage_occurred:
return {
'text': 'Früher erkannt',
'state': 'past',
}
return {
'text': 'Nein',
'state': 'clear',
}
def format_uptime(): def format_uptime():
try: try:
with open('/proc/uptime', 'r', encoding='utf-8') as handle: with open('/proc/uptime', 'r', encoding='utf-8') as handle:
@@ -100,11 +135,25 @@ def format_disk_free():
def get_ip_addresses(): def get_ip_addresses():
stdout, _, returncode = run_command(['ip', '-4', '-o', 'addr', 'show', 'dev', 'wlan0', 'scope', 'global'])
if returncode == 0 and stdout:
for line in stdout.splitlines():
parts = line.split()
if 'inet' in parts:
inet_index = parts.index('inet')
if inet_index + 1 < len(parts):
return parts[inet_index + 1].split('/')[0]
stdout, _, returncode = run_command(['hostname', '-I']) stdout, _, returncode = run_command(['hostname', '-I'])
if returncode != 0 or not stdout: if returncode != 0 or not stdout:
return 'unbekannt' return 'unbekannt'
ips = [item for item in stdout.split() if item]
return ', '.join(ips) if ips else 'unbekannt' ipv4_addresses = []
for item in stdout.split():
if item.count('.') == 3 and not item.startswith('172.17.'):
ipv4_addresses.append(item)
return ipv4_addresses[0] if ipv4_addresses else 'unbekannt'
def get_wifi_status(): def get_wifi_status():
@@ -149,11 +198,14 @@ def get_motor_test_container_status():
def collect_status(): def collect_status():
undervoltage = read_undervoltage_status()
return { return {
'status': 'Bereit', 'status': 'Bereit',
'system': { 'system': {
'wifi': get_wifi_status(), 'wifi': get_wifi_status(),
'ips': get_ip_addresses(), 'ips': get_ip_addresses(),
'undervoltage': undervoltage['text'],
'undervoltage_state': undervoltage['state'],
'cpu_temperature': read_cpu_temperature(), 'cpu_temperature': read_cpu_temperature(),
'cpu_usage': read_cpu_usage(), 'cpu_usage': read_cpu_usage(),
'memory_usage': read_memory_usage(), 'memory_usage': read_memory_usage(),
@@ -315,17 +367,17 @@ def get_drive_neutral_status():
try: try:
result = get_drive_neutral_value() result = get_drive_neutral_value()
return 200, { return 200, {
'status': 'Fahr-Neutralwert geladen', 'status': 'Fahr-Neutralwerte geladen',
'drive_neutral': result, 'drive_neutral': result,
'container_status': container_status, 'container_status': container_status,
} }
except FileNotFoundError: except FileNotFoundError:
return 500, {'status': 'Motor-Konfiguration fehlt'} return 500, {'status': 'Motor-Konfiguration fehlt'}
except Exception: except Exception:
return 500, {'status': 'Fahr-Neutralwert konnte nicht geladen werden'} return 500, {'status': 'Fahr-Neutralwerte konnten nicht geladen werden'}
def preview_drive_neutral_action(value): def preview_drive_neutral_action(values):
from admin_motor_test import preview_drive_neutral_value from admin_motor_test import preview_drive_neutral_value
container_status = get_motor_test_container_status() container_status = get_motor_test_container_status()
@@ -336,9 +388,9 @@ def preview_drive_neutral_action(value):
} }
try: try:
result = preview_drive_neutral_value(int(value)) result = preview_drive_neutral_value(values)
return 200, { return 200, {
'status': f"Fahr-Neutralwert: PWM {result['value']}", 'status': 'Fahr-Neutralwerte angefahren',
'drive_neutral': result, 'drive_neutral': result,
'container_status': container_status, 'container_status': container_status,
} }
@@ -350,7 +402,7 @@ def preview_drive_neutral_action(value):
return 500, {'status': 'Fahr-Vorschau fehlgeschlagen'} return 500, {'status': 'Fahr-Vorschau fehlgeschlagen'}
def save_drive_neutral_action(value): def save_drive_neutral_action(values):
from admin_motor_test import save_drive_neutral_value from admin_motor_test import save_drive_neutral_value
container_status = get_motor_test_container_status() container_status = get_motor_test_container_status()
@@ -361,9 +413,9 @@ def save_drive_neutral_action(value):
} }
try: try:
result = save_drive_neutral_value(int(value)) result = save_drive_neutral_value(values)
return 200, { return 200, {
'status': 'Fahr-Neutralwert gespeichert', 'status': 'Fahr-Neutralwerte gespeichert',
'drive_neutral': result, 'drive_neutral': result,
'container_status': container_status, 'container_status': container_status,
} }
@@ -374,7 +426,7 @@ def save_drive_neutral_action(value):
except FileNotFoundError: except FileNotFoundError:
return 500, {'status': 'Motor-Konfiguration fehlt'} return 500, {'status': 'Motor-Konfiguration fehlt'}
except Exception: except Exception:
return 500, {'status': 'Fahr-Neutralwert konnte nicht gespeichert werden'} return 500, {'status': 'Fahr-Neutralwerte konnten nicht gespeichert werden'}
class Handler(http.server.BaseHTTPRequestHandler): class Handler(http.server.BaseHTTPRequestHandler):
@@ -431,7 +483,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
self.write_json(400, {'status': 'Ungültige Anfrage'}) self.write_json(400, {'status': 'Ungültige Anfrage'})
return return
status_code, response = preview_drive_neutral_action(payload.get('value', 0)) values = payload.get('values')
if not isinstance(values, dict):
self.write_json(400, {'status': 'Fahr-Neutralwerte fehlen'})
return
status_code, response = preview_drive_neutral_action(values)
self.write_json(status_code, response) self.write_json(status_code, response)
return return
@@ -449,7 +506,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
self.write_json(400, {'status': 'Ungültige Anfrage'}) self.write_json(400, {'status': 'Ungültige Anfrage'})
return return
status_code, response = save_drive_neutral_action(payload.get('value', 0)) values = payload.get('values')
if not isinstance(values, dict):
self.write_json(400, {'status': 'Fahr-Neutralwerte fehlen'})
return
status_code, response = save_drive_neutral_action(values)
self.write_json(status_code, response) self.write_json(status_code, response)
return return
@@ -0,0 +1,49 @@
#!/bin/bash
set -euo pipefail
WIFI_IFACE="wlan0"
PRIMARY_CONN="netplan-wlan0-eskimue.de"
SECONDARY_CONN="4pi"
FALLBACK_AP_CONN="CUNO-AP"
CHECK_INTERVAL=20
get_active_connection() {
nmcli -t -f GENERAL.CONNECTION device show "$WIFI_IFACE" 2>/dev/null | head -n 1 | cut -d: -f2-
}
activate_connection() {
local connection_name="$1"
nmcli --wait 15 connection up "$connection_name" ifname "$WIFI_IFACE" >/dev/null 2>&1
}
ensure_best_connection() {
local active_connection
active_connection="$(get_active_connection)"
if [[ "$active_connection" == "$PRIMARY_CONN" ]]; then
return
fi
if activate_connection "$PRIMARY_CONN"; then
return
fi
active_connection="$(get_active_connection)"
if [[ "$active_connection" == "$SECONDARY_CONN" ]]; then
return
fi
if activate_connection "$SECONDARY_CONN"; then
return
fi
active_connection="$(get_active_connection)"
if [[ "$active_connection" != "$FALLBACK_AP_CONN" ]]; then
activate_connection "$FALLBACK_AP_CONN" || true
fi
}
while true; do
ensure_best_connection
sleep "$CHECK_INTERVAL"
done
@@ -0,0 +1,99 @@
#!/usr/bin/env python
import errno
import os
import select
import struct
import time
import rospy
from sensor_msgs.msg import Joy
DEVICE_PATH = '/dev/input/js0'
AXIS_COUNT = 8
BUTTON_COUNT = 12
PUBLISH_RATE_HZ = 20.0
JS_EVENT_BUTTON = 0x01
JS_EVENT_AXIS = 0x02
JS_EVENT_INIT = 0x80
EVENT_SIZE = struct.calcsize('IhBB')
def open_device():
while not rospy.is_shutdown():
try:
fd = os.open(DEVICE_PATH, os.O_RDONLY | os.O_NONBLOCK)
rospy.loginfo('Joystick verbunden: %s', DEVICE_PATH)
return fd
except OSError as exc:
if exc.errno != errno.ENOENT:
rospy.logwarn('Joystick kann nicht geoeffnet werden: %s', exc)
rospy.sleep(1.0)
return None
def close_device(fd):
if fd is None:
return
try:
os.close(fd)
except OSError:
pass
if __name__ == '__main__':
rospy.init_node('f710_joy_node')
publisher = rospy.Publisher('/joy', Joy, queue_size=5)
rate = rospy.Rate(PUBLISH_RATE_HZ)
axes = [0.0] * AXIS_COUNT
buttons = [0] * BUTTON_COUNT
joystick_fd = None
while not rospy.is_shutdown():
if joystick_fd is None:
joystick_fd = open_device()
axes = [0.0] * AXIS_COUNT
buttons = [0] * BUTTON_COUNT
if joystick_fd is None:
break
try:
readable, _, _ = select.select([joystick_fd], [], [], 0.0)
if readable:
event = os.read(joystick_fd, EVENT_SIZE)
while event and len(event) == EVENT_SIZE:
_, value, event_type, number = struct.unpack('IhBB', event)
event_type &= ~JS_EVENT_INIT
if event_type == JS_EVENT_AXIS and number < len(axes):
axes[number] = max(-1.0, min(1.0, value / 32767.0))
elif event_type == JS_EVENT_BUTTON and number < len(buttons):
buttons[number] = 1 if value else 0
try:
event = os.read(joystick_fd, EVENT_SIZE)
except OSError as exc:
if exc.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
break
raise
message = Joy()
message.header.stamp = rospy.Time.now()
message.axes = list(axes)
message.buttons = list(buttons)
publisher.publish(message)
rate.sleep()
except OSError as exc:
if exc.errno in (errno.ENODEV, errno.EIO, errno.ENXIO, errno.EBADF):
rospy.logwarn('Joystick getrennt, warte auf Neuverbindung.')
else:
rospy.logwarn('Joystick-Lesefehler: %s', exc)
close_device(joystick_fd)
joystick_fd = None
time.sleep(1.0)
close_device(joystick_fd)
@@ -14,6 +14,8 @@ locomotion_mode = LocomotionMode.ACKERMANN.value
motors_enabled = True motors_enabled = True
AXIS_DEADZONE = 0.1 AXIS_DEADZONE = 0.1
last_start_button_pressed = False last_start_button_pressed = False
last_webgui_time = None
WEBGUI_PRIORITY_TIMEOUT = 2.0
VELOCITY_EXPO = 2.0 VELOCITY_EXPO = 2.0
@@ -23,6 +25,7 @@ CONTROLLER_FUNCTION_MAPS = {
"x_axis": 0, "x_axis": 0,
"y_axis": 1, "y_axis": 1,
"invert_x_axis": True, "invert_x_axis": True,
"invert_y_axis": False,
"X_button": 0, "X_button": 0,
"Y_button": 3, "Y_button": 3,
"A_button": 1, "A_button": 1,
@@ -34,7 +37,8 @@ CONTROLLER_FUNCTION_MAPS = {
"logitech-F710": { "logitech-F710": {
"x_axis": 0, "x_axis": 0,
"y_axis": 1, "y_axis": 1,
"invert_x_axis": False, "invert_x_axis": True,
"invert_y_axis": True,
"X_button": 0, "X_button": 0,
"Y_button": 3, "Y_button": 3,
"A_button": 1, "A_button": 1,
@@ -47,6 +51,7 @@ CONTROLLER_FUNCTION_MAPS = {
"x_axis": 0, "x_axis": 0,
"y_axis": 1, "y_axis": 1,
"invert_x_axis": False, "invert_x_axis": False,
"invert_y_axis": False,
"X_button": 2, "X_button": 2,
"Y_button": 3, "Y_button": 3,
"A_button": 0, "A_button": 0,
@@ -94,6 +99,15 @@ def callback(data):
global locomotion_mode global locomotion_mode
global motors_enabled global motors_enabled
global last_start_button_pressed global last_start_button_pressed
global last_webgui_time
is_webgui = data.header.frame_id == "webgui"
now = rospy.Time.now()
if is_webgui:
last_webgui_time = now
elif last_webgui_time is not None and (now - last_webgui_time).to_sec() < WEBGUI_PRIORITY_TIMEOUT:
return
rover_cmd = RoverCommand() rover_cmd = RoverCommand()
@@ -118,6 +132,8 @@ def callback(data):
if controller_function_map["invert_x_axis"]: if controller_function_map["invert_x_axis"]:
x *= -1 x *= -1
if controller_function_map["invert_y_axis"]:
y *= -1
# Reading out button data to set locomotion mode # Reading out button data to set locomotion mode
# X Button # X Button
+10 -7
View File
@@ -53,7 +53,7 @@ class Motors():
self.pins['steer'][self.RR] = rospy.get_param("pin_steer_rr") self.pins['steer'][self.RR] = rospy.get_param("pin_steer_rr")
# PWM characteristics # PWM characteristics
self.pwm = Adafruit_PCA9685.PCA9685() self.pwm = Adafruit_PCA9685.PCA9685(busnum=1)
self.pwm.set_pwm_freq(50) # Hz self.pwm.set_pwm_freq(50) # Hz
self.steering_pwm_neutral = [None] * 6 self.steering_pwm_neutral = [None] * 6
@@ -67,7 +67,13 @@ class Motors():
self.steering_pwm_range = rospy.get_param("steer_pwm_range") self.steering_pwm_range = rospy.get_param("steer_pwm_range")
self.driving_pwm_low_limit = 100 self.driving_pwm_low_limit = 100
self.driving_pwm_neutral = rospy.get_param("drive_pwm_neutral") self.driving_pwm_neutral = [None] * 6
self.driving_pwm_neutral[self.FL] = rospy.get_param("drive_pwm_neutral_fl")
self.driving_pwm_neutral[self.FR] = rospy.get_param("drive_pwm_neutral_fr")
self.driving_pwm_neutral[self.CL] = rospy.get_param("drive_pwm_neutral_cl")
self.driving_pwm_neutral[self.CR] = rospy.get_param("drive_pwm_neutral_cr")
self.driving_pwm_neutral[self.RL] = rospy.get_param("drive_pwm_neutral_rl")
self.driving_pwm_neutral[self.RR] = rospy.get_param("drive_pwm_neutral_rr")
self.driving_pwm_upper_limit = 500 self.driving_pwm_upper_limit = 500
self.driving_pwm_range = rospy.get_param("drive_pwm_range") self.driving_pwm_range = rospy.get_param("drive_pwm_range")
@@ -111,14 +117,11 @@ class Motors():
def setDriving(self, driving_command): def setDriving(self, driving_command):
# Loop through pin dictionary. The items key is the wheel_name and the value the pin. # Loop through pin dictionary. The items key is the wheel_name and the value the pin.
for wheel_name, motor_pin in self.pins['drive'].items(): for wheel_name, motor_pin in self.pins['drive'].items():
duty_cycle = int(self.driving_pwm_neutral + duty_cycle = int(self.driving_pwm_neutral[wheel_name] +
driving_command[wheel_name]/100.0 * self.driving_pwm_range * self.wheel_directions[wheel_name]) driving_command[wheel_name]/100.0 * self.driving_pwm_range * self.wheel_directions[wheel_name])
self.pwm.set_pwm(motor_pin, 0, duty_cycle) self.pwm.set_pwm(motor_pin, 0, duty_cycle)
def stopMotors(self): def stopMotors(self):
# Set driving wheels to neutral position to stop them
duty_cycle = int(self.driving_pwm_neutral)
for wheel_name, motor_pin in self.pins['drive'].items(): for wheel_name, motor_pin in self.pins['drive'].items():
self.pwm.set_pwm(motor_pin, 0, duty_cycle) self.pwm.set_pwm(motor_pin, 0, int(self.driving_pwm_neutral[wheel_name]))
+9 -4
View File
@@ -24,6 +24,7 @@ class Rover():
self.wheel_ry = 20.3 self.wheel_ry = 20.3
self.wheel_fx = 16.0 self.wheel_fx = 16.0
self.wheel_fy = 20.3 self.wheel_fy = 20.3
self.point_turn_max_angle = 45
max_steering_angle = 45 max_steering_angle = 45
self.ackermann_r_max = 250 self.ackermann_r_max = 250
@@ -152,10 +153,14 @@ class Rover():
return steering_angles return steering_angles
if(self.locomotion_mode == LocomotionMode.POINT_TURN.value): if(self.locomotion_mode == LocomotionMode.POINT_TURN.value):
point_turn_angle = int(math.degrees( raw_point_turn_angle = math.degrees(
math.atan((self.wheel_rx + self.wheel_fx) / self.wheel_ry))) math.atan((self.wheel_rx + self.wheel_fx) / self.wheel_ry))
point_turn_angle_center = int(math.degrees( raw_point_turn_angle_center = math.degrees(
math.atan((((self.wheel_rx + self.wheel_fx) / 2) - self.wheel_fx) / (self.wheel_ry / 2)))) math.atan((((self.wheel_rx + self.wheel_fx) / 2) - self.wheel_fx) / (self.wheel_ry / 2)))
point_turn_angle = int(min(self.point_turn_max_angle, raw_point_turn_angle))
center_scale = 0.0 if raw_point_turn_angle == 0 else abs(raw_point_turn_angle_center / raw_point_turn_angle)
point_turn_angle_center = int(point_turn_angle * center_scale)
steering_angles[self.FL] = point_turn_angle steering_angles[self.FL] = point_turn_angle
steering_angles[self.FR] = -point_turn_angle steering_angles[self.FR] = -point_turn_angle
+7 -2
View File
@@ -1,10 +1,10 @@
# ExoMy Setup-Status # ExoMy Setup-Status
Stand: 2026-05-18 Stand: 2026-05-21
## Raspberry Pi ## Raspberry Pi
- Hostname: `cuno` - Hostname: `ExoMyCuno`
- Benutzer: `pi` - Benutzer: `pi`
- LAN-IP: `192.168.1.83` - LAN-IP: `192.168.1.83`
- WLAN-IP: `192.168.1.9` - WLAN-IP: `192.168.1.9`
@@ -30,6 +30,7 @@ Stand: 2026-05-18
- Bekannte WLANs: - Bekannte WLANs:
- `eskimue.de` - `eskimue.de`
- `4pi` - `4pi`
- Passwort `4pi`: `st89Saf6H86n`
- Fallback-Access-Point: - Fallback-Access-Point:
- SSID: `CUNO` - SSID: `CUNO`
- Passwort: `astr0cun042` - Passwort: `astr0cun042`
@@ -46,6 +47,10 @@ Stand: 2026-05-18
- Wenn `eskimue.de` erreichbar ist, verbindet sich der Pi bevorzugt damit. - Wenn `eskimue.de` erreichbar ist, verbindet sich der Pi bevorzugt damit.
- Wenn `eskimue.de` nicht erreichbar ist, aber `4pi` sichtbar ist, verbindet sich der Pi mit `4pi`. - Wenn `eskimue.de` nicht erreichbar ist, aber `4pi` sichtbar ist, verbindet sich der Pi mit `4pi`.
- Wenn keines der beiden WLANs erreichbar ist, soll `CUNO` als eigener Access Point einspringen. - Wenn keines der beiden WLANs erreichbar ist, soll `CUNO` als eigener Access Point einspringen.
- Ein Host-Dienst prueft laufend die Reihenfolge:
- erst `eskimue.de`
- dann `4pi`
- sonst `CUNO`
- Das alte WLAN-Profil `FRITZ!Box 6660 Cable AI` ist nicht mehr auf Autoverbindung gesetzt. - Das alte WLAN-Profil `FRITZ!Box 6660 Cable AI` ist nicht mehr auf Autoverbindung gesetzt.
## Aktueller Aufbau ## Aktueller Aufbau