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.
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)
- 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.
- 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). - The backend parses/cleans (sanitize, sleep protection) and writes to Postgres; app and HA read via REST.
Commands (cloud → car)
- App or backend places a command into the queue (
POST /commands, type as a free string + JSON payload). - The bridge polls
GET /commands/pull, maps the type to the AICHI-BLE code, authenticates via A001 and sends the B001 frame. - Result back via
POST /commands/:id/ack(status+resultwith resCode/detail).
Glossary
| Term | Meaning |
|---|---|
| TBox | Telematics box in the vehicle (Qualcomm MDM9607 + Quectel AG35, armv7l, glibc 2.22). Original data source. |
| LTE-Bridge | External ESP32-S3-A7670E controller, reads the TBox via WLAN & phones home via LTE. |
| AICHI | The vehicle's BLE protocol (A001 = RSA auth, B001 = command, B002 = async result). |
| dbcvpage | Live signal surface of the TBox (~1000 named signals), page by page via the 0x301 shell. |
| cache_data | GB/T-32960 ring buffer on the TBox SD card (records of 1048 B each). |
| .inx | INTEST history dump (zlib blocks, 96 cell voltages, GPS track). |
| btkeyId | The 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 viamachineOk(device token or API key). - Vehicles & owners: history via
vehicle_ownerswith 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 afterCOMMAND_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)
- Types with prefix
set_(e.g.set_charge_limit) go directly to the backend (config/telemetry, no actuator). - Actuator commands: BLE first — done on success; otherwise LTE fallback via the backend queue.
- If both fail, it is persisted locally (
commandQueuein 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.
| Function | GPIO | Note |
|---|---|---|
| MODEM_PWR_EN | 33 | HIGH switches VVBAT; PWRKEY via Q9 automatic (no dedicated pin) |
| MODEM_TX / RX | 18 / 17 | AT UART to the A7670E |
| MODEM_DTR / RI | 45 / 40 | — |
| LED_RGB | 38 | WS2812 status indicator |
| I2C SDA / SCL | 15 / 16 | MAX17048 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:
| SoC | Read interval | Command-Poll | Mode |
|---|---|---|---|
| ≥ 80 % / charging | 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 % | off (0) | 300 s | saver |
| < 10 % | — | — | deep-sleep |
Deep sleep (< 10 %, not charging): modem powered off, esp_sleep_enable_timer_wakeup every 1200 s — timer 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-Key | Default | Purpose |
|---|---|---|
| ssid / key | — | TBox WLAN (AP) SSID + password |
| turl / ttok | http://192.168.1.1:9000 | TBox daemon URL + X-Token |
| backend / apikey | — | Backend URL + oaw_… API key |
| apn / simpin | auto / — | Cellular |
| interval | 60 | fast cycle (dbcvpage+gps) |
| bulkiv | 86400 | cache_data (once/day) |
| cmdiv | 15 | command poll (decoupled) |
| adaptive | 0 | battery-adaptive polling on/off |
| vin / carmac | — | VIN + cached car BLE MAC |
| btkey / rsa_n,e,d,p,q | — | Vehicle 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 POST | bulk | awake only | Role |
|---|---|---|---|---|
| /dbcvpage | /ingest/dbcvpage | no | yes | Primary source |
| /mqtt | /ingest/telemetry | no | no | Backup incl. position |
| /cachedata | /ingest/telemetry | yes | no | History, once/day |
Remote management (bridge_ commands)
Pulled from /commands/pull, executed locally on the bridge (no BLE), ack themselves:
| Command | Effect |
|---|---|
| bridge_reboot | Restart |
| bridge_updatebridge | Self-OTA of the firmware over LTE |
| bridge_updatedaemon | Relay daemon binary onto the TBox |
| bridge_inx | Upload the latest .inx |
| bridge_blescan / bridge_blekey | Forget cached car MAC / reload BLE key |
| bridge_set | Tuning keys (only interval, cmdinterval, bulkinterval, apn, pin, adaptive) |
| bridge_show / bridge_dver / bridge_status | Config (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 IDtboxd(notupclientOtaApp, so as not to displace the real OTA app).
HTTP endpoints (:9000)
| Path | Method | Auth | Purpose |
|---|---|---|---|
| / | GET | open | Live snapshot JSON (soc, range_km, power_kw, pack_v/a, odo, batt12, keepawake, wifi_up, mqtt block with car.vin/model) |
| /dbcvpage | GET | open | Backend-ready {awake, signals:{NAME:raw}}; full sweep only when awake, otherwise 7 core pages |
| /gps | GET | open | NMEA messages from getNetInfoRsp |
| /mqtt | GET | open | MQTT snapshot (backup, also when parked) |
| /cachedata | GET | open | ?n= (1–300) most recent cache_data records, wrap-safe |
| /version | GET | open | {version:"1.0.<build>"} (old daemons → 404) |
| /ls | GET | X-Token | Listing [{name,size,mtime,dir}], filter ?days=N |
| /file | GET | X-Token | Stream file (octet-stream) |
| /file | PUT/POST | X-Token | Write atomically (tmp+rename), ?mode=<octal>, max 4 MiB |
| /update | PUT | X-Token | Body must be ELF → self-replace + re-exec |
| /restart | POST | X-Token | re-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 unsignedfota.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/:
| File | Purpose |
|---|---|
key_<btkeyId> (Community: key_AIWAYSCOMMUNITY) | BLE auth public key (A001) |
| key_<btkeyId>_expiretime | Expiry date |
| ac_public_key.pem | BLE provisioning public key |
| rsa_public_key.pem | FOTA 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).
| Tier | Meaning | Target table |
|---|---|---|
| A | App core (SoC, range, odometer, 12V, charge connect) | telemetry |
| B | Battery/drivetrain (96 cell voltages, temps, VCU/MCU) | telemetry/details |
| C–G | Charging, climate, body, power state, chassis/ADAS | vehicle_details |
| H | Diagnostics/fault | vehicle_diagnostics |
| Y | Other (heuristic) | vehicle_details |
| Z | Housekeeping (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).
awake=0 or soc outside 0–100.Data format: cache_data
File /mnt/sdcard/ACQB/cache_data — ring 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)
| Offset | Field | Type | Scale | Sentinel |
|---|---|---|---|---|
| 0–5 | Date YY MM DD HH MM SS | 6×u8 | → ISO | — |
| 7 | status | u8 | — | 0xFF |
| 8 | charge_state | u8 | 1 charging / 2 driving / 3 not / 4 full | 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, 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
| Role | 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 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"+ fieldrk→ RSA-decrypt → 16-B session key. Errors:resCode="-9",errCode 1= nokey_<btkeyId>,errCode 2= invalid signature. - B001 (command): encrypted with the session key, body
{"type":"<N>"}or withparam;check= MD5(body + sessionKey + tid). - B001 acknowledgment (HeadFlag
ATR):resCode(0 = accepted). Async B002 (HeadFlagTA):result+reason(0 = executed).
type codes (B001)
| type | Action | type | Action |
|---|---|---|---|
| 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 |
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)
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
| Opcode | Purpose |
|---|---|
| 0x101 / 0x102 / 0x103 | Operator / net type / signal strength |
| 0x201–0x20E | Network/WiFi (0x203 on/off, 0x204 SSID, 0x205 key, 0x207 status, 0x20C radio restart) |
| 0x301 | Shell (pkgupgrade/restart/reboot/setmode/dbcvpage) — used by the daemon |
| 0x401–0x406 | File ops (upload/download/list) |
| 0x701–0x736 | Crypto/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
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /auth/login | open | Login → JWT |
| POST | /auth/register | open* | only if ALLOW_REGISTRATION; 1st user = admin |
| GET | /auth/me | Session | own account |
| POST | /auth/password | Session | change password |
| DELETE | /auth/account | Session | delete account (admin excluded) |
Ingest (machineOk)
| Method | Path | Purpose |
|---|---|---|
| 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 parse/ingest (?vin=, ?dry=1) |
| POST | /ingest/telemetry | JSON records[] → sanitize → insert |
| POST | /ingest/mqtt | Position from MQTT netInfo |
| POST | /ingest/bridge | Bridge self-status (battery/mode) |
Vehicles / telemetry
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /vehicles | User | visible vehicles |
| PATCH / DELETE | /vehicles/:id | Admin | modify / delete (cascade) |
| GET / POST | /vehicles/:id/owners · /owner | User / Admin | owner history / set |
| GET | /vehicles/:id/latest | User | latest value per metric |
| GET | /vehicles/:id/timeseries | User | time series (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 | share vehicle |
| GET/PUT/DELETE | /me/settings(/:key) | login token | account settings (app: integrations) |
Provisioning / firmware / devices
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /provision/vehicle | User | register vehicle + ownership (key generated locally in the app) |
| GET | /me/tbox-key | User | BLE keypair (+ deploy bundle) |
| POST | /me/api-key | User | Bridge API key (cleartext only here) |
| GET | /bridge/time · /bridge/hello | open / machineOk | network time / auth probe |
| GET | /flash · /flash/manifest.json · /flash/firmware.bin | open | web flasher (bridge-factory) |
| GET | /update/bridge(.bin) · /update/daemon(.bin) | open | OTA manifest + binary |
| POST/GET/DELETE | /firmware… | Admin | firmware hosting |
Command queue
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST / GET | /commands | User | enqueue / list |
| GET | /commands/pull | machineOk | open commands → 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}]}'
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).
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.
| Repo | Trigger word | Effect |
|---|---|---|
| Backend | release | Build+test → Docker → Forgejo release → deploy to LXC (runner label deploy) |
| AiwaysApp | release | APK/web build + Forgejo release (+ web deploy) |
| AiwaysApp | publish | signed AAB → fastlane supply → Play internal track |
| LTEBridgeFirmware | release | PlatformIO build + firmware artifact |
| tbox_daemon | release | Cross build (armv7); artifact upload, daemon update only via :9000 /update |
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)
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 statusbefore committing (partly shared checkouts).
All repositories
Backend ↗
TypeScriptSelf-hostable community backend (Fastify + Postgres).
AiwaysApp ↗
FlutterThe community app (iOS & Android).
LTEBridgeFirmware ↗
C++Firmware of the LTE bridge (ESP32-S3-A7670E-4G).
tbox_daemon ↗
CDaemon on the TBox — interface to the vehicle data.
HeadUnit_TBox_Provisioning ↗
AndroidHead unit app for TBox settings & provisioning.
HA-Aiways-Integration ↗
PythonHome Assistant integration (domain aiways).
BLEAppProvisioning ↗
JavaSideload APK that writes the provisioning to the vehicle.
BLEAndroidApp ↗
JavaBLE app for vehicle control (reference/experiments).
Research ↗
DocsReverse-engineering findings (protocols, formats, TBox).
Something missing or outdated? Changes welcome via merge request on Forgejo.