Aiways Community · Wiki Guide Forgejo ↗ DE|EN

Developer Wiki

The technical deep-dive of the Aiways community solution — a self-hosted telematics stack that reconnects the Aiways U5 after the manufacturer's cloud shutdown. All open source on Forgejo.

Who it's for: Developers who want to understand, self-host or extend the stack. Independent community project, not affiliated with Aiways (Europe).
Secrets: Protocol constants (UUIDs, AES-IV, byte layouts) are documented here. Real per-vehicle/-user secrets (RSA private keys, real VIN, API keys, device tokens) never appear in cleartext — only as structure/placeholder.

Overview

The stack replaces the shut-down manufacturer cloud. At its core is an LTE bridge in the vehicle that reads the vehicle data via a daemon on the TBox and sends it over cellular to a self-hosted backend. A Flutter app shows the data and sends remote commands; Home Assistant/EVCC attach as consumers.

  • Two ways to the car: BLE (local, app direct) and LTE (remote, via the bridge) — both speak the car's own AICHI protocol.
  • Two ways for data: live signal surface (dbcvpage, ~1000 signals) and GB/T-32960 ring buffer (cache_data) plus history dumps (.inx).
  • Self-determined: no manufacturer cloud, no ad/tracking SDKs.

Architecture

   ┌────────────────────── Vehicle (Aiways U5) ─────────────────────┐
   │  CAN / vehicle bus                                              │
   │       │                                                        │
   │  ┌────▼─────────────────────┐        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 direct)      │ Fastify + PG │
      └──────────────┘                       └──────┬───────┘
                                        REST/API-Key │
                                   ┌─────────────────┼───────────────┐
                              ┌────▼─────┐                     ┌──────▼────┐
                              │   Home   │                     │   EVCC    │
                              │ Assistant│                     │           │
                              └──────────┘                     └───────────┘
      

The app can send commands directly via BLE (when in range) or place them in the backend command queue; the bridge polls them and delivers them to the car via BLE. Everything on the bridge runs sequentially — one shared LTE modem, one BLE stack.

Data flow

Telemetry (car → cloud)

  1. The tbox_daemon reads the live signals via the 0x301 shell (port 50000) and the GB/T-32960 ring buffer from the file system and keeps them in RAM.
  2. The bridge connects to the TBox WLAN, fetches /dbcvpage (only when the car is awake), /mqtt (backup incl. position) and once/day /cachedata, and POSTs them over LTE to the ingest routes (X-Api-Key).
  3. The backend parses/cleans (sanitize, sleep protection) and writes to Postgres; app and HA read via REST.

Commands (cloud → car)

  1. App or backend places a command into the queue (POST /commands, type as a free string + JSON payload).
  2. The bridge polls GET /commands/pull, maps the type to the AICHI-BLE code, authenticates via A001 and sends the B001 frame.
  3. Result back via POST /commands/:id/ack (status + result with resCode/detail).

Glossary

TermMeaning
TBoxTelematics box in the vehicle (Qualcomm MDM9607 + Quectel AG35, armv7l, glibc 2.22). Original data source.
LTE-BridgeExternal ESP32-S3-A7670E controller, reads the TBox via WLAN & phones home via LTE.
AICHIThe vehicle's BLE protocol (A001 = RSA auth, B001 = command, B002 = async result).
dbcvpageLive signal surface of the TBox (~1000 named signals), page by page via the 0x301 shell.
cache_dataGB/T-32960 ring buffer on the TBox SD card (records of 1048 B each).
.inxINTEST history dump (zlib blocks, 96 cell voltages, GPS track).
btkeyIdThe only per-vehicle identifier of the BLE auth (community default AIWAYSCOMMUNITY).

Backend TypeScript

Fastify + PostgreSQL (ESM/TypeScript). Accepts telemetry, manages users/vehicles/sharing, the command queue, firmware hosting & web flasher, and serves the app API, admin dashboard, landing page and this 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 or API key).
  • Vehicles & owners: history via vehicle_owners with half-open [from_ts, to_ts) intervals — several open owners = shared vehicle.
  • Command queue: lifecycle pending → sent → acked | failed | expired, pull marks atomically (FOR UPDATE SKIP LOCKED), expiry after COMMAND_MAX_AGE_MIN=5.
  • Auth: JWT sessions + API keys (oaw_…), roles admin/user/device.
  • Frontends: /, /admindashboard, /anleitung, /datenschutz, /konto-loeschen, /wiki, /sandbox (only outside production).

