Initial push
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
# Handover Mobile Base Station
|
||||
|
||||
Dieses Repository ist eine vollständige Kopie eines fertigen Uni-Projekts zur Demonstration eines WLAN-Handover-Szenarios mit:
|
||||
|
||||
- einem mobilen Fahrzeug/Roboter (AlphaBot2 + Raspberry Pi + ESP32),
|
||||
- mehreren ESP32-basierten Basisstationen,
|
||||
- einem Webinterface zur Live-Visualisierung von Verbindungsdaten und Kamerabildern.
|
||||
|
||||
Das Projekt kombiniert Robotik, Embedded-Firmware, serielle Kommunikation, UDP-Weiterleitung und ein Web-Dashboard.
|
||||
|
||||
## Projektziel
|
||||
|
||||
Ziel ist es, ein mobiles System zu bauen, das sich zwischen zwei WLAN-Basisstationen bewegt und dabei:
|
||||
|
||||
- kontinuierlich Verbindungsdaten (RSSI, verbundene Basisstation, alternative Basisstation) überträgt,
|
||||
- bei besserem Signal automatisch einen Handover ausführt,
|
||||
- Kamerabilder vom Fahrzeug streamt,
|
||||
- alle Daten in einem Webinterface visualisiert.
|
||||
|
||||
## Repository-Struktur
|
||||
|
||||
```text
|
||||
HandoverMobileBaseStation/
|
||||
├── doc_vault/ # Hardware-Doku, Datenblätter, Pinout-Grafiken
|
||||
├── esp_firmware/ # Arduino-ESP32-Firmware (MBS1, MBS2, Fahrzeug)
|
||||
│ ├── MBS1_stripped/
|
||||
│ ├── MBS2_stripped/
|
||||
│ └── car_stripped/
|
||||
├── robot/ # Raspberry Pi / AlphaBot2 Python-Skripte
|
||||
│ ├── AlphaBot2.py
|
||||
│ ├── TRSensors.py
|
||||
│ ├── line_follow.py
|
||||
│ ├── pid_line_follow1.py
|
||||
│ ├── IRremote.py
|
||||
│ ├── cam_stream.py
|
||||
│ └── ReadMe.md # Teil-Doku zum Robotik-Teil
|
||||
└── webinterface/
|
||||
└── cotmw/ # Vue 3 + Vite Frontend + Node Serial-Bridge
|
||||
├── src/
|
||||
└── serial/
|
||||
```
|
||||
|
||||
## Gesamtarchitektur (vereinfacht)
|
||||
|
||||
```text
|
||||
[Raspberry Pi am Roboter]
|
||||
├─ Kamera -> cam_stream.py -> JSON (image) über USB-Serial (115200)
|
||||
└─ AlphaBot2 / IR / Line-Follow (lokal auf Pi)
|
||||
|
|
||||
v
|
||||
[ESP32 im Fahrzeug: car_stripped]
|
||||
- sendet Telemetrie + Kamera-JSON per UDP (Port 6666)
|
||||
- wechselt zwischen AP1/AP2 je nach RSSI
|
||||
|
|
||||
+---------+---------+
|
||||
| |
|
||||
v v
|
||||
[MBS1 / AP1] [MBS2 / AP2]
|
||||
| |
|
||||
| (direkt) | (über MBS2_1 -> UART -> MBS2_2 -> AP1)
|
||||
+---------+---------+
|
||||
|
|
||||
v
|
||||
[ESP32 MBS1 per USB an PC]
|
||||
|
|
||||
v
|
||||
[Node Serial Backend :6666]
|
||||
|
|
||||
v
|
||||
[Vue Webinterface (Vite)]
|
||||
```
|
||||
|
||||
Hinweis zu `MBS2`: Im Code ist die zweite Basisstation in zwei Rollen aufgeteilt (`MBS2_1` und `MBS2_2`). Dafür werden in der vollständigen Kette typischerweise zwei ESP32 mit derselben Sketch-Datei (aber unterschiedlichen `#define`-Modi) verwendet.
|
||||
|
||||
## Komponenten im Detail
|
||||
|
||||
### 1) `esp_firmware/` (ESP32 / Arduino)
|
||||
|
||||
### `MBS1_stripped` (AP1 + UDP -> USB-Serial Bridge)
|
||||
|
||||
Datei: `esp_firmware/MBS1_stripped/MBS1_stripped.ino`
|
||||
|
||||
Funktion:
|
||||
|
||||
- startet einen Access Point `ESP32-AP1`,
|
||||
- hört auf UDP Port `6666`,
|
||||
- schreibt empfangene UDP-Pakete auf `Serial` (USB, 115200 Baud),
|
||||
- dient als zentrale Einspeisung ins Webinterface (über den PC-USB-Port).
|
||||
|
||||
Wichtige Parameter (im Code):
|
||||
|
||||
- SSID: `ESP32-AP1`
|
||||
- Passwort: `PASS0000`
|
||||
- AP-IP: `192.168.178.1`
|
||||
- UDP-Port: `6666`
|
||||
- Baudrate: `115200`
|
||||
|
||||
### `MBS2_stripped` (zweite Basisstation / Relay, zwei Modi)
|
||||
|
||||
Datei: `esp_firmware/MBS2_stripped/MBS2_stripped.ino`
|
||||
|
||||
Die Sketch enthält zwei Rollen, die über `#define` ausgewählt werden:
|
||||
|
||||
- `MBS2_1`: hostet `ESP32-AP2` (Access Point)
|
||||
- `MBS2_2`: verbindet sich als Client mit `ESP32-AP1` und tunnelt Daten weiter
|
||||
|
||||
#### Modus `MBS2_1` (AP2)
|
||||
|
||||
- startet Access Point `ESP32-AP2`
|
||||
- empfängt UDP-Pakete auf Port `6666`
|
||||
- schreibt empfangene Daten auf `Serial` und `Serial2`
|
||||
- kann damit Daten an eine zweite ESP32-Instanz weitergeben
|
||||
|
||||
#### Modus `MBS2_2` (Relay zu AP1)
|
||||
|
||||
- verbindet sich mit `ESP32-AP1`
|
||||
- liest Daten von `Serial2`
|
||||
- sendet diese Daten per UDP an AP1 (`192.168.178.1:6666`)
|
||||
|
||||
#### UART-Verbindung in `MBS2_stripped`
|
||||
|
||||
`Serial2` ist auf folgende Pins gelegt:
|
||||
|
||||
- RX: GPIO `18`
|
||||
- TX: GPIO `17`
|
||||
|
||||
Siehe auch `doc_vault/esp32s3_pico_uart1_rx_tx.png`.
|
||||
|
||||
### `car_stripped` (Fahrzeug-ESP / Handover-Client)
|
||||
|
||||
Datei: `esp_firmware/car_stripped/car_stripped.ino`
|
||||
|
||||
Funktion:
|
||||
|
||||
- scannt verfügbare Netze (`ESP32-AP1`, `ESP32-AP2`)
|
||||
- verbindet sich initial mit der stärkeren Basisstation
|
||||
- sendet Statusdaten regelmäßig per UDP an das Gateway (`UDP_PORT 6666`)
|
||||
- nimmt Kameradaten/JSON von `Serial` entgegen und leitet sie per UDP weiter
|
||||
- führt automatischen Netzwechsel aus, wenn das andere Netz deutlich stärker ist
|
||||
|
||||
Handover-Logik (aus Code):
|
||||
|
||||
- Wechsel, wenn `otherRSSI` mindestens ca. `3 dBm` besser ist als aktuelles RSSI.
|
||||
|
||||
Gesendete Statusdaten (JSON, newline-terminiert):
|
||||
|
||||
```json
|
||||
{"type":"text","IP":"192.168.178.x","Base Station":"ESP32-AP1","RSSI":"-56","Other Network":"ESP32-AP2","Other RSSI":"-68"}
|
||||
```
|
||||
|
||||
Kameradaten (vom Raspberry Pi kommend) werden als JSON-Pakete weitergeleitet:
|
||||
|
||||
```json
|
||||
{"type":"image","data":"<base64-jpeg>"}
|
||||
```
|
||||
|
||||
### 2) `robot/` (Raspberry Pi + AlphaBot2)
|
||||
|
||||
Dieser Teil läuft auf dem Raspberry Pi auf dem Fahrzeug/Roboter.
|
||||
|
||||
### `cam_stream.py` (Kamera -> ESP über Serial)
|
||||
|
||||
Datei: `robot/cam_stream.py`
|
||||
|
||||
Funktion:
|
||||
|
||||
- liest Frames von einer Pi-Kamera (`Picamera2`),
|
||||
- konvertiert zu Graustufen,
|
||||
- komprimiert/verkürzt JPEGs so, dass Base64-Daten klein bleiben (im Code `MAX_B64 = 1200`),
|
||||
- sendet die Daten als JSON (`type=image`) über Serial an den Fahrzeug-ESP,
|
||||
- versucht bei Serial-Abbruch automatisch die Verbindung neu aufzubauen.
|
||||
|
||||
Wichtige Details:
|
||||
|
||||
- serielle Schnittstelle ist aktuell hartkodiert auf `/dev/ttyACM1`
|
||||
- Baudrate: `115200`
|
||||
- Ausgabeformat ist newline-terminiertes JSON (wichtig für den Parser im Backend)
|
||||
|
||||
### `line_follow.py` (regelbasierter Linienfolger)
|
||||
|
||||
Datei: `robot/line_follow.py`
|
||||
|
||||
Funktion:
|
||||
|
||||
- automatische Sensor-Kalibrierung (`TRSensor`)
|
||||
- Erkennung schwarzer/weißer Linie
|
||||
- einfache Korrekturlogik für Spurhaltung
|
||||
- zusätzliche Erkennung stärkerer Kurven (90°-ähnliche Abbiegungen)
|
||||
|
||||
### `pid_line_follow1.py` (PID-Linienfolger)
|
||||
|
||||
Datei: `robot/pid_line_follow1.py`
|
||||
|
||||
Funktion:
|
||||
|
||||
- PID-Regelung für glatteres Folgen der Linie
|
||||
- Suchroutine bei Linienverlust
|
||||
- konfigurierbare Parameter (`Kp`, `Ki`, `Kd`, Geschwindigkeit, Timeouts)
|
||||
|
||||
### `IRremote.py` (Fernsteuerung + Start/Stopp von PID-Line-Follow)
|
||||
|
||||
Datei: `robot/IRremote.py`
|
||||
|
||||
Funktion:
|
||||
|
||||
- liest IR-Signale über GPIO
|
||||
- steuert den AlphaBot2 manuell (vor/zurück/links/rechts/stop)
|
||||
- passt PWM-Geschwindigkeit an
|
||||
- startet `pid_line_follow1.py` als separaten Prozess per Tastendruck
|
||||
- stoppt den PID-Line-Follow-Prozess erneut per Tastendruck
|
||||
|
||||
Hinweis:
|
||||
|
||||
- Die konkreten Tasten-Codes sind auf die verwendete Fernbedienung abgestimmt (NEC-artige IR-Codes im Skript).
|
||||
|
||||
### `AlphaBot2.py` und `TRSensors.py`
|
||||
|
||||
Dateien:
|
||||
|
||||
- `robot/AlphaBot2.py`
|
||||
- `robot/TRSensors.py`
|
||||
|
||||
Funktion:
|
||||
|
||||
- Hardware-Abstraktion für Motoransteuerung (PWM, Fahrtrichtung)
|
||||
- Ansteuerung und Kalibrierung des 5-Kanal-TR-Linienfolgesensors
|
||||
|
||||
### Vorhandene Teil-Doku
|
||||
|
||||
Datei: `robot/ReadMe.md`
|
||||
|
||||
Enthält zusätzliche Projektnotizen zu:
|
||||
|
||||
- Kamera-Streaming
|
||||
- Line Follow (Entwicklungsideen)
|
||||
- Fernbedienungssteuerung
|
||||
- systemd/Daemon-Setup auf dem Raspberry Pi
|
||||
|
||||
### 3) `webinterface/cotmw/` (Vue 3 + Vite + Serial Backend)
|
||||
|
||||
Das Webinterface besteht aus zwei Teilen:
|
||||
|
||||
- einem Vue-Frontend (`webinterface/cotmw`)
|
||||
- einem Node/Express-Backend für den Zugriff auf lokale Serial-Ports (`webinterface/cotmw/serial`)
|
||||
|
||||
### Frontend (Vue 3 + Vite)
|
||||
|
||||
Wichtige Dateien:
|
||||
|
||||
- `webinterface/cotmw/src/App.vue`
|
||||
- `webinterface/cotmw/src/components/*`
|
||||
- `webinterface/cotmw/vite.config.js`
|
||||
|
||||
Funktionen im UI:
|
||||
|
||||
- HUD mit Live-Werten:
|
||||
- IP
|
||||
- aktuelle Basisstation
|
||||
- RSSI
|
||||
- alternative Basisstation + RSSI
|
||||
- Polling-Zähler
|
||||
- Kamera-Widget (drag & drop)
|
||||
- RSSI-Plot (aktuelle vs. alternative Basisstation)
|
||||
- Topologie/Locations-Visualisierung (inkl. Handover-Animation)
|
||||
- optionale Weiterleitung der Daten an externes System (`External.vue`)
|
||||
|
||||
#### Verstecktes Serial-Menü
|
||||
|
||||
Der Serial-Connector ist absichtlich versteckt und wird per Tastenkombination eingeblendet:
|
||||
|
||||
- `Ctrl + B` (Windows/Linux)
|
||||
- `Cmd + B` (macOS)
|
||||
|
||||
Danach kann im UI:
|
||||
|
||||
- ein lokaler Serial-Port ausgewählt werden
|
||||
- die Verbindung mit `115200` Baud aufgebaut werden
|
||||
- der eingehende Datenstrom eingesehen werden
|
||||
|
||||
Das Frontend pollt anschließend standardmäßig alle `200 ms` den Backend-Endpunkt `/api/serial-utils/latest`.
|
||||
|
||||
### Backend (Node + Express + serialport)
|
||||
|
||||
Wichtige Dateien:
|
||||
|
||||
- `webinterface/cotmw/serial/server.js`
|
||||
- `webinterface/cotmw/serial/serial.js`
|
||||
- `webinterface/cotmw/serial/routes/serialUtils.js`
|
||||
|
||||
Funktion:
|
||||
|
||||
- listet lokale Serial-Ports
|
||||
- verbindet sich mit ausgewähltem Port
|
||||
- liest eingehende Zeilen (newline-basiert)
|
||||
- speichert das zuletzt empfangene `text`-Paket und `image`-Paket
|
||||
- stellt diese Daten per HTTP-API fürs Frontend bereit
|
||||
|
||||
API-Endpunkte (Port `6666`):
|
||||
|
||||
- `GET /api/serial-utils/serialports`
|
||||
- `POST /api/serial-utils/connect` mit Body `{ "path": "/dev/tty..." }`
|
||||
- `GET /api/serial-utils/latest`
|
||||
|
||||
#### Vite Proxy
|
||||
|
||||
In `webinterface/cotmw/vite.config.js` ist im Dev-Server ein Proxy konfiguriert:
|
||||
|
||||
- `/api` -> `http://localhost:6666`
|
||||
|
||||
Dadurch kann das Frontend lokal ohne CORS-Probleme auf das Serial-Backend zugreifen.
|
||||
|
||||
## Hardware-Übersicht (praktisch)
|
||||
|
||||
Je nach Aufbau werden typischerweise benötigt:
|
||||
|
||||
- 1x AlphaBot2 (oder kompatibles Chassis mit Motorsteuerung)
|
||||
- 1x Raspberry Pi (z. B. Pi 4) auf dem Fahrzeug
|
||||
- 1x Pi-Kamera (Picamera2-kompatibel)
|
||||
- 1x TR-Linienfolgesensor (5-Kanal)
|
||||
- 1x IR-Empfänger + passende Fernbedienung
|
||||
- 1x ESP32 auf dem Fahrzeug (`car_stripped`)
|
||||
- 1x ESP32 für `MBS1_stripped`
|
||||
- 2x ESP32 für `MBS2_stripped` (einmal `MBS2_1`, einmal `MBS2_2`) bei vollem AP2-Relay-Aufbau
|
||||
- 1x PC/Laptop für Webinterface + Node-Backend
|
||||
|
||||
## Software-Voraussetzungen
|
||||
|
||||
### PC / Laptop (Webinterface + Serial Backend)
|
||||
|
||||
- Node.js (empfohlen: aktuelle LTS-Version)
|
||||
- npm
|
||||
- Zugriff auf den USB-Serial-Port des MBS1-ESP32 (Linux: ggf. Gruppe `dialout`)
|
||||
|
||||
### Raspberry Pi (Robot / Kamera)
|
||||
|
||||
- Raspberry Pi OS
|
||||
- Python 3
|
||||
- `Picamera2` / `libcamera`
|
||||
- `RPi.GPIO`
|
||||
- `pyserial`
|
||||
- `Pillow`
|
||||
- optional/je nach Skript-Imports: `Flask`, `opencv-python`
|
||||
|
||||
In `robot/cam_stream.py` sind bereits Beispiel-Schritte für die Kamera-Umgebung notiert (APT + venv).
|
||||
|
||||
### ESP32-Entwicklung
|
||||
|
||||
- Arduino IDE mit ESP32-Boardpaket oder PlatformIO
|
||||
- passende Board-Konfiguration für die verwendeten ESP32-Module
|
||||
|
||||
## Setup und Inbetriebnahme
|
||||
|
||||
### A) ESP32-Firmware flashen
|
||||
|
||||
1. `MBS1_stripped.ino` auf den ESP für AP1 flashen.
|
||||
1. `MBS2_stripped.ino` einmal mit `#define MBS2_1` auf den AP2-ESP flashen.
|
||||
1. `MBS2_stripped.ino` einmal mit `#define MBS2_2` auf den Relay-ESP flashen.
|
||||
1. `car_stripped.ino` auf den Fahrzeug-ESP flashen.
|
||||
|
||||
Hinweise:
|
||||
|
||||
- SSIDs/Passwort sind im Code hartkodiert (`ESP32-AP1`, `ESP32-AP2`, `PASS0000`).
|
||||
- UDP-Port ist durchgehend `6666`.
|
||||
- Baudrate ist durchgehend `115200`.
|
||||
|
||||
### B) Verkabelung / Verbindungen herstellen
|
||||
|
||||
Mindestens notwendig:
|
||||
|
||||
- Fahrzeug-ESP per USB/Serial mit Raspberry Pi verbinden (für Kamera-/JSON-Übergabe)
|
||||
- MBS1-ESP per USB/Serial mit PC/Laptop verbinden (für Webinterface-Datenquelle)
|
||||
|
||||
Für den vollen AP2-Relay-Aufbau zusätzlich:
|
||||
|
||||
- `MBS2_1` und `MBS2_2` per UART (`Serial2`) verbinden (Pins siehe Firmware / `doc_vault`)
|
||||
|
||||
### C) Serial-Backend starten (PC)
|
||||
|
||||
```bash
|
||||
cd webinterface/cotmw/serial
|
||||
npm install
|
||||
node server.js
|
||||
```
|
||||
|
||||
Das Backend läuft anschließend auf `http://localhost:6666`.
|
||||
|
||||
### D) Frontend starten (PC)
|
||||
|
||||
```bash
|
||||
cd webinterface/cotmw
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Danach im Browser die von Vite ausgegebene URL öffnen (typisch `http://localhost:5173`).
|
||||
|
||||
### E) Serial-Port im Webinterface verbinden
|
||||
|
||||
1. Webinterface öffnen.
|
||||
1. `Ctrl + B` / `Cmd + B` drücken (Serial-Menü einblenden).
|
||||
1. Den USB-Port des MBS1-ESP32 auswählen.
|
||||
1. `Connect (Baud: 115200)` klicken.
|
||||
|
||||
Wenn Daten ankommen, sollten Telemetrie und Kamerabild im Dashboard erscheinen.
|
||||
|
||||
### F) Robotik-/Kamera-Skripte auf dem Raspberry Pi starten
|
||||
|
||||
Beispiele:
|
||||
|
||||
### Kamerastream zum Fahrzeug-ESP
|
||||
|
||||
```bash
|
||||
cd robot
|
||||
python3 cam_stream.py
|
||||
```
|
||||
|
||||
### IR-Fernsteuerung + PID-Line-Follow-Start/Stopp
|
||||
|
||||
```bash
|
||||
cd robot
|
||||
python3 IRremote.py
|
||||
```
|
||||
|
||||
### Direkter Start des PID-Line-Followers
|
||||
|
||||
```bash
|
||||
cd robot
|
||||
python3 pid_line_follow1.py
|
||||
```
|
||||
|
||||
### Direkter Start des regelbasierten Line-Followers
|
||||
|
||||
```bash
|
||||
cd robot
|
||||
python3 line_follow.py
|
||||
```
|
||||
|
||||
## Kommunikationsformate und Schnittstellen
|
||||
|
||||
### Serial (Pi -> Fahrzeug-ESP)
|
||||
|
||||
Format:
|
||||
|
||||
- newline-terminiertes JSON
|
||||
- `115200` Baud
|
||||
|
||||
Beispiel:
|
||||
|
||||
```json
|
||||
{"type":"image","data":"<base64-jpeg>"}\n
|
||||
```
|
||||
|
||||
### UDP (Fahrzeug-ESP -> Basisstationen)
|
||||
|
||||
- Port: `6666`
|
||||
- Payload: JSON-Strings (Text-Status und Kamera-Frames)
|
||||
|
||||
### HTTP (Web-Frontend -> Serial-Backend)
|
||||
|
||||
Basis-API (über Vite-Proxy als `/api/...`):
|
||||
|
||||
- `/api/serial-utils/serialports`
|
||||
- `/api/serial-utils/connect`
|
||||
- `/api/serial-utils/latest`
|
||||
|
||||
### Optional: Externe Datenweiterleitung (im Webinterface)
|
||||
|
||||
Komponente: `webinterface/cotmw/src/components/External.vue`
|
||||
|
||||
Funktion:
|
||||
|
||||
- sendet Änderungen von `rssi`, `other_rssi` und `image` an einen externen HTTP-Endpunkt
|
||||
- Ziel-URL: `http://<ip>:<port>/api/send`
|
||||
- Authentifizierung via Header `x-api-key`
|
||||
|
||||
Standardwerte im UI:
|
||||
|
||||
- IP: `138.68.114.176`
|
||||
- Port: `3000`
|
||||
|
||||
## Bekannte Einschränkungen / technische Schulden
|
||||
|
||||
- Viele Parameter sind hartkodiert (SSID, Passwort, Ports, Serial-Pfade, IPs).
|
||||
- `cam_stream.py` verwendet fest `/dev/ttyACM1`.
|
||||
- Es gibt keine zentrale `.env`-Konfiguration.
|
||||
- Das Serial-Backend hält nur das zuletzt empfangene `text`- und `image`-Paket im Speicher.
|
||||
- Fehlerbehandlung und Logging sind für Demo/Prototyping ausgelegt, nicht für Produktion.
|
||||
- Das Vue-Frontend enthält bewusst versteckte UI-Elemente (Serial-Menü per Shortcut).
|
||||
- Die `webinterface/cotmw/package.json`-Scripts sind eher Frontend-orientiert; der Serial-Server wird separat gestartet.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Keine Daten im Webinterface
|
||||
|
||||
- Prüfen, ob `webinterface/cotmw/serial/server.js` läuft (Port `6666`).
|
||||
- Prüfen, ob im Browser das Serial-Menü geöffnet und ein Port verbunden wurde (`Ctrl/Cmd + B`).
|
||||
- Prüfen, ob der richtige USB-Port des MBS1-ESP32 gewählt wurde.
|
||||
- Prüfen, ob Baudrate `115200` verwendet wird.
|
||||
- Prüfen, ob tatsächlich newline-terminierte JSON-Pakete gesendet werden.
|
||||
|
||||
### Kamera erscheint nicht / instabil
|
||||
|
||||
- Kamerazugriff auf dem Raspberry Pi prüfen (`libcamera`, `Picamera2`).
|
||||
- Serielle Verbindung zum Fahrzeug-ESP prüfen (`/dev/ttyACM1` in `robot/cam_stream.py` anpassen).
|
||||
- Paketgröße kann kritisch sein: `MAX_B64` in `cam_stream.py` beeinflusst Bildqualität und Übertragbarkeit.
|
||||
|
||||
### ESP verbindet sich nicht mit Basisstationen
|
||||
|
||||
- SSID/Passwort im ESP-Code prüfen (`ESP32-AP1`, `ESP32-AP2`, `PASS0000`).
|
||||
- Sicherstellen, dass beide Access Points gestartet sind.
|
||||
- Versorgungsspannung und Antennen/Position prüfen (RSSI).
|
||||
|
||||
### Rechteprobleme bei Serial/GPIO (Linux)
|
||||
|
||||
- Benutzer in passende Gruppen aufnehmen (z. B. `dialout`, ggf. GPIO-spezifisch je Setup).
|
||||
- Skripte testweise mit ausreichenden Rechten starten.
|
||||
|
||||
## Hinweise zur Weiterentwicklung
|
||||
|
||||
Sinnvolle nächste Schritte für eine robustere Version:
|
||||
|
||||
- zentrale Konfiguration (`.env` / Config-Dateien) für SSIDs, Ports, Serial-Geräte
|
||||
- strukturiertere Paketprotokolle (Sequenznummern, Timestamps, Checks)
|
||||
- besserer Image-Transport (Chunking statt einzelner kurzer Base64-Payloads)
|
||||
- Telemetrie-Logging / Replay im Backend
|
||||
- sauberer Start per `docker-compose` (Web + Serial-API, sofern Host-Serial durchgereicht)
|
||||
- automatische Geräteerkennung und Health-Checks
|
||||
|
||||
## Zusatzmaterial im Repository
|
||||
|
||||
`doc_vault/` enthält u. a.:
|
||||
|
||||
- `esp32s3_pico_1_datasheet.pdf`
|
||||
- `esp32s3_pico_uart1_rx_tx.png`
|
||||
- weitere Projektdokumente (`doc_esp.odt`)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1,51 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <lwip/sockets.h>
|
||||
|
||||
#define _SSID "ESP32-AP1"
|
||||
#define _PASSWD "PASS0000"
|
||||
#define UDP_PORT 6666
|
||||
|
||||
#define MTU 100000
|
||||
|
||||
IPAddress apIP(192,168,178,1);
|
||||
IPAddress subnetMask(255,255,255,0);
|
||||
|
||||
static int udp_sock;
|
||||
|
||||
void udp_task(void* /*args*/) {
|
||||
struct sockaddr_in sourceAddr;
|
||||
socklen_t addrLen = sizeof(sourceAddr);
|
||||
static uint8_t udp_buf[MTU];
|
||||
|
||||
while (true) {
|
||||
int len = recvfrom(udp_sock, udp_buf, MTU - 1, 0, (struct sockaddr*)&sourceAddr, &addrLen);
|
||||
if (len > 0) {
|
||||
udp_buf[len] = '\n';
|
||||
Serial.write(udp_buf, len + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { delay(10); }
|
||||
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAPConfig(apIP, apIP, subnetMask);
|
||||
WiFi.softAP(_SSID, _PASSWD, /*channel*/1, /*hidden*/false, /*maxConn*/4);
|
||||
|
||||
udp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
struct sockaddr_in localAddr;
|
||||
memset(&localAddr, 0, sizeof(localAddr));
|
||||
localAddr.sin_family = AF_INET;
|
||||
localAddr.sin_port = htons(UDP_PORT);
|
||||
localAddr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
bind(udp_sock, (struct sockaddr*)&localAddr, sizeof(localAddr));
|
||||
|
||||
xTaskCreatePinnedToCore(udp_task, "UDPReceiver", 4096, nullptr, 5, nullptr, 1);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
#include <WiFi.h>
|
||||
#include <WiFiUdp.h>
|
||||
|
||||
#define DEBUG
|
||||
|
||||
// #define MBS2_1 // AP hosting ESP32-AP2
|
||||
#define MBS2_2 // Client connected to ESP32-AP1
|
||||
|
||||
#ifdef MBS2_1
|
||||
#ifdef MBS2_2
|
||||
#undef MBS2_2
|
||||
#endif
|
||||
#endif
|
||||
#ifdef MBS2_2
|
||||
#ifdef MBS2_1
|
||||
#undef MBS2_1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef MBS2_1
|
||||
#define _NAME "MBS2_1"
|
||||
#define ap_ssid "ESP32-AP2"
|
||||
#define ap_passwd "PASS0000"
|
||||
IPAddress apIP(192, 168, 178, 1);
|
||||
IPAddress subnetMask(255, 255, 255, 0);
|
||||
#endif
|
||||
|
||||
#ifdef MBS2_2
|
||||
#define _NAME "MBS2_2"
|
||||
#define base_ssid "ESP32-AP1"
|
||||
#define base_passwd "PASS0000"
|
||||
#endif
|
||||
|
||||
#define MAX_PACKET_SIZE 1500
|
||||
#define UDP_PORT 6666
|
||||
|
||||
WiFiUDP udp;
|
||||
|
||||
IPAddress localIP = IPAddress();
|
||||
|
||||
void (*routeMBS)(void);
|
||||
|
||||
#pragma region Access Point Functions
|
||||
|
||||
#ifdef MBS2_1
|
||||
void init_wifi_access_point(){
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAPConfig(apIP, apIP, subnetMask);
|
||||
WiFi.softAP(ap_ssid, ap_passwd, /* channel */ 1, /* hidden */ false, /* maxConn */ 4);
|
||||
}
|
||||
|
||||
char *receive(){
|
||||
int packetSize = udp.parsePacket();
|
||||
if(packetSize <= 0) return NULL;
|
||||
packetSize = packetSize > MAX_PACKET_SIZE ? MAX_PACKET_SIZE: packetSize;
|
||||
char *buffer = (char*)malloc(packetSize + 1);
|
||||
|
||||
udp.read(buffer, packetSize);
|
||||
buffer[packetSize] = '\0';
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void send(char *buf){
|
||||
Serial.write(buf);
|
||||
Serial2.write(buf);
|
||||
}
|
||||
|
||||
String readSerialUntil(const String& delimiter) {
|
||||
String res = "";
|
||||
unsigned long start = millis();
|
||||
|
||||
while (!res.endsWith(delimiter) && (millis() - start < 1000)) {
|
||||
if (Serial.available()) {
|
||||
char c = Serial.read();
|
||||
res += c;
|
||||
start = millis();
|
||||
} else {
|
||||
// yield the CPU so the watchdog won’t fire
|
||||
taskYIELD();
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void doSerialReading(){
|
||||
if(Serial.available()){
|
||||
Serial2.print(readSerialUntil("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
void MBS2_1_loop(){
|
||||
#ifdef DEBUG
|
||||
/*
|
||||
To debug loop through instances
|
||||
MBS2_1 Serial2 out -> MBS2_1 Serial2 in UDP out
|
||||
*/
|
||||
doSerialReading();
|
||||
#endif
|
||||
char *recv = receive();
|
||||
if(recv != NULL){
|
||||
log(String("Received: "));
|
||||
log(recv);
|
||||
send(recv);
|
||||
free(recv);
|
||||
recv = NULL;
|
||||
}
|
||||
free(recv); // NULL free is safe
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
#endif // #ifdef MBS2_1
|
||||
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Client (Base Station) Functions
|
||||
|
||||
#ifdef MBS2_2
|
||||
IPAddress apIP(192,168,178,1);
|
||||
|
||||
void init_wifi_base_station_connection(){
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(base_ssid, base_passwd);
|
||||
|
||||
#ifdef DEBUG
|
||||
size_t start = millis();
|
||||
#endif
|
||||
while(WiFi.status() != WL_CONNECTED){ // while esps aren't connected nothing can run either
|
||||
#ifdef DEBUG
|
||||
log(String("Trying to connect to: ") + String(base_ssid) + String("\nTime: ") + String(millis() - start));
|
||||
Serial.flush();
|
||||
#endif
|
||||
// Serial.println("Trying to connect to: " + this->base_ssid + "\nTime: " + String(millis() - start) + "/" + String(connection_timeout));
|
||||
delay(50);
|
||||
}
|
||||
|
||||
if(WiFi.status() == WL_CONNECTED){
|
||||
localIP = WiFi.localIP();
|
||||
}
|
||||
}
|
||||
|
||||
void send(const String& msg){
|
||||
udp.beginPacket(apIP, UDP_PORT);
|
||||
udp.write((uint8_t*)msg.c_str(), msg.length());
|
||||
udp.endPacket();
|
||||
}
|
||||
|
||||
String readSerialUntil(const String& delimiter) {
|
||||
String res = "";
|
||||
unsigned long start = millis();
|
||||
|
||||
while (!res.endsWith(delimiter) && (millis() - start < 1000)) {
|
||||
if (Serial2.available()) {
|
||||
char c = Serial2.read();
|
||||
res += c;
|
||||
start = millis();
|
||||
} else {
|
||||
// yield the CPU so the watchdog won’t fire
|
||||
taskYIELD();
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void MBS2_2_loop(){
|
||||
if(Serial2.available()){
|
||||
String recv = readSerialUntil("\n");
|
||||
log("Received: " + recv);
|
||||
send(recv);
|
||||
}
|
||||
// always give os a tick
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
#endif // #ifdef MBS2_2
|
||||
|
||||
#pragma endregion
|
||||
|
||||
void log(String buf){
|
||||
#ifdef DEBUG
|
||||
Serial.println(buf);
|
||||
#endif
|
||||
}
|
||||
|
||||
void log(char *buf){
|
||||
#ifdef DEBUG
|
||||
Serial.write(buf);
|
||||
#endif
|
||||
}
|
||||
|
||||
void setup() {
|
||||
#ifdef DEBUG
|
||||
Serial.begin(115200);
|
||||
#endif
|
||||
Serial2.begin(115200, SERIAL_8N1, 18, 17);
|
||||
|
||||
#ifdef MBS2_1
|
||||
init_wifi_access_point();
|
||||
routeMBS = MBS2_1_loop;
|
||||
#endif
|
||||
|
||||
#ifdef MBS2_2
|
||||
init_wifi_base_station_connection();
|
||||
routeMBS = MBS2_2_loop;
|
||||
#endif
|
||||
|
||||
udp.begin(UDP_PORT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
#ifdef DEBUG
|
||||
static size_t start = millis();
|
||||
if(millis() - start >= 1000){
|
||||
log(String(_NAME));
|
||||
start = millis();
|
||||
}
|
||||
#endif
|
||||
routeMBS();
|
||||
taskYIELD();
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <lwip/sockets.h>
|
||||
|
||||
#define _PASSWD "PASS0000"
|
||||
#define UDP_PORT 6666
|
||||
|
||||
#define MTU 100000
|
||||
|
||||
WiFiUDP udp;
|
||||
SemaphoreHandle_t netw_mtx;
|
||||
|
||||
String SSIDs[] = { "ESP32-AP1", "ESP32-AP2" };
|
||||
int scanStatus = WIFI_SCAN_FAILED;
|
||||
unsigned long lastScanRequest = 0;
|
||||
int otherRSSI = -9999;
|
||||
String otherSSID = "";
|
||||
|
||||
String readSerialUntil(const String& delimiter) {
|
||||
String res = "";
|
||||
unsigned long start = millis();
|
||||
|
||||
while (!res.endsWith(delimiter) && (millis() - start < 1000)) {
|
||||
if (Serial.available()) {
|
||||
char c = Serial.read();
|
||||
res += c;
|
||||
start = millis();
|
||||
} else {
|
||||
// yield the CPU so the watchdog won’t fire
|
||||
taskYIELD();
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int findSSIDIndex(const String &ssid, size_t network_count) {
|
||||
for(int i = 0; i < network_count; i++) {
|
||||
if(WiFi.SSID(i) == ssid) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int getSignalStrengthFromStation(const String& ssid) {
|
||||
if(ssid == "") {
|
||||
if(WiFi.isConnected()) {
|
||||
return WiFi.RSSI();
|
||||
} else {
|
||||
return -9999;
|
||||
}
|
||||
}
|
||||
size_t found_network_count = WiFi.scanNetworks(false);
|
||||
int index = -1;
|
||||
if((index = findSSIDIndex(ssid, found_network_count)) == -1) return -9999;
|
||||
return WiFi.RSSI(index);
|
||||
}
|
||||
|
||||
String getAvailableInfo() {
|
||||
String ret = "{\"type\":\"text\",\"IP\":\"";
|
||||
ret += WiFi.localIP().toString();
|
||||
ret += "\",\"Base Station\":\"";
|
||||
ret += WiFi.SSID();
|
||||
ret += "\",\"RSSI\":\"";
|
||||
ret += WiFi.RSSI();
|
||||
ret += "\",\"Other Network\":\"";
|
||||
ret += otherSSID;
|
||||
ret += "\",\"Other RSSI\":\"";
|
||||
ret += otherRSSI;
|
||||
ret += "\"}\n";
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void send_udp(void *args){
|
||||
String* msg = static_cast<String*>(args);
|
||||
udp.beginPacket(WiFi.gatewayIP(), UDP_PORT);
|
||||
udp.write((uint8_t*)msg->c_str(), msg->length());
|
||||
udp.endPacket();
|
||||
}
|
||||
|
||||
void invoke_fn_mtx(SemaphoreHandle_t mtx, void (*fn)(void*), void* args){
|
||||
xSemaphoreTake(mtx, portMAX_DELAY);
|
||||
fn(args);
|
||||
xSemaphoreGive(mtx);
|
||||
}
|
||||
|
||||
void changeNetwork(){
|
||||
if (otherRSSI < -1000) return;
|
||||
if(WiFi.RSSI() < otherRSSI - 3){
|
||||
String networkSwitchJson = "{\"type\": \"info\", \"data\": \"netsw\"}";
|
||||
invoke_fn_mtx(netw_mtx, send_udp, (void*)&networkSwitchJson);
|
||||
|
||||
String prevSSID = WiFi.SSID();
|
||||
otherRSSI = WiFi.RSSI();
|
||||
udp.stop();
|
||||
WiFi.disconnect();
|
||||
WiFi.begin(otherSSID, _PASSWD);
|
||||
if (WiFi.waitForConnectResult() == WL_CONNECTED) {
|
||||
udp.begin(UDP_PORT);
|
||||
otherSSID = prevSSID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void udp_task(void* /*args*/) {
|
||||
int start = millis();
|
||||
while(true){
|
||||
if(Serial.available()){
|
||||
String recv = readSerialUntil("\n");
|
||||
invoke_fn_mtx(netw_mtx, send_udp, (void*)&recv);
|
||||
}
|
||||
|
||||
if(millis() - start >= 1000){
|
||||
if(scanStatus == WIFI_SCAN_FAILED && millis() - lastScanRequest > 10000){
|
||||
scanStatus = WiFi.scanNetworks(true);
|
||||
lastScanRequest = millis();
|
||||
}
|
||||
int n = WiFi.scanComplete();
|
||||
if(n >= 0){
|
||||
for(int i = 0; i < n; i++){
|
||||
if(WiFi.SSID(i) == otherSSID){
|
||||
otherRSSI = WiFi.RSSI(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
WiFi.scanDelete();
|
||||
scanStatus = WIFI_SCAN_FAILED;
|
||||
}
|
||||
|
||||
String recv = getAvailableInfo();
|
||||
invoke_fn_mtx(netw_mtx, send_udp, (void*)&recv);
|
||||
start = millis();
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { delay(10); }
|
||||
|
||||
netw_mtx = xSemaphoreCreateMutex();
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
int ap1_rssi = -9999;
|
||||
int ap2_rssi = -9999;
|
||||
|
||||
int n = WiFi.scanNetworks();
|
||||
for(int i = 0; i < n; i++){
|
||||
String ssid = WiFi.SSID(i);
|
||||
int rssi = WiFi.RSSI(i);
|
||||
|
||||
if(ssid == SSIDs[0]) ap1_rssi = rssi;
|
||||
if(ssid == SSIDs[1]) ap2_rssi = rssi;
|
||||
}
|
||||
|
||||
WiFi.scanDelete();
|
||||
|
||||
if(ap1_rssi >= ap2_rssi){
|
||||
WiFi.begin(SSIDs[0], _PASSWD);
|
||||
otherSSID = SSIDs[1];
|
||||
otherRSSI = ap2_rssi;
|
||||
}
|
||||
else{
|
||||
WiFi.begin(SSIDs[1], _PASSWD);
|
||||
otherSSID = SSIDs[0];
|
||||
otherRSSI = ap1_rssi;
|
||||
}
|
||||
|
||||
if(WiFi.waitForConnectResult() != WL_CONNECTED)
|
||||
Serial.println("WiFi-Verbindung fehlgeschlagen!");
|
||||
else{
|
||||
Serial.print("Verbunden mit ");
|
||||
Serial.print(WiFi.SSID());
|
||||
Serial.print(" (RSSI ");
|
||||
Serial.print(WiFi.RSSI());
|
||||
Serial.println(" dBm)");
|
||||
}
|
||||
|
||||
udp.begin(UDP_PORT);
|
||||
|
||||
xTaskCreatePinnedToCore( udp_task, "UDPReceiver", 4096, nullptr, 5, nullptr, 1);
|
||||
}
|
||||
|
||||
|
||||
void loop() {
|
||||
changeNetwork();
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
class AlphaBot2(object):
|
||||
|
||||
def __init__(self,ain1=12,ain2=13,ena=6,bin1=20,bin2=21,enb=26):
|
||||
self.AIN1 = ain1
|
||||
self.AIN2 = ain2
|
||||
self.BIN1 = bin1
|
||||
self.BIN2 = bin2
|
||||
self.ENA = ena
|
||||
self.ENB = enb
|
||||
self.PA = 50
|
||||
self.PB = 50
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(self.AIN1,GPIO.OUT)
|
||||
GPIO.setup(self.AIN2,GPIO.OUT)
|
||||
GPIO.setup(self.BIN1,GPIO.OUT)
|
||||
GPIO.setup(self.BIN2,GPIO.OUT)
|
||||
GPIO.setup(self.ENA,GPIO.OUT)
|
||||
GPIO.setup(self.ENB,GPIO.OUT)
|
||||
self.PWMA = GPIO.PWM(self.ENA,500)
|
||||
self.PWMB = GPIO.PWM(self.ENB,500)
|
||||
self.PWMA.start(self.PA)
|
||||
self.PWMB.start(self.PB)
|
||||
self.stop()
|
||||
|
||||
def forward(self):
|
||||
self.PWMA.ChangeDutyCycle(self.PA)
|
||||
self.PWMB.ChangeDutyCycle(self.PB)
|
||||
GPIO.output(self.AIN1,GPIO.LOW)
|
||||
GPIO.output(self.AIN2,GPIO.HIGH)
|
||||
GPIO.output(self.BIN1,GPIO.LOW)
|
||||
GPIO.output(self.BIN2,GPIO.HIGH)
|
||||
|
||||
|
||||
def stop(self):
|
||||
self.PWMA.ChangeDutyCycle(0)
|
||||
self.PWMB.ChangeDutyCycle(0)
|
||||
GPIO.output(self.AIN1,GPIO.LOW)
|
||||
GPIO.output(self.AIN2,GPIO.LOW)
|
||||
GPIO.output(self.BIN1,GPIO.LOW)
|
||||
GPIO.output(self.BIN2,GPIO.LOW)
|
||||
|
||||
def backward(self):
|
||||
self.PWMA.ChangeDutyCycle(self.PA)
|
||||
self.PWMB.ChangeDutyCycle(self.PB)
|
||||
GPIO.output(self.AIN1,GPIO.HIGH)
|
||||
GPIO.output(self.AIN2,GPIO.LOW)
|
||||
GPIO.output(self.BIN1,GPIO.HIGH)
|
||||
GPIO.output(self.BIN2,GPIO.LOW)
|
||||
|
||||
|
||||
def left(self):
|
||||
self.PWMA.ChangeDutyCycle(30)
|
||||
self.PWMB.ChangeDutyCycle(30)
|
||||
GPIO.output(self.AIN1,GPIO.HIGH)
|
||||
GPIO.output(self.AIN2,GPIO.LOW)
|
||||
GPIO.output(self.BIN1,GPIO.LOW)
|
||||
GPIO.output(self.BIN2,GPIO.HIGH)
|
||||
|
||||
|
||||
def right(self):
|
||||
self.PWMA.ChangeDutyCycle(30)
|
||||
self.PWMB.ChangeDutyCycle(30)
|
||||
GPIO.output(self.AIN1,GPIO.LOW)
|
||||
GPIO.output(self.AIN2,GPIO.HIGH)
|
||||
GPIO.output(self.BIN1,GPIO.HIGH)
|
||||
GPIO.output(self.BIN2,GPIO.LOW)
|
||||
|
||||
def setPWMA(self,value):
|
||||
self.PA = value
|
||||
self.PWMA.ChangeDutyCycle(self.PA)
|
||||
|
||||
def setPWMB(self,value):
|
||||
self.PB = value
|
||||
self.PWMB.ChangeDutyCycle(self.PB)
|
||||
|
||||
def setMotor(self, left, right):
|
||||
if((right >= 0) and (right <= 100)):
|
||||
GPIO.output(self.AIN1,GPIO.HIGH)
|
||||
GPIO.output(self.AIN2,GPIO.LOW)
|
||||
self.PWMA.ChangeDutyCycle(right)
|
||||
elif((right < 0) and (right >= -100)):
|
||||
GPIO.output(self.AIN1,GPIO.LOW)
|
||||
GPIO.output(self.AIN2,GPIO.HIGH)
|
||||
self.PWMA.ChangeDutyCycle(0 - right)
|
||||
if((left >= 0) and (left <= 100)):
|
||||
GPIO.output(self.BIN1,GPIO.HIGH)
|
||||
GPIO.output(self.BIN2,GPIO.LOW)
|
||||
self.PWMB.ChangeDutyCycle(left)
|
||||
elif((left < 0) and (left >= -100)):
|
||||
GPIO.output(self.BIN1,GPIO.LOW)
|
||||
GPIO.output(self.BIN2,GPIO.HIGH)
|
||||
self.PWMB.ChangeDutyCycle(0 - left)
|
||||
|
||||
if __name__=='__main__':
|
||||
|
||||
Ab = AlphaBot2()
|
||||
Ab.forward()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
GPIO.cleanup()
|
||||
@@ -0,0 +1,166 @@
|
||||
import RPi.GPIO as GPIO
|
||||
import threading
|
||||
import time
|
||||
import subprocess
|
||||
import os, signal, sys
|
||||
from pid_line_follow1 import *
|
||||
from AlphaBot2 import AlphaBot2
|
||||
from TRSensors import TRSensor
|
||||
|
||||
|
||||
Ab = AlphaBot2()
|
||||
|
||||
IR = 17
|
||||
PWM = 50
|
||||
|
||||
'''def move_and_stop(func, duration=0.5):
|
||||
func()
|
||||
time.sleep(duration)
|
||||
Ab.stop()'''
|
||||
def setup_gpio():
|
||||
"""Initialisiert die GPIO-Pins."""
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(IR, GPIO.IN)
|
||||
|
||||
def getkey():
|
||||
if GPIO.input(IR) == 0:
|
||||
count = 0
|
||||
while GPIO.input(IR) == 0 and count < 200: #9ms
|
||||
count += 1
|
||||
time.sleep(0.00006)
|
||||
|
||||
if(count < 10):
|
||||
#print("None returned")
|
||||
return None
|
||||
count = 0
|
||||
while GPIO.input(IR) == 1 and count < 80: #4.5ms
|
||||
count += 1
|
||||
time.sleep(0.00006)
|
||||
#print("High-Burst-Length:", count)
|
||||
if (count < 20):
|
||||
#print("repeat returned")
|
||||
return "repeat"
|
||||
|
||||
idx = 0
|
||||
cnt = 0
|
||||
data = [0,0,0,0]
|
||||
for i in range(0,32):
|
||||
count = 0
|
||||
while GPIO.input(IR) == 0 and count < 15: #0.56ms
|
||||
count += 1
|
||||
time.sleep(0.00006)
|
||||
|
||||
count = 0
|
||||
while GPIO.input(IR) == 1 and count < 40: #0: 0.56mx
|
||||
count += 1 #1: 1.69ms
|
||||
time.sleep(0.00006)
|
||||
|
||||
if count > 7:
|
||||
data[idx] |= 1<<cnt
|
||||
if cnt == 7:
|
||||
cnt = 0
|
||||
idx += 1
|
||||
else:
|
||||
cnt += 1
|
||||
# print data
|
||||
if data[0]+data[1] == 0xFF and data[2]+data[3] == 0xFF: #check
|
||||
print("OK")
|
||||
return data[2]
|
||||
return None
|
||||
|
||||
def stop_listener():
|
||||
"""
|
||||
Deine Remote‑API muss hier so lange blocken, bis
|
||||
ein Key‑Event eintrifft und dir den Code 0x43 liefert.
|
||||
"""
|
||||
while True:
|
||||
code = getkey()
|
||||
if code == 0x43:
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
return
|
||||
|
||||
setup_gpio()
|
||||
print('IRremote Test Start ...')
|
||||
Ab.stop()
|
||||
last_key = 0
|
||||
last_key_press_time = 0
|
||||
current_action = None
|
||||
try:
|
||||
while True:
|
||||
key = getkey()
|
||||
# 0x43 == line_follow
|
||||
|
||||
if key == "repeat":
|
||||
key = last_key
|
||||
last_key_press_time = time.time()
|
||||
elif key is not None:
|
||||
print("getkey:")
|
||||
print(hex(key))
|
||||
last_key = key
|
||||
last_key_press_time = time.time()
|
||||
|
||||
if key is not None:
|
||||
last_key_press_time = time.time()
|
||||
|
||||
if key == 0x18 and current_action != "forward":
|
||||
Ab.forward()
|
||||
print("forward")
|
||||
current_action = "forward"
|
||||
elif key == 0x08 and current_action != "left":
|
||||
Ab.left()
|
||||
print("left")
|
||||
current_action = "left"
|
||||
elif key == 0x1c and current_action != "stop":
|
||||
Ab.stop()
|
||||
print("stop")
|
||||
current_action = "stop"
|
||||
elif key == 0x5a and current_action != "right":
|
||||
Ab.right()
|
||||
print("right")
|
||||
current_action = "right"
|
||||
elif key == 0x52 and current_action != "backward":
|
||||
Ab.backward()
|
||||
print("backward")
|
||||
current_action = "backward"
|
||||
elif key == 0x15:
|
||||
if(PWM + 10 < 101):
|
||||
PWM += 10
|
||||
Ab.setPWMA(PWM)
|
||||
Ab.setPWMB(PWM)
|
||||
print("PWM:", PWM)
|
||||
elif key == 0x07:
|
||||
if(PWM - 10 > -1):
|
||||
PWM -= 10
|
||||
Ab.setPWMA(PWM)
|
||||
Ab.setPWMB(PWM)
|
||||
print("PWM:", PWM)
|
||||
elif key == 0x43:
|
||||
tr = TRSensor()
|
||||
running = threading.Event()
|
||||
process = subprocess.Popen(
|
||||
["python", "pid_line_follow1.py"],
|
||||
)
|
||||
# follow_thread = threading.Thread(target=follow, args=(tr, Ab, running), daemon=True)
|
||||
# follow_thread.start()
|
||||
key = getkey()
|
||||
while(key != 0x43):
|
||||
key = getkey()
|
||||
|
||||
# running.set()
|
||||
process.terminate()
|
||||
|
||||
Ab.setPWMA(PWM)
|
||||
Ab.setPWMB(PWM)
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
else:
|
||||
if time.time() - last_key_press_time > 0.1 and current_action != "stop":
|
||||
Ab.stop()
|
||||
print("stopping bc no key is being pressed")
|
||||
current_action = "stop"
|
||||
|
||||
except KeyboardInterrupt:
|
||||
GPIO.cleanup()
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
# Robot
|
||||
|
||||
## Cam-stream
|
||||
|
||||
Files:
|
||||
- cam_stream.py
|
||||
|
||||
### Ziel
|
||||
|
||||
Kamera läuft und schickt Bilder via seriell an den ESP.
|
||||
|
||||
### Voraussetzung
|
||||
|
||||
- Die Kamera 🤡
|
||||
- ESP am Serial-Port `/dev/ttyACM1` (115200 Baud)
|
||||
|
||||
### Umsetzung
|
||||
|
||||
In Endlosschleife wird ein 640x480-Graustufen-Frame von der Kamera gelesen und als JPEG komprimiert bis die Größe unter 1200 Bytes liegt.\
|
||||
Danach wird der Frame in JSON-Format über seriell an den ESP gesendet.\
|
||||
Sollte es zu einem Verbindungsabbruch zwischen Pi und ESP kommen wird automatisch so lange eine Neuverbindung versucht, bis diese Verbindung wieder steht.
|
||||
|
||||
### Probleme während der Bearbeitung und ihre Lösungen
|
||||
|
||||
Die Kamera wurde nicht konstant erkannt (beim Suchen der Kamera wurde sie manchmal gefunden und manchmal nicht, meistens nicht).\
|
||||
Eine Neuinstallation von Raspbian hat das Problem gelöst, allerdings wurde diesmal die full-version installiert.
|
||||
|
||||
-----------------------
|
||||
|
||||
## Line follow
|
||||
|
||||
Files:
|
||||
- AlphaBot2.py
|
||||
- TRSensors.py
|
||||
- line_follow.py
|
||||
|
||||
### Ziel
|
||||
|
||||
Roboter soll in der Lage sein einer Linie auf dem Boden zu folgen,
|
||||
ohne diesen zu verlieren
|
||||
|
||||
### Voraussetzung
|
||||
|
||||
Linienbreite deckt 3 der 5 Sensoren ab (die mittleren), äußeren Sensoren müssen den boden sehen.
|
||||
|
||||
### Umsetzung
|
||||
|
||||
#### erste Idee: (Verworfen)
|
||||
|
||||
Von 5 Sensoren müssen die mittleren 3 immer die Linie sehen.\
|
||||
Mithilfe der äußeren Sensoren werden Korrekturen durchgeführt, damit der Roboter auf der Linie bleibt.
|
||||
|
||||
#### zweite Idee:
|
||||
|
||||
Es müssen wieder die mittleren 3 Sensoren auf der Linie sein.\
|
||||
Diesmal wird die Korrektur aber von Sensor 1 und Sensor 3 durchgeführt (Index beginnt bei 0).\
|
||||
Mithilfe der äußeren Sensoren, soll entschieden werden, ob eine Abzweigung existiert und falls sie existiert wird eine stärkere Korrektur durchgeführt damit der Roboter sich richtig dreht.
|
||||
|
||||
### Probleme während der Bearbeitung und ihre Lösungen
|
||||
|
||||
#### erste Idee:
|
||||
|
||||
Der Roboter hat zu sehr gezappelt und es war schwer zu unterscheiden ob nur leicht korrigiert werden musste oder ob die Linie die Richtung gewechselt hat.
|
||||
Das Problem sollte durch die zweite Idee gelöst werden.
|
||||
|
||||
#### zweite Idee: (Idee wurde vereinfacht auf nur starke Kurven = Roboter muss eine 90° Drehung machen um zu folgen)
|
||||
|
||||
Der Roboter interpretiert, je nach Untergrund, Kurven falsch.\
|
||||
Obwohl keine Kurve vorhanden ist biegt er doch ab.\
|
||||
Wenn eine Kurve vorhanden ist nimmt er diese nicht korrekt war und bleibt am Ende der Linie stehen.
|
||||
|
||||
-------------------
|
||||
|
||||
## remote steering
|
||||
|
||||
Files:
|
||||
- AlphaBot2.py
|
||||
- IRremote.py
|
||||
|
||||
### Ziel
|
||||
|
||||
Steuerung des kompletten Roboters über Fernbedienung.
|
||||
Möglich machen:
|
||||
- Roboter bewegen
|
||||
- Linienfolgen starten und stoppen + Kalibrierung manuell starten
|
||||
- (Kameraausrichtung ermöglichen)
|
||||
|
||||
### Voraussetzung
|
||||
|
||||
Die Fernbedienung 🤡
|
||||
|
||||
### Umsetzung Buttons
|
||||
|
||||
- play/pause = starten bzw. stoppen des line follows
|
||||
- 2 = geradeaus fahren
|
||||
- 4 = nach links drehen um die eigene Achse
|
||||
- 6 = nach rechts drehen um die eigene Achse
|
||||
- 8 = rückwärts fahren
|
||||
|
||||
### Probleme während der Bearbeitung und ihre Lösungen
|
||||
Keine Probleme
|
||||
|
||||
-------------------
|
||||
|
||||
## Daemon
|
||||
|
||||
Files: Kein
|
||||
|
||||
### Ziel
|
||||
|
||||
Automatisches starten des Kamerastreams,
|
||||
sowie die Steuerung über eine Fernbedienung.
|
||||
|
||||
### Umsetzung
|
||||
|
||||
1. **Service-Datei anlegen**
|
||||
|
||||
Erstelle unter `/etc/systemd/system/` eine Datei namens `<service_name>.service` mit folgendem Inhalt:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=<Kurzbeschreibung des Dienstes>
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=<system_user>
|
||||
WorkingDirectory=<Arbeitsverzeichnis>
|
||||
ExecStart=<Pfad zu Interpreter> <Pfad zum Skript>
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
2. **Systemd neu einlesen**
|
||||
|
||||
``sudo systemctl daemon-reload``
|
||||
3. **Dienst aktivierten (Boot-Start)**
|
||||
|
||||
``sudo systemctl enable <service_name>.service``
|
||||
4. **Dienst sofort starten**
|
||||
|
||||
``sudo systemctl start <service_name>.service``
|
||||
5. **Status und Logs prüfen**
|
||||
- Status anzeigen:
|
||||
|
||||
``sudo systemctl status <service_name>.service``
|
||||
- Live-Logs:
|
||||
|
||||
``sudo journalctl -u <service_name>.service -f``
|
||||
|
||||
### Probleme während der Bearbeitung und ihre Lösungen
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding:utf-8 -*-
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
CS = 5
|
||||
Clock = 25
|
||||
Address = 24
|
||||
DataOut = 23
|
||||
Button = 7
|
||||
|
||||
class TRSensor(object):
|
||||
def __init__(self,numSensors = 5):
|
||||
self.numSensors = numSensors
|
||||
self.calibratedMin = [0] * self.numSensors
|
||||
self.calibratedMax = [1023] * self.numSensors
|
||||
self.last_value = 0
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(Clock,GPIO.OUT)
|
||||
GPIO.setup(Address,GPIO.OUT)
|
||||
GPIO.setup(CS,GPIO.OUT)
|
||||
GPIO.setup(DataOut,GPIO.IN,GPIO.PUD_UP)
|
||||
GPIO.setup(Button,GPIO.IN,GPIO.PUD_UP)
|
||||
|
||||
"""
|
||||
Reads the sensor values into an array. There *MUST* be space
|
||||
for as many values as there were sensors specified in the constructor.
|
||||
Example usage:
|
||||
unsigned int sensor_values[8];
|
||||
sensors.read(sensor_values);
|
||||
The values returned are a measure of the reflectance in abstract units,
|
||||
with higher values corresponding to lower reflectance (e.g. a black
|
||||
surface or a void).
|
||||
"""
|
||||
def AnalogRead(self):
|
||||
value = [0]*(self.numSensors+1)
|
||||
#Read Channel0~channel6 AD value
|
||||
for j in range(0,self.numSensors+1):
|
||||
GPIO.output(CS, GPIO.LOW)
|
||||
for i in range(0,8):
|
||||
#sent 8-bit Address
|
||||
if i<4:
|
||||
if(((j) >> (3 - i)) & 0x01):
|
||||
GPIO.output(Address,GPIO.HIGH)
|
||||
else:
|
||||
GPIO.output(Address,GPIO.LOW)
|
||||
else:
|
||||
GPIO.output(Address,GPIO.LOW)
|
||||
#read MSB 4-bit data
|
||||
value[j] <<= 1
|
||||
if(GPIO.input(DataOut)):
|
||||
value[j] |= 0x01
|
||||
GPIO.output(Clock,GPIO.HIGH)
|
||||
GPIO.output(Clock,GPIO.LOW)
|
||||
for i in range(0,4):
|
||||
#read LSB 8-bit data
|
||||
value[j] <<= 1
|
||||
if(GPIO.input(DataOut)):
|
||||
value[j] |= 0x01
|
||||
GPIO.output(Clock,GPIO.HIGH)
|
||||
GPIO.output(Clock,GPIO.LOW)
|
||||
#no mean ,just delay
|
||||
# for i in range(0,6):
|
||||
# GPIO.output(Clock,GPIO.HIGH)
|
||||
# GPIO.output(Clock,GPIO.LOW)
|
||||
time.sleep(0.0001)
|
||||
GPIO.output(CS,GPIO.HIGH)
|
||||
for i in range(0,6):
|
||||
value[i] >>= 2
|
||||
# print (value[1:])
|
||||
return value[1:]
|
||||
|
||||
"""
|
||||
Reads the sensors 10 times and uses the results for
|
||||
calibration. The sensor values are not returned; instead, the
|
||||
maximum and minimum values found over time are stored internally
|
||||
and used for the readCalibrated() method.
|
||||
"""
|
||||
def calibrate(self):
|
||||
max_sensor_values = [0]*self.numSensors
|
||||
min_sensor_values = [0]*self.numSensors
|
||||
for j in range(0,10):
|
||||
|
||||
sensor_values = self.AnalogRead()
|
||||
|
||||
for i in range(0,self.numSensors):
|
||||
|
||||
# set the max we found THIS time
|
||||
if((j == 0) or max_sensor_values[i] < sensor_values[i]):
|
||||
max_sensor_values[i] = sensor_values[i]
|
||||
|
||||
# set the min we found THIS time
|
||||
if((j == 0) or min_sensor_values[i] > sensor_values[i]):
|
||||
min_sensor_values[i] = sensor_values[i]
|
||||
|
||||
# record the min and max calibration values
|
||||
for i in range(0,self.numSensors):
|
||||
if(min_sensor_values[i] > self.calibratedMin[i]):
|
||||
self.calibratedMin[i] = min_sensor_values[i]
|
||||
if(max_sensor_values[i] < self.calibratedMax[i]):
|
||||
self.calibratedMax[i] = max_sensor_values[i]
|
||||
|
||||
"""
|
||||
Returns values calibrated to a value between 0 and 1000, where
|
||||
0 corresponds to the minimum value read by calibrate() and 1000
|
||||
corresponds to the maximum value. Calibration values are
|
||||
stored separately for each sensor, so that differences in the
|
||||
sensors are accounted for automatically.
|
||||
"""
|
||||
def readCalibrated(self):
|
||||
value = 0
|
||||
#read the needed values
|
||||
sensor_values = self.AnalogRead()
|
||||
|
||||
for i in range (0,self.numSensors):
|
||||
|
||||
denominator = self.calibratedMax[i] - self.calibratedMin[i]
|
||||
|
||||
if(denominator != 0):
|
||||
value = (sensor_values[i] - self.calibratedMin[i])* 1000 / denominator
|
||||
|
||||
if(value < 0):
|
||||
value = 0
|
||||
elif(value > 1000):
|
||||
value = 1000
|
||||
|
||||
sensor_values[i] = value
|
||||
|
||||
#print("readCalibrated",sensor_values)
|
||||
return sensor_values
|
||||
|
||||
"""
|
||||
Operates the same as read calibrated, but also returns an
|
||||
estimated position of the robot with respect to a line. The
|
||||
estimate is made using a weighted average of the sensor indices
|
||||
multiplied by 1000, so that a return value of 0 indicates that
|
||||
the line is directly below sensor 0, a return value of 1000
|
||||
indicates that the line is directly below sensor 1, 2000
|
||||
indicates that it's below sensor 2000, etc. Intermediate
|
||||
values indicate that the line is between two sensors. The
|
||||
formula is:
|
||||
|
||||
0*value0 + 1000*value1 + 2000*value2 + ...
|
||||
--------------------------------------------
|
||||
value0 + value1 + value2 + ...
|
||||
|
||||
By default, this function assumes a dark line (high values)
|
||||
surrounded by white (low values). If your line is light on
|
||||
black, set the optional second argument white_line to true. In
|
||||
this case, each sensor value will be replaced by (1000-value)
|
||||
before the averaging.
|
||||
"""
|
||||
def readLine(self, white_line = 0):
|
||||
|
||||
sensor_values = self.readCalibrated()
|
||||
avg = 0
|
||||
sum = 0
|
||||
on_line = 0
|
||||
for i in range(0,self.numSensors):
|
||||
value = sensor_values[i]
|
||||
if(white_line):
|
||||
value = 1000-value
|
||||
# keep track of whether we see the line at all
|
||||
if(value > 200):
|
||||
on_line = 1
|
||||
|
||||
# only average in values that are above a noise threshold
|
||||
if(value > 50):
|
||||
avg += value * (i * 1000); # this is for the weighted total,
|
||||
sum += value; #this is for the denominator
|
||||
|
||||
if(on_line != 1):
|
||||
# If it last read to the left of center, return 0.
|
||||
if(self.last_value < (self.numSensors - 1)*1000/2):
|
||||
#print("left")
|
||||
self.last_value = 0
|
||||
|
||||
# If it last read to the right of center, return the max.
|
||||
else:
|
||||
#print("right")
|
||||
self.last_value = (self.numSensors - 1)*1000
|
||||
else:
|
||||
self.last_value = avg/sum
|
||||
|
||||
return self.last_value,sensor_values
|
||||
|
||||
|
||||
|
||||
# Simple example prints accel/mag data once per second:
|
||||
if __name__ == '__main__':
|
||||
TR = TRSensor()
|
||||
print("TRSensor Example")
|
||||
while True:
|
||||
try:
|
||||
print(TR.AnalogRead())
|
||||
time.sleep(0.2)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Requirements Step by Step:
|
||||
sudo apt update
|
||||
sudo apt install libjpeg62-turbo-dev python3-prctl python3-picamera2 python3-libcamera libcamera-apps libcamera-dev
|
||||
python3 -m venv --system-site-packages env
|
||||
source env/bin/activate
|
||||
pip install flask pillow
|
||||
"""
|
||||
|
||||
from flask import Flask, Response
|
||||
from picamera2 import Picamera2
|
||||
from PIL import Image
|
||||
import io
|
||||
import base64
|
||||
import serial
|
||||
import time
|
||||
import cv2
|
||||
|
||||
s = serial.Serial('/dev/ttyACM1', 115200, timeout=1) # serielle verbindung zum ESP aufbauen
|
||||
time.sleep(2)
|
||||
|
||||
'''
|
||||
Kamera Konfigurieren mit einem Format von 640x480
|
||||
'''
|
||||
picam2 = Picamera2()
|
||||
config = picam2.create_preview_configuration(main={"size": (640, 480)})
|
||||
picam2.configure(config)
|
||||
picam2.start()
|
||||
|
||||
MAX_RAW = 1200
|
||||
MAX_B64 = 1200
|
||||
|
||||
# TODO: map image to MAX_B64
|
||||
|
||||
def encode_image_to_b64_with_limit(img: Image.Image, max_b64: int = 1024, scale_step: float = 0.9, initial_quality: int = 30, min_quality: int = 5) -> str:
|
||||
quality = initial_quality
|
||||
working_img = img.copy()
|
||||
|
||||
while True:
|
||||
buf = io.BytesIO()
|
||||
working_img.save(buf, format="JPEG", quality=quality, optimize=True)
|
||||
jpeg_bytes = buf.getvalue()
|
||||
|
||||
b64 = base64.b64encode(jpeg_bytes).decode("utf-8")
|
||||
|
||||
if len(b64) <= max_b64 or (working_img.width < 2 or working_img.height < 2):
|
||||
return b64
|
||||
|
||||
|
||||
# if len(jpeg_bytes) <= MAX_RAW:
|
||||
# return jpeg_bytes
|
||||
|
||||
new_w = max(1, int(working_img.width * scale_step))
|
||||
new_h = max(1, int(working_img.height * scale_step))
|
||||
working_img = working_img.resize((new_w, new_h), Image.LANCZOS)
|
||||
|
||||
if quality > min_quality:
|
||||
quality = max(min_quality, int(quality * scale_step))
|
||||
|
||||
def init_serial():
|
||||
global s
|
||||
s = None
|
||||
while s is None:
|
||||
try:
|
||||
s = serial.Serial('/dev/ttyACM1', 115200, timeout=1)
|
||||
except Exception:
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
def gen():
|
||||
global s
|
||||
while True:
|
||||
arr = picam2.capture_array("main") # Einen frame holen
|
||||
img = Image.fromarray(arr).convert("L") # Bild in Graustufen umwandeln
|
||||
# TODO: skip config with 640x480
|
||||
img = img.resize((640, 480), Image.LANCZOS)
|
||||
|
||||
b64 = encode_image_to_b64_with_limit(img, MAX_B64) # Bildqualität reduzieren bis es eine Größe von maximal MAX_B64 (1200) hat
|
||||
print("Image_b64 size: " + str(len(b64)))
|
||||
|
||||
try:
|
||||
s.write(("{\"type\":\"image\",\"data\":\"" + b64 + "\"}\n").encode("utf-8")) # Frame als JSON-String an den ESP verschicken
|
||||
# s.write((b64 + "\n").encode("utf-8"))
|
||||
except Exception:
|
||||
'''
|
||||
War das schreiben über seriell nicht möglich wird ein erneuter Verbindungsaufbau versucht bis die Verbindung wieder steht.
|
||||
Es wird davon ausgegangen das wir die Verbindung verloren haben und deswegen das schreiben nicht möglich war.
|
||||
'''
|
||||
init_serial()
|
||||
time.sleep(0.1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# app.run(host="0.0.0.0", port=1234, threaded=True)
|
||||
try:
|
||||
gen()
|
||||
except KeyboardInterrupt:
|
||||
serial.close()
|
||||
print("Uebertragung beenden")
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Line following robot for Raspberry Pi 4 using AlphaBot2 and TRSensor
|
||||
Ohne LED-Visualisierung
|
||||
"""
|
||||
import time
|
||||
import RPi.GPIO as GPIO
|
||||
from AlphaBot2 import AlphaBot2
|
||||
from TRSensors import TRSensor
|
||||
|
||||
MAX_SPEED = 25
|
||||
CORRECTION_STRENGTH = 8
|
||||
EXTRA_CURVE_CORRECTION = 2
|
||||
LINE_DETECTED_THRESHOLD = 650
|
||||
|
||||
# Liste für gleitenden Durchschnitt
|
||||
last_outer = []
|
||||
|
||||
def setup_gpio():
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
|
||||
def calibrate_sensors(tr, robot):
|
||||
print("Auto-calibrating sensors...")
|
||||
|
||||
for i in range(5):
|
||||
tr.calibratedMin[i] = 1023
|
||||
tr.calibratedMax[i] = 0
|
||||
|
||||
for _ in range(3):
|
||||
robot.setPWMA(20)
|
||||
robot.setPWMB(15)
|
||||
robot.forward()
|
||||
for _ in range(30):
|
||||
raw = tr.AnalogRead()
|
||||
for i in range(5):
|
||||
tr.calibratedMin[i] = min(tr.calibratedMin[i], raw[i])
|
||||
tr.calibratedMax[i] = max(tr.calibratedMax[i], raw[i])
|
||||
time.sleep(0.02)
|
||||
|
||||
robot.backward()
|
||||
for _ in range(30):
|
||||
raw = tr.AnalogRead()
|
||||
for i in range(5):
|
||||
tr.calibratedMin[i] = min(tr.calibratedMin[i], raw[i])
|
||||
tr.calibratedMax[i] = max(tr.calibratedMax[i], raw[i])
|
||||
time.sleep(0.02)
|
||||
|
||||
robot.stop()
|
||||
print("Calibration complete.")
|
||||
print(" Min:", tr.calibratedMin)
|
||||
print(" Max:", tr.calibratedMax)
|
||||
|
||||
def detect_line_type(tr):
|
||||
sensor_values = tr.readCalibrated()
|
||||
center_avg = sum(sensor_values[1:4]) / 3
|
||||
outer_avg = (sensor_values[0] + sensor_values[4]) / 2
|
||||
white_line = outer_avg < center_avg
|
||||
print("Detected line type:", "WHITE on BLACK" if white_line else "BLACK on WHITE")
|
||||
return white_line
|
||||
|
||||
def compute_correction(sensors):
|
||||
left = sensors[1]
|
||||
right = sensors[3]
|
||||
correction = 0
|
||||
|
||||
if abs(left - right) < 50:
|
||||
correction = 0
|
||||
elif left < LINE_DETECTED_THRESHOLD and right > LINE_DETECTED_THRESHOLD:
|
||||
correction = -CORRECTION_STRENGTH
|
||||
elif right < LINE_DETECTED_THRESHOLD and left > LINE_DETECTED_THRESHOLD:
|
||||
correction = CORRECTION_STRENGTH
|
||||
|
||||
if sensors[0] < LINE_DETECTED_THRESHOLD:
|
||||
correction -= EXTRA_CURVE_CORRECTION
|
||||
elif sensors[4] < LINE_DETECTED_THRESHOLD:
|
||||
correction += EXTRA_CURVE_CORRECTION
|
||||
|
||||
return correction
|
||||
|
||||
def perform_turn_if_needed(sensors, robot, position):
|
||||
global last_outer
|
||||
l = sensors[0]
|
||||
r = sensors[4]
|
||||
last_outer.append((l, r))
|
||||
if len(last_outer) > 5:
|
||||
last_outer.pop(0)
|
||||
|
||||
avg_l = sum(x for x, _ in last_outer) / len(last_outer)
|
||||
avg_r = sum(y for _, y in last_outer) / len(last_outer)
|
||||
delta = avg_r - avg_l
|
||||
|
||||
print(f"Turn-Check: AvgL={avg_l:.0f}, AvgR={avg_r:.0f}, Δ={delta:.0f}, pos={position:.1f}")
|
||||
|
||||
if not (1500 <= position <= 2500):
|
||||
return False
|
||||
if avg_l > 800 and avg_r > 800:
|
||||
return False
|
||||
|
||||
if avg_r > 800 and avg_l < 500 and delta > 400:
|
||||
print("→ 90° right-curve detected")
|
||||
robot.stop()
|
||||
time.sleep(0.1)
|
||||
robot.right()
|
||||
time.sleep(0.3)
|
||||
robot.forward()
|
||||
time.sleep(0.35)
|
||||
return True
|
||||
elif avg_l > 800 and avg_r < 500 and delta < -600 and sensors[2] < LINE_DETECTED_THRESHOLD:
|
||||
print("← 90° left-curve detected")
|
||||
robot.stop()
|
||||
time.sleep(0.1)
|
||||
robot.left()
|
||||
time.sleep(0.3)
|
||||
robot.forward()
|
||||
time.sleep(0.35)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def line_follow_loop(tr, robot, white_line):
|
||||
robot.forward()
|
||||
|
||||
while True:
|
||||
position, sensors = tr.readLine(white_line=white_line)
|
||||
|
||||
print(f"Sensors: {sensors}, pos={position:.1f}")
|
||||
|
||||
if perform_turn_if_needed(sensors, robot, position):
|
||||
robot.forward()
|
||||
time.sleep(0.35)
|
||||
robot.stop()
|
||||
break
|
||||
|
||||
if all(s > LINE_DETECTED_THRESHOLD for s in sensors):
|
||||
time.sleep(0.05)
|
||||
robot.backward()
|
||||
time.sleep(0.35)
|
||||
robot.stop()
|
||||
continue
|
||||
|
||||
correction = compute_correction(sensors)
|
||||
left_speed = min(MAX_SPEED, max(0, MAX_SPEED + correction))
|
||||
right_speed = min(MAX_SPEED, max(0, MAX_SPEED - correction))
|
||||
|
||||
robot.setPWMA(int(left_speed))
|
||||
robot.setPWMB(int(right_speed))
|
||||
|
||||
time.sleep(0.02)
|
||||
|
||||
def line_follow(tr=None, robot=None):
|
||||
if tr is None or robot is None:
|
||||
setup_gpio()
|
||||
robot = AlphaBot2()
|
||||
tr = TRSensor()
|
||||
robot.stop()
|
||||
calibrate_sensors(tr, robot)
|
||||
|
||||
white_line = detect_line_type(tr)
|
||||
|
||||
print("Starting line follow...")
|
||||
try:
|
||||
line_follow_loop(tr, robot, white_line)
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping and cleaning up...")
|
||||
robot.stop()
|
||||
GPIO.cleanup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup_gpio()
|
||||
robot = AlphaBot2()
|
||||
tr = TRSensor()
|
||||
robot.stop()
|
||||
|
||||
calibrate_sensors(tr, robot)
|
||||
input("Press Enter to continue...")
|
||||
line_follow(tr, robot)
|
||||
@@ -0,0 +1,119 @@
|
||||
from enum import Enum
|
||||
import enum
|
||||
import time
|
||||
import RPi.GPIO as GPIO
|
||||
from AlphaBot2 import AlphaBot2
|
||||
from TRSensors import TRSensor
|
||||
import datetime
|
||||
|
||||
class LINE_MODE(Enum):
|
||||
UNSET = -1
|
||||
WHITE_LINE_MODE = 0
|
||||
BLACK_LINE_MODE = 1
|
||||
|
||||
class STATE(Enum):
|
||||
INIT_SUCCESS = 0
|
||||
INIT_FAIL = -1
|
||||
|
||||
BTN_PIN = 7
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(BTN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
|
||||
MAX_SPEED = 40
|
||||
OVERDRIVE_SPEED = 50
|
||||
MAX_REVERSE_SPEED = 20
|
||||
MAX_CORRECTION_STRENGTH = 5
|
||||
|
||||
LineColorDescriptor: LINE_MODE = LINE_MODE.UNSET
|
||||
CurrentCorrectionStrength: int = 1
|
||||
logging: bool = True
|
||||
LineSearchingDirection: int = 0 # -1 -> left, 1 -> right
|
||||
|
||||
TR = TRSensor()
|
||||
Ab = AlphaBot2()
|
||||
Ab.stop()
|
||||
|
||||
def log(msg: str) -> None:
|
||||
if not logging: return;
|
||||
print("-> Logger")
|
||||
print(f"Timestamp: {datetime.datetime.now()}")
|
||||
print(f"Message: {msg}")
|
||||
print("<- Logger")
|
||||
|
||||
def LineModeBoolConverter(mode: LINE_MODE) -> bool:
|
||||
return True if mode == LINE_MODE.WHITE_LINE_MODE else False
|
||||
|
||||
def setup() -> int:
|
||||
log("Initializing TR!")
|
||||
for i in range(1, 4):
|
||||
TR.calibratedMin[i] = 1023
|
||||
TR.calibratedMax[i] = 0
|
||||
|
||||
log("Calibration sensors!")
|
||||
for _ in range(100):
|
||||
values = TR.AnalogRead()
|
||||
for i in range(1, 5): # Index 1 bis 3 → Sensor 2–4
|
||||
TR.calibratedMin[i] = min(TR.calibratedMin[i], values[i])
|
||||
TR.calibratedMax[i] = max(TR.calibratedMax[i], values[i])
|
||||
Ab.stop()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Determine line color
|
||||
sensor_values = TR.readCalibrated()
|
||||
center_avg = sum(sensor_values[1:4]) / 3
|
||||
outer_avg = (sensor_values[0] + sensor_values[4]) / 2
|
||||
LineColorDescriptor = LINE_MODE.WHITE_LINE_MODE if outer_avg < center_avg else LINE_MODE.BLACK_LINE_MODE # If outer sensors are dark → white line on dark
|
||||
log("Detected line type: " + "WHITE on BLACK" if LineColorDescriptor == LINE_MODE.WHITE_LINE_MODE else "BLACK on WHITE")
|
||||
|
||||
# Wait for button press to start
|
||||
print("Press the button to start line following")
|
||||
while GPIO.input(BTN_PIN):
|
||||
time.sleep(0.1)
|
||||
|
||||
LineSearchingDirection = 0
|
||||
|
||||
log("Setup done!")
|
||||
return 0;
|
||||
|
||||
def loop() -> None:
|
||||
Ab.forward()
|
||||
while True:
|
||||
position, sensors = TR.readLine(white_line=LineModeBoolConverter(LineColorDescriptor))
|
||||
background_level = (sensors[0] + sensors[1]) / 2
|
||||
|
||||
correction_strength = (background_level / 1023.0) * 2 * MAX_CORRECTION_STRENGTH - MAX_CORRECTION_STRENGTH
|
||||
|
||||
if sensors[2] < background_level:
|
||||
Ab.backward()
|
||||
Ab.setPWMA(MAX_REVERSE_SPEED)
|
||||
Ab.setPWMB(MAX_REVERSE_SPEED)
|
||||
while True:
|
||||
position, sensors = TR.readLine(white_line=LineModeBoolConverter(LineColorDescriptor))
|
||||
if sensors[2] >= background_level:
|
||||
break
|
||||
|
||||
Ab.forward()
|
||||
continue
|
||||
|
||||
if sensors[1] < background_level:
|
||||
Ab.setPWMA(OVERDRIVE_SPEED)
|
||||
Ab.setPWMB(MAX_SPEED)
|
||||
|
||||
elif sensors[3] < background_level:
|
||||
Ab.setPWMB(OVERDRIVE_SPEED)
|
||||
Ab.setPWMA(MAX_SPEED)
|
||||
|
||||
else:
|
||||
if correction_strength < 0:
|
||||
Ab.setPWMB(int(MAX_SPEED + correction_strength)) # decrease power from right motor if sensor indicates missing line left
|
||||
elif correction_strength > 0:
|
||||
Ab.setPWMA(int(MAX_SPEED - correction_strength))
|
||||
|
||||
# time.sleep(0.01); # we drive fast so handle steering as fast as possible
|
||||
|
||||
if __name__ == '__main__':
|
||||
if setup():
|
||||
loop()
|
||||
else:
|
||||
log("Setup failed!")
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Linienfolgeroboter für Raspberry Pi 4 mit AlphaBot2 und TRSensor
|
||||
Verwendet einen PID-Regler und eine Suchfunktion bei Linienverlust.
|
||||
(Version 3: Korrektur für Stopp nach Liniensuche)
|
||||
"""
|
||||
import time
|
||||
import RPi.GPIO as GPIO
|
||||
from AlphaBot2 import AlphaBot2
|
||||
from TRSensors import TRSensor
|
||||
|
||||
# Geschwindigkeits- und PID-Einstellungen
|
||||
MAX_SPEED = 40 # 50
|
||||
BASE_SPEED = 30 # 40
|
||||
SEARCH_SPEED = 30 # Geschwindigkeit während der Liniensuche
|
||||
|
||||
# PID-Konstanten - DIESE MÜSSEN EVENTUELL ANGEPASST WERDEN!
|
||||
Kp = 0.045 # 0.025
|
||||
Ki = 0.0001 # 0.0005
|
||||
Kd = 0.3
|
||||
|
||||
# Zielwert der Sensoren (Mitte der Linie)
|
||||
TARGET_POSITION = 2000
|
||||
|
||||
# Schwelle, ab der ein Sensor die Linie als "verloren" betrachtet
|
||||
LINE_LOST_THRESHOLD = 950
|
||||
# Zeitlimit für die Suche in Sekunden
|
||||
SEARCH_TIMEOUT = 3.0
|
||||
|
||||
IR = 17
|
||||
|
||||
def setup_gpio():
|
||||
"""Initialisiert die GPIO-Pins."""
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(IR, GPIO.IN)
|
||||
|
||||
|
||||
def calibrate_sensors(tr, robot):
|
||||
"""Führt eine automatische Kalibrierung der Sensoren durch."""
|
||||
print("Starte Sensor-Kalibrierung...")
|
||||
robot.setPWMA(20)
|
||||
robot.setPWMB(20)
|
||||
# Drehung zur Kalibrierung auf weißem und schwarzem Untergrund
|
||||
for _ in range(2):
|
||||
robot.right()
|
||||
for _ in range(50):
|
||||
tr.calibrate()
|
||||
time.sleep(0.02)
|
||||
robot.left()
|
||||
for _ in range(100):
|
||||
tr.calibrate()
|
||||
time.sleep(0.02)
|
||||
robot.right()
|
||||
for _ in range(50):
|
||||
tr.calibrate()
|
||||
time.sleep(0.02)
|
||||
robot.stop()
|
||||
print("Kalibrierung abgeschlossen.")
|
||||
print(" Min:", tr.calibratedMin)
|
||||
print(" Max:", tr.calibratedMax)
|
||||
|
||||
|
||||
def search_for_line(robot, tr, last_error):
|
||||
"""
|
||||
Sucht die Linie, indem der Roboter sich in die Richtung dreht,
|
||||
in der die Linie zuletzt war.
|
||||
"""
|
||||
print("Linie verloren! Suche...")
|
||||
robot.stop()
|
||||
time.sleep(0.1)
|
||||
|
||||
# Setze zuerst die Geschwindigkeit für beide Motoren
|
||||
robot.setPWMA(SEARCH_SPEED)
|
||||
robot.setPWMB(SEARCH_SPEED)
|
||||
|
||||
# Bestimme dann die Suchrichtung
|
||||
if last_error > 0:
|
||||
print("Suche nach links...")
|
||||
robot.left()
|
||||
else:
|
||||
print("Suche nach rechts...")
|
||||
robot.right()
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < SEARCH_TIMEOUT:
|
||||
# Lies kontinuierlich die Sensoren, während du dich drehst
|
||||
_, sensors = tr.readLine(white_line=False)
|
||||
|
||||
# Prüfe, ob einer der Sensoren die Linie wieder sieht
|
||||
if any(s < LINE_LOST_THRESHOLD for s in sensors):
|
||||
print("Linie wiedergefunden!")
|
||||
robot.stop()
|
||||
time.sleep(0.1)
|
||||
return True # Suche war erfolgreich
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
# Schleife beendet, ohne die Linie zu finden (Timeout)
|
||||
print(f"Suche nach {SEARCH_TIMEOUT}s erfolglos. Roboter stoppt.")
|
||||
robot.stop()
|
||||
return False # Suche war erfolglos
|
||||
|
||||
|
||||
def line_follow_loop(tr, robot):
|
||||
"""Hauptschleife für die Linienverfolgung mittels PID-Regelung."""
|
||||
# Setzt die anfängliche Richtung auf vorwärts
|
||||
robot.forward()
|
||||
|
||||
last_error = 0
|
||||
integral = 0
|
||||
|
||||
while True:
|
||||
# Lese die Position der Linie. False für schwarze Linie auf weißem Grund.
|
||||
position, sensors = tr.readLine(white_line=False)
|
||||
|
||||
# Wenn alle Sensoren weiß sehen, starte die Liniensuche
|
||||
if all(s > LINE_LOST_THRESHOLD for s in sensors):
|
||||
search_successful = search_for_line(robot, tr, last_error)
|
||||
if search_successful:
|
||||
# Setze PID-Werte zurück, um einen Sprung zu vermeiden
|
||||
last_error = 0
|
||||
integral = 0
|
||||
# WICHTIG: Setze die Richtung wieder auf vorwärts, bevor die Schleife weitergeht
|
||||
robot.forward()
|
||||
continue
|
||||
else:
|
||||
# Beende die Hauptschleife, wenn die Suche fehlschlägt
|
||||
break
|
||||
|
||||
# --- PID-Berechnung ---
|
||||
error = position - TARGET_POSITION
|
||||
integral += error
|
||||
derivative = error - last_error
|
||||
last_error = error
|
||||
|
||||
correction = (Kp * error) + (Ki * integral) + (Kd * derivative)
|
||||
|
||||
# Anpassung der Motor-Geschwindigkeiten
|
||||
left_speed = BASE_SPEED + correction
|
||||
right_speed = BASE_SPEED - correction
|
||||
|
||||
# Geschwindigkeiten auf den gültigen Bereich begrenzen
|
||||
left_speed = max(0, min(MAX_SPEED, left_speed))
|
||||
right_speed = max(0, min(MAX_SPEED, right_speed))
|
||||
|
||||
# print(f"Pos: {position:.0f} | Err: {error:.0f} | Corr: {correction:.2f} | L: {left_speed:.0f} | R: {right_speed:.0f}")
|
||||
|
||||
robot.setPWMA(int(left_speed))
|
||||
robot.setPWMB(int(right_speed))
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
|
||||
def follow(tr , robot):
|
||||
"""Hauptfunktion des Programms."""
|
||||
#robot = AlphaBot2()
|
||||
#tr = TRSensor()
|
||||
#robot.stop()
|
||||
|
||||
try:
|
||||
calibrate_sensors(tr, robot)
|
||||
print("Kalibrierung abgeschlossen")
|
||||
time.sleep(5.0)
|
||||
#input("Kalibrierung abgeschlossen. Drücke Enter zum Starten...")
|
||||
line_follow_loop(tr, robot)
|
||||
except KeyboardInterrupt:
|
||||
print("\nProgramm gestoppt.")
|
||||
finally:
|
||||
robot.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup_gpio()
|
||||
robot = AlphaBot2()
|
||||
tr = TRSensor()
|
||||
robot.stop()
|
||||
follow(tr, robot)
|
||||
print("Räume GPIO auf.")
|
||||
GPIO.cleanup()
|
||||
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# cotmw
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connection on the Move - Webinterface</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+4445
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "cotmw",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"serve": "npx -y vite build && npx -y vite preview --port 3000 --host 0.0.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"bootstrap": "^5.3.5",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"nodemon": "^3.1.10",
|
||||
"vite": "^6.3.4",
|
||||
"vite-plugin-vue-devtools": "^7.7.2"
|
||||
},
|
||||
"description": "This template should help get you started developing with Vue 3 in Vite.",
|
||||
"main": "vite.config.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
+1496
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@serialport/parser-readline": "^13.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"serialport": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// server/routes/serialUtils.js
|
||||
const router = require('express').Router();
|
||||
const { listSerialPorts, connectSerial, getLatestPacket } = require('../serial');
|
||||
|
||||
router.get('/serialports', async (req, res) => {
|
||||
try {
|
||||
const ports = await listSerialPorts();
|
||||
res.json(ports);
|
||||
} catch (err) {
|
||||
console.error('Error listing ports:', err);
|
||||
res.status(500).json({ error: 'Could not list serial ports' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/latest', (req, res) => {
|
||||
const payload = getLatestPacket();
|
||||
if (!payload) {
|
||||
return res.status(204).end(); // No Content
|
||||
}
|
||||
res.json(payload);
|
||||
});
|
||||
|
||||
router.post('/connect', (req, res) => {
|
||||
const { path } = req.body;
|
||||
if (!path) {
|
||||
return res.status(400).json({ error: 'Path is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
connectSerial(path);
|
||||
res.json({
|
||||
ok: true,
|
||||
connectedTo: path,
|
||||
baudRate: 115200
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to connect:', err);
|
||||
res.status(500).json({ error: 'Could not open serial port' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,39 @@
|
||||
// server/serial.js
|
||||
const { SerialPort } = require('serialport');
|
||||
const { ReadlineParser } = require('@serialport/parser-readline');
|
||||
|
||||
let portInstance = null;
|
||||
let parser = null;
|
||||
let latestPayload = null;
|
||||
let latestImage = null;
|
||||
|
||||
function connectSerial(path) {
|
||||
if (portInstance?.isOpen) portInstance.close();
|
||||
|
||||
portInstance = new SerialPort({ path: path, baudRate: 115200 });
|
||||
parser = portInstance.pipe(new ReadlineParser({ delimiter: '\n' }));
|
||||
parser.on('data', line => {
|
||||
if(line.includes("image")){
|
||||
if(latestImage != null && !latestImage.endsWith("}"))
|
||||
latestImage += line.trim().replace("\n", "");
|
||||
else
|
||||
latestImage = line.trim().replace("\n", "");
|
||||
console.log(latestImage)
|
||||
}
|
||||
else if (line.includes("text")){
|
||||
console.log(latestPayload)
|
||||
latestPayload = line.trim();
|
||||
}
|
||||
});
|
||||
portInstance.on('error', console.error);
|
||||
}
|
||||
|
||||
function listSerialPorts() {
|
||||
return SerialPort.list();
|
||||
}
|
||||
|
||||
function getLatestPacket() {
|
||||
return [latestPayload, latestImage];
|
||||
}
|
||||
|
||||
module.exports = { connectSerial, listSerialPorts, getLatestPacket };
|
||||
@@ -0,0 +1,11 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const serialRouter = require('./routes/serialUtils');
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.use('/api/serial-utils', serialRouter);
|
||||
app.listen(6666, () => console.log('running on port 6666'));
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup>
|
||||
import 'bootstrap/js/dist/modal.js'
|
||||
import HUD from './components/HUD.vue'
|
||||
import SerialConnector from "@/components/SerialConnector.vue";
|
||||
import { ref, computed } from 'vue';
|
||||
import Camera from "@/components/Camera.vue";
|
||||
import Location from "@/components/Location.vue";
|
||||
import Plot from './components/Plot.vue';
|
||||
import External from './components/External.vue';
|
||||
|
||||
const serialPacket = ref(null);
|
||||
let ip = ref("***.***.***.***");
|
||||
let base_station = ref("not connected");
|
||||
let rssi = ref("not available");
|
||||
let other_network_ssid = ref("not available")
|
||||
let other_network_rssi = ref("not available")
|
||||
let pollingCount = ref(0);
|
||||
let imageData = ref("");
|
||||
|
||||
const rowProps = computed(() =>
|
||||
[{
|
||||
key: "IP",
|
||||
value: ip,
|
||||
importance: "info"
|
||||
},{
|
||||
key: "Station",
|
||||
value: base_station,
|
||||
importance: "important"
|
||||
}, {
|
||||
key: "RSSI",
|
||||
value: rssi,
|
||||
importance: "important"
|
||||
}, {
|
||||
key: "Other Network",
|
||||
value: other_network_ssid,
|
||||
importance: "info2"
|
||||
}, {
|
||||
key: "Other RSSI",
|
||||
value: other_network_rssi,
|
||||
importance: "info2"
|
||||
}, {
|
||||
key: "Polling Counter",
|
||||
value: pollingCount,
|
||||
importance: "info2"
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
function parseTextPacket(packet){
|
||||
ip.value = packet["IP"];
|
||||
base_station.value = packet["Base Station"];
|
||||
rssi.value = packet["RSSI"];
|
||||
other_network_ssid.value = packet["Other Network"];
|
||||
other_network_rssi.value = packet["Other RSSI"];
|
||||
}
|
||||
|
||||
function parseImagePacket(packet){
|
||||
// console.log(packet["data"]);
|
||||
let val = packet["data"];
|
||||
if(!packet["data"].includes("data:image/jpeg;base64,"))
|
||||
val = "data:image/jpeg;base64," + val;
|
||||
if(!val.endsWith("\n")) val += "\n";
|
||||
console.log(val);
|
||||
imageData.value = val;
|
||||
}
|
||||
|
||||
function onSerialPacket(payload){
|
||||
if(payload[0] && payload[0]["type"] === "text"){
|
||||
parseTextPacket(payload[0]);
|
||||
}
|
||||
if(payload[1] && payload[1]["type"] === "image"){
|
||||
parseImagePacket(payload[1]);
|
||||
}
|
||||
pollingCount.value++;
|
||||
// console.log(payload);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<!--
|
||||
<img alt="Raspberry Pi Minimalistic Icon" class="logo" src="./assets/icon_pi.png" width="128" height="128"/>
|
||||
-->
|
||||
<meta charset="utf-8">
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div>
|
||||
<HUD :rowProps="rowProps" @CameraButtonClicked="enableCamera" @LocationButtonClicked="enableLocation"></HUD>
|
||||
<SerialConnector @data="onSerialPacket"></SerialConnector>
|
||||
<Camera :data="imageData"></Camera>
|
||||
<Location :baseStation="base_station" :otherNetwork="other_network_ssid" ></Location>
|
||||
<Plot :rssi="rssi" :otherRssi="other_network_rssi" />
|
||||
<External :rssi="rssi" :other_rssi="other_network_rssi" :image="imageData"/>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
.hud {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 240px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-family: monospace;
|
||||
color: #ddd;
|
||||
user-select: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.hud-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
color: #eeeeee;
|
||||
}
|
||||
|
||||
.hud-body {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.hud-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
font-family: monospace;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hud-inner-btn{
|
||||
width: 100px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.hud-toggle-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #ddd;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.spacer{
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #d6d6d6;
|
||||
}
|
||||
|
||||
.important {
|
||||
color: rgb(255, 52, 52);
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #48f;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #4f4;
|
||||
}
|
||||
|
||||
.info2 {
|
||||
color: #8cf;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps(['data']);
|
||||
|
||||
const cameraBox = ref(null);
|
||||
|
||||
let isDragging = false;
|
||||
let offset = { x: 0, y: 0 };
|
||||
|
||||
const startDrag = (event) => {
|
||||
isDragging = true;
|
||||
const box = cameraBox.value;
|
||||
const rect = box.getBoundingClientRect();
|
||||
offset.x = event.clientX - rect.left;
|
||||
offset.y = event.clientY - rect.top;
|
||||
document.addEventListener('mousemove', onDrag);
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
};
|
||||
|
||||
const onDrag = (event) => {
|
||||
if (!isDragging) return;
|
||||
const x = event.clientX - offset.x;
|
||||
const y = event.clientY - offset.y;
|
||||
const box = cameraBox.value;
|
||||
box.style.left = `${x}px`;
|
||||
box.style.top = `${y}px`;
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
isDragging = false;
|
||||
document.removeEventListener('mousemove', onDrag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const box = cameraBox.value;
|
||||
box.style.left = 'calc(100vw - 30vw)';
|
||||
box.style.top = 'calc(100vh - 30vw)';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="cameraBox" class="camera-box">
|
||||
<div class="camera-header" @mousedown="startDrag">Camera Feed</div>
|
||||
<img class="camera-image" :src="props.data" alt="Camera" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.camera-box {
|
||||
position: fixed;
|
||||
width: 25vw;
|
||||
height: 25vw;
|
||||
background-color: #000;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.camera-header {
|
||||
background-color: #222;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #444;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.camera-image {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
// Props empfangen
|
||||
const props = defineProps({
|
||||
rssi: String,
|
||||
other_rssi: String,
|
||||
image: String
|
||||
})
|
||||
|
||||
const status = ref('Nothing send yet...')
|
||||
const passwort = ref('')
|
||||
const ipAdresse = ref('138.68.114.176')
|
||||
//const ipAdresse = ref('localhost')
|
||||
const port = ref('3000')
|
||||
const isAuthenticated = ref(false)
|
||||
|
||||
const senden = async (payload) => {
|
||||
const url = `http://${ipAdresse.value}:${port.value}/api/send`
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': passwort.value
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error('Error wile sending')
|
||||
|
||||
status.value = 'Sending!'
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
status.value = 'Error'
|
||||
}
|
||||
}
|
||||
|
||||
// sende nur wenn sich was verändert
|
||||
watch(
|
||||
() => [props.rssi, props.other_rssi, props.image],
|
||||
([rssi, other_rssi, newImage]) => {
|
||||
if (!isAuthenticated.value) return
|
||||
if (!rssi && !other_rssi && !newImage) return
|
||||
|
||||
senden({
|
||||
rssi: rssi,
|
||||
other_rssi: other_rssi,
|
||||
image: newImage
|
||||
})
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
const aktivieren = () => {
|
||||
if (!passwort.value.trim() || !ipAdresse.value.trim() || !port.value.trim()) {
|
||||
status.value = 'Please fill in all fields'
|
||||
} else {
|
||||
isAuthenticated.value = true
|
||||
status.value = 'Activated'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="data-box">
|
||||
<p class="title">External Data Transmission</p>
|
||||
|
||||
<div v-if="!isAuthenticated">
|
||||
<input
|
||||
v-model="ipAdresse"
|
||||
type="text"
|
||||
placeholder="IP Address"
|
||||
class="input"
|
||||
/>
|
||||
<input
|
||||
v-model="port"
|
||||
type="text"
|
||||
placeholder="Port"
|
||||
class="input"
|
||||
/>
|
||||
<input
|
||||
v-model="passwort"
|
||||
type="password"
|
||||
placeholder="API Key"
|
||||
class="input"
|
||||
/>
|
||||
<button @click="aktivieren" class="button">Activate</button>
|
||||
</div>
|
||||
|
||||
<p class="status">{{ status }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.data-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 1rem;
|
||||
transform: translateY(-50%);
|
||||
width: 18vw;
|
||||
background: rgba(50, 50, 50, 0.85);
|
||||
z-index: 10;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: #fff;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
margin-bottom: 0.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem;
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import HUDRow from "@/components/HUDRow.vue";
|
||||
|
||||
const collapsed = ref(false);
|
||||
|
||||
const hud = ref(null);
|
||||
const pos = reactive({ x: 20, y: 20 });
|
||||
const dragging = ref(false);
|
||||
const dragOffset = { x: 0, y: 0 };
|
||||
|
||||
const logging = false;
|
||||
|
||||
function onMouseDown(e){
|
||||
log("Dragging started!");
|
||||
dragging.value = true;
|
||||
dragOffset.x = e.clientX - pos.x;
|
||||
dragOffset.y = e.clientY - pos.y;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onMouseMove(e){
|
||||
if(!dragging.value) return;
|
||||
|
||||
const x = e.clientX - dragOffset.x;
|
||||
const y = e.clientY - dragOffset.y;
|
||||
|
||||
const hudRect = hud.value.getBoundingClientRect();
|
||||
const hudW = hudRect.width;
|
||||
const hudH = hudRect.height;
|
||||
|
||||
const docW = document.documentElement.scrollWidth;
|
||||
const docH = document.documentElement.scrollHeight;
|
||||
|
||||
const minX = 20;
|
||||
const minY = 20;
|
||||
const maxX = docW - hudW - 20;
|
||||
const maxY = docH - hudH - 20;
|
||||
|
||||
pos.x = Math.min(Math.max(x, minX), maxX);
|
||||
pos.y = Math.min(Math.max(y, minY), maxY);
|
||||
|
||||
log(`Location ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
function onMouseUp(){
|
||||
log("Dragging done!");
|
||||
dragging.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
})
|
||||
|
||||
function toggleHUD() {
|
||||
collapsed.value = !collapsed.value;
|
||||
log("HUD toggled!");
|
||||
}
|
||||
|
||||
function log(message){
|
||||
if(logging)
|
||||
console.log(message);
|
||||
}
|
||||
// HUD TEMPLATE
|
||||
let HUD_name = "Auto HUD"
|
||||
|
||||
const props = defineProps(["rowProps"]);
|
||||
const rowProps = props.rowProps;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div ref="hud" class="hud" id="Control" :style="{ top: pos.y + 'px', left: pos.x + 'px', position: 'absolute', cursor: dragging ? 'grabbing' : 'grab' }" @mousedown="onMouseDown">
|
||||
|
||||
<div class="hud-header" @click="toggleHUD">
|
||||
<span>{{ HUD_name }}</span>
|
||||
<button class="hud-toggle-btn">{{ collapsed ? '▼' : '▲' }}</button>
|
||||
</div>
|
||||
|
||||
<transition name="expand">
|
||||
<div v-show="!collapsed" class="hud-body">
|
||||
<div class="spacer"></div>
|
||||
|
||||
<HUDRow v-for="rowProp in rowProps" :rowProps="rowProp"></HUDRow>
|
||||
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.expand-enter-from{
|
||||
opacity: 0;
|
||||
}
|
||||
.expand-enter-to{
|
||||
opacity: 1;
|
||||
}
|
||||
.expand-enter-active{
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
|
||||
const props = defineProps(['rowProps'])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hud-row">
|
||||
<div class="label">{{ props.rowProps.key }}:</div>
|
||||
<div :class="props.rowProps.importance">{{ props.rowProps.value }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import IconAntenna from './icons/IconAntenna.vue'
|
||||
import IconVan from './icons/IconVan.vue'
|
||||
import IconCar from './icons/IconCar.vue'
|
||||
|
||||
// Props: Die aktuellen Netzwerknamen (kommen aus App.vue)
|
||||
const props = defineProps({
|
||||
baseStation: String,
|
||||
otherNetwork: String
|
||||
})
|
||||
|
||||
const width = ref(window.innerWidth)
|
||||
const height = ref(window.innerHeight)
|
||||
|
||||
const pointA = ref({ x: 0, y: 0, tx: 0, ty: 0 })
|
||||
const pointB = ref({ x: 0, y: 0, tx: 0, ty: 0 })
|
||||
const pointC = ref({ x: 0, y: 0, tx: 0, ty: 0 })
|
||||
|
||||
const handover = ref(false)
|
||||
const previousBaseStation = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
function updateSize() {
|
||||
width.value = window.innerWidth
|
||||
height.value = window.innerHeight
|
||||
|
||||
// Mittelpunkt des Screens
|
||||
const centerX = width.value / 2
|
||||
const centerY = height.value / 2
|
||||
const radius = 150 // Abstand der Punkte zum Zentrum
|
||||
|
||||
// Drei gleichmäßig im Kreis verteilte Punkte (Dreieck)
|
||||
pointA.value = { x: centerX, y: centerY - radius } // oben
|
||||
pointB.value = { x: centerX - radius * Math.sin(Math.PI / 3), y: centerY + radius / 2 } // unten links
|
||||
pointC.value = { x: centerX + radius * Math.sin(Math.PI / 3), y: centerY + radius / 2 } // unten rechts
|
||||
}
|
||||
|
||||
updateSize()
|
||||
window.addEventListener('resize', updateSize)
|
||||
})
|
||||
|
||||
// wenn sich die Base Station ändert => handover
|
||||
watch(() => props.baseStation, (newVal) => {
|
||||
if (previousBaseStation.value && newVal !== previousBaseStation.value) {
|
||||
handover.value = true
|
||||
} else {
|
||||
handover.value = false
|
||||
}
|
||||
previousBaseStation.value = newVal
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="checkmate-pattern">
|
||||
<svg class="overlay" :width="width" :height="height">
|
||||
<line :x1="pointA.x" :y1="pointA.y" :x2="pointB.x" :y2="pointB.y" stroke="red" stroke-width="3" class="flow-line"/>
|
||||
<line v-if="handover" :x1="pointB.x" :y1="pointB.y" :x2="pointC.x" :y2="pointC.y" stroke="red" stroke-width="3" class="flow-line"/>
|
||||
<line v-if="!handover" :x1="pointA.x" :y1="pointA.y" :x2="pointC.x" :y2="pointC.y" stroke="black" stroke-width="3" class="flow-line"/>
|
||||
|
||||
<circle :cx="pointA.x" :cy="pointA.y" r="25" fill="black" />
|
||||
<foreignObject
|
||||
:x="pointA.x - 15"
|
||||
:y="pointA.y - 15"
|
||||
width="30"
|
||||
height="30"
|
||||
>
|
||||
<IconAntenna color="white" style="width: 30px; height: 30px;" />
|
||||
</foreignObject>
|
||||
|
||||
<circle :cx="pointB.x" :cy="pointB.y" r="25" fill="red" />
|
||||
<foreignObject
|
||||
:x="pointB.x - 15"
|
||||
:y="pointB.y - 15"
|
||||
width="30"
|
||||
height="30"
|
||||
>
|
||||
<IconVan color="white" style="width: 30px; height: 30px;" />
|
||||
</foreignObject>
|
||||
|
||||
<circle :cx="pointC.x" :cy="pointC.y" r="25" fill="blue" />
|
||||
<foreignObject
|
||||
:x="pointC.x - 15"
|
||||
:y="pointC.y - 15"
|
||||
width="30"
|
||||
height="30"
|
||||
>
|
||||
<IconCar color="white" style="width: 30px; height: 30px;" />
|
||||
</foreignObject>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@keyframes flowing {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: -20;
|
||||
}
|
||||
}
|
||||
|
||||
.flow-line {
|
||||
stroke-dasharray: 10;
|
||||
stroke-dashoffset: 0;
|
||||
animation: flowing 1s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
.checkmate-pattern {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
background-image:
|
||||
repeating-linear-gradient(to bottom, #ccc 0px, #ccc 1px, transparent 1px, transparent 20px),
|
||||
repeating-linear-gradient(to right, #ccc 0px, #ccc 1px, transparent 1px, transparent 20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div
|
||||
class="canvas-container"
|
||||
:style="{ top: `${position.y}px`, left: `${position.x}px` }"
|
||||
@mousedown="startDrag"
|
||||
>
|
||||
<h2 class="chart-title">RSSI</h2>
|
||||
<canvas ref="canvas" :width="width" :height="height"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RSS Plot',
|
||||
props: {
|
||||
rssi: {
|
||||
type: [String, Number],
|
||||
default: "not available"
|
||||
},
|
||||
otherRssi: {
|
||||
type: [String, Number],
|
||||
default: "not available"
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: 600,
|
||||
height: 120,
|
||||
maxPoints: 50,
|
||||
currentNetworkData: [],
|
||||
otherNetworkData: [],
|
||||
ctx: null,
|
||||
interval: null,
|
||||
padding: {
|
||||
top: 10,
|
||||
right: 10,
|
||||
bottom: 20,
|
||||
left: 35
|
||||
},
|
||||
isDragging: false,
|
||||
dragOffset: { x: 0, y: 0 },
|
||||
position: { x: 50, y: window.innerHeight - 150 }
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const canvas = this.$refs.canvas;
|
||||
this.ctx = canvas.getContext('2d');
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
this.addRealData();
|
||||
this.draw();
|
||||
}, 1000);
|
||||
|
||||
window.addEventListener('mousemove', this.onDrag);
|
||||
window.addEventListener('mouseup', this.stopDrag);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.interval);
|
||||
window.removeEventListener('mousemove', this.onDrag);
|
||||
window.removeEventListener('mouseup', this.stopDrag);
|
||||
},
|
||||
methods: {
|
||||
startDrag(e) {
|
||||
if (e.target.classList.contains('chart-title')) {
|
||||
this.isDragging = true;
|
||||
this.dragOffset = {
|
||||
x: e.clientX - this.position.x,
|
||||
y: e.clientY - this.position.y
|
||||
};
|
||||
}
|
||||
},
|
||||
onDrag(e) {
|
||||
if (this.isDragging) {
|
||||
this.position.x = e.clientX - this.dragOffset.x;
|
||||
this.position.y = e.clientY - this.dragOffset.y;
|
||||
}
|
||||
},
|
||||
stopDrag() {
|
||||
this.isDragging = false;
|
||||
},
|
||||
addRealData() {
|
||||
if (this.currentNetworkData.length >= this.maxPoints) this.currentNetworkData.shift();
|
||||
if (this.otherNetworkData.length >= this.maxPoints) this.otherNetworkData.shift();
|
||||
|
||||
let currentRssi = parseFloat(this.rssi);
|
||||
if (isNaN(currentRssi) || this.rssi === "not available") currentRssi = -100;
|
||||
this.currentNetworkData.push(currentRssi);
|
||||
|
||||
let otherRssi = parseFloat(this.otherRssi);
|
||||
if (isNaN(otherRssi) || this.otherRssi === "not available") otherRssi = -100;
|
||||
this.otherNetworkData.push(otherRssi);
|
||||
},
|
||||
draw() {
|
||||
const { ctx, width, height, maxPoints, padding } = this;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const drawArea = {
|
||||
x: padding.left,
|
||||
y: padding.top,
|
||||
w: width - padding.left - padding.right,
|
||||
h: height - padding.top - padding.bottom
|
||||
};
|
||||
|
||||
const drawYAxis = () => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(drawArea.x, drawArea.y);
|
||||
ctx.lineTo(drawArea.x, drawArea.y + drawArea.h);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const drawXAxis = () => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(drawArea.x, drawArea.y + drawArea.h);
|
||||
ctx.lineTo(drawArea.x + drawArea.w, drawArea.y + drawArea.h);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.lineWidth = 1;
|
||||
drawYAxis();
|
||||
drawXAxis();
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.textAlign = 'right';
|
||||
|
||||
const maxY = -30;
|
||||
const minY = -90;
|
||||
const steps = 6;
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const value = maxY - ((maxY - minY) / steps) * i;
|
||||
const y = drawArea.y + ((i / steps) * drawArea.h);
|
||||
ctx.fillText(value.toFixed(0), drawArea.x - 5, y + 3);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(drawArea.x, y);
|
||||
ctx.lineTo(drawArea.x + 4, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
const drawLine = (data, color, label) => {
|
||||
if (data.length === 0) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
data.forEach((yVal, i) => {
|
||||
const x = drawArea.x + (drawArea.w / maxPoints) * i;
|
||||
const normalized = Math.max(0, Math.min(1, (yVal - minY) / (maxY - minY)));
|
||||
const y = drawArea.y + (1 - normalized) * drawArea.h;
|
||||
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
if (data.length > 0) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = '12px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
const legendY = drawArea.y + 15 + (label === 'Current' ? 0 : 15);
|
||||
ctx.fillText(`${label}: ${data[data.length - 1].toFixed(0)} dBm`, drawArea.x + drawArea.w - 120, legendY);
|
||||
}
|
||||
};
|
||||
|
||||
drawLine(this.currentNetworkData, '#4CAF50', 'Current');
|
||||
drawLine(this.otherNetworkData, '#f44336', 'Other');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.canvas-container {
|
||||
position: absolute;
|
||||
width: 50vw;
|
||||
height: 15vh;
|
||||
background: rgba(50, 50, 50, 0.8);
|
||||
z-index: 10;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 0.25rem;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import {
|
||||
getSerialPorts,
|
||||
connectSerial,
|
||||
getLatestPacket
|
||||
} from '@/services/serialService';
|
||||
|
||||
const emit = defineEmits(['data']);
|
||||
let enableSerialConnectorMenu = ref(false);
|
||||
|
||||
const ports = ref([]);
|
||||
const selectedPort = ref('');
|
||||
const connecting = ref(false);
|
||||
const connected = ref(false);
|
||||
const error = ref('');
|
||||
const content = ref('');
|
||||
const connectedPort = ref('');
|
||||
let intervalId;
|
||||
|
||||
// Keyboard event handler for menu toggle
|
||||
async function handleKeyDown(event) {
|
||||
// Log SHA256 of the event.key
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(event.key);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
console.log(`SHA256 of key "${event.key}": ${hashHex}`);
|
||||
|
||||
const SECRET_ACTIVATION_KEY_HASH_HIDDEN = "3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d";
|
||||
|
||||
// Check for Ctrl+ (Windows/Linux) or Cmd+ (Mac)
|
||||
if ((event.ctrlKey || event.metaKey) && hashHex === SECRET_ACTIVATION_KEY_HASH_HIDDEN) {
|
||||
event.preventDefault(); // Prevent default browser behavior
|
||||
switchBtnState();
|
||||
console.log("Menu toggled");
|
||||
}
|
||||
else {
|
||||
console.log("Key not recognized -- guess again ;)");
|
||||
}
|
||||
}
|
||||
|
||||
// load ports
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await getSerialPorts();
|
||||
ports.value = res.data;
|
||||
} catch {
|
||||
error.value = 'Could not load ports!';
|
||||
}
|
||||
|
||||
// Add keyboard event listener
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(intervalId);
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
async function onConnect() {
|
||||
connecting.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await connectSerial(selectedPort.value);
|
||||
connected.value = true;
|
||||
connectedPort.value = selectedPort.value;
|
||||
intervalId = setInterval(pollLatest, 200);
|
||||
} catch {
|
||||
error.value = 'Connection failed!';
|
||||
} finally {
|
||||
connecting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollLatest() {
|
||||
try {
|
||||
const res = await getLatestPacket();
|
||||
if (res.status === 200 && res.data) {
|
||||
// const line = typeof res.data === 'object'
|
||||
// ? JSON.stringify(res.data)
|
||||
// : res.data;
|
||||
content.value = res.data[0] + '\n' + res.data[1] + '\n';
|
||||
emit('data', [JSON.parse(res.data[0]), JSON.parse(res.data[1])]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function switchBtnState(){
|
||||
enableSerialConnectorMenu.value = !enableSerialConnectorMenu.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="serial-connector">
|
||||
<transition name="slide">
|
||||
<nav class="navbar-side-panel" v-if="enableSerialConnectorMenu">
|
||||
<div class="container-fluid">
|
||||
<label for="port">Choose Serial Port:</label>
|
||||
<select id="port" v-model="selectedPort">
|
||||
<option disabled value="">–– select device ––</option>
|
||||
<option v-for="port in ports" :key="port.path" :value="port.path">
|
||||
{{ port.path }}
|
||||
<small v-if="port.manufacturer">({{ port.manufacturer }})</small>
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
@click="onConnect"
|
||||
:disabled="!selectedPort || connecting || (connected && selectedPort === connectedPort)"
|
||||
>
|
||||
{{ connecting ? 'Connecting...' : 'Connect (Baud: 115200)' }}
|
||||
</button>
|
||||
|
||||
<p v-if="connected">
|
||||
Connected to <strong>{{ selectedPort }}</strong> @ 115200 bps
|
||||
</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<h4>Incoming Data:</h4>
|
||||
<pre class="log">{{ content }}</pre>
|
||||
</div>
|
||||
</nav>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.serial-connector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.navbar-side-panel {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 250px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-family: monospace;
|
||||
color: #ddd;
|
||||
user-select: none;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
box-shadow: -4px 0 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.navbar-side-panel .container-fluid {
|
||||
margin: 5px;
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.keyboard-hint {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.keyboard-hint small {
|
||||
color: #aaa;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.navbar-side-panel label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #d6d6d6;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.navbar-side-panel select, .navbar-side-panel button {
|
||||
font-family: monospace;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #ddd;
|
||||
padding: 4px 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.navbar-side-panel button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.navbar-side-panel p {
|
||||
margin: 4px 0;
|
||||
font-size: 14px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.navbar-side-panel .error {
|
||||
color: rgb(255, 52, 52);
|
||||
}
|
||||
|
||||
.navbar-side-panel p strong {
|
||||
color: #4f4;
|
||||
}
|
||||
|
||||
.navbar-side-panel h4 {
|
||||
font-size: 14px;
|
||||
color: #d6d6d6;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.navbar-side-panel .log {
|
||||
background: #111;
|
||||
padding: 8px;
|
||||
height: 120px;
|
||||
width: 95%;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.slide-enter-from, .slide-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.slide-enter-to, .slide-leave-from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.slide-enter-active, .slide-leave-active {
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
:fill="color"
|
||||
height="800px"
|
||||
width="800px"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
viewBox="0 0 508.075 508.075"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M446.05,4.225c-5.5-5.5-14.4-5.6-20-0.1c-5.5,5.5-5.6,14.4-0.1,20c71.8,72.5,71.8,190.4,0,262.8c-5.5,5.5-5.4,14.4,0.1,20 c7.6,7.6,17.2,2.7,20-0.1C528.75,223.325,528.75,87.625,446.05,4.225z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M392.95,43.725c-5.5-5.5-14.4-5.6-20-0.1c-5.5,5.5-5.6,14.4-0.1,20c50.2,50.7,50.2,133.2,0,183.9 c-5.5,5.5-5.4,14.4,0.1,20c8.2,8.2,17.2,2.7,20-0.1C454.05,205.625,454.05,105.325,392.95,43.725z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M339.95,83.225c-5.5-5.5-14.4-5.6-20-0.1c-5.5,5.5-5.6,14.4-0.1,20c28.6,28.8,28.6,75.9,0,104.8c-5.5,5.5-5.4,14.4,0.1,20 c6.4,6.4,17.2,2.7,20-0.1C379.45,187.925,379.45,123.025,339.95,83.225z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M82.05,24.125c5.5-5.5,5.4-14.5-0.1-20s-14.5-5.4-20,0.1c-82.6,83.4-82.6,219.1,0,302.6c2.8,2.8,12.4,7.6,20,0.1 c5.5-5.5,5.6-14.4,0.1-20C10.25,214.425,10.25,96.525,82.05,24.125z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M135.15,63.525c5.5-5.5,5.4-14.5-0.1-20s-14.5-5.4-20,0.1c-61.1,61.6-61.1,162,0,223.6c2.8,2.9,11.8,8.4,20,0.2 c5.5-5.5,5.6-14.4,0.1-20C84.95,196.725,84.95,114.225,135.15,63.525z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M188.35,103.025c5.5-5.5,5.4-14.5-0.1-20s-14.5-5.4-20,0.1c-39.5,39.9-39.5,104.8,0,144.7c2.8,2.8,13.5,6.5,20,0.1 c5.5-5.5,5.6-14.4,0.1-20C159.65,179.025,159.65,131.925,188.35,103.025z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M375.35,479.825h-29.5l-74.4-281.3c16.9-6.9,28.8-23.6,28.8-43.1c0-25.6-20.7-46.5-46.2-46.5s-46.2,20.9-46.2,46.5 c0,19.4,11.9,36.1,28.8,43.1l-74.4,281.3h-29.5c-7.8,0-14.1,6.3-14.1,14.1c0,7.8,6.3,14.1,14.1,14.1h242.7 c7.8,0,14.1-6.3,14.1-14.1C389.55,486.125,383.15,479.825,375.35,479.825z M236.05,155.525c0-10.1,8.1-18.3,18-18.3 c9.9,0,18,8.2,18,18.3c0,10.1-8.1,18.3-18,18.3C244.15,173.825,236.05,165.625,236.05,155.525z M191.35,479.825l62.7-236.8 l62.7,236.8H191.35z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'IconAntenna',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" :fill="color" height="800px" width="800px" version="1.1" id="Layer_1" viewBox="0 0 511.999 511.999" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M358.57,0H153.43c-27.195,0-49.319,22.124-49.319,49.319v413.36c0,27.195,22.124,49.319,49.319,49.319H358.57 c27.195,0,49.319-22.124,49.319-49.319V49.319C407.89,22.124,385.764,0,358.57,0z M124.508,49.319 c0-15.948,12.974-28.921,28.921-28.921h205.141c15.948,0,28.921,12.974,28.921,28.921v355.589H124.508V49.319z M387.492,462.681 L387.492,462.681c-0.001,15.947-12.975,28.921-28.922,28.921H153.43c-15.948,0-28.921-12.974-28.921-28.921v-37.374h262.983 V462.681z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M270.789,34.677h-29.578c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h29.578 c5.633,0,10.199-4.566,10.199-10.199C280.988,39.243,276.422,34.677,270.789,34.677z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M242.204,371.251h-91.255c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h91.255 c5.633,0,10.199-4.566,10.199-10.199C252.403,375.817,247.837,371.251,242.204,371.251z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M282.518,371.251h-5.346c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h5.346 c5.633,0,10.199-4.566,10.199-10.199C292.717,375.817,288.151,371.251,282.518,371.251z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,430.406c-14.903,0-27.028,12.125-27.028,27.028s12.125,27.028,27.028,27.028s27.028-12.125,27.028-27.028 S270.903,430.406,256,430.406z M256,464.064c-3.655,0-6.629-2.974-6.629-6.63s2.974-6.629,6.629-6.629s6.63,2.974,6.63,6.629 S259.655,464.064,256,464.064z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M442.611,198.005c-3.983-3.983-10.441-3.983-14.425,0c-3.983,3.983-3.983,10.441,0,14.425 c23.462,23.462,23.462,61.639,0,85.1c-3.983,3.983-3.983,10.441,0,14.425c1.992,1.992,4.602,2.987,7.212,2.987 s5.221-0.995,7.212-2.987C474.026,280.539,474.026,229.421,442.611,198.005z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M470.016,170.6c-3.983-3.983-10.441-3.983-14.425,0c-3.983,3.983-3.983,10.441,0,14.425 c38.574,38.573,38.574,101.337,0,139.912c-3.983,3.983-3.983,10.441,0,14.425c1.992,1.992,4.602,2.987,7.212,2.987 s5.221-0.995,7.212-2.987c22.538-22.539,34.952-52.506,34.952-84.38S492.555,193.139,470.016,170.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M83.812,212.43c3.984-3.984,3.984-10.442,0.001-14.425s-10.441-3.983-14.425,0c-31.416,31.416-31.416,82.533,0,113.95 c1.992,1.992,4.602,2.987,7.212,2.987s5.221-0.995,7.212-2.987c3.983-3.983,3.983-10.441,0-14.425 C60.35,274.068,60.35,235.891,83.812,212.43z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M56.408,185.025c3.983-3.983,3.983-10.441,0-14.425c-3.983-3.983-10.441-3.983-14.425,0 c-22.538,22.539-34.951,52.505-34.951,84.38s12.412,61.841,34.952,84.38c1.992,1.992,4.602,2.987,7.212,2.987 s5.221-0.995,7.212-2.987c3.983-3.983,3.983-10.441,0-14.425C17.833,286.362,17.833,223.598,56.408,185.025z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M351.794,144.829H160.206c-5.633,0-10.199,4.566-10.199,10.199v111.276c0,5.633,4.566,10.199,10.199,10.199h150.951 l33.095,36.341c1.971,2.165,4.726,3.332,7.543,3.332c1.235,0,2.482-0.224,3.68-0.688c3.929-1.521,6.517-5.3,6.517-9.512V155.028 C361.993,149.395,357.427,144.829,351.794,144.829z M315.663,256.104H170.405v-90.877h171.19v114.402l-18.39-20.193 C321.272,257.314,318.534,256.104,315.663,256.104z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M310.566,183.586H201.434c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h109.131 c5.633,0,10.199-4.566,10.199-10.199C320.765,188.152,316.199,183.586,310.566,183.586z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M310.566,217.243H201.434c-5.633,0-10.199,4.566-10.199,10.199c0,5.633,4.566,10.199,10.199,10.199h109.131 c5.633,0,10.199-4.566,10.199-10.199C320.765,221.809,316.199,217.243,310.566,217.243z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'IconCar',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" :fill="color" width="800px" height="800px" viewBox="0 0 64 64" id="Layer_1_1_" version="1.1" xml:space="preserve">
|
||||
|
||||
<g>
|
||||
|
||||
<path d="M52,53c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S53.654,53,52,53z M52,57c-0.552,0-1-0.448-1-1s0.448-1,1-1 s1,0.448,1,1S52.552,57,52,57z"/>
|
||||
|
||||
<path d="M14,53c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S15.654,53,14,53z M14,57c-0.552,0-1-0.448-1-1s0.448-1,1-1 s1,0.448,1,1S14.552,57,14,57z"/>
|
||||
|
||||
<path d="M61,51V30c0-2.757-2.243-5-5-5H42.899c-0.465-2.279-2.484-4-4.899-4v-1.586l1.226-1.226C40.494,19.354,42.124,20,43.857,20 c1.832,0,3.554-0.713,4.85-2.009l0.707-0.707l-4.142-4.142l2.443-2.443C48.106,10.886,48.538,11,49,11c1.654,0,3-1.346,3-3 s-1.346-3-3-3s-3,1.346-3,3c0,0.462,0.114,0.894,0.301,1.285l-2.443,2.443l-4.142-4.142l-0.707,0.707 C37.713,9.588,37,11.31,37,13.142c0,1.248,0.338,2.44,0.958,3.486L36,18.586V21c-2.414,0-4.434,1.721-4.899,4H19.195 c-1.718,0-3.295,0.866-4.218,2.315l-5.631,8.848L6.419,37.14C4.374,37.821,3,39.728,3,41.883V51c-1.103,0-2,0.897-2,2v4 c0,1.103,0.897,2,2,2h4.685c1.126,2.361,3.53,4,6.315,4s5.188-1.639,6.315-4h25.37c1.126,2.361,3.53,4,6.315,4s5.188-1.639,6.315-4 H61c1.103,0,2-0.897,2-2v-4C63,51.897,62.103,51,61,51z M49,7c0.552,0,1,0.449,1,1s-0.448,1-1,1s-1-0.449-1-1S48.448,7,49,7z M39.788,10.486l6.726,6.726C45.73,17.725,44.815,18,43.857,18c-1.298,0-2.518-0.505-3.435-1.423C39.505,15.66,39,14.439,39,13.142 C39,12.185,39.275,11.269,39.788,10.486z M36,23h2c1.302,0,2.402,0.839,2.816,2h-7.631C33.598,23.839,34.698,23,36,23z M5.923,39.724C6.59,40.288,7,41.107,7,42c0,1.302-0.839,2.402-2,2.816v-2.933C5,41.047,5.348,40.277,5.923,39.724z M5,46.899 C7.279,46.434,9,44.414,9,42c0-1.2-0.435-2.331-1.184-3.219l2.837-0.945l6.012-9.447c0.554-0.87,1.5-1.39,2.53-1.39H56 c1.654,0,3,1.346,3,3v21h-2V31c0-1.103-0.897-2-2-2H35v22h-2V29H19.555c-0.694,0-1.329,0.352-1.696,0.94L13,37.713V49.08 c-1.502,0.216-2.852,0.906-3.889,1.92H5V46.899z M47.111,51H37V31h18v18.685C54.089,49.251,53.074,49,52,49 C50.098,49,48.374,49.765,47.111,51z M18.889,51c-1.037-1.014-2.387-1.704-3.889-1.92V39h5h2h3v1c0,1.654,1.346,3,3,3h3v8H18.889z M27,39h4v2h-3c-0.552,0-1-0.448-1-1V39z M31,37h-9v-1c0-1.654-1.346-3-3-3h-0.695l1.25-2H31V37z M17.055,35H19 c0.552,0,1,0.448,1,1v1h-4.195L17.055,35z M3,57v-4h4.685C7.251,53.911,7,54.926,7,56c0,0.34,0.033,0.672,0.08,1H3z M14,61 c-2.757,0-5-2.243-5-5s2.243-5,5-5s5,2.243,5,5S16.757,61,14,61z M20.92,57c0.047-0.328,0.08-0.66,0.08-1 c0-1.074-0.251-2.089-0.685-3h25.37C45.251,53.911,45,54.926,45,56c0,0.34,0.033,0.672,0.08,1H20.92z M52,61c-2.757,0-5-2.243-5-5 s2.243-5,5-5s5,2.243,5,5S54.757,61,52,61z M61,57h-2.08c0.047-0.328,0.08-0.66,0.08-1c0-1.074-0.251-2.089-0.685-3H61V57z"/>
|
||||
|
||||
<path d="M49,3c1.335,0,2.591,0.52,3.535,1.464S54,6.665,54,8s-0.521,2.591-1.465,3.536l1.414,1.414C55.271,11.627,56,9.87,56,8 s-0.729-3.627-2.051-4.95S50.87,1,49,1s-3.627,0.728-4.949,2.05l1.414,1.414C46.409,3.52,47.665,3,49,3z"/>
|
||||
|
||||
<path d="M46,33c-3.859,0-7,3.141-7,7s3.141,7,7,7s7-3.141,7-7S49.859,33,46,33z M46,45c-2.757,0-5-2.243-5-5s2.243-5,5-5 s5,2.243,5,5S48.757,45,46,45z"/>
|
||||
|
||||
<rect height="2" width="2" x="27" y="45"/>
|
||||
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'IconVan',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import './assets/hud.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,7 @@
|
||||
// src/services/api.js
|
||||
import axios from 'axios'
|
||||
|
||||
export default axios.create({
|
||||
baseURL: '/api',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import api from './api';
|
||||
|
||||
export function getSerialPorts() {
|
||||
return api.get('/serial-utils/serialports');
|
||||
}
|
||||
|
||||
export function connectSerial(path) {
|
||||
return api.post('/serial-utils/connect', { path });
|
||||
}
|
||||
|
||||
export function getLatestPacket(){
|
||||
return api.get('/serial-utils/latest');
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:6666', // express server for serial
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user