Alles verbessert
This commit is contained in:
+146
-26
@@ -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 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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user