Alles verbessert
This commit is contained in:
+146
-26
@@ -1,34 +1,154 @@
|
||||
# ExoMy - Software Repository
|
||||
This repository contains the software to run Exomy. The [wiki](https://github.com/esa-prl/ExoMy/wiki) explains you how to use it.
|
||||
# ExoMy CUNO — Software
|
||||
|
||||

|
||||
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/)
|
||||
There is a website about ExoMy. It does not help you build it, but is still nice to look at.
|
||||
## Zugang zum Raspberry Pi
|
||||
|
||||
### [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)
|
||||
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).
|
||||
```bash
|
||||
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 -->
|
||||
<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>
|
||||
## 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
|
||||
```
|
||||
|
||||
**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
|
||||
steer_pwm_range: 250
|
||||
|
||||
# PWM value for the stop position of driving motors
|
||||
drive_pwm_neutral: 307
|
||||
# PWM values for the stop position of driving motors
|
||||
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
|
||||
drive_pwm_range: 100
|
||||
|
||||
@@ -38,8 +38,13 @@ steer_pwm_neutral_rr: 300
|
||||
# PWW range around steering neutral position
|
||||
steer_pwm_range: 250
|
||||
|
||||
# PWM value for the stop position of driving motors
|
||||
drive_pwm_neutral: 307
|
||||
# PWM values for the stop position of driving motors
|
||||
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
|
||||
drive_pwm_range: 100
|
||||
|
||||
@@ -10,15 +10,12 @@ RUN apt-get update && \
|
||||
|
||||
# Install additional ros packages
|
||||
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
|
||||
RUN apt-get update && \
|
||||
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
|
||||
|
||||
COPY ./entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -1,19 +1,55 @@
|
||||
#!/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" ]]
|
||||
then
|
||||
cd /root/exomy_ws/src/exomy/scripts
|
||||
bash
|
||||
elif [[ $1 == "autostart" ]]
|
||||
then
|
||||
trap cleanup EXIT INT TERM
|
||||
source /opt/ros/melodic/setup.bash
|
||||
cd /root/exomy_ws
|
||||
catkin_make
|
||||
http-server src/exomy/gui -p 8000 &
|
||||
|
||||
export ROS_MASTER_URI=http://localhost:11311
|
||||
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" ]]
|
||||
then
|
||||
cd /root/exomy_ws
|
||||
@@ -24,4 +60,3 @@ then
|
||||
else
|
||||
bash
|
||||
fi
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="motor_test_neutral_toolbar">
|
||||
@@ -146,22 +146,7 @@
|
||||
<button id="drive_neutral_save_button" class="button buttonA" onclick="saveDriveNeutralValue()">Speichern</button>
|
||||
</div>
|
||||
|
||||
<article class="drive_neutral_card" id="drive_neutral_card">
|
||||
<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 id="drive_neutral_grid" class="steering_neutral_grid"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -188,9 +173,8 @@
|
||||
var activeMotorTestTab = 'impulse';
|
||||
var steeringNeutralValues = {};
|
||||
var steeringNeutralSavedValues = {};
|
||||
var driveNeutralValue = null;
|
||||
var driveNeutralSavedValue = null;
|
||||
var driveNeutralPins = {};
|
||||
var driveNeutralValues = {};
|
||||
var driveNeutralSavedValues = {};
|
||||
var STEERING_NEUTRAL_STEP = 5;
|
||||
var WHEEL_ORDER = ['fl', 'fr', 'cl', 'cr', 'rl', 'rr'];
|
||||
|
||||
@@ -288,7 +272,7 @@
|
||||
loadSteeringNeutralValues();
|
||||
}
|
||||
|
||||
if (isDriveNeutral && driveNeutralValue === null) {
|
||||
if (isDriveNeutral && !Object.keys(driveNeutralValues).length) {
|
||||
loadDriveNeutralValue();
|
||||
}
|
||||
}
|
||||
@@ -348,38 +332,59 @@
|
||||
button.disabled = isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || !hasUnsavedNeutralChanges();
|
||||
}
|
||||
|
||||
function renderDriveNeutralCard() {
|
||||
var valueNode = document.getElementById('drive_neutral_value');
|
||||
var pinsNode = document.getElementById('drive_neutral_pins');
|
||||
var card = document.getElementById('drive_neutral_card');
|
||||
|
||||
if (valueNode) {
|
||||
valueNode.textContent = driveNeutralValue === null ? '-' : String(driveNeutralValue);
|
||||
function renderDriveNeutralCards() {
|
||||
var grid = document.getElementById('drive_neutral_grid');
|
||||
if (!grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pinsNode) {
|
||||
var pinValues = WHEEL_ORDER
|
||||
.filter(function (wheel) { return Object.prototype.hasOwnProperty.call(driveNeutralPins, wheel); })
|
||||
.map(function (wheel) { return wheel.toUpperCase() + ':' + driveNeutralPins[wheel]; });
|
||||
pinsNode.textContent = pinValues.length ? 'Pins: ' + pinValues.join(' ') : 'Pins: -';
|
||||
}
|
||||
var html = [];
|
||||
WHEEL_ORDER.forEach(function (wheel) {
|
||||
var entry = driveNeutralValues[wheel];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (card) {
|
||||
var isDirty = driveNeutralValue !== null && driveNeutralSavedValue !== null && Number(driveNeutralValue) !== Number(driveNeutralSavedValue);
|
||||
card.classList.toggle('is_dirty', isDirty);
|
||||
}
|
||||
var savedEntry = driveNeutralSavedValues[wheel];
|
||||
var isDirty = !savedEntry || Number(savedEntry.value) !== Number(entry.value);
|
||||
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();
|
||||
}
|
||||
|
||||
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() {
|
||||
var button = document.getElementById('drive_neutral_save_button');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hasChanges = driveNeutralValue !== null && driveNeutralSavedValue !== null && Number(driveNeutralValue) !== Number(driveNeutralSavedValue);
|
||||
button.disabled = isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving || !hasChanges;
|
||||
button.disabled = isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving || !hasUnsavedDriveNeutralChanges();
|
||||
}
|
||||
|
||||
async function fetchMotorTestStatus() {
|
||||
@@ -606,7 +611,7 @@
|
||||
|
||||
isDriveNeutralLoading = true;
|
||||
updateDriveNeutralSaveButton();
|
||||
setMotorTestStatus('Lade Fahr-Neutralwert...');
|
||||
setMotorTestStatus('Lade Fahr-Neutralwerte...');
|
||||
|
||||
try {
|
||||
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral');
|
||||
@@ -616,37 +621,44 @@
|
||||
throw new Error(data.status || ('status ' + response.status));
|
||||
}
|
||||
|
||||
driveNeutralValue = Number(data.drive_neutral && data.drive_neutral.value);
|
||||
driveNeutralSavedValue = driveNeutralValue;
|
||||
driveNeutralPins = data.drive_neutral && data.drive_neutral.pins ? data.drive_neutral.pins : {};
|
||||
renderDriveNeutralCard();
|
||||
driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : {};
|
||||
driveNeutralSavedValues = JSON.parse(JSON.stringify(driveNeutralValues));
|
||||
renderDriveNeutralCards();
|
||||
renderContainerStatus(data);
|
||||
setMotorTestStatus(data.status || 'Fahr-Neutralwert geladen');
|
||||
setMotorTestStatus(data.status || 'Fahr-Neutralwerte geladen');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setMotorTestStatus('Fehler');
|
||||
alert(error.message || 'Der Fahr-Neutralwert konnte nicht geladen werden.');
|
||||
alert(error.message || 'Die Fahr-Neutralwerte konnten nicht geladen werden.');
|
||||
} finally {
|
||||
isDriveNeutralLoading = false;
|
||||
updateDriveNeutralSaveButton();
|
||||
}
|
||||
}
|
||||
|
||||
async function adjustDriveNeutralValue(delta) {
|
||||
async function adjustDriveNeutralValue(wheel, delta) {
|
||||
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (driveNeutralValue === null) {
|
||||
var entry = driveNeutralValues[wheel];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
driveNeutralValue = Math.max(0, Math.min(4095, Number(driveNeutralValue) + Number(delta)));
|
||||
renderDriveNeutralCard();
|
||||
entry.value = Math.max(0, Math.min(4095, Number(entry.value) + Number(delta)));
|
||||
renderDriveNeutralCards();
|
||||
|
||||
isDriveNeutralPreviewRunning = true;
|
||||
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 {
|
||||
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral/preview', {
|
||||
@@ -655,7 +667,7 @@
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
value: driveNeutralValue
|
||||
values: values
|
||||
})
|
||||
});
|
||||
var data = await response.json();
|
||||
@@ -664,10 +676,10 @@
|
||||
throw new Error(data.status || ('status ' + response.status));
|
||||
}
|
||||
|
||||
driveNeutralPins = data.drive_neutral && data.drive_neutral.pins ? data.drive_neutral.pins : driveNeutralPins;
|
||||
renderDriveNeutralCard();
|
||||
driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : driveNeutralValues;
|
||||
renderDriveNeutralCards();
|
||||
renderContainerStatus(data);
|
||||
setMotorTestStatus(data.status || 'Fahr-Neutralwert angefahren');
|
||||
setMotorTestStatus(data.status || 'Fahr-Neutralwerte angefahren');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setMotorTestStatus('Fehler');
|
||||
@@ -683,13 +695,21 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (driveNeutralValue === null || driveNeutralSavedValue === null || Number(driveNeutralValue) === Number(driveNeutralSavedValue)) {
|
||||
if (!hasUnsavedDriveNeutralChanges()) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDriveNeutralSaving = true;
|
||||
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 {
|
||||
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral/save', {
|
||||
@@ -698,7 +718,7 @@
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
value: driveNeutralValue
|
||||
values: values
|
||||
})
|
||||
});
|
||||
var data = await response.json();
|
||||
@@ -707,16 +727,15 @@
|
||||
throw new Error(data.status || ('status ' + response.status));
|
||||
}
|
||||
|
||||
driveNeutralValue = Number(data.drive_neutral && data.drive_neutral.value);
|
||||
driveNeutralSavedValue = driveNeutralValue;
|
||||
driveNeutralPins = data.drive_neutral && data.drive_neutral.pins ? data.drive_neutral.pins : driveNeutralPins;
|
||||
renderDriveNeutralCard();
|
||||
driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : driveNeutralValues;
|
||||
driveNeutralSavedValues = JSON.parse(JSON.stringify(driveNeutralValues));
|
||||
renderDriveNeutralCards();
|
||||
renderContainerStatus(data);
|
||||
setMotorTestStatus(data.status || 'Fahr-Neutralwert gespeichert');
|
||||
setMotorTestStatus(data.status || 'Fahr-Neutralwerte gespeichert');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setMotorTestStatus('Fehler');
|
||||
alert(error.message || 'Der Fahr-Neutralwert konnte nicht gespeichert werden.');
|
||||
alert(error.message || 'Die Fahr-Neutralwerte konnten nicht gespeichert werden.');
|
||||
} finally {
|
||||
isDriveNeutralSaving = false;
|
||||
updateDriveNeutralSaveButton();
|
||||
|
||||
@@ -89,6 +89,10 @@
|
||||
<span class="info_label">CPU-Temperatur</span>
|
||||
<strong id="info_temp">-</strong>
|
||||
</div>
|
||||
<div class="info_card">
|
||||
<span class="info_label">Unterspannung</span>
|
||||
<strong id="info_undervoltage" class="state_unknown">-</strong>
|
||||
</div>
|
||||
<div class="info_card">
|
||||
<span class="info_label">Uptime</span>
|
||||
<strong id="info_uptime">-</strong>
|
||||
@@ -145,12 +149,47 @@
|
||||
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) {
|
||||
setAdminStatus(data.status || 'Bereit');
|
||||
|
||||
setText('info_ips', data.system && data.system.ips);
|
||||
setText('info_wifi', data.system && data.system.wifi);
|
||||
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_disk', data.system && data.system.disk_free);
|
||||
|
||||
|
||||
@@ -187,6 +187,12 @@ header {
|
||||
color: #fff;
|
||||
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-port {
|
||||
@@ -205,19 +211,6 @@ header {
|
||||
background: #05070d;
|
||||
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 {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -230,42 +223,19 @@ header {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.c-xhair::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: rgba(0,255,145,.45);
|
||||
transform: translateY(-50%);
|
||||
@keyframes xh-spin { to { transform: rotate(360deg); } }
|
||||
@keyframes xh-pulse {
|
||||
0% { transform: scale(1); opacity: .7; }
|
||||
100% { transform: scale(2.8); opacity: 0; }
|
||||
}
|
||||
.c-xhair::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
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; }
|
||||
.xh-rot { animation: xh-spin 14s linear infinite; transform-box: fill-box; transform-origin: center; }
|
||||
.xh-rot-rev { animation: xh-spin 8s linear infinite reverse; transform-box: fill-box; transform-origin: center; }
|
||||
.xh-pulse { animation: xh-pulse 2.5s ease-out infinite; transform-box: fill-box; transform-origin: center; }
|
||||
.c-rec {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
@@ -328,11 +298,20 @@ header {
|
||||
.sval.g { color: var(--ok); }
|
||||
.sval.o { color: var(--sec); }
|
||||
.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; }
|
||||
.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-i { height: 100%; border-radius: 1px; background: var(--pri); }
|
||||
.sbar-i.w { background: var(--sec); }
|
||||
@keyframes uv-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
#joy-panel .p-hdr { padding: 14px 12px; }
|
||||
.mode-toggle { display: flex; gap: 2px; margin-left: auto; }
|
||||
@@ -563,10 +542,41 @@ header {
|
||||
STREAM: 8081
|
||||
</div>
|
||||
<div class="c-xhair">
|
||||
<div class="xc tl"></div><div class="xc tr"></div>
|
||||
<div class="xc bl"></div><div class="xc br"></div>
|
||||
<svg viewBox="-90 -90 180 180" xmlns="http://www.w3.org/2000/svg" width="180" height="180">
|
||||
<!-- ä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 class="c-scanlines"></div>
|
||||
|
||||
<div class="c-vignette"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -633,6 +643,10 @@ header {
|
||||
<span class="slbl">IP Adressen</span>
|
||||
<span class="sval b" id="stat-ips">--</span>
|
||||
</div>
|
||||
<div class="srow warn">
|
||||
<span class="slbl">Unterspannung</span>
|
||||
<span class="sval b" id="stat-undervoltage">--</span>
|
||||
</div>
|
||||
<div class="srow info">
|
||||
<span class="slbl">Uptime Pi</span>
|
||||
<span class="sval b" id="stat-uptime">--</span>
|
||||
@@ -704,6 +718,7 @@ header {
|
||||
var hostUrl = window.location.hostname;
|
||||
var ros = null;
|
||||
var joyListener = null;
|
||||
var roverCommandListener = null;
|
||||
var publishTimer = null;
|
||||
var statusTimer = null;
|
||||
var axes = [0, 0, 0, 0, 0, 0];
|
||||
@@ -771,6 +786,45 @@ function updateModeDisplay() {
|
||||
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() {
|
||||
setText("stat-motors", motorsEnabled ? "Aktiv" : "Aus");
|
||||
}
|
||||
@@ -787,11 +841,35 @@ function setRosStatus(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) {
|
||||
var system = payload.system || {};
|
||||
var services = payload.services || {};
|
||||
setText("stat-wifi", system.wifi || "unbekannt");
|
||||
setText("stat-ips", system.ips || "unbekannt");
|
||||
updateUndervoltageDisplay(system.undervoltage || "unbekannt", system.undervoltage_state || "unknown");
|
||||
setText("stat-uptime", system.uptime || "unbekannt");
|
||||
setText("stat-disk", system.disk_free || "unbekannt");
|
||||
setText("val-cpu", system.cpu_usage || "--");
|
||||
@@ -832,10 +910,10 @@ function publishJoy() {
|
||||
}
|
||||
|
||||
function startPublishing() {
|
||||
publishJoy();
|
||||
if (publishTimer) {
|
||||
clearInterval(publishTimer);
|
||||
return;
|
||||
}
|
||||
publishJoy();
|
||||
publishTimer = window.setInterval(publishJoy, 50);
|
||||
}
|
||||
|
||||
@@ -868,12 +946,7 @@ function pulseButton(buttonIndex) {
|
||||
}
|
||||
|
||||
function selectDriveMode(mode, buttonIndex) {
|
||||
driveMode = mode;
|
||||
document.querySelectorAll(".dbtn[data-mode]").forEach(function (btn) {
|
||||
btn.classList.toggle("active", btn.dataset.mode === mode);
|
||||
});
|
||||
updateModeDisplay();
|
||||
updateControlLocks();
|
||||
applyDriveMode(mode);
|
||||
pulseButton(buttonIndex);
|
||||
}
|
||||
|
||||
@@ -940,6 +1013,7 @@ function moveThumb(clientX, clientY) {
|
||||
var nx = dx / data.maxD;
|
||||
var ny = -dy / data.maxD;
|
||||
setAxes(nx, ny);
|
||||
publishJoy();
|
||||
startPublishing();
|
||||
}
|
||||
|
||||
@@ -1013,6 +1087,30 @@ window.addEventListener("load", function () {
|
||||
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) {
|
||||
button.addEventListener("click", function () {
|
||||
selectDriveMode(button.dataset.mode, parseInt(button.dataset.button, 10));
|
||||
|
||||
@@ -455,6 +455,15 @@ h2 {
|
||||
color: #8a6b35;
|
||||
}
|
||||
|
||||
@keyframes statusBlink {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-page {
|
||||
color: #1b2730;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -54,6 +54,15 @@ DRIVE_PIN_KEYS = {
|
||||
'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 = {
|
||||
'steer_left': {
|
||||
'kind': 'steer',
|
||||
@@ -94,7 +103,10 @@ class PCA9685Direct:
|
||||
try:
|
||||
import smbus
|
||||
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.bus = smbus.SMBus(busnum)
|
||||
@@ -176,7 +188,10 @@ class MotorTester:
|
||||
self.config = _load_config(self.config_path)
|
||||
self.pwm.set_pwm_freq(50)
|
||||
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'])
|
||||
|
||||
def _steer_pin(self, wheel):
|
||||
@@ -188,10 +203,12 @@ class MotorTester:
|
||||
def _drive_pin(self, wheel):
|
||||
return int(self.config[DRIVE_PIN_KEYS[wheel]])
|
||||
|
||||
def set_drive_neutral_pwm(self, pwm_value):
|
||||
duty_cycle = int(pwm_value)
|
||||
def _drive_neutral(self, wheel):
|
||||
return int(self.drive_pwm_neutral[wheel])
|
||||
|
||||
def set_drive_neutral_pwm(self, values):
|
||||
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)
|
||||
|
||||
def center_all_steering(self):
|
||||
@@ -201,7 +218,7 @@ class MotorTester:
|
||||
|
||||
def stop_all_driving(self):
|
||||
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)
|
||||
|
||||
def set_steering_angle(self, wheel, angle):
|
||||
@@ -237,12 +254,12 @@ class MotorTester:
|
||||
def test_driving(self, wheel, speed, duration):
|
||||
self.pwm.set_pwm(self._steer_pin(wheel), 0, self._steer_neutral(wheel))
|
||||
duty_cycle = int(
|
||||
self.drive_pwm_neutral +
|
||||
self._drive_neutral(wheel) +
|
||||
speed / 100.0 * self.drive_pwm_range * WHEEL_DIRECTIONS[wheel]
|
||||
)
|
||||
self.pwm.set_pwm(self._drive_pin(wheel), 0, duty_cycle)
|
||||
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):
|
||||
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):
|
||||
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 {
|
||||
'config_path': tester.config_path,
|
||||
'value': int(tester.config['drive_pwm_neutral']),
|
||||
'pins': {wheel: tester._drive_pin(wheel) for wheel in WHEELS},
|
||||
'values': values,
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
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.set_drive_neutral_pwm(value)
|
||||
tester.set_drive_neutral_pwm(preview_values)
|
||||
return {
|
||||
'value': value,
|
||||
'pins': {wheel: tester._drive_pin(wheel) for wheel in WHEELS},
|
||||
'values': {
|
||||
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)
|
||||
value = int(pwm_value)
|
||||
_update_config_values(tester.config_path, {'drive_pwm_neutral': value})
|
||||
updates = {}
|
||||
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.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.set_drive_neutral_pwm(tester.drive_pwm_neutral)
|
||||
return get_drive_neutral_value(config_path=tester.config_path)
|
||||
|
||||
@@ -4,31 +4,29 @@ import time
|
||||
import os
|
||||
|
||||
config_filename = '../config/exomy.yaml'
|
||||
WHEELS = ('fl', 'fr', 'cl', 'cr', 'rl', 'rr')
|
||||
|
||||
|
||||
def get_driving_pins():
|
||||
pin_list = []
|
||||
with open(config_filename, 'r') as file:
|
||||
param_dict = yaml.load(file)
|
||||
|
||||
for key, value in param_dict.items():
|
||||
if('pin_drive_' in str(key)):
|
||||
pin_list.append(value)
|
||||
return pin_list
|
||||
return [param_dict['pin_drive_' + wheel] for wheel in WHEELS]
|
||||
|
||||
def get_drive_pwm_neutral():
|
||||
|
||||
def get_drive_pwm_neutral_values():
|
||||
with open(config_filename, 'r') as file:
|
||||
param_dict = yaml.load(file)
|
||||
|
||||
for key, value in param_dict.items():
|
||||
if('drive_pwm_neutral' in str(key)):
|
||||
return value
|
||||
|
||||
default_value = 300
|
||||
print('The parameter drive_pwm_neutral could not be found in the exomy.yaml \n')
|
||||
print('It was set to the default value: '+ default_value + '\n')
|
||||
return default_value
|
||||
values = {}
|
||||
for wheel in WHEELS:
|
||||
key = 'drive_pwm_neutral_' + wheel
|
||||
if key not in param_dict:
|
||||
print('The parameter ' + key + ' could not be found in the exomy.yaml \n')
|
||||
print('It was set to the default value: 300\n')
|
||||
values[wheel] = 300
|
||||
else:
|
||||
values[wheel] = param_dict[key]
|
||||
return values
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(
|
||||
@@ -62,7 +60,7 @@ On each motor you have to turn the correction screw until the motor really stand
|
||||
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,
|
||||
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 = get_drive_pwm_neutral()
|
||||
value_dict = get_drive_pwm_neutral_values()
|
||||
pin_list = get_driving_pins()
|
||||
|
||||
for pin in pin_list:
|
||||
pwm.set_pwm(pin, 0, value)
|
||||
for index, pin in enumerate(pin_list):
|
||||
pwm.set_pwm(pin, 0, value_dict[WHEELS[index]])
|
||||
time.sleep(0.1)
|
||||
|
||||
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'
|
||||
|
||||
|
||||
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():
|
||||
try:
|
||||
with open('/proc/uptime', 'r', encoding='utf-8') as handle:
|
||||
@@ -100,11 +135,25 @@ def format_disk_free():
|
||||
|
||||
|
||||
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'])
|
||||
if returncode != 0 or not stdout:
|
||||
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():
|
||||
@@ -149,11 +198,14 @@ def get_motor_test_container_status():
|
||||
|
||||
|
||||
def collect_status():
|
||||
undervoltage = read_undervoltage_status()
|
||||
return {
|
||||
'status': 'Bereit',
|
||||
'system': {
|
||||
'wifi': get_wifi_status(),
|
||||
'ips': get_ip_addresses(),
|
||||
'undervoltage': undervoltage['text'],
|
||||
'undervoltage_state': undervoltage['state'],
|
||||
'cpu_temperature': read_cpu_temperature(),
|
||||
'cpu_usage': read_cpu_usage(),
|
||||
'memory_usage': read_memory_usage(),
|
||||
@@ -315,17 +367,17 @@ def get_drive_neutral_status():
|
||||
try:
|
||||
result = get_drive_neutral_value()
|
||||
return 200, {
|
||||
'status': 'Fahr-Neutralwert geladen',
|
||||
'status': 'Fahr-Neutralwerte geladen',
|
||||
'drive_neutral': result,
|
||||
'container_status': container_status,
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return 500, {'status': 'Motor-Konfiguration fehlt'}
|
||||
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
|
||||
|
||||
container_status = get_motor_test_container_status()
|
||||
@@ -336,9 +388,9 @@ def preview_drive_neutral_action(value):
|
||||
}
|
||||
|
||||
try:
|
||||
result = preview_drive_neutral_value(int(value))
|
||||
result = preview_drive_neutral_value(values)
|
||||
return 200, {
|
||||
'status': f"Fahr-Neutralwert: PWM {result['value']}",
|
||||
'status': 'Fahr-Neutralwerte angefahren',
|
||||
'drive_neutral': result,
|
||||
'container_status': container_status,
|
||||
}
|
||||
@@ -350,7 +402,7 @@ def preview_drive_neutral_action(value):
|
||||
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
|
||||
|
||||
container_status = get_motor_test_container_status()
|
||||
@@ -361,9 +413,9 @@ def save_drive_neutral_action(value):
|
||||
}
|
||||
|
||||
try:
|
||||
result = save_drive_neutral_value(int(value))
|
||||
result = save_drive_neutral_value(values)
|
||||
return 200, {
|
||||
'status': 'Fahr-Neutralwert gespeichert',
|
||||
'status': 'Fahr-Neutralwerte gespeichert',
|
||||
'drive_neutral': result,
|
||||
'container_status': container_status,
|
||||
}
|
||||
@@ -374,7 +426,7 @@ def save_drive_neutral_action(value):
|
||||
except FileNotFoundError:
|
||||
return 500, {'status': 'Motor-Konfiguration fehlt'}
|
||||
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):
|
||||
@@ -431,7 +483,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.write_json(400, {'status': 'Ungültige Anfrage'})
|
||||
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)
|
||||
return
|
||||
|
||||
@@ -449,7 +506,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.write_json(400, {'status': 'Ungültige Anfrage'})
|
||||
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)
|
||||
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
|
||||
AXIS_DEADZONE = 0.1
|
||||
last_start_button_pressed = False
|
||||
last_webgui_time = None
|
||||
WEBGUI_PRIORITY_TIMEOUT = 2.0
|
||||
|
||||
VELOCITY_EXPO = 2.0
|
||||
|
||||
@@ -23,6 +25,7 @@ CONTROLLER_FUNCTION_MAPS = {
|
||||
"x_axis": 0,
|
||||
"y_axis": 1,
|
||||
"invert_x_axis": True,
|
||||
"invert_y_axis": False,
|
||||
"X_button": 0,
|
||||
"Y_button": 3,
|
||||
"A_button": 1,
|
||||
@@ -34,7 +37,8 @@ CONTROLLER_FUNCTION_MAPS = {
|
||||
"logitech-F710": {
|
||||
"x_axis": 0,
|
||||
"y_axis": 1,
|
||||
"invert_x_axis": False,
|
||||
"invert_x_axis": True,
|
||||
"invert_y_axis": True,
|
||||
"X_button": 0,
|
||||
"Y_button": 3,
|
||||
"A_button": 1,
|
||||
@@ -47,6 +51,7 @@ CONTROLLER_FUNCTION_MAPS = {
|
||||
"x_axis": 0,
|
||||
"y_axis": 1,
|
||||
"invert_x_axis": False,
|
||||
"invert_y_axis": False,
|
||||
"X_button": 2,
|
||||
"Y_button": 3,
|
||||
"A_button": 0,
|
||||
@@ -94,6 +99,15 @@ def callback(data):
|
||||
global locomotion_mode
|
||||
global motors_enabled
|
||||
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()
|
||||
|
||||
@@ -118,6 +132,8 @@ def callback(data):
|
||||
|
||||
if controller_function_map["invert_x_axis"]:
|
||||
x *= -1
|
||||
if controller_function_map["invert_y_axis"]:
|
||||
y *= -1
|
||||
|
||||
# Reading out button data to set locomotion mode
|
||||
# X Button
|
||||
|
||||
@@ -53,7 +53,7 @@ class Motors():
|
||||
self.pins['steer'][self.RR] = rospy.get_param("pin_steer_rr")
|
||||
|
||||
# PWM characteristics
|
||||
self.pwm = Adafruit_PCA9685.PCA9685()
|
||||
self.pwm = Adafruit_PCA9685.PCA9685(busnum=1)
|
||||
self.pwm.set_pwm_freq(50) # Hz
|
||||
|
||||
self.steering_pwm_neutral = [None] * 6
|
||||
@@ -67,7 +67,13 @@ class Motors():
|
||||
self.steering_pwm_range = rospy.get_param("steer_pwm_range")
|
||||
|
||||
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_range = rospy.get_param("drive_pwm_range")
|
||||
|
||||
@@ -111,14 +117,11 @@ class Motors():
|
||||
def setDriving(self, driving_command):
|
||||
# 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():
|
||||
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])
|
||||
|
||||
self.pwm.set_pwm(motor_pin, 0, duty_cycle)
|
||||
|
||||
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():
|
||||
self.pwm.set_pwm(motor_pin, 0, duty_cycle)
|
||||
self.pwm.set_pwm(motor_pin, 0, int(self.driving_pwm_neutral[wheel_name]))
|
||||
|
||||
@@ -24,6 +24,7 @@ class Rover():
|
||||
self.wheel_ry = 20.3
|
||||
self.wheel_fx = 16.0
|
||||
self.wheel_fy = 20.3
|
||||
self.point_turn_max_angle = 45
|
||||
|
||||
max_steering_angle = 45
|
||||
self.ackermann_r_max = 250
|
||||
@@ -152,10 +153,14 @@ class Rover():
|
||||
return steering_angles
|
||||
|
||||
if(self.locomotion_mode == LocomotionMode.POINT_TURN.value):
|
||||
point_turn_angle = int(math.degrees(
|
||||
math.atan((self.wheel_rx + self.wheel_fx) / self.wheel_ry)))
|
||||
point_turn_angle_center = int(math.degrees(
|
||||
math.atan((((self.wheel_rx + self.wheel_fx) / 2) - self.wheel_fx) / (self.wheel_ry / 2))))
|
||||
raw_point_turn_angle = math.degrees(
|
||||
math.atan((self.wheel_rx + self.wheel_fx) / self.wheel_ry))
|
||||
raw_point_turn_angle_center = math.degrees(
|
||||
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.FR] = -point_turn_angle
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# ExoMy Setup-Status
|
||||
|
||||
Stand: 2026-05-18
|
||||
Stand: 2026-05-21
|
||||
|
||||
## Raspberry Pi
|
||||
|
||||
- Hostname: `cuno`
|
||||
- Hostname: `ExoMyCuno`
|
||||
- Benutzer: `pi`
|
||||
- LAN-IP: `192.168.1.83`
|
||||
- WLAN-IP: `192.168.1.9`
|
||||
@@ -30,6 +30,7 @@ Stand: 2026-05-18
|
||||
- Bekannte WLANs:
|
||||
- `eskimue.de`
|
||||
- `4pi`
|
||||
- Passwort `4pi`: `st89Saf6H86n`
|
||||
- Fallback-Access-Point:
|
||||
- SSID: `CUNO`
|
||||
- Passwort: `astr0cun042`
|
||||
@@ -46,6 +47,10 @@ Stand: 2026-05-18
|
||||
- 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 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.
|
||||
|
||||
## Aktueller Aufbau
|
||||
|
||||
Reference in New Issue
Block a user