Operation: docker-compose (Postgres 16 + app), start npm run migrate && npm start, in front a Caddy reverse proxy (TLS + cache).

AiwaysApp Flutter / Dart

Community app for iOS and Android: vehicle overview, charge/drive history, climate & remote commands, guided provisioning. https://forgejo.timo-erdbruegger.de/aiways/AiwaysApp ↗

Command routing (lib/command_queue.dart)

  1. Types with prefix set_ (e.g. set_charge_limit) go directly to the backend (config/telemetry, no actuator).
  2. Actuator commands: BLE first — done on success; otherwise LTE fallback via the backend queue.
  3. If both fail, it is persisted locally (commandQueue in SharedPreferences) and retried on restart. Deliberately never both (no double execution).

Backend→BLE type mapping (bleTypeFor) is identical to the firmware (backendToBleType). Data reading via /vehicles/:id/latest|timeseries|snapshots|details|diagnostics|bridge.

Bundle IDs: Android de.itperspective.aiways_app, iOS de.itperspective.aiwaysApp. Release process: see PLAY_STORE_RELEASE.md / IOS_RELEASE.md in the app repo.

LTE bridge (firmware) C++ / PlatformIO

Firmware on the Waveshare ESP32-S3-A7670E-4G. Reads the TBox via WLAN, posts over LTE, controls the car via BLE — battery-adaptive. https://forgejo.timo-erdbruegger.de/aiways/LTEBridgeFirmware ↗

Hardware & pins

ESP32-S3R8, 16 MB flash; PSRAM disabled and memory_type = qio_qspi (not OPI!) — otherwise OPI PSRAM collides with GPIO33 (modem power) → boot loop. PlatformIO env: waveshare-s3-a7670e.

FunctionGPIONote
MODEM_PWR_EN33HIGH switches VVBAT; PWRKEY via Q9 automatic (no dedicated pin)
MODEM_TX / RX18 / 17AT UART to the A7670E
MODEM_DTR / RI45 / 40
LED_RGB38WS2812 status indicator
I2C SDA / SCL15 / 16MAX17048 on the camera SCCB bus (internal pullups forced)

