Entwickler-Wiki
Der technische Deep-Dive der Aiways-Community-Lösung — ein selbstgehosteter Telematik-Stack, der den Aiways U5 nach dem Cloud-Aus des Herstellers wieder vernetzt. Alles Open Source auf Forgejo.
Überblick
Der Stack ersetzt die abgeschaltete Hersteller-Cloud. Kern ist eine LTE-Bridge im Fahrzeug, die über einen Daemon auf der TBox die Fahrzeugdaten ausliest und per Mobilfunk an ein selbstgehostetes Backend schickt. Eine Flutter-App zeigt die Daten und sendet Fernbefehle; Home Assistant/EVCC hängen als Consumer dran.
- Zwei Wege ans Auto: BLE (lokal, App direkt) und LTE (remote, über die Bridge) — beide sprechen dasselbe fahrzeugeigene AICHI-Protokoll.
- Zwei Wege für Daten: Live-Signalfläche (
dbcvpage, ~1000 Signale) und GB/T-32960-Ringpuffer (cache_data) plus History-Dumps (.inx). - Selbstbestimmt: keine Hersteller-Cloud, keine Ad-/Tracking-SDKs.
Architektur
┌───────────────────── Fahrzeug (Aiways U5) ─────────────────────┐
│ CAN / Fahrzeugbus │
│ │ │
│ ┌────▼─────────────────────┐ WLAN-AP 192.168.1.1:9000 │
│ │ TBox (MDM9607 + AG35) │◄───────────────┐ │
│ │ tbox_app + tbox_daemon │ /dbcvpage /cachedata /mqtt … │
│ └───┬──────────────────────┘ │ │
│ │ Port 50000 (0x301-Shell) / :7090 MQTT │ │
│ │ ┌─────┴───────┐ │
│ │ BLE (AICHI, A001/B001) │ LTE-Bridge │ │
│ └◄────────────────────────────────┤ ESP32-S3 │ │
│ │ + A7670E │ │
└─────────────────────────────────────────┴─────┬───────┘ │
│ LTE (AT-HTTPS, X-Api-Key)
┌──────────────┐ REST / JWT ┌──────▼───────┐
│ AiwaysApp │◄────────────────────► │ Backend │
│ (Flutter) │ (+ BLE direkt) │ Fastify + PG │
└──────────────┘ └──────┬───────┘
REST/API-Key │
┌─────────────────┼───────────────┐
┌────▼─────┐ ┌──────▼────┐
│ Home │ │ EVCC │
│ Assistant│ │ │
└──────────┘ └───────────┘
Die App kann Befehle direkt per BLE senden (in Reichweite) oder in die Backend-Command-Queue legen; die Bridge pollt sie und fährt sie per BLE ans Auto. Alles auf der Bridge läuft sequenziell — ein geteiltes LTE-Modem, ein BLE-Stack.
Datenfluss
Telemetrie (Auto → Cloud)
- Der tbox_daemon liest die Live-Signale über die 0x301-Shell (Port 50000) und den GB/T-32960-Ringpuffer aus dem Dateisystem und hält sie im RAM.
- Die Bridge verbindet sich mit dem TBox-WLAN, holt
/dbcvpage(nur wenn das Auto wach ist),/mqtt(Backup inkl. Position) und 1×/Tag/cachedata, und POSTet sie über LTE an die Ingest-Routen (X-Api-Key). - Das Backend parst/bereinigt (Sanitize, Sleep-Schutz) und schreibt in Postgres; App und HA lesen per REST.
Befehle (Cloud → Auto)
- App oder Backend legen ein Command in die Queue (
POST /commands, Typ als freier String + JSON-Payload). - Die Bridge pollt
GET /commands/pull, mappt den Typ auf den AICHI-BLE-Code, authentifiziert sich per A001 und sendet den B001-Frame. - Ergebnis zurück per
POST /commands/:id/ack(status+resultmit resCode/Detail).
Glossar
| Begriff | Bedeutung |
|---|---|
| TBox | Telematics Box im Fahrzeug (Qualcomm MDM9607 + Quectel AG35, armv7l, glibc 2.22). Original-Datenquelle. |
| LTE-Bridge | Externer ESP32-S3-A7670E-Controller, liest die TBox per WLAN & telefoniert per LTE nach Hause. |
| AICHI | BLE-Protokoll des Fahrzeugs (A001 = RSA-Auth, B001 = Kommando, B002 = Async-Ergebnis). |
| dbcvpage | Live-Signalfläche der TBox (~1000 benannte Signale), seitenweise über die 0x301-Shell. |
| cache_data | GB/T-32960-Ringpuffer auf der TBox-SD (Records à 1048 B). |
| .inx | INTEST-History-Dump (zlib-Blöcke, 96 Zellspannungen, GPS-Spur). |
| btkeyId | Einzige per-Fahrzeug-Kennung der BLE-Auth (Community-Default AIWAYSCOMMUNITY). |
Backend TypeScript
Fastify + PostgreSQL (ESM/TypeScript). Nimmt Telemetrie an, verwaltet Nutzer/Fahrzeuge/Sharing, die Command-Queue, Firmware-Hosting & Web-Flasher, und liefert App-API, Admin-Dashboard, Landing-Page und dieses Wiki. https://forgejo.timo-erdbruegger.de/aiways/Backend ↗
- Ingest:
/ingest/dbcvpage,/ingest/cache-data,/ingest/cache-info,/ingest/inx,/ingest/telemetry,/ingest/mqtt,/ingest/bridge. Auth viamachineOk(Device-Token oder API-Key). - Fahrzeuge & Halter: Historie über
vehicle_ownersmit halboffenen[from_ts, to_ts)-Intervallen — mehrere offene Halter = geteiltes Fahrzeug. - Command-Queue: Lifecycle
pending → sent → acked | failed | expired, Pull markiert atomar (FOR UPDATE SKIP LOCKED), Ablauf nachCOMMAND_MAX_AGE_MIN=5. - Auth: JWT-Sessions + API-Keys (
oaw_…), Rollen admin/user/device. - Frontends:
/,/admindashboard,/anleitung,/datenschutz,/konto-loeschen,/wiki,/sandbox(nur außerhalb Production).
Betrieb: docker-compose (Postgres 16 + App), Start npm run migrate && npm start, davor ein Caddy-Reverse-Proxy (TLS + Cache).
AiwaysApp Flutter / Dart
Community-App für iOS und Android: Fahrzeugübersicht, Lade-/Fahrhistorie, Klima & Fernbefehle, geführte Provisionierung. https://forgejo.timo-erdbruegger.de/aiways/AiwaysApp ↗
Command-Routing (lib/command_queue.dart)
- Typen mit Präfix
set_(z.B.set_charge_limit) gehen direkt ans Backend (Konfig/Telemetrie, kein Aktor). - Aktor-Befehle: BLE zuerst — bei Erfolg fertig; sonst LTE-Fallback über die Backend-Queue.
- Scheitert beides, wird lokal persistiert (
commandQueuein SharedPreferences) und bei Neustart erneut versucht. Bewusst nie beides (keine Doppelausführung).
Backend→BLE-Typ-Mapping (bleTypeFor) ist identisch zur Firmware (backendToBleType). Datenlesen über /vehicles/:id/latest|timeseries|snapshots|details|diagnostics|bridge.
Bundle-IDs: Android de.itperspective.aiways_app, iOS de.itperspective.aiwaysApp. Release-Prozess: siehe PLAY_STORE_RELEASE.md / IOS_RELEASE.md im App-Repo.
LTE-Bridge (Firmware) C++ / PlatformIO
Firmware auf Waveshare ESP32-S3-A7670E-4G. Liest die TBox per WLAN, postet über LTE, steuert das Auto per BLE — akku-adaptiv. https://forgejo.timo-erdbruegger.de/aiways/LTEBridgeFirmware ↗
Hardware & Pins
ESP32-S3R8, 16 MB Flash; PSRAM deaktiviert und memory_type = qio_qspi (nicht OPI!) — sonst kollidiert OPI-PSRAM mit GPIO33 (Modem-Power) → Boot-Loop. PlatformIO-Env: waveshare-s3-a7670e.
| Funktion | GPIO | Notiz |
|---|---|---|
| MODEM_PWR_EN | 33 | HIGH schaltet VVBAT; PWRKEY über Q9 automatisch (kein eigener Pin) |
| MODEM_TX / RX | 18 / 17 | AT-UART zum A7670E |
| MODEM_DTR / RI | 45 / 40 | — |
| LED_RGB | 38 | WS2812 Statusanzeige |
| I2C SDA / SCL | 15 / 16 | MAX17048 am Kamera-SCCB-Bus (interne Pullups erzwungen) |
Fuel-Gauge MAX17048 (I2C-Adresse 0x36): VCELL 0x02 (78,125 µV/LSB), SOC 0x04 (1/256 %/LSB), CRATE 0x16 (0,208 %/h, signed). Modem A7670E: LTE Cat-1 + 2G + GNSS, IO 1,8 V über TXB0104-Level-Shifter.
Akku-adaptives Polling (power_policy.h)
Reine, host-getestete powerPlan(). Am Laden (CRATE > −0,5 %/h) oder ohne Fuel-Gauge → immer Normalbetrieb. Sonst nach SoC:
| SoC | Read-Intervall | Command-Poll | Modus |
|---|---|---|---|
| ≥ 80 % / am Laden | cfg (Default 60 s) | cfg (Default 15 s) | normal |
| 50–80 % | 300 s | 120 s | saver |
| 30–50 % | 600 s | 240 s | saver |
| 10–30 % | aus (0) | 300 s | saver |
| < 10 % | — | — | deep-sleep |
Deep-Sleep (< 10 %, nicht am Laden): Modem stromlos, esp_sleep_enable_timer_wakeup alle 1200 s — nur Timer-Wake (kein Wake-on-USB). Akkustufe wird alle 30 s neu bestimmt; g_powerMode (normal|saver|deepsleep) geht ans Backend (/ingest/bridge).
Config / NVS (Namespace bridge)
| NVS-Key | Default | Zweck |
|---|---|---|
| ssid / key | — | TBox-WLAN (AP) SSID + Passwort |
| turl / ttok | http://192.168.1.1:9000 | TBox-Daemon-URL + X-Token |
| backend / apikey | — | Backend-URL + oaw_…-API-Key |
| apn / simpin | auto / — | Mobilfunk |
| interval | 60 | schneller Zyklus (dbcvpage+gps) |
| bulkiv | 86400 | cache_data (1×/Tag) |
| cmdiv | 15 | Command-Poll (entkoppelt) |
| adaptive | 0 | Akku-adaptives Polling an/aus |
| vin / carmac | — | VIN + gecachte Auto-BLE-MAC |
| btkey / rsa_n,e,d,p,q | — | Fahrzeug-BLE-Key (RSA-Privkomponenten, Dezimalstrings) |
Bridge-Modus aktiv wenn ssid + backend + apikey gesetzt, sonst Diagnose-Modus. Einrichtung per serieller cfg-Konsole (cfg set …, cfg show, cfg test, cfg bat, cfg i2cscan, cfg blescan, cfg blekey, cfg blecmd <type>, cfg bletest, cfg inx[efs][dry]) — oder per BLE-Provisioning aus der App (nur im Diagnose-Modus advertised).
Read-/Post-Strecke
| TBox GET (:9000) | Backend POST | bulk | nur wach | Rolle |
|---|---|---|---|---|
| /dbcvpage | /ingest/dbcvpage | nein | ja | Primärquelle |
| /mqtt | /ingest/telemetry | nein | nein | Backup inkl. Position |
| /cachedata | /ingest/telemetry | ja | nein | Historie, 1×/Tag |
Remote-Verwaltung (bridge_-Commands)
Aus /commands/pull gezogen, lokal auf der Bridge ausgeführt (kein BLE), acken selbst:
| Command | Wirkung |
|---|---|
| bridge_reboot | Neustart |
| bridge_updatebridge | Self-OTA der Firmware über LTE |
| bridge_updatedaemon | Daemon-Binary auf die TBox relayen |
| bridge_inx | neueste .inx hochladen |
| bridge_blescan / bridge_blekey | gecachte Auto-MAC vergessen / BLE-Key neu laden |
| bridge_set | Tuning-Keys (nur interval, cmdinterval, bulkinterval, apn, pin, adaptive) |
| bridge_show / bridge_dver / bridge_status | Config (ohne Secrets) / TBox-Version / TBox-Status |
Self-OTA: Update.begin(U_FLASH) in die inaktive OTA-Partition; große Downloads laufen erst ins Modem-EFS (AT+HTTPREADFILE) und dann fensterweise per AT+CFTRANTX ins ESP (CAT-1 hat nur ~10 KB HTTP-Puffer).
TBox-Daemon C
Läuft auf der TBox parallel zur originalen tbox_app. Single-Thread, hält die Fahrzeugdaten im RAM und serialisiert einen HTTP-Server auf :9000.
https://forgejo.timo-erdbruegger.de/aiways/tbox_daemon ↗
Quellen
- 0x301-Shell auf
127.0.0.1:50000— dbcvpage-Seiten, setmode (Keep-Awake). - MQTT-IPC (admups) auf
127.0.0.1:7090— carState/battery/power/netInfo/carInfo. Client-IDtboxd(nichtupclientOtaApp, um die echte OTA-App nicht zu verdrängen).
HTTP-Endpunkte (:9000)
| Pfad | Methode | Auth | Zweck |
|---|---|---|---|
| / | GET | offen | Live-Snapshot JSON (soc, range_km, power_kw, pack_v/a, odo, batt12, keepawake, wifi_up, mqtt-Block mit car.vin/model) |
| /dbcvpage | GET | offen | Backend-fertig {awake, signals:{NAME:roh}}; voller Sweep nur wenn wach, sonst 7 Kern-Seiten |
| /gps | GET | offen | NMEA-Messages aus getNetInfoRsp |
| /mqtt | GET | offen | MQTT-Snapshot (Backup, auch geparkt) |
| /cachedata | GET | offen | ?n= (1–300) letzte cache_data-Records, wrap-sicher |
| /version | GET | offen | {version:"1.0.<build>"} (alte Daemons → 404) |
| /ls | GET | X-Token | Listing [{name,size,mtime,dir}], Filter ?days=N |
| /file | GET | X-Token | Datei streamen (octet-stream) |
| /file | PUT/POST | X-Token | atomar schreiben (tmp+rename), ?mode=<oktal>, max 4 MiB |
| /update | PUT | X-Token | Body muss ELF sein → self-replace + re-exec |
| /restart | POST | X-Token | re-exec |
Auth: Header X-Token == API_TOKEN. Datei-Zugriff nur unter FILE_ROOTS (Default /usrdata/,/mnt/sdcard/), kein ... Leerer Token ⇒ /file//ls ganz aus.
Cross-Build: Quectel ql-ol-crosstool (arm-oe-linux-gnueabi-), dynamisch gegen glibc 2.22; bei GLIBC_… not found als -static neu bauen. Version via -DDAEMON_BUILD=$(git rev-list --count HEAD). Installation per FTP nach /usrdata/tbox_daemon, Autostart-Hook in fota.sh.
TBox & Head-Unit
TBox-System
- SoC/Modem: Qualcomm MDM9607 + Quectel AG35 (armv7l), Hostname
mdm9607, glibc 2.22. - Netz: QCMAP + dnsmasq, AP-Gateway
192.168.1.1; SIM/WAN i.d.R. tot (kein DNS/WAN). - Zugang (Reverse-Engineering): FTP als root mit werksseitigem Default-Passwort (
root:oelinux123) → voller FS-Zugriff; Root ursprünglich über unsigniertefota.sh. Details im Research-Repo. - Wichtige Pfade:
/usrapp/current/(tbox_app),/usrdata/pem/(BLE-/FOTA-Schlüssel),/mnt/sdcard/ACQB/(cache_data),/usrdata/tbox_carinfo(VIN).
Head-Unit (Dev-Werkzeug)
Freescale i.MX6DQ, Android 4.4.2 (armeabi-v7a), kein dm-verity → Root trivial. Dient nur zum Provisionieren; im Endprodukt nicht nötig. USB-Update-Partitionen via reference/imx6.ini.
Provisionierung
Die Provisioning-App wird per USB-Stick (ES-Browser) sideloaded. Sie stellt einen QR-Code bereit, den die AiwaysApp scannt:
{ "type":"aiways-provision", "api_token":"<tbx_…>",
"ap_ssid":"MyAiways-XXXXXXXX", "ap_key":"<wlan_pw>", "vin":"<VIN>" }
Die App entdeckt die TBox (GET :9000/), erzeugt das Fahrzeug-Keypair lokal und registriert das Fahrzeug (POST /provision/vehicle) — der private Schlüssel verlässt das Gerät nie und geht per BLE an die LTE-Bridge. Das öffentliche Bundle deployt sie per PUT :9000/file?path=…&mode=600 (X-Token) nach /usrdata/pem/:
| Datei | Zweck |
|---|---|
key_<btkeyId> (Community: key_AIWAYSCOMMUNITY) | BLE-Auth-Public-Key (A001) |
| key_<btkeyId>_expiretime | Gültigkeitsende |
| ac_public_key.pem | BLE-Provisioning-Public-Key |
| rsa_public_key.pem | FOTA-Trust-Anchor |
Head-Unit-Bedienung („Entwickleroptionen freischalten"): die konkrete Tipp-Geste steht in der Nutzer-Anleitung; sie ist geräteseitig, nicht im Code. Bebilderte Schritte dort.
Home-Assistant-Integration Python
Custom-Integration (Domain aiways): legt in Home Assistant eine Fahrzeug-Entität mit Sensoren an (Reichweite, Akku, Reifendruck, Standort …), gespeist aus der Backend-API.
https://forgejo.timo-erdbruegger.de/aiways/HA-Aiways-Integration ↗
Für die Ladeoptimierung lässt sich analog EVCC anbinden; das System ist offen für weitere Consumer der REST-API.
Datenformat: dbcvpage
Primärer Lesepfad. Quelle: TBox-Port 50000, Kommando dbcvpage <n> liefert je „Seite" einen Teil der Live-Signalfläche. Ein voller Sweep ≈ 1000 benannte Signale (mehr als MQTT/cache_data: HV-Pack, 96 Zellspannungen, Temps, Odometer).
Sweep-Log-Format
===== dbcvpage 24 (15 Signale) =====
BMSStateOfCharge = 30
BMSCellVoltA1 = 3.649000
...
Seiten-Header-Regex ^=+\s*dbcvpage\s+(\d+), Signalzeile ^\s*([A-Za-z_]\w*)\s*=\s*(-?\d+(\.\d+)?)\s*$; Erst-Treffer gewinnt bei Dopplung. Zeitstempel aus Dateiname aichi_dbcvpage_<unix>.txt.
Einordnung & Tiers
Jedes Signal bekommt Domäne (Präfix: BMS, VCU, MCU, OBC, DCDC, TPMS, BCM, …) und einen Tier. Physikalischer Wert = raw × scale + offset (aus dem Katalog dbcvpageCatalog.ts).
| Tier | Bedeutung | Ziel-Tabelle |
|---|---|---|
| A | App-Kern (SoC, Reichweite, Odometer, 12V, Lade-Connect) | telemetry |
| B | Batterie/Antrieb (96 Zellspannungen, Temps, VCU/MCU) | telemetry/details |
| C–G | Laden, Klima, Karosserie, Power-State, Fahrwerk/ADAS | vehicle_details |
| H | Diagnose/Fault | vehicle_diagnostics |
| Y | Sonstige (heuristisch) | vehicle_details |
| Z | Housekeeping (Rolling-Counter, Checksummen, Gültigkeitsbits) | verworfen |
Kern-Signale (→ telemetry)
Erster vorhandener Kandidat gewinnt: soc ← BMSStateOfCharge; packVoltage ← BMSPackVoltage; packCurrent ← BMSPackCurrent; odometer ← IPTotalOdometer; rangeKm ← VCUDrivingrange; cells ← BMSCellVoltA1..96; temps ← BMSCellTempA1..24. Keine Position (dbcvpage hat kein GPS).
awake=0 oder soc außerhalb 0–100.Datenformat: cache_data
Datei /mnt/sdcard/ACQB/cache_data — Ringpuffer aus Fixed-Size-Records à 0x418 = 1048 Byte, GB/T-32960-Realtime-Struktur, Big-Endian. cache_info (9 B) liefert index_r/index_w; gültig sind die ersten index_w Records, neuester bei index_w−1 (Ring kann wrappen).
0x01-Block (Offsets, gültig wenn rec[6]==0x01)
| Offset | Feld | Typ | Skala | Sentinel |
|---|---|---|---|---|
| 0–5 | Datum YY MM DD HH MM SS | 6×u8 | → ISO | — |
| 7 | status | u8 | — | 0xFF |
| 8 | charge_state | u8 | 1 lädt / 2 fahrend / 3 nicht / 4 voll | 0xFF |
| 10–11 | speed | u16 | ×0,1 km/h | 0xFFFF |
| 12–15 | odometer | u32 | ×0,1 km | 0xFFFFFFFF |
| 16–17 | packVoltage | u16 | ×0,1 V | 0xFFFF |
| 18–19 | packCurrent | u16 | (raw−10000)×0,1 A | 0xFFFF |
| 20 | soc | u8 | % | 0xFF |
| 22 | gear (Nibble) | u8 & 0x0F | 0xE=D, 0xD=R, 0xF=P, sonst N | — |
Verifiziert per Ghidra (FUN_00030594) + echten Daten. Zell-/Temp-Block kommt hier nicht mit (nur über JSON/.inx-Pfad).
Datenformat: .inx
INTEST-History-Dump. Container-Header „INTEST 2.1"/„INTEST INZ V001", danach viele zlib-Blöcke (78 9c):
- Text-Blöcke: Schema mit ~991 CAN-Signalnamen + Einheiten + GPS/Summary-Spalten.
- Binär-Blöcke: float32-LE-Snapshots (~ein Sample alle 2–3 s); ~1835 Floats vs ~1050 Namen → nicht positionsstabil, daher signaturbasiert dekodiert.
- Geliefert je Sample: Datum/Zeit, Pack-Spannung, 96 Zellspannungen (Median-gefiltert), ~24 Zelltemperaturen, GPS-Spur (lat/lon/Höhe/Kurs), Speed, Odometer.
SoC/Reichweite/GPS-Skalare noch nicht kalibriert → beim .inx-Ingest bleibt soc null. Upload-Route POST /ingest/inx?vin=… (auch ?dry=1).
Protokoll: AICHI-BLE
Fernbefehle laufen über das fahrzeugeigene AICHI-Protokoll (V1). Gerätename-Präfix AICHI*. Verbindung ist transient pro Command (Scan/connect → A001 → B001 → deinit), Auto-MAC wird gecacht (NVS carmac) für spätere Direktverbindung.
GATT
| Rolle | UUID |
|---|---|
| Write | 0x2B70 bzw. 5db803c8-3db3-49c8-983f-af3ed48ce849 |
| Notify | 0x2B71 bzw. 5701cebb-6dac-44e5-80a3-42ca901c06ad |
Write in 20-Byte-Chunks (write-with-response, ~30 ms Pause); Notify-Reassembly per LE4-Längenpräfix im 4 KB-Puffer.
Krypto (Konstanten)
- AES-128-CBC + PKCS7, fester IV = ASCII
0123456789ABCDEF. - LOGIN_KEY = ASCII
1234567890123456(für A001 + A001-Antwort). - Session-Key (16 B) aus der A001-Antwort → für alle B001/B002.
- RSA-1024, PKCS#1 v1.5: Signatur = private-encrypt; der per-Fahrzeug-Privatschlüssel liegt im NVS (n/e/d/p/q als Dezimalstrings) — nie im Klartext hier.
Frame-Aufbau
Wire: LE4(ct_len) + AES(origin). Origin (Klartext, Leerzeichen-getrennt): <HeadFlag> <FuncCode> 1.0 <tid> <date> <body-json> <check>. tid = UUIDv4-Hex (32), date = yyMMddHHmmssSSS (das Auto lehnt alte Zeitstempel ab → Netzzeit nötig).
- A001 (Auth): Body
{"userType":"<btkeyId>","btkeyId":"<btkeyId>"},check= RSA-Signatur über MD5(body + LOGIN_KEY + tid). Antwort mit LOGIN_KEY entschlüsseln →resCode="0"+ Feldrk→ RSA-decrypt → 16-B-Session-Key. Fehler:resCode="-9",errCode 1= keinkey_<btkeyId>,errCode 2= Signatur ungültig. - B001 (Command): mit Session-Key verschlüsselt, Body
{"type":"<N>"}bzw. mitparam;check= MD5(body + sessionKey + tid). - Quittung B001 (HeadFlag
ATR):resCode(0 = angenommen). Async B002 (HeadFlagTA):result+reason(0 = ausgeführt).
type-Codes (B001)
| type | Aktion | type | Aktion |
|---|---|---|---|
| 1 / 2 | unlock / lock | 8 | trunk_unlock |
| 3 / 4 | window close / open | 9 / 10 | climate on / off |
| 5 / 6 / 7 | sunroof tilt / open / close | 11 / 12 | honk / flash |
| 13 | releaseStartAuth | 14 / 15 | seatHeat on / off |
Klima-Zieltemperatur als param (z.B. {"temp":"215"} = 21,5 °C; App-Bereich 16–32 °C). Mapping identisch in App (bleTypeFor) und Firmware (backendToBleType) — das Backend selbst reicht type nur durch.
resCode / reason
resCode (Quittung): 0 OK, −1 Format, −2 unbekannter Typ, −3 Version, −4 Service abgelaufen, −5 andere App verbunden, −9 unbekannt. reason (Async): 1 anderer Befehl läuft, 2 nicht ausgeschaltet, 3 Spannungsfehler, 4 Tür offen, 6 fährt, 8 Schlüssel im Fahrzeug, 11 nicht verriegelt, 13 Bremse getreten, 241/242 Ausführbedingung/ECU-Fehler.
Referenz-Implementierung/Experimente: BLEAndroidApp ↗, dekompiliertes App-Protokoll im Research-Repo.
Protokoll: A7670E-Modem (AT)
Kommunikation per AT über UART. Start-Baud 115200, Ziel 38400 (AT+IPR=38400); Fallback 19200 bei defekten großen POSTs (kein HW-Flow-Control). Server-Zert wird pragmatisch nicht geprüft.
HTTPS-POST (RAM / HTTPDATA)
AT+HTTPINIT
AT+CSSLCFG="authmode",0,0
AT+HTTPPARA="URL","<url>"
AT+HTTPPARA="CONTENT","<ctype>"
AT+HTTPPARA="USERDATA","X-Api-Key: <key>"
AT+HTTPDATA=<len>,60 ; auf DOWNLOAD-Prompt warten, Body in 256-B-Chunks
AT+HTTPACTION=1 ; 1 = POST, async: +HTTPACTION: 1,<code>,<len>
AT+HTTPREAD=0,700
AT+HTTPTERM
Große Bodies via EFS (CFTRANRX + HTTPPOSTFILE)
Der A7670 lehnt ~MB-Bodies bei HTTPDATA ab (kein DOWNLOAD-Prompt), daher der Umweg über das Modem-EFS:
AT+FSCD=C:
AT+FSDEL=inx.bin
AT+CFTRANRX="c:/inx.bin",<len> ; auf '>' warten, Bytes host→EFS
AT+HTTPINIT ... HTTPPARA ...
AT+HTTPPOSTFILE="inx.bin",1,1,0 ; path=1(C:/), method=1(POST)
TBox Port 50000 (Assist-Modul)
TCP 50000 in tbox_app.bin. Wire-Format (verifiziert am Fahrzeug):
Req : 7E | cnt(4 LE) | 01 | opLo opHi [args] | XOR | 7E
Resp: 7E | cnt(4) | 01 | opLo opHi | len(2 LE) | [format,data] | XOR | 7E
Byte-Stuffing 7D→7D01 / 7E→7D02 ; resp-op = req-op | 0x8000
| Opcode | Zweck |
|---|---|
| 0x101 / 0x102 / 0x103 | Operator / Nettype / Signalstärke |
| 0x201–0x20E | Netz/WiFi (0x203 an/aus, 0x204 SSID, 0x205 Key, 0x207 Status, 0x20C Radio-Restart) |
| 0x301 | Shell (pkgupgrade/restart/reboot/setmode/dbcvpage) — vom Daemon genutzt |
| 0x401–0x406 | Datei-Ops (Upload/Download/List) |
| 0x701–0x736 | Krypto/SKF |
Der Daemon nutzt v.a. 0x301 (dbcvpage, setmode) und 0x20C. Details/Opcode-Inventur im Research-Repo.
Backend-API
Referenzinstanz: https://aiways.it-perspective.de. Auth-Header: X-API-Key: oaw_… oder Authorization: Bearer <oaw_… | jwt>. machineOk = Device-Token oder API-Key.
Auth / Konto
| Methode | Pfad | Auth | Zweck |
|---|---|---|---|
| POST | /auth/login | offen | Login → JWT |
| POST | /auth/register | offen* | nur wenn ALLOW_REGISTRATION; 1. Nutzer = Admin |
| GET | /auth/me | Session | eigenes Konto |
| POST | /auth/password | Session | Passwort ändern |
| DELETE | /auth/account | Session | Konto löschen (Admin ausgenommen) |
Ingest (machineOk)
| Methode | Pfad | Zweck |
|---|---|---|
| POST | /ingest/dbcvpage | Sweep-Log/JSON → telemetry + details + diagnostics (?dry=1) |
| POST | /ingest/cache-data | cache_data (octet-stream) + index_w |
| POST | /ingest/cache-info | cache_info (9 B) |
| POST | /ingest/inx | .inx parsen/ingesten (?vin=, ?dry=1) |
| POST | /ingest/telemetry | JSON records[] → sanitize → insert |
| POST | /ingest/mqtt | Position aus MQTT-netInfo |
| POST | /ingest/bridge | Bridge-Eigenstatus (Akku/Modus) |
Fahrzeuge / Telemetrie
| Methode | Pfad | Auth | Zweck |
|---|---|---|---|
| GET | /vehicles | User | sichtbare Fahrzeuge |
| PATCH / DELETE | /vehicles/:id | Admin | ändern / löschen (cascade) |
| GET / POST | /vehicles/:id/owners · /owner | User / Admin | Halter-Historie / setzen |
| GET | /vehicles/:id/latest | User | letzter Stand je Messgröße |
| GET | /vehicles/:id/timeseries | User | Zeitreihe (every/limit/offset) |
| GET | /vehicles/:id/snapshots · /details · /diagnostics | User | dbcvpage-Sweeps |
| GET | /vehicles/:id/bridge | User | Bridge-Status (online/offline) |
| POST | /vehicles/:id/invite · /invites/:code/redeem | User | Fahrzeug teilen |
| GET/PUT/DELETE | /me/settings(/:key) | Login-Token | Konto-Einstellungen (App: integrations) |
Provisioning / Firmware / Geräte
| Methode | Pfad | Auth | Zweck |
|---|---|---|---|
| POST | /provision/vehicle | User | Fahrzeug + Ownership registrieren (Key lokal in der App) |
| GET | /me/tbox-key | User | BLE-Keypair (+ deploy-Bundle) |
| POST | /me/api-key | User | Bridge-API-Key (Klartext nur hier) |
| GET | /bridge/time · /bridge/hello | offen / machineOk | Netzzeit / Auth-Probe |
| GET | /flash · /flash/manifest.json · /flash/firmware.bin | offen | Web-Flasher (bridge-factory) |
| GET | /update/bridge(.bin) · /update/daemon(.bin) | offen | OTA-Manifest + Binary |
| POST/GET/DELETE | /firmware… | Admin | Firmware-Hosting |
Command-Queue
| Methode | Pfad | Auth | Zweck |
|---|---|---|---|
| POST / GET | /commands | User | einreihen / Liste |
| GET | /commands/pull | machineOk | offene Befehle → status sent |
| POST | /commands/:id/ack | machineOk | acked/failed + result |
curl -X POST https://aiways.it-perspective.de/ingest/telemetry \
-H "X-Api-Key: oaw_…" -H "Content-Type: application/json" \
-d '{"vin":"…","records":[{"ts":"2026-09-07T20:00:00Z","soc":72,"rangeKm":305}]}'
Interaktive Übersicht mit Try-Buttons: /sandbox (außer Production).
Auth & Sicherheit
- JWT-Sessions (
Bearer <jwt>) — Nutzer/App. Kontoverwaltung & API-Key-Ausstellung nur mit Session, nie per API-Key. - API-Keys (
oaw_…) — Geräte/Integrationen; SHA-256-Hash gespeichert, Rollen admin/user/device, owner-gebunden, einzeln widerrufbar. - Fahrzeug-BLE-Auth: pro Fahrzeug ein RSA-1024-Keypair; Privatschlüssel bei App/Bridge, Public-Key auf der TBox (
key_<btkeyId>). App/Bridge signieren, TBox verifiziert. - Selbstregistrierung standardmäßig aus (
ALLOW_REGISTRATION=false).
fota.sh — nur am eigenen Fahrzeug anwenden. Keine echten Schlüssel/VINs/Tokens in öffentliche Repos committen.CI/CD & Deployment
Jedes Repo hat eine Forgejo-Actions-Pipeline mit einem Gate auf die Commit-Message.
| Repo | Trigger-Wort | Effekt |
|---|---|---|
| Backend | release | Build+Test → Docker → Forgejo-Release → Deploy auf LXC (Runner-Label deploy) |
| AiwaysApp | release | APK/Web-Build + Forgejo-Release (+ Web-Deploy) |
| AiwaysApp | publish | signiertes AAB → fastlane supply → Play-Internal-Track |
| LTEBridgeFirmware | release | PlatformIO-Build + Firmware-Artefakt |
| tbox_daemon | release | Cross-Build (armv7); Artefakt-Upload, Daemon-Update nur über :9000 /update |
Build & lokale Entwicklung
Backend
git clone https://forgejo.timo-erdbruegger.de/aiways/Backend.git && cd Backend
npm install && npm run build
docker compose up -d # Postgres + App
# lokal alternativ: npm run migrate && npm start
AiwaysApp
git clone https://forgejo.timo-erdbruegger.de/aiways/AiwaysApp.git && cd AiwaysApp
flutter pub get
flutter run # Debug aufs Gerät
flutter build appbundle --release # signiert via android/key.properties
LTE-Bridge (PlatformIO)
git clone https://forgejo.timo-erdbruegger.de/aiways/LTEBridgeFirmware.git && cd LTEBridgeFirmware
pio run -e waveshare-s3-a7670e # Compile-Check vor dem Push
pio run -e waveshare-s3-a7670e -t upload # Flashen (oder Web-Flasher /flash)
TBox-Daemon (Cross)
git clone https://forgejo.timo-erdbruegger.de/aiways/tbox_daemon.git && cd tbox_daemon
make cross CROSS=/opt/ql-ol-crosstool/.../arm-oe-linux-gnueabi-
# bei GLIBC-Fehler: statisch bauen (-static)
Beitragen
- Issues & Merge Requests auf Forgejo; Reverse-Engineering-Notizen ins Research-Repo.
- Commit-Prefixes
feat: / fix: / docs:; Deploy/Publish nur mit den Trigger-Wörtern. - Keine Secrets committen (Keystores, API-Keys, private Schlüssel, echte VINs) — CI-Secrets nutzen.
- Backend/Firmware: vor dem Commit
git statusprüfen (teils geteilte Checkouts).
Alle Repositories
Backend ↗
TypeScriptSelbst hostbares Community-Backend (Fastify + Postgres).
AiwaysApp ↗
FlutterDie Community-App (iOS & Android).
LTEBridgeFirmware ↗
C++Firmware der LTE-Bridge (ESP32-S3-A7670E-4G).
tbox_daemon ↗
CDaemon auf der TBox — Schnittstelle zu den Fahrzeugdaten.
HeadUnit_TBox_Provisioning ↗
AndroidHead-Unit-App für TBox-Einstellungen & Provisionierung.
HA-Aiways-Integration ↗
PythonHome-Assistant-Integration (Domain aiways).
BLEAppProvisioning ↗
JavaSideload-APK, die die Provisionierung ans Fahrzeug schreibt.
BLEAndroidApp ↗
JavaBLE-App zur Fahrzeugsteuerung (Referenz/Experimente).
Research ↗
DocsReverse-Engineering-Erkenntnisse (Protokolle, Formate, TBox).
Fehlt etwas oder ist etwas veraltet? Änderungen gern per Merge Request auf Forgejo.