Aiways Community · Wiki Anleitung Forgejo ↗ DE|EN

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.

Für wen: Entwickler, die den Stack verstehen, selbst hosten oder weiterentwickeln wollen. Unabhängiges Community-Projekt, nicht mit Aiways (Europe) verbunden.
Secrets: Protokoll-Konstanten (UUIDs, AES-IV, Byte-Layouts) sind hier dokumentiert. Echte per-Fahrzeug/-Nutzer-Geheimnisse (RSA-Privatschlüssel, reale VIN, API-Keys, Device-Tokens) erscheinen nie im Klartext — nur als Struktur/Platzhalter.

Ü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)

  1. 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.
  2. 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).
  3. Das Backend parst/bereinigt (Sanitize, Sleep-Schutz) und schreibt in Postgres; App und HA lesen per REST.

Befehle (Cloud → Auto)

  1. App oder Backend legen ein Command in die Queue (POST /commands, Typ als freier String + JSON-Payload).
  2. Die Bridge pollt GET /commands/pull, mappt den Typ auf den AICHI-BLE-Code, authentifiziert sich per A001 und sendet den B001-Frame.
  3. Ergebnis zurück per POST /commands/:id/ack (status + result mit resCode/Detail).

Glossar

BegriffBedeutung
TBoxTelematics Box im Fahrzeug (Qualcomm MDM9607 + Quectel AG35, armv7l, glibc 2.22). Original-Datenquelle.
LTE-BridgeExterner ESP32-S3-A7670E-Controller, liest die TBox per WLAN & telefoniert per LTE nach Hause.
AICHIBLE-Protokoll des Fahrzeugs (A001 = RSA-Auth, B001 = Kommando, B002 = Async-Ergebnis).
dbcvpageLive-Signalfläche der TBox (~1000 benannte Signale), seitenweise über die 0x301-Shell.
cache_dataGB/T-32960-Ringpuffer auf der TBox-SD (Records à 1048 B).
.inxINTEST-History-Dump (zlib-Blöcke, 96 Zellspannungen, GPS-Spur).
btkeyIdEinzige 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 via machineOk (Device-Token oder API-Key).
  • Fahrzeuge & Halter: Historie über vehicle_owners mit 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 nach COMMAND_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)

  1. Typen mit Präfix set_ (z.B. set_charge_limit) gehen direkt ans Backend (Konfig/Telemetrie, kein Aktor).
  2. Aktor-Befehle: BLE zuerst — bei Erfolg fertig; sonst LTE-Fallback über die Backend-Queue.
  3. Scheitert beides, wird lokal persistiert (commandQueue in 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.

FunktionGPIONotiz
MODEM_PWR_EN33HIGH schaltet VVBAT; PWRKEY über Q9 automatisch (kein eigener Pin)
MODEM_TX / RX18 / 17AT-UART zum A7670E
MODEM_DTR / RI45 / 40
LED_RGB38WS2812 Statusanzeige
I2C SDA / SCL15 / 16MAX17048 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:

SoCRead-IntervallCommand-PollModus
≥ 80 % / am Ladencfg (Default 60 s)cfg (Default 15 s)normal
50–80 %300 s120 ssaver
30–50 %600 s240 ssaver
10–30 %aus (0)300 ssaver
< 10 %deep-sleep

Deep-Sleep (< 10 %, nicht am Laden): Modem stromlos, esp_sleep_enable_timer_wakeup alle 1200 snur 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-KeyDefaultZweck
ssid / keyTBox-WLAN (AP) SSID + Passwort
turl / ttokhttp://192.168.1.1:9000TBox-Daemon-URL + X-Token
backend / apikeyBackend-URL + oaw_…-API-Key
apn / simpinauto / —Mobilfunk
interval60schneller Zyklus (dbcvpage+gps)
bulkiv86400cache_data (1×/Tag)
cmdiv15Command-Poll (entkoppelt)
adaptive0Akku-adaptives Polling an/aus
vin / carmacVIN + gecachte Auto-BLE-MAC
btkey / rsa_n,e,d,p,qFahrzeug-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 POSTbulknur wachRolle
/dbcvpage/ingest/dbcvpageneinjaPrimärquelle
/mqtt/ingest/telemetryneinneinBackup inkl. Position
/cachedata/ingest/telemetryjaneinHistorie, 1×/Tag

Remote-Verwaltung (bridge_-Commands)

Aus /commands/pull gezogen, lokal auf der Bridge ausgeführt (kein BLE), acken selbst:

CommandWirkung
bridge_rebootNeustart
bridge_updatebridgeSelf-OTA der Firmware über LTE
bridge_updatedaemonDaemon-Binary auf die TBox relayen
bridge_inxneueste .inx hochladen
bridge_blescan / bridge_blekeygecachte Auto-MAC vergessen / BLE-Key neu laden
bridge_setTuning-Keys (nur interval, cmdinterval, bulkinterval, apn, pin, adaptive)
bridge_show / bridge_dver / bridge_statusConfig (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-ID tboxd (nicht upclientOtaApp, um die echte OTA-App nicht zu verdrängen).

HTTP-Endpunkte (:9000)

PfadMethodeAuthZweck
/GEToffenLive-Snapshot JSON (soc, range_km, power_kw, pack_v/a, odo, batt12, keepawake, wifi_up, mqtt-Block mit car.vin/model)
/dbcvpageGEToffenBackend-fertig {awake, signals:{NAME:roh}}; voller Sweep nur wenn wach, sonst 7 Kern-Seiten
/gpsGEToffenNMEA-Messages aus getNetInfoRsp
/mqttGEToffenMQTT-Snapshot (Backup, auch geparkt)
/cachedataGEToffen?n= (1–300) letzte cache_data-Records, wrap-sicher
/versionGEToffen{version:"1.0.<build>"} (alte Daemons → 404)
/lsGETX-TokenListing [{name,size,mtime,dir}], Filter ?days=N
/fileGETX-TokenDatei streamen (octet-stream)
/filePUT/POSTX-Tokenatomar schreiben (tmp+rename), ?mode=<oktal>, max 4 MiB
/updatePUTX-TokenBody muss ELF sein → self-replace + re-exec
/restartPOSTX-Tokenre-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 unsignierte fota.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/:

DateiZweck
key_<btkeyId> (Community: key_AIWAYSCOMMUNITY)BLE-Auth-Public-Key (A001)
key_<btkeyId>_expiretimeGültigkeitsende
ac_public_key.pemBLE-Provisioning-Public-Key
rsa_public_key.pemFOTA-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).

TierBedeutungZiel-Tabelle
AApp-Kern (SoC, Reichweite, Odometer, 12V, Lade-Connect)telemetry
BBatterie/Antrieb (96 Zellspannungen, Temps, VCU/MCU)telemetry/details
C–GLaden, Klima, Karosserie, Power-State, Fahrwerk/ADASvehicle_details
HDiagnose/Faultvehicle_diagnostics
YSonstige (heuristisch)vehicle_details
ZHousekeeping (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).

Sleep-Schutz: Sweeps eines schlafenden Autos (0xFF-Sentinels: soc=255, V=6553.5, odo=429496729.5) werden verworfen, wenn awake=0 oder soc außerhalb 0–100.

Datenformat: cache_data

Datei /mnt/sdcard/ACQB/cache_dataRingpuffer 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)