Fuel gauge MAX17048 (I2C address 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 via TXB0104 level shifter.

Battery-adaptive polling (power_policy.h)

Pure, host-tested powerPlan(). While charging (CRATE > −0,5 %/h) or without a fuel gauge → always normal operation. Otherwise by SoC:

SoCRead intervalCommand-PollMode
≥ 80 % / chargingcfg (default 60 s)cfg (default 15 s)normal
50–80 %300 s120 ssaver
30–50 %600 s240 ssaver
10–30 %off (0)300 ssaver
< 10 %deep-sleep

Deep sleep (< 10 %, not charging): modem powered off, esp_sleep_enable_timer_wakeup every 1200 stimer wake only (no wake-on-USB). Battery level is re-evaluated every 30 s; g_powerMode (normal|saver|deepsleep) goes to the backend (/ingest/bridge).

Config / NVS (namespace bridge)

NVS-KeyDefaultPurpose
ssid / keyTBox WLAN (AP) SSID + password
turl / ttokhttp://192.168.1.1:9000TBox daemon URL + X-Token
backend / apikeyBackend URL + oaw_… API key
apn / simpinauto / —Cellular
interval60fast cycle (dbcvpage+gps)
bulkiv86400cache_data (once/day)
cmdiv15command poll (decoupled)
adaptive0battery-adaptive polling on/off
vin / carmacVIN + cached car BLE MAC
btkey / rsa_n,e,d,p,qVehicle BLE key (RSA private components, decimal strings)

Bridge mode is active when ssid + backend + apikey are set, otherwise diagnostic mode. Setup via the serial cfg console (cfg set …, cfg show, cfg test, cfg bat, cfg i2cscan, cfg blescan, cfg blekey, cfg blecmd <type>, cfg bletest, cfg inx[efs][dry]) — or via BLE provisioning from the app (advertised only in diagnostic mode).

Read/post path

TBox GET (:9000)Backend POSTbulkawake onlyRole
/dbcvpage/ingest/dbcvpagenoyesPrimary source
/mqtt/ingest/telemetrynonoBackup incl. position
/cachedata/ingest/telemetryyesnoHistory, once/day

Remote management (bridge_ commands)

Pulled from /commands/pull, executed locally on the bridge (no BLE), ack themselves:

CommandEffect
bridge_rebootRestart
bridge_updatebridgeSelf-OTA of the firmware over LTE
bridge_updatedaemonRelay daemon binary onto the TBox
bridge_inxUpload the latest .inx
bridge_blescan / bridge_blekeyForget cached car MAC / reload BLE key
bridge_setTuning keys (only interval, cmdinterval, bulkinterval, apn, pin, adaptive)
bridge_show / bridge_dver / bridge_statusConfig (without secrets) / TBox version / TBox status

Self-OTA: Update.begin(U_FLASH) into the inactive OTA partition; large downloads first go into the modem EFS (AT+HTTPREADFILE) and then window by window via AT+CFTRANTX into the ESP (CAT-1 has only ~10 KB HTTP buffer).

TBox-Daemon C

Runs on the TBox alongside the original tbox_app. Single-threaded, keeps the vehicle data in RAM and serializes an HTTP server on :9000. https://forgejo.timo-erdbruegger.de/aiways/tbox_daemon ↗

Sources

  • 0x301 shell on 127.0.0.1:50000 — dbcvpage pages, setmode (keep-awake).
  • MQTT IPC (admups) on 127.0.0.1:7090 — carState/battery/power/netInfo/carInfo. Client ID tboxd (not upclientOtaApp, so as not to displace the real OTA app).

HTTP endpoints (:9000)

PathMethodAuthPurpose
/GETopenLive snapshot JSON (soc, range_km, power_kw, pack_v/a, odo, batt12, keepawake, wifi_up, mqtt block with car.vin/model)
/dbcvpageGETopenBackend-ready {awake, signals:{NAME:raw}}; full sweep only when awake, otherwise 7 core pages
/gpsGETopenNMEA messages from getNetInfoRsp
/mqttGETopenMQTT snapshot (backup, also when parked)
/cachedataGETopen?n= (1–300) most recent cache_data records, wrap-safe
/versionGETopen{version:"1.0.<build>"} (old daemons → 404)
/lsGETX-TokenListing [{name,size,mtime,dir}], filter ?days=N
/fileGETX-TokenStream file (octet-stream)
/filePUT/POSTX-TokenWrite atomically (tmp+rename), ?mode=<octal>, max 4 MiB
/updatePUTX-TokenBody must be ELF → self-replace + re-exec
/restartPOSTX-Tokenre-exec

Auth: header X-Token == API_TOKEN. File access only under FILE_ROOTS (default /usrdata/,/mnt/sdcard/), no ... Empty token ⇒ /file//ls fully off.

Cross-build: Quectel ql-ol-crosstool (arm-oe-linux-gnueabi-), dynamically against glibc 2.22; on GLIBC_… not found rebuild as -static. Version via -DDAEMON_BUILD=$(git rev-list --count HEAD). Installation via FTP to /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.
  • Network: QCMAP + dnsmasq, AP gateway 192.168.1.1; SIM/WAN usually dead (no DNS/WAN).
  • Access (reverse engineering): FTP as root with the factory default password (root:oelinux123) → full FS access; root originally via unsigned fota.sh. Details in the Research repo.
  • Important paths: /usrapp/current/ (tbox_app), /usrdata/pem/ (BLE/FOTA keys), /mnt/sdcard/ACQB/ (cache_data), /usrdata/tbox_carinfo (VIN).

Head unit (dev tool)

Freescale i.MX6DQ, Android 4.4.2 (armeabi-v7a), no dm-verity → root trivial. Only used for provisioning; not needed in the final product. USB update partitions via reference/imx6.ini.

Provisioning

The provisioning app is sideloaded via USB stick (ES browser). It provides a QR code that the AiwaysApp scans:

{ "type":"aiways-provision", "api_token":"<tbx_…>",
  "ap_ssid":"MyAiways-XXXXXXXX", "ap_key":"<wlan_pw>", "vin":"<VIN>" }

The app discovers the TBox (GET :9000/), generates the vehicle keypair locally and registers the vehicle (POST /provision/vehicle) — the private key never leaves the device and is pushed to the LTE bridge over BLE. The public bundle is deployed via PUT :9000/file?path=…&mode=600 (X-Token) to /usrdata/pem/:

FilePurpose
key_<btkeyId> (Community: key_AIWAYSCOMMUNITY)BLE auth public key (A001)
key_<btkeyId>_expiretimeExpiry date
ac_public_key.pemBLE provisioning public key
rsa_public_key.pemFOTA trust anchor

Head unit operation ("unlock developer options"): the exact tap gesture is described in the user guide; it is device-side, not in the code. Illustrated steps there.

Home Assistant integration Python

Custom integration (domain aiways): creates a vehicle entity with sensors in Home Assistant (range, battery, tire pressure, location …), fed from the backend API. https://forgejo.timo-erdbruegger.de/aiways/HA-Aiways-Integration ↗

For charge optimization, EVCC can be attached in the same way; the system is open to further consumers of the REST API.

Data format: dbcvpage

Primary read path. Source: TBox port 50000, command dbcvpage <n> returns part of the live signal surface per "page". A full sweep ≈ 1000 named signals (more than MQTT/cache_data: HV pack, 96 cell voltages, temps, odometer).

Sweep log format

===== dbcvpage 24  (15 Signale) =====
BMSStateOfCharge = 30
BMSCellVoltA1 = 3.649000
...

Page header regex ^=+\s*dbcvpage\s+(\d+), signal line ^\s*([A-Za-z_]\w*)\s*=\s*(-?\d+(\.\d+)?)\s*$; first match wins on duplicates. Timestamp from filename aichi_dbcvpage_<unix>.txt.

Classification & tiers

Each signal gets a domain (prefix: BMS, VCU, MCU, OBC, DCDC, TPMS, BCM, …) and a tier. Physical value = raw × scale + offset (from the catalog dbcvpageCatalog.ts).

TierMeaningTarget table
AApp core (SoC, range, odometer, 12V, charge connect)telemetry
BBattery/drivetrain (96 cell voltages, temps, VCU/MCU)telemetry/details
C–GCharging, climate, body, power state, chassis/ADASvehicle_details
HDiagnostics/faultvehicle_diagnostics
YOther (heuristic)vehicle_details
ZHousekeeping (rolling counter, checksums, validity bits)discarded

Core signals (→ telemetry)

First available candidate wins: soc ← BMSStateOfCharge; packVoltage ← BMSPackVoltage; packCurrent ← BMSPackCurrent; odometer ← IPTotalOdometer; rangeKm ← VCUDrivingrange; cells ← BMSCellVoltA1..96; temps ← BMSCellTempA1..24. No position (dbcvpage has no GPS).

Sleep protection: sweeps of a sleeping car (0xFF sentinels: soc=255, V=6553.5, odo=429496729.5) are discarded if awake=0 or soc outside 0–100.

Data format: cache_data

File /mnt/sdcard/ACQB/cache_dataring buffer of fixed-size records of 0x418 = 1048 bytes each, GB/T-32960 realtime structure, big-endian. cache_info (9 B) provides index_r/index_w; the first index_w records are valid, newest at index_w−1 (ring may wrap).

0x01 block (offsets, valid when rec[6]==0x01)

OffsetFieldTypeScaleSentinel
0–5Date YY MM DD HH MM SS6×u8→ ISO
7statusu80xFF
8charge_stateu81 charging / 2 driving / 3 not / 4 full0xFF
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, else N

Verified via Ghidra (FUN_00030594) + real data. Cell/temp block is not included here (only via the JSON/.inx path).

Data format: .inx

INTEST history dump. Container header "INTEST 2.1"/"INTEST INZ V001", then many zlib blocks (78 9c):

  • Text blocks: schema with ~991 CAN signal names + units + GPS/summary columns.
  • Binary blocks: float32-LE snapshots (~one sample every 2–3 s); ~1835 floats vs ~1050 names → not position-stable, therefore decoded signature-based.
  • Provided per sample: date/time, pack voltage, 96 cell voltages (median-filtered), ~24 cell temperatures, GPS track (lat/lon/altitude/heading), speed, odometer.

SoC/range/GPS scalars not yet calibrated → on .inx ingest soc stays null. Upload route POST /ingest/inx?vin=… (also ?dry=1).

Protocol: AICHI-BLE

Remote commands run over the vehicle's own AICHI protocol (V1). Device name prefix AICHI*. The connection is transient per command (scan/connect → A001 → B001 → deinit), car MAC is cached (NVS carmac) for later direct connection.

GATT

RoleUUID
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 via LE4 length prefix in the 4 KB buffer.

Crypto (constants)

  • AES-128-CBC + PKCS7, fixed IV = ASCII 0123456789ABCDEF.
  • LOGIN_KEY = ASCII 1234567890123456 (for A001 + A001 response).
  • Session key (16 B) from the A001 response → for all B001/B002.
  • RSA-1024, PKCS#1 v1.5: signature = private-encrypt; the per-vehicle private key lives in NVS (n/e/d/p/q as decimal strings) — never in cleartext here.

Frame structure

Wire: LE4(ct_len) + AES(origin). Origin (cleartext, space-separated): <HeadFlag> <FuncCode> 1.0 <tid> <date> <body-json> <check>. tid = UUIDv4 hex (32), date = yyMMddHHmmssSSS (the car rejects old timestamps → network time required).

  • A001 (auth): body {"userType":"<btkeyId>","btkeyId":"<btkeyId>"}, check = RSA signature over MD5(body + LOGIN_KEY + tid). Decrypt the response with LOGIN_KEY → resCode="0" + field rk → RSA-decrypt → 16-B session key. Errors: resCode="-9", errCode 1 = no key_<btkeyId>, errCode 2 = invalid signature.
  • B001 (command): encrypted with the session key, body {"type":"<N>"} or with param; check = MD5(body + sessionKey + tid).
  • B001 acknowledgment (HeadFlag ATR): resCode (0 = accepted). Async B002 (HeadFlag TA): result + reason (0 = executed).

type codes (B001)

typeActiontypeAction
1 / 2unlock / lock8trunk_unlock
3 / 4window close / open9 / 10climate on / off
5 / 6 / 7sunroof tilt / open / close11 / 12honk / flash
13releaseStartAuth14 / 15seatHeat on / off

Climate target temperature as param (e.g. {"temp":"215"} = 21,5 °C; app range 16–32 °C). Mapping identical in app (bleTypeFor) and firmware (backendToBleType) — the backend itself just passes type through.

resCode / reason

resCode (acknowledgment): 0 OK, −1 format, −2 unknown type, −3 version, −4 service expired, −5 another app connected, −9 unknown. reason (async): 1 another command running, 2 not switched off, 3 voltage error, 4 door open, 6 driving, 8 key in vehicle, 11 not locked, 13 brake pressed, 241/242 execution condition/ECU error.

Reference implementation/experiments: BLEAndroidApp ↗, decompiled app protocol in the Research repo.

Protocol: A7670E modem (AT)

Communication via AT over UART. Start baud 115200, target 38400 (AT+IPR=38400); fallback 19200 on broken large POSTs (no HW flow control). The server cert is pragmatically not verified.

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          ; wait for DOWNLOAD prompt, body in 256-B chunks
AT+HTTPACTION=1                ; 1 = POST, async: +HTTPACTION: 1,<code>,<len>
AT+HTTPREAD=0,700
AT+HTTPTERM

Large bodies via EFS (CFTRANRX + HTTPPOSTFILE)

The A7670 rejects ~MB bodies with HTTPDATA (no DOWNLOAD prompt), hence the detour via the modem EFS:

AT+FSCD=C:
AT+FSDEL=inx.bin
AT+CFTRANRX="c:/inx.bin",<len>   ; wait for '>', bytes host→EFS
AT+HTTPINIT ... HTTPPARA ...
AT+HTTPPOSTFILE="inx.bin",1,1,0  ; path=1(C:/), method=1(POST)
Error 706 = "Receive/send socket data failed" — transient socket error, not a logic error. Only 703/705/706 are retried up to 3× with a 1,5 s pause.

TBox port 50000 (assist module)

TCP 50000 in tbox_app.bin. Wire format (verified on the vehicle):

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
OpcodePurpose
0x101 / 0x102 / 0x103Operator / net type / signal strength
0x201–0x20ENetwork/WiFi (0x203 on/off, 0x204 SSID, 0x205 key, 0x207 status, 0x20C radio restart)
0x301Shell (pkgupgrade/restart/reboot/setmode/dbcvpage) — used by the daemon
0x401–0x406File ops (upload/download/list)
0x701–0x736Crypto/SKF

The daemon mainly uses 0x301 (dbcvpage, setmode) and 0x20C. Details/opcode inventory in the Research repo.

Backend-API

Reference instance: https://aiways.it-perspective.de. Auth header: X-API-Key: oaw_… or Authorization: Bearer <oaw_… | jwt>. machineOk = device token or API key.

Auth / account

MethodPathAuthPurpose
POST/auth/loginopenLogin → JWT
POST/auth/registeropen*only if ALLOW_REGISTRATION; 1st user = admin
GET/auth/meSessionown account
POST/auth/passwordSessionchange password
DELETE/auth/accountSessiondelete account (admin excluded)

Ingest (machineOk)

MethodPathPurpose
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 parse/ingest (?vin=, ?dry=1)
POST/ingest/telemetryJSON records[] → sanitize → insert
POST/ingest/mqttPosition from MQTT netInfo
POST/ingest/bridgeBridge self-status (battery/mode)

Vehicles / telemetry

MethodPathAuthPurpose
GET/vehiclesUservisible vehicles
PATCH / DELETE/vehicles/:idAdminmodify / delete (cascade)
GET / POST/vehicles/:id/owners · /ownerUser / Adminowner history / set
GET/vehicles/:id/latestUserlatest value per metric
GET/vehicles/:id/timeseriesUsertime series (every/limit/offset)
GET/vehicles/:id/snapshots · /details · /diagnosticsUserdbcvpage sweeps
GET/vehicles/:id/bridgeUserbridge status (online/offline)
POST/vehicles/:id/invite · /invites/:code/redeemUsershare vehicle
GET/PUT/DELETE/me/settings(/:key)login tokenaccount settings (app: integrations)

Provisioning / firmware / devices

MethodPathAuthPurpose
POST/provision/vehicleUserregister vehicle + ownership (key generated locally in the app)
GET/me/tbox-keyUserBLE keypair (+ deploy bundle)
POST/me/api-keyUserBridge API key (cleartext only here)
GET/bridge/time · /bridge/helloopen / machineOknetwork time / auth probe
GET/flash · /flash/manifest.json · /flash/firmware.binopenweb flasher (bridge-factory)
GET/update/bridge(.bin) · /update/daemon(.bin)openOTA manifest + binary
POST/GET/DELETE/firmware…Adminfirmware hosting

Command queue

MethodPathAuthPurpose
POST / GET/commandsUserenqueue / list
GET/commands/pullmachineOkopen commands → 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}]}'

Interactive overview with try buttons: /sandbox (except production).

Auth & security

  • JWT sessions (Bearer <jwt>) — user/app. Account management & API key issuance only with a session, never via API key.
  • API keys (oaw_…) — devices/integrations; SHA-256 hash stored, roles admin/user/device, owner-bound, individually revocable.
  • Vehicle BLE auth: one RSA-1024 keypair per vehicle; private key at app/bridge, public key on the TBox (key_<btkeyId>). App/bridge sign, TBox verifies.
  • Self-registration off by default (ALLOW_REGISTRATION=false).
Reverse-engineering note: TBox root access relies on factory defaults / unsigned fota.sh — only apply it to your own vehicle. Do not commit real keys/VINs/tokens to public repos.

CI/CD & Deployment

Each repo has a Forgejo Actions pipeline with a gate on the commit message.

RepoTrigger wordEffect
BackendreleaseBuild+test → Docker → Forgejo release → deploy to LXC (runner label deploy)
AiwaysAppreleaseAPK/web build + Forgejo release (+ web deploy)
AiwaysApppublishsigned AAB → fastlane supply → Play internal track
LTEBridgeFirmwarereleasePlatformIO build + firmware artifact
tbox_daemonreleaseCross build (armv7); artifact upload, daemon update only via :9000 /update
Important: builds always run via CI — never cross-build locally and push into the backend. Signing/deploy secrets live as CI secrets, never in the repo.

Build & local development

Backend

git clone https://forgejo.timo-erdbruegger.de/aiways/Backend.git && cd Backend
npm install && npm run build
docker compose up -d        # Postgres + app
# local alternative: npm run migrate && npm start

AiwaysApp

git clone https://forgejo.timo-erdbruegger.de/aiways/AiwaysApp.git && cd AiwaysApp
flutter pub get
flutter run                          # debug to device
flutter build appbundle --release    # signed 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 before push
pio run -e waveshare-s3-a7670e -t upload   # flash (or 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-
# on GLIBC error: build statically (-static)
Compile firmware/daemon locally before pushing (compile check), but build/deploy binaries only via CI.

Contributing

  • Issues & merge requests on Forgejo; reverse-engineering notes into the Research repo.
  • Commit prefixes feat: / fix: / docs:; deploy/publish only with the trigger words.
  • Do not commit secrets (keystores, API keys, private keys, real VINs) — use CI secrets.
  • Backend/firmware: check git status before committing (partly shared checkouts).

All repositories

Something missing or outdated? Changes welcome via merge request on Forgejo.