On-device predictive maintenance. A fault alert arrives over MQTT, relevant failure modes are retrieved from an on-device knowledge base, an on-device LLM turns them into ranked causes & fixes, the operator picks one and resolves it — all on the phone, no cloud round-trip.
Kotlin Multiplatform + Compose (Android target) · Qualcomm GenieX on-device LLM · HiveMQ MQTT 5 · RAG grounding.
A live fault alert — the issue (M-01 · Fan-Motor, WARNING) with on-device AI-generated likely causes & fixes (the solution).
Detect Retrieve Diagnose Review Resolve
MQTT alert → RAG lookup → GenieX LLM → Dashboard → Ack over MQTT
machine+part known failure ~5 ranked operator picks resolution + ack
temp/humidity modes for part causes + fixes the real cause published
- Detect — a
FaultAlert(machine no + part, optional temperature/humidity) arrives onqsense/machine/monitoring. - Retrieve —
InMemoryKnowledgeBaselooks up known failure modes for that part (deterministic keyword match — no embeddings). - Diagnose — the retrieved issues + sensor context are built into a grounded prompt; the on-device GenieX LLM ranks and rewrites them into ~5 causes + fixes, sourced from the known-issues dataset.
- Review — the dashboard shows the causes/fixes; the operator selects the real one and adds notes.
- Resolve — publishes a
Resolution(+ a minimal ack) over MQTT; the alert is marked resolved only after both publishes are acknowledged.
Everything left of the broker runs on the phone.
The domain is pure Kotlin behind interfaces; platform SDKs are adapters. The domain never imports an Android SDK, so use cases and ViewModels are unit-testable on the JVM with fakes, and each vendor SDK can be swapped without touching logic.
| Layer | Responsibility |
|---|---|
shared/commonMain (pure Kotlin) |
Domain models; service interfaces (TextGenerator, MqttGateway, Clock); use cases; RAG (KnowledgeBase + InMemoryKnowledgeBase); prompt builder + tolerant JsonDiagnosisParser; Compose UI + ViewModels + theme |
shared/androidMain (SDK adapters) |
GenieXTextGenerator (GenieX), HiveMqttGateway (HiveMQ), SystemClock, CameraX capture + Compose expect/actual for camera & image |
androidApp (thin shell) |
Builds the AppContainer once (manual DI), hosts the activity, loads fonts, launcher icon |
server/ (Python) |
v2 vision service: paho-mqtt + Pillow (→ RTMDet/ONNX); pure handler() unit-testable without a broker |
DI: no framework — AppContainer is a constructor-injected class built from platform services
and handed to ViewModels via viewModel { … }.
| Component | Version | Role & why we use it |
|---|---|---|
| Kotlin Multiplatform | — | Splits pure logic from platform code → JVM-testable, iOS-ready. Android is the only shipping target today. |
| Compose Multiplatform | — | Declarative UI beside the shared code; one design system everywhere. |
| Qualcomm GenieX (SDK) | 0.3.1 | On-device LLM via llama.cpp/GGUF (QAIRT/Genie). The only runtime that loads our GGUF model; runs on Snapdragon 8 Elite. |
| HiveMQ MQTT client | 1.3.15 | MQTT 5 transport, QoS 1. Async PUBACK futures, automatic reconnect, and a WebSocket transport fallback for networks that block/reset raw 1883. |
| kotlinx-serialization | 1.11.0 | JSON for MQTT + LLM payloads. Multiplatform, compile-time, no reflection. strict for MQTT, lenient for model output. |
| kotlinx-coroutines | — | Flows model the MQTT/LLM event streams; deterministic ViewModel tests via an injected test dispatcher. |
| CameraX | 1.4.1 | v2 vision capture. Lifecycle-aware; imageProxy.toBitmap() → downscale to 640px. |
| paho-mqtt · Pillow | 2.1 · 11.0 | PC vision service — reference Python MQTT client + lightweight image annotation. |
- The LLM is asked to select-and-adapt over retrieved failure modes, not invent freely — a small model handles that far more reliably.
- Output is constrained by an ASCII-only GBNF grammar (the crash guarantee: GenieX aborts on invalid UTF-8); JSON structure comes from the prompt + a tolerant parser, not the grammar.
- Causes/fixes are grounded in — and sourced from — the on-device known-issues dataset, so the
operator always sees real, known failure modes for that part. A larger (1B-class) instruct model
would let the LLM lead the ranking/rewrite. See
docs/reports/2026-07-12-on-device-llm-iteration.md.
Measured on-device (Snapdragon 8 Elite Gen 5 / SM8850) with the side-loaded
Qwen3-0.6B as a Q4_0 GGUF (~409 MB, 4-bit). No external profiler — just adb logcat -s QSenseGenieX
(timestamps) and adb shell dumpsys meminfo com.example.qsense:
| Metric | Value | How it's read |
|---|---|---|
Cold load: SDK init → model READY |
~13.8 s | Δ between the load: model dir and load: model READY log lines |
├ LlmWrapper build (GGUF → runtime) |
~13.7 s | the dominant step; SDK init + plugin + manager ≈ 20 ms |
| App PSS with model resident | ~470 MB | dumpsys meminfo → TOTAL PSS |
| App RSS | ~647 MB | dumpsys meminfo → TOTAL RSS |
| Native heap (mmapped GGUF weights) | ~332 MB | dominates memory; ≈ the 4-bit model size |
Generation latency is read per call from the generate: start → generate: done log delta; see
docs/reports/2026-07-12-on-device-llm-iteration.md for the model's output behaviour. Everything is
profiled on the phone — no laptop or cloud involved.
./gradlew :androidApp:assembleDebug # build APK
./gradlew :androidApp:installDebug # install on a connected device
./gradlew :shared:testAndroidHostTest # unit + integration tests (JVM host, no phone)All runtime config lives in shared/src/androidMain/kotlin/com/example/qsense/di/AndroidConfig.kt
as data-class defaults, wired once in androidApp/.../QSenseApplication.kt. There is no .env file.
| Setting | Field in AndroidConfig.kt |
Default |
|---|---|---|
| MQTT broker host | MqttConfig.host |
192.168.8.153 (a LAN broker) |
| Port / transport | MqttConfig.port · useWebSocket |
1883 · plain TCP |
| Topics | MqttConfig.alertsTopic … |
qsense/machine/monitoring, …/resolutions, …/ack |
| On-device model folder | GenieXConfig.modelName |
qsense-llm |
| Context / gen timeout | GenieXConfig.nCtx · generationTimeoutMs |
4096 · 60s |
| Seed demo alerts (offline) | AndroidConfig.seedSampleAlerts |
false |
- Point it at your broker: set
MqttConfig.hostand keep the phone on the same Wi-Fi. If a network resets raw MQTT on1883, useuseWebSocket = true,port = 8080,webSocketPath = "mqtt". - Side-load the model (needed for the on-device LLM; without it the app falls back to the RAG
dataset and still works):
It's a pre-quantized 4-bit GGUF (
adb push Qwen3-0.6B-Q4_0.gguf \ /sdcard/Android/data/com.example.qsense/files/models/qsense-llm/Q4_0) — QSense loads it as-is; there is no quantization step.
PC vision service — future upgrade (from repo root):
py -m pip install -r server/requirements.txt
py -m server.main # the service
py -m server.tools.roundtrip # MQTT round-trip proofFull setup (LLM side-load, network notes, on-device vision) → docs/SETUP.md.
- Camera-based part inspection (vision). The phone captures a part photo, sends it over MQTT to a
PC service that runs object detection (RTMDet/ONNX) and returns an annotated image + diagnosis.
This is prototyped in
server/(Python) with CameraX capture adapters inshared/androidMain, but is not part of the shipping v1 flow — it's a planned upgrade. Seeserver/README.mdanddocs/superpowers/.
docs/SETUP.md— build, install, model side-load, run the vision service.docs/MQTT-CONTRACT.md— every topic + payload field (integration spec).docs/reports/2026-07-12-on-device-llm-iteration.md— on-device LLM findings & why a small model.docs/superpowers/— design specs + implementation plan for the vision pipeline.server/README.md— PC vision service details.
The on-device LLM runs on Qualcomm's stack — QSense uses both as-is (a pre-quantized GGUF; no quantization of our own):
- Model — Qwen3-0.6B (4-bit
Q4_0GGUF) from Qualcomm AI Hub · implementation reference: ai-hub-models / qwen3_0_6b - Runtime — GenieX (QAIRT/Genie, llama.cpp/GGUF) · SDK: github.com/qualcomm/geniex
shared/commonMain pure Kotlin: models, interfaces, use cases, RAG, prompt, Compose UI, ViewModels
shared/androidMain adapters: GenieXTextGenerator, HiveMqttGateway, SystemClock, CameraX
androidApp app shell: builds AppContainer, MainActivity, fonts, launcher icon
server/ Python vision service (paho-mqtt + Pillow, stub → RTMDet)
docs/ setup, MQTT contract, reports, specs/plans
Tests live in shared/src/commonTest (run via the Android host-test target) with fakes in
testutil/ (FakeTextGenerator, FakeMqttGateway, FixedClock). EndToEndFlowTest exercises the
full wired flow with only the two platform services faked. On-device GenieX inference and a
real-broker check are validated manually on the phone.