OffsetFeldTypSkalaSentinel
0–5Datum YY MM DD HH MM SS6×u8→ ISO
7statusu80xFF
8charge_stateu81 lädt / 2 fahrend / 3 nicht / 4 voll0xFF
10–11speedu16×0,1 km/h0xFFFF
12–15odometeru32×0,1 km0xFFFFFFFF
16–17packVoltageu16×0,1 V0xFFFF
18–19packCurrentu16(raw−10000)×0,1 A0xFFFF
20socu8%0xFF
22gear (Nibble)u8 & 0x0F0xE=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

RolleUUID
Write0x2B70 bzw. 5db803c8-3db3-49c8-983f-af3ed48ce849
Notify0x2B71 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" + Feld rk → RSA-decrypt → 16-B-Session-Key. Fehler: resCode="-9", errCode 1 = kein key_<btkeyId>, errCode 2 = Signatur ungültig.
  • B001 (Command): mit Session-Key verschlüsselt, Body {"type":"<N>"} bzw. mit param; check = MD5(body + sessionKey + tid).
  • Quittung B001 (HeadFlag ATR): resCode (0 = angenommen). Async B002 (HeadFlag TA): result + reason (0 = ausgeführt).

type-Codes (B001)

typeAktiontypeAktion
1 / 2unlock / lock8trunk_unlock
3 / 4window close / open9 / 10climate on / off
5 / 6 / 7sunroof tilt / open / close11 / 12honk / flash
13releaseStartAuth14 / 15seatHeat 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)
Fehler 706 = „Receive/send socket data failed" — transienter Socket-Fehler, kein Logikfehler. Nur 703/705/706 werden bis zu 3× mit 1,5 s Pause wiederholt.

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
OpcodeZweck
0x101 / 0x102 / 0x103Operator / Nettype / Signalstärke
0x201–0x20ENetz/WiFi (0x203 an/aus, 0x204 SSID, 0x205 Key, 0x207 Status, 0x20C Radio-Restart)
0x301Shell (pkgupgrade/restart/reboot/setmode/dbcvpage) — vom Daemon genutzt
0x401–0x406Datei-Ops (Upload/Download/List)
0x701–0x736Krypto/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

MethodePfadAuthZweck
POST/auth/loginoffenLogin → JWT
POST/auth/registeroffen*nur wenn ALLOW_REGISTRATION; 1. Nutzer = Admin
GET/auth/meSessioneigenes Konto
POST/auth/passwordSessionPasswort ändern
DELETE/auth/accountSessionKonto löschen (Admin ausgenommen)

Ingest (machineOk)

MethodePfadZweck
POST/ingest/dbcvpageSweep-Log/JSON → telemetry + details + diagnostics (?dry=1)
POST/ingest/cache-datacache_data (octet-stream) + index_w
POST/ingest/cache-infocache_info (9 B)
POST/ingest/inx.inx parsen/ingesten (?vin=, ?dry=1)
POST/ingest/telemetryJSON records[] → sanitize → insert
POST/ingest/mqttPosition aus MQTT-netInfo
POST/ingest/bridgeBridge-Eigenstatus (Akku/Modus)

Fahrzeuge / Telemetrie

MethodePfadAuthZweck
GET/vehiclesUsersichtbare Fahrzeuge
PATCH / DELETE/vehicles/:idAdminändern / löschen (cascade)
GET / POST/vehicles/:id/owners · /ownerUser / AdminHalter-Historie / setzen
GET/vehicles/:id/latestUserletzter Stand je Messgröße
GET/vehicles/:id/timeseriesUserZeitreihe (every/limit/offset)
GET/vehicles/:id/snapshots · /details · /diagnosticsUserdbcvpage-Sweeps
GET/vehicles/:id/bridgeUserBridge-Status (online/offline)
POST/vehicles/:id/invite · /invites/:code/redeemUserFahrzeug teilen
GET/PUT/DELETE/me/settings(/:key)Login-TokenKonto-Einstellungen (App: integrations)

Provisioning / Firmware / Geräte

MethodePfadAuthZweck
POST/provision/vehicleUserFahrzeug + Ownership registrieren (Key lokal in der App)
GET/me/tbox-keyUserBLE-Keypair (+ deploy-Bundle)
POST/me/api-keyUserBridge-API-Key (Klartext nur hier)
GET/bridge/time · /bridge/hellooffen / machineOkNetzzeit / Auth-Probe
GET/flash · /flash/manifest.json · /flash/firmware.binoffenWeb-Flasher (bridge-factory)
GET/update/bridge(.bin) · /update/daemon(.bin)offenOTA-Manifest + Binary
POST/GET/DELETE/firmware…AdminFirmware-Hosting

Command-Queue

MethodePfadAuthZweck
POST / GET/commandsUsereinreihen / Liste
GET/commands/pullmachineOkoffene Befehle → status sent
POST/commands/:id/ackmachineOkacked/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).
Reverse-Engineering-Hinweis: Der TBox-Root-Zugang beruht auf werksseitigen Defaults / unsignierter 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.

RepoTrigger-WortEffekt
BackendreleaseBuild+Test → Docker → Forgejo-Release → Deploy auf LXC (Runner-Label deploy)
AiwaysAppreleaseAPK/Web-Build + Forgejo-Release (+ Web-Deploy)
AiwaysApppublishsigniertes AAB → fastlane supply → Play-Internal-Track
LTEBridgeFirmwarereleasePlatformIO-Build + Firmware-Artefakt
tbox_daemonreleaseCross-Build (armv7); Artefakt-Upload, Daemon-Update nur über :9000 /update
Wichtig: Builds laufen immer über die CI — nie lokal cross-bauen und ins Backend pushen. Signing-/Deploy-Secrets liegen als CI-Secrets, nie im Repo.

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)
Firmware/Daemon vor dem Push lokal kompilieren (Compile-Check), Binaries aber nur über die CI bauen/deployen.

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 status prüfen (teils geteilte Checkouts).

Alle Repositories

Fehlt etwas oder ist etwas veraltet? Änderungen gern per Merge Request auf Forgejo.