Dieser Daemon läuft parallel zur TBox_app und ist die Schnittstelle für unsere MCU LTE-Bridge um mit dem Auto zu kommunizieren
  • C 63.1%
  • Python 34.9%
  • Shell 1.4%
  • Makefile 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Timo Erdbrügger f83c6fa8cd release: WiFi-Selbsttest (WIFI_ERR) — defektes Funkmodul klar melden
Aus einer langen Vor-Ort-Debugsession (Community-Box: tbox_daemon-Log sah gut
aus, aber der AP kam nie hoch — keine Fehlermeldung). Ursache war ein
HARDWARE-Defekt des WiFi-Moduls: Firmware-DTC WIFI_ERR=1, `Wifi status: closed`,
leere wlan0-MAC. SSID/Key/enable werden brav gesetzt, aber ohne funktionierendes
Funkmodul beacont nichts — und der Daemon meldete das bisher nicht.

Neu: einmaliger WiFi-Selbsttest nach dem ersten ap_setup — liest den
Firmware-Fehlerspeicher (0x301 `dumperr`), parst `WIFI_ERR-<n>` und schreibt bei
`WIFI_ERR!=0` eine unuebersehbare FATAL-Zeile ins Log ("Funkmodul kommt nicht
hoch -> HARDWARE-/Varianten-Fehler, keine Fehl-Config"). Zusaetzlich als
`wifi_err`-Flag in der :9000-Telemetrie (-1 unbekannt, 0 ok, >0 Modul defekt),
in beiden Cache-Pfaden gesetzt (Hauptschleife + refresh_telem).

Damit heisst "Log sieht gut aus" nie wieder faelschlich "alles ok" — der naechste
Fall ist in Sekunden statt Stunden geklaert.

Lokal nativ kompiliert (make, exit 0, keine neuen Warnungen); parse_wifi_err
gegen die echte dumperr-Zeile unit-geprueft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011shUomooZ2agMQJjKmjHpi
2026-09-10 16:06:57 +02:00
.github/workflows CI: nach erfolgreichem Release den HeadUnit-APK-Build anstossen 2026-09-01 00:53:20 +02:00
config release: AP-Warmlauf/Setup/Watchdog ehrlich machen (Issue #4) 2026-09-10 14:29:16 +02:00
scripts rebrand: key_OPENAIWAYS -> key_AIWAYSCOMMUNITY (Legacy-Provisioning-Helfer) 2026-09-09 20:50:08 +02:00
src release: WiFi-Selbsttest (WIFI_ERR) — defektes Funkmodul klar melden 2026-09-10 16:06:57 +02:00
test MQTT-IPC-Bus als zweite Lesequelle gemerged (alle Signale im RAM) — release 2026-09-01 00:09:01 +02:00
.gitignore MQTT-IPC-Bus als zweite Lesequelle gemerged (alle Signale im RAM) — release 2026-09-01 00:09:01 +02:00
Makefile release: /version-Endpoint + git-abgeleitete Version (v1.0.<build>) 2026-09-04 11:07:07 +02:00
README.md release: AP-Warmlauf/Setup/Watchdog ehrlich machen (Issue #4) 2026-09-10 14:29:16 +02:00
SPEC.md release: AP-Warmlauf/Setup/Watchdog ehrlich machen (Issue #4) 2026-09-10 14:29:16 +02:00
tbox_daemon_new_arm release: cache_data ringpuffer-korrekt lesen (neueste N per index_w-Wraparound) 2026-09-03 01:27:16 +02:00

tbox_daemon v1 — TBox-Daemon (Read + Keep-Awake + WiFi-Watchdog)

Ein kleiner nativer C-Daemon, der auf der TBox läuft. Er liest die Fahrzeug-Telemetrie lokal aus zwei Quellen — per dbcvpage (0x301-Shell auf 127.0.0.1:50000) und über den lokalen MQTT-IPC-Bus (admups auf 127.0.0.1:7090) —, hält die Box per setmode 0 wach, richtet den WiFi-AP mit eigener SSID/Key aus seiner Config ein und überwacht ihn: fällt er weg, fährt der Daemon ihn per 0x2xx-Opcode wieder hoch — abgesichert durch einen 12V-SoC-Cutoff, damit der Pufferakku nie leergezogen wird.

Kein Cloud-Umweg: der Daemon redet direkt mit den TBox-Kanälen auf :50000 (0x301-Shell für dbcvpage/setmode, 0x2xx-Opcodes für WiFi) und mit dem lokalen MQTT-Broker. Sämtliche Signale beider Quellen liegen im RAM; :9000 serialisiert sie pro Anfrage, mit eigenem age_s je Quelle.

WiFi läuft über 0x2xx, NICHT über setwifi (0x301). Am Auto bestätigt: setwifi/setssid per Shell wirken nicht (auch nicht nach Reboot). Die dedizierten Opcodes wirken: 0x204 setzt SSID, 0x205 Key (beide schreiben tbox_apps persistenten Store → überleben Reboot), 0x203 schaltet AN/AUS. 0x20c wird bewusst gemieden — es überschreibt SSID/Key mit den Werks-Defaults Arcfox-WIFI/00000000. Auth (WPA2) bleibt unangetastet persistiert.

Warum ein Watchdog? Getestet am Auto: setmode 0 hält die CPU wach, aber der WiFi-AP (ql_wifi) fällt nach Stunden trotzdem weg und kommt nicht von allein zurück. Der Daemon läuft lokal auf der Box (braucht selbst kein WiFi), erkennt den Ausfall und startet den AP neu (0x203 on, bei Bedarf SSID+Key neu).

Grenze (ehrlich): Der Watchdog rettet nur den Fall „Box wach, AP-Radio tot" (laut Halter der Regelfall, da tbox_app geparkt weiterläuft). Sollte doch mal die ganze Box schlafen, ist auch der Daemon eingefroren — dann bräuchte es einen externen Weck-Trigger (timerwake/BLE), ein eigener Ausbau.


Was er tut

Beim Start: lädt die Config (/usrdata/tbox_daemon.conf) und richtet den AP mit eigener SSID/Key ein — 0x204 (SSID) → 0x205 (Key) → 0x203 (an). Auth bleibt persistiert (WPA2). Abschaltbar mit --no-ap-setup.

Jeden Zyklus (default 20 s):

  1. Liest die dbcvpage-Seiten {13,30,33,47,59,61,64} und extrahiert die relevanten Signale. Danach stellt er dieselbe Runde Read-Anfragen über den MQTT-IPC-Bus (getCarState, getBatteryPower, getPowerStatus, getNetInfo, einmalig getCarInfo — siehe MQTT-Quelle).
  2. Entscheidet über Keep-Awake:
    • erlaubt, wenn 12V-SoC > Cutoff oder das Auto lädt → setmode 0 (running, Box bleibt wach).
    • sonst (12V unter Cutoff und nicht am Laden) → setmode 3 (auto, Box darf schlafen). So wird die 12V nie unter die Schwelle gezogen.
  3. Legt den Snapshot im Speicher ab. Anfragen auf TCP :9000 werden pro Anfrage frisch aus diesem Snapshot serialisiert (mit aktuellem age_s) — nicht aus einer Datei gelesen. Eine Zustandsdatei schreibt der Daemon standardmäßig gar nicht; wer sie zum Debuggen will, schaltet sie mit --state PATH bzw. state = … ein. Siehe Warum ein Cache?.

Zusätzlich jede Sekunde (im Wartefenster zwischen den Telemetrie-Zyklen):

  1. WiFi-Watchdog: prüft lokal per getifaddrs() zwei Dinge — ob die AP-Gateway-IP (ap_ip, default 192.168.1.1) an ein Interface gebunden ist und ob ein hochgezogenes AP-Radio-Interface (wlan_prefix, default wlan) existiert. Fehlt eines von beiden, gilt der AP als weg:
    • erst sanft 0x203 on (bringt den AP mit den persistierten Creds zurück),
    • nach 3 aufeinanderfolgenden Fehl-Ticks zusätzlich SSID+Key neu (0x204+0x205) + 0x203 on.
    • Frühestens alle --wifi-retry Sekunden (default 10), damit nicht dauergehämmert wird.
    • Bleibt der AP trotzdem wifi_warmup_after Sekunden (default 60) weg, ist das Radio vermutlich kalt — das bringt 0x203 nicht hoch. Dann eskaliert der Watchdog einmalig 0x20c (Radio-Neustart) und fährt sofort ap_setup hinterher, weil 0x20c auf die Werks-SSID zurücksetzt. Streng begrenzt: frühestens alle wifi_warmup_retry s (default 300) und höchstens wifi_warmup_max mal (default 3) am Stück ohne Erfolg. Nur mit wifi_warmup = 1.
    • Greift nur, wenn Wachbleiben gerade erlaubt ist (nicht im Cutoff) — im Cutoff soll die Box ja schlafen, da wird der AP bewusst nicht wiederbelebt.

Warum die IP allein nicht reicht: auf der TBox hängt 192.168.1.1 an bridge0 — und die bleibt auch bei kaltem Radio bestehen, während gar kein wlan-Interface existiert. Die alte Prüfung sah deshalb „AP oben", obwohl kein WLAN in der Luft war (Issue #4). Heißt die Schnittstelle auf einer Box anders, wlan_prefix anpassen oder mit wifi_strict = 0 auf die reine IP-Prüfung zurückschalten.

Ablauf: ein Thread, kein Parallelbetrieb

Der Daemon hat genau einen Thread — es läuft nie etwas „nebenher". Der :9000-Listener ist ein nicht-blockierender Socket, der im Sekundentakt abgefragt wird:

while (läuft) {
    Telemetrie-Zyklus   # 7x dbcvpage, danach setmode, dann Snapshot in den RAM
    MQTT-Runde          # 4-5 Read-Anfragen abschicken (Antworten kommen asynchron)
    for (s = 0; s < interval; s++) {   # Wartefenster, default 20x
        accept() auf :9000 -> wartende Clients aus dem RAM beantworten
        MQTT-Socket leeren -> eingetroffene Antworten in den RAM
        WiFi-Watchdog-Tick
        sleep(1)
    }
}

Daraus folgt:

  • Der WiFi-Check läuft jede Sekunde, nicht alle interval Sekunden. Der teure dbcvpage-Zyklus läuft alle interval Sekunden (default 20).
  • :9000 antwortet aus dem RAM, ohne die Box zu befragen — deshalb sofort.
  • Trifft eine Anfrage während des Lesezyklus ein, wird sie nicht abgewiesen: sie liegt im Listen-Backlog und wird beantwortet, sobald der Zyklus fertig ist (also spätestens ein paar Sekunden später — bei hängender Box im Worst Case ~42 s, siehe unten). Das ist der einzige Fall, in dem :9000 „langsam" ist.
  • MQTT blockiert nie. Der Broker-Socket ist nicht-blockierend und wird im selben Sekundentakt geleert; Anfragen werden abgeschickt und die Antworten landen ein paar Millisekunden später im RAM. Fällt der Broker aus, versucht der Daemon alle 30 s neu zu verbinden — der dbcvpage-Zweig läuft davon unberührt weiter (und umgekehrt).

JSON-Felder

{"ts":1788040000,"ok":1,"age_s":7,"soc":29,"soh":100,"range_km":99.0,
 "energy_kwh":18.2,"power_kw":-2.62,"pack_v":349.5,"pack_a":-7.5,
 "charge_connected":0,"odo_km":43038.0,"batt12_v":11.780,"batt12_a":-0.178,
 "batt12_soc":82,"keepawake":1,"cutoff_soc":70,"wifi_up":1,
 "wifi_recover_count":0,"est_hours_left":30.3,
 "wifi":{"wlan_iface":"wlan0","ap_ip_bound":1,"strict":1,"warmup_rc":0,
         "warmup_tries":1,"setup_rc":{"ssid":0,"key":0,"enable":0}},
 "mqtt":{"connected":1,"msgs":42,
   "carstate":{"age_s":3,"speed":0,"batt_level":29,"parking_brake":1,"gear":15,
               "engine_rate":0,"key":0,"ignition":0,"charge_state":3},
   "batt12_pct":{"age_s":3,"value":82},
   "power_state":{"age_s":3,"value":2},
   "net":{"age_s":3,"cid":12345,"lac":678,"mcc":262,"mnc":1,"net_type":7,
          "rx_lev":-71,"lon":8.123456,"lat":52.987654},
   "car":{"vin":"LJ1…","model":"U5"}}}

Die dbcvpage-Felder stehen unverändert auf oberster Ebene (bestehende Clients brauchen nichts zu ändern), die Bus-Signale hängen unter mqtt.

Feld Bedeutung Skalierung/Quelle
ts Unix-Zeit des Snapshots (TBox-Uhr!) time() beim Lesezyklus
ok 1 = letzter Lesezyklus hat geklappt, 0 = TBox gerade nicht erreichbar (Werte sind dann der letzte bekannte Stand) Laufzeit
age_s Alter des Snapshots in Sekunden zum Zeitpunkt der Anfrage, −1 = noch nie gelesen pro Anfrage berechnet
soc HV-Ladestand % BMSStateOfCharge
soh HV-Gesundheit % BMSStateOfHealth
range_km Restreichweite km VCUDrivingrange ×0.1
energy_kwh Restenergie kWh BMSStateOfEnergy ×0.1
power_kw HV-Leistung kW (− = laden) PackV × PackA / 1000
pack_v / pack_a HV-Pack Spannung/Strom BMSPackVoltage / BMSPackCurrent (− = laden)
charge_connected Ladestecker verbunden BMSACChgConnectStt
odo_km Kilometerstand IPTotalOdometer
batt12_v / batt12_a 12V Spannung/Strom BCMIBSUBATT ×0.001, (BCMIBSBatCur−32768)/1000 (+ = laden)
batt12_soc 12V-SoC % BCMIBSSOC
keepawake 1 = Box wird gerade wachgehalten Laufzeit-Entscheidung
wifi_up 1 = AP belegt oben (AP-IP und WLAN-Iface) getifaddrs, siehe wifi
wifi_recover_count wie oft der AP seit Start wiederbelebt wurde Laufzeit-Zähler
est_hours_left grobe Rest-Wachzeit bei aktueller 12V-Last bis Cutoff aus --batt-ah

Der Block wifi belegt, was der Daemon bei den AP-Operationen wirklich erreicht hat — ohne telnet auf der Box:

Feld Bedeutung
wlan_iface Name des gefundenen AP-Radio-Ifaces; leer = Radio kalt
ap_ip_bound AP-Gateway-IP an irgendein Iface gebunden (auf der TBox: bridge0)
strict 1 = wifi_up verlangt beide Belege, 0 = nur die IP (alte Logik)
warmup_rc letztes 0x20c: 0 quittiert, -1 keine Antwort, -2 nie versucht
warmup_tries wie oft 0x20c abgesetzt wurde (Start + Eskalationen)
setup_rc.ssid / .key / .enable letztes 0x204/0x205/0x203 on, gleiche rc-Konvention

-1 heißt: die Box hat auf den Opcode nicht geantwortet — die Einstellung ist also nicht belegt angekommen. Genau diese Fälle liefen früher still ins Leere, während das Log Erfolg meldete.

est_hours_left ist eine grobe Schätzung (nur bei 12V-Entladung gefüllt) und hängt von der angenommenen 12V-Kapazität (--batt-ah, default 45 Ah) ab.

MQTT-Quelle: der lokale IPC-Bus

Auf der TBox läuft ein anonymer MQTT-Broker (admups, 127.0.0.1:7090, Klartext). Darüber reden tbox_app/sysi und die OTA-App miteinander. Der Daemon hängt sich als weiterer Client dran, stellt dieselben Read-Anfragen und liest zusätzlich alles mit, was die echte OTA-App abfragt.

Das Protokoll ist aus der Firmware belegt (libuccore.so / libsysi4tbox.so):

Baustein Wert Fundstelle
Topic-Schema ota/<produkt>/<empfänger>/<msgName> ups_notify, ups_add_method
<produkt> mas861 adm_ups_getUpProducutName
Endpunkte 0X701 = tbox_app/sysi, upclientOtaApp = OTA-App adm_sysi4tbox_initUP("0X701"), adm_ups_getOtaAppInstallType
Envelope {"sender":…,"msgName":…,"msgInd":…} (msgBody fehlt bei Gettern) adm_ups_sendMsg
Anfrage / Antwort get<X>Ind an 0X701 → get<X>Rsp an upclientOtaApp msg-id-Tabelle in .data

Die Kennung 0X7B4, die im IPC-Mitschnitt auftaucht, ist die UE/HMI-Kennung (adm_ups_getUeInstallType) — die OTA-App wird als upclientOtaApp adressiert.

Der Daemon abonniert ota/mas861/upclientOtaApp/+ und wertet jede passende …Rsp aus — auch die auf Anfragen der echten OTA-App (msgInd wird bewusst ignoriert, das macht das passive Mitlesen gratis). Ein result/retVal != 0 im msgBody wird verworfen (Fehlercode der Firmware, Nutzdaten dann ungültig); ebenso bleibt der alte Stand samt seinem age_s stehen, wenn eine Antwort kein einziges bekanntes Feld enthält — Altes wird nie als frisch markiert.

Feld im JSON Quelle (msgName) msgBody-Feld
carstate.speed getCarStateRsp carSpeed
carstate.batt_level " batteryLevel
carstate.parking_brake " parkingBrake
carstate.gear " gear (roh; 0xF=P, 0xE=R, D=D, 0=N)
carstate.engine_rate " engineRate
carstate.key / .ignition " key / ignitesState
carstate.charge_state " chargeState
batt12_pct.value getBatteryPowerRsp littleBatteryPower (12V in %)
power_state.value getPowerStatusRsp type (PowerState 0–5, 4 = IN_OTA)
net.* getNetInfoRsp cid lac mcc mnc netType rxLev longitude latitude
car.vin / .model getCarInfoRsp vin / model (einmalig abgefragt)

Die Werte sind roh wie auf dem Bus — bewusst nicht umgerechnet, solange die Skalierung nicht am Auto gegengeprüft ist. age_s je Block sagt, wie alt die jeweilige Antwort ist (gear/chargeState etc. gibt es auch aus dbcvpage nicht — deshalb der Merge).

Sicherheitsleitplanken:

  • Nur get*-Anfragen. setPowerStatus (der „IN_OTA"-Park-Hebel aus SPEC.md) wird nie gesendet — das ist ein Eingriff ins Fahrzeug und gehört nicht in einen Read-Daemon.
  • Eigene Client-ID (mqtt_client_id, default tboxd). Nicht auf upclientOtaApp setzen: die Client-ID der Endpunkte ist ihr installType, und MQTT wirft bei doppelter Client-ID den bestehenden Client raus — das wäre die echte OTA-App. Relevant ist das nur in einem Fall: sysi verschickt eine Antwort nur, wenn es den Empfänger für „online" hält (isDstOnlineWhenOnline). Läuft die OTA-App gar nicht, bleiben eigene Anfragen also womöglich unbeantwortet — dann wäre das Verdrängen der letzte Hebel, bewusst und wissend, dass die OTA-App solange weg ist.
  • Abschaltbar mit --no-mqtt; nur lauschen, nichts fragen mit --mqtt-no-poll.

Testen ohne Auto: test/mockmqtt.py ist ein Minimal-Broker, der zugleich tbox_app spielt (antwortet mit dem echten msgBody-Schema):

python3 test/mock301.py 50055 &      # 0x301-Kanal
python3 test/mockmqtt.py 7091 &      # IPC-Bus
./tbox_daemon --no-conf --no-ap-setup --tbox-port 50055 --mqtt-port 7091 --once

Warum ein Cache (und nicht pro Anfrage frisch lesen)?

Die JSON-Antwort wird sehr wohl pro Anfrage frisch gebaut — gecacht sind nur die Rohwerte aus dem Fahrzeug, weil genau die teuer sind:

  • Ein Lesezyklus = 7 dbcvpage-Kommandos, jedes mit eigener TCP-Verbindung zur 0x301-Shell und 6 s Timeout. Hängt die Box, dauert eine Anfrage im Worst Case ~42 s.
  • Der Daemon ist single-threaded: er würde währenddessen weder setmode (Keep-Awake) senden noch den WiFi-Watchdog ticken lassen. Ein Client, der alle 2 s pollt, könnte den Watchdog faktisch stilllegen.
  • Die 0x301-Shell ist eine geteilte Ressource (die Head-Unit nutzt sie auch) — sie pro Client-Anfrage zu hämmern ist keine gute Idee.

Der Client sieht über age_s immer, wie alt die Werte sind (uhrunabhängig, die TBox-Uhr darf schief stehen), und über ok, ob die Box beim letzten Versuch erreichbar war. Wer es trotzdem on-demand will: --refresh-max-age N lässt den Daemon vor dem Antworten frisch lesen, wenn die Daten älter als N Sekunden sind (default 0 = aus). N begrenzt gleichzeitig die Lese-Rate.

Und die Race-Frage — Anfrage kommt rein, während die Datei geschrieben wird? Kann nicht passieren: Die Antwort auf :9000 kommt aus dem Speicher, die Datei wird dafür nie gelesen. Und da alles in einem Thread läuft (Lesezyklus und :9000 wechseln sich ab), gibt es keinen halb aktualisierten Snapshot — eine Anfrage sieht immer den kompletten alten oder den kompletten neuen Stand. Auch ein fremder Leser der Datei kann nichts Halbes erwischen: geschrieben wird nach *.tmp und dann per rename() atomar umgehängt.

Die Zustandsdatei (default aus)

Weil :9000 sie nicht braucht und sie sonst niemand liest, ist sie seit v1.0.13 abgeschaltet. Eingeschaltet (--state /usrdata/tbox_state.json) gilt:

  • Es entsteht eine Datei, kein File pro Zyklus oder Lauf — dieselbe Datei wird jeden erfolgreichen Zyklus überschrieben (kurzzeitig existiert daneben …json.tmp, das per rename() drübergezogen wird).
  • Das sind bei interval = 20 rund 4.300 Flash-Schreibvorgänge pro Tag auf /usrdata. Genau deshalb ist der Default aus.
  • Sinnvoll ist sie nur zum Debuggen, wenn man nicht im TBox-WLAN hängt und den Stand z.B. per FTP abholen will.

Bauen

Nativ (nur zum Testen auf dem Laptop)

make

Cross für die TBox (armv7, glibc 2.22)

Die TBox ist ein Quectel AG35 (MDM9607, armv7l, glibc 2.22). Cross-Compilen musst du lokal mit dem Quectel-Toolchain ql-ol-crosstool machen — in der Cloud-Umgebung liegt kein armv7-Toolchain.

# Toolchain-Umgebung setzen (Pfad je nach deiner Installation):
source /opt/ql-ol-crosstool/ql-ol-crosstool-env-init
# oder direkt den Prefix übergeben:
make cross CROSS=/opt/ql-ol-crosstool/sysroots/x86_64-oesdk-linux/usr/bin/arm-oe-linux-gnueabi/arm-oe-linux-gnueabi-

Ergebnis: tbox_daemon_arm. Dynamisch gegen glibc 2.22 gelinkt — passt zur Box. Falls es beim Start GLIBC_x.xx not found gibt, mit -static neu bauen:

make cross CROSS=<prefix> CFLAGS="-O2 -Wall -Wextra -std=gnu99 -static"

Deployen

  1. Binary auf die Box kopieren (FTP root:oelinux123@192.168.1.1:21):

    # z.B. per curl/lftp — Zielordner /usrdata (überlebt Reboots)
    curl -T tbox_daemon_arm ftp://root:oelinux123@192.168.1.1/usrdata/tbox_daemon
    
  2. Auf der Box ausführbar machen und testen (telnet :2323, via fota.sh-Block):

    chmod +x /usrdata/tbox_daemon
    /usrdata/tbox_daemon --once        # ein Zyklus, JSON auf stdout
    
  3. Dauerbetrieb + auslesen:

    /usrdata/tbox_daemon &             # im Hintergrund
    # vom Laptop im TBox-WLAN:
    curl http://192.168.1.1:9000        # aktueller JSON-Snapshot (aus dem Speicher)
    # nur falls man die Zustandsdatei ausdrücklich einschaltet (default: keine):
    /usrdata/tbox_daemon --state /usrdata/tbox_state.json &
    cat /usrdata/tbox_state.json
    

Optionen

--interval N     Zyklus in s (default 20)
--cutoff N       12V-SoC-Cutoff % (default 70)
--no-keepawake   Keep-Awake aus (nur lesen, kein setmode)
--api-port N     JSON-Port (default 9000)
--state PATH     zusätzlich Zustandsdatei schreiben (default: keine — :9000
                 antwortet aus dem RAM, das schont den Flash)
--no-state       Zustandsdatei aus (Default; überstimmt `state =` aus der Config)
--refresh-max-age N  :9000 liest bei älteren Daten frisch nach (0 = aus, default;
                 blockiert währenddessen Keep-Awake/Watchdog — siehe „Warum ein Cache?")
--batt-ah N      12V-Kapazität Ah für Restlaufzeit (default 45)
--no-wifi-watch  WiFi-Watchdog aus
--ap-ip IP       AP-Gateway-IP für Liveness-Check (default 192.168.1.1)
--wifi-retry N   min. Sekunden zwischen AP-Recover-Versuchen (default 10)
--wlan-prefix S  Iface-Präfix des AP-Radios (default wlan) — dessen Existenz ist
                 der Beleg für ein WARMES Radio
--no-wifi-strict AP-Liveness nur über die AP-IP prüfen (alte Logik; Achtung:
                 bridge0 trägt die IP auch bei kaltem Radio)
--warmup         Radio-Warmlauf (0x20c) nach dem ersten erfolgreichen Read
--warmup-after N s AP-weg, bis 0x20c eskaliert wird (default 60)
--warmup-retry N min. Sekunden zwischen zwei 0x20c-Eskalationen (default 300)
--conf PATH      Config-Datei (default /usrdata/tbox_daemon.conf)
--no-conf        Config-Datei nicht laden
--no-ap-setup    AP beim Start nicht (neu) konfigurieren
--once           1 Zyklus + JSON auf stdout, dann Ende (Test)
--no-mqtt        MQTT-Quelle aus (default an: liest den lokalen IPC-Bus)
--mqtt-host IP   Broker-IP (default 127.0.0.1)
--mqtt-port N    Broker-Port (default 7090)
--mqtt-client-id S  eigene MQTT-Client-ID (default tboxd — NICHT
                 upclientOtaApp: das würde die echte OTA-App rauswerfen)
--mqtt-product S Topic-Präfix ota/<S>/... (default mas861)
--mqtt-self S    Endpunkt, dessen Antworten wir lesen (default upclientOtaApp)
--mqtt-peer S    Endpunkt, den wir fragen (default 0X701 = tbox_app)
--mqtt-sender S  `sender` im Envelope (default upclientOtaApp)
--mqtt-no-poll   nichts selbst anfragen, nur passiv mitlesen
--mqtt-debug     jede empfangene MQTT-Antwort loggen

CLI-Flags haben Vorrang vor der Config-Datei.

Config-Datei (/usrdata/tbox_daemon.conf, chmod 600)

key = value, #-Kommentare. Hier stehen v.a. deine eigenen AP-Creds:

wifi_ssid   = MeinGeheimerAP
wifi_key    = SuperSicher2026!
ap_ip       = 192.168.1.1
cutoff_soc  = 70
interval    = 20
keepawake   = 1
wifi_watch  = 1
wifi_retry  = 10
wlan_prefix = wlan         # Iface-Präfix des AP-Radios (Beleg für warmes Radio)
wifi_strict = 1            # 0 = nur AP-IP prüfen (alte, blinde Logik)
wifi_warmup = 1            # 0x20c nach dem ersten erfolgreichen Read (fota-Autostart)
wifi_warmup_wait  = 15     # s nach 0x20c warten, bevor SSID/Key gesetzt werden
wifi_warmup_after = 60     # s AP-weg, bis 0x20c eskaliert wird
wifi_warmup_retry = 300    # min. s zwischen zwei 0x20c-Eskalationen
wifi_warmup_max   = 3      # max. erfolglose Eskalationen am Stück
batt12_ah   = 45
api_port    = 9000
# state     = /usrdata/tbox_state.json   # default: keine Datei; nur zum Debuggen setzen
# refresh_max_age = 0                     # >0: :9000 liest bei älteren Daten nach

# MQTT-IPC-Bus (default an, Werte = die der echten Firmware)
mqtt        = 1
mqtt_host   = 127.0.0.1
mqtt_port   = 7090
mqtt_client_id = tboxd     # NICHT upclientOtaApp — sonst fliegt die echte OTA-App raus
# mqtt_poll = 1            # 0 = nur mitlesen, nichts selbst anfragen

Ist wifi_ssid/wifi_key leer, lässt der Daemon die SSID/Key unangetastet und schaltet nur 0x203 on. Auth (WPA2) wird nie angefasst — bleibt persistiert.

Beispiel — konservativer Cutoff bei 75 %, längerer Zyklus:

/usrdata/tbox_daemon --cutoff 75 --interval 30 &

Nur lesen, ohne die Box wachzuhalten:

/usrdata/tbox_daemon --no-keepawake &

Autostart via fota.sh

fota.sh ist der Block, über den wir bereits telnet/Shell aktivieren. Dort ans Ende einen idempotenten Starter hängen (verhindert Doppelstart):

# --- tbox_daemon autostart ---
if [ -x /usrdata/tbox_daemon ] && ! pgrep -f /usrdata/tbox_daemon >/dev/null 2>&1; then
    /usrdata/tbox_daemon --cutoff 70 --interval 20 >/usrdata/tbox_daemon.log 2>&1 &
fi
# --- ende ---

Log liegt dann in /usrdata/tbox_daemon.log.


Sicherheit / Grenzen (v1 bewusst schlank)

  • Nur Read + Keep-Awake. Keine Fahrzeug-Steuerung (Klima/Fenster/Blinker), kein HMAC — kommt in einer späteren Version.
  • Der setmode-Hebel hält die Box über den 12V-Puffer wach (kein HV-Nachschub im geparkten Zustand). Der Cutoff ist das einzige Sicherheitsnetz — bei zu niedrigem --batt-ah oder falsch skaliertem BCMIBSSOC würde er zu spät greifen. Konservativ testen, 12V-SoC im JSON im Auge behalten.
  • Kein Auto-Reset beim Beenden: setmode 0 bleibt gesetzt, wenn der Daemon hart stirbt, bis der nächste Zyklus (oder ein manuelles setmode 3) greift. Das ist so gewollt.
  • :9000 ist ungesichert und offen im TBox-WLAN — dort hängt nur dein Laptop dran, aber es ist kein Auth-Layer.