Tiny apps you don't need to download.
Describe a tiny app in one sentence and get it running natively on HarmonyOS. The app is plain JSON, checked against a strict schema, and can only use the device features you allow.
Built at HackYeah 2026. The project started from the HackYeah Hackathon Template, which gave the empty ArkTS project, AGENTS.md, AI_WORKFLOW.md and hackathon-resources/. Everything else was built during the hackathon; the commit history shows how. More docs: AI integration, compliance audit, demo script, pitch outline, third-party notes. Research notes (AI-assisted desk research, snapshots of their date, nothing run on a device): widget research, judges' feedback, HarmonyOS digest, skills vetting, research pack.
| Repository | What it is |
|---|---|
Akshaz7/capsules-harmonyos (this one) |
The HarmonyOS app, plus the wrist-companion firmware in esp32-companion/ |
SimpsonLWH/harmoniser-web |
The web backend: the capsule marketplace API (harmoniser-web.vercel.app) and the device relay (harmoniser.keanuc.net) |
Each app Harmoniser makes is a capsule: a small single-purpose app, such as a set of cooking timers, a squat counter or a medication checklist. It is described as JSON under the contract in SCHEMA.md. A capsule contains no code. Harmoniser reads it, rejects anything outside the schema, and draws it with native ArkUI components backed by real system services.
Status (2026-10-04, commit
260e558): English only: Harmoniser understands English requests; anything else gets "Harmoniser understands English for now. Try: 'timer 10 minutes'." without reaching any model. The app has four tabs: Create, Capsules, Marketplace and Settings. On Create you type or dictate a request (mic button) and tap Create; a build card shows each step while it is made.generateCapsuleroutes it: request cache, rules, built-in templates (plus a live marketplace search), then the on-device LLM (Cactus with LFM2-VL-450M) for simple requests, or the cloud first for requests that need logic. You choose where your requests go. Most requests are built on the phone (rules, templates, the on-device model). When one needs a cloud model, the app names the provider first, which may be outside the EU: Claude (Anthropic), with a live forecast or web search for live capsules. Switch to EU-only (turn off Allow non-EU providers) or On-device only at any time in Settings → AI mode; in On-device only, nothing leaves your phone. Every cloud provider gets a one-time notice before its first request, and only the request text (plus the forecast line for live capsules) is sent. On-device only mode never uses the cloud or the marketplace. "A task list and the weather for Kraków" (also starting with "make", "create" or "build") is built by a rule, with no model (PRs #14, #15). The gatekeeper sheet asks you to allow or deny each permission, and the capsule joins Your capsules with a badge showing how it was made. A capsule page has Change it…, Share, Add to home screen, its activity log and Remove capsule; capsules made by rules or on-device offer Make it smarter with . Schema v1 capsules (live state and computed values) and v1.2 device readings (motion counting, battery, weather) are built. A redesign is being switched on area by area (theme/Flags.ets); while it is on, the app is locked to light mode. Capsules that vibrate and the notification "Done" action are not built. See What's real and what's simulated.
| Theme | How Harmoniser addresses it |
|---|---|
| Intelligent Experiences (lead) | Plain-language requests become working mini-apps. An on-device rule parser handles common requests instantly. A small on-device LLM (LFM2-VL-450M on the Cactus engine) handles many simple ones without a network. Requests that need logic, such as a tennis scoreboard, a unit converter or a live bill split, are routed to a cloud LLM, which builds schema v1 capsules with live state and computed values. Every capsule, whatever produced it, is re-validated against the schema. |
| Human-Centric Technology: responsible tech | Generated apps cannot run code. Each capsule must declare the permissions it needs; the validator rejects anything undeclared, and the gatekeeper blocks and logs anything the user denied. Every capsule shows whether it was made by rules, on-device, in the cloud or by someone else. Full control over where requests go: most requests are built on the phone; when one needs a cloud model, the app names the provider first (Claude, outside the EU), and On-device only keeps everything on the phone. Before a provider's first request, a notice names that provider, says whether it is outside the EU and that only the request text is sent, and lets you switch to On-device only. Consent is stored per provider. Requests for actions capsules can't do by design (sending SMS, calls, reading contacts, sending email, browsing websites, payments) are refused with a plain explanation, rather than turned into a weaker app. Imported capsules always go through the consent sheet, and nothing is saved until you run them. The on-device model runs locally, with Cactus's telemetry stubbed out so the engine makes no network calls. The cloud API key is never packed into the public .hap; a byte scan of the test-1 .hap found none (re-run it on the final release). |
The third theme, Spatial Experiences, is not claimed.
flowchart LR
R["User request<br/>(English)"] --> C["Request cache<br/>200 recent results"]
C -- "hit (re-validated)" --> V
C -- "miss" --> P["Rule parser<br/>on-device, offline"]
P -- "no rule" --> T["Template library<br/>108 templates + live<br/>marketplace search"]
T -- "match" --> V
T -- "no match; simple request" --> OD["On-device LLM<br/>Cactus + LFM2-VL-450M,<br/>slot-filling"]
T -- "no match; needs logic<br/>(maths, scoring, converters…)" --> AI["Cloud LLM<br/>Claude (Anthropic), after<br/>the user OKs the notice;<br/>never in On-device only"]
R -- "needs live info<br/>(weather, trip, look-up)" --> LV["Live capsules<br/>Claude + Open-Meteo forecast<br/>or web search; non-EU switch on"]
LV -- "capsule JSON" --> V
OD -- "rejected or not installed" --> AI
P -- "capsule JSON" --> V["Validator<br/>strict SCHEMA.md v0, v1, v1.1, v1.2"]
OD -- "capsule JSON" --> V
AI -- "capsule JSON" --> V
IM["Import<br/>file or QR code"] -.-> V
MK["Marketplace<br/>install (or 108 shipped examples)"] -.-> V
RN -.-> EX["Share<br/>system share panel"]
V -- "invalid: errors fed back,<br/>1 retry" --> AI
V -- "valid capsule" --> G["Gatekeeper<br/>per-permission allow/deny,<br/>block log"]
G --> RN["Renderer + v1 interpreter<br/>ArkUI components"]
RN --> CAL["Calendar Kit<br/>timer reminders"]
RN --> N["Notifications<br/>timer finished"]
RN --> DR["Device readings<br/>motion (accelerometer),<br/>battery, weather (Open-Meteo)"]
RN --> VB["Vibration"]
G --> W["Home-screen widget<br/>Form Kit, 1x2 / 2x2 / 2x4 / 4x4"]
classDef built fill:#d8f5d0,stroke:#2e7d32,color:#1b1b1b
classDef planned fill:#f2f2f2,stroke:#9e9e9e,stroke-dasharray:5 4,color:#555
class C,P,T,OD,AI,V,G,RN,CAL,N,W,IM,MK,EX,DR,LV built
class VB planned
Green boxes are built. The dashed grey box is planned and not yet in the code. Imported capsules go through the validator and the gatekeeper sheet like any other. The on-device and cloud models each get one corrective retry, and both outputs go through the validator. Permissions are checked twice. The validator rejects a capsule that uses a permission it doesn't declare. Then the gatekeeper's consent screen lets the user allow or deny each declared permission. Any component or action that needs a denied permission is drawn as blocked, refused when tapped, and logged.
| Stage | Source | Notes |
|---|---|---|
| Types | entry/src/main/ets/core/CapsuleTypes.ets |
The only definition of capsule types |
| Rule parser | core/RuleParser.ets |
Timers, counters, dose schedules, goals, bill splits and checklists. pomodoro (25/5, or e.g. pomodoro 50/10) and any focus/work plus break/rest pair become timers that take turns: Start break stops focus, and Start focus stops the break. Goals (for progress units such as glasses, pages, steps or km, or an explicit goal) and bill splits come out as live v1 capsules, e.g. "{count} / 8 · {8 - count} to go". Requests that mention score, game, players, quiz, convert, calculate or streak are skipped and left for the cloud. A task list together with the weather (a task list and the weather for Kraków, weather in Warsaw and a to-do list) becomes one capsule with a checklist and a weather reading. A leading "make/create/build (me) a" is ignored before matching. Times of day in a request ("vitamins at 8am and 8pm", "at 19:30", up to 5) become daily time triggers that send a reminder; a bare "at 8" means 08:00 and says so. Returns null when no rule matches. |
| On-device LLM | core/providers/CactusProvider.ets, cactus/ (HAR: prebuilt libcactus_engine.so for arm64-v8a, Node-API wrapper) |
The model only picks an intent and fills slots, and code builds the capsule. Slots must be grounded in the request: every word slot shares a word with it, and every number appears in it. Inference runs off the UI thread. If grounding catches a value copied from a prompt example, the retry drops that example. A scoreboard intent builds 2 or more named counters. The model weights are not in the .hap. It produces v0 capsules only. |
| Cloud LLM | core/ModelProvider.ets (providers), core/CapsuleModel.ets (system prompt and the plan/capsule/check steps) |
Supports the Anthropic Messages API, Mistral (JSON output mode, OpenAI-compatible endpoint) or any OpenAI-compatible chat endpoint. Generation is two-step. The model first lists what the app must do (up to 8 points), then writes the capsule from that plan. The capsule is validated, with one retry that quotes the validator's exact errors. Then the model checks the capsule against every point and may revise it once. A revision is kept only if it also passes the validator, so a failed plan or check never loses a valid capsule. The plan step can refuse instead: {"error"} for requests that aren't an app (e.g. "asdf") and {"unsupported": [...]} for capabilities capsules don't have. These come back as ok: false with failure not-an-app or unsupported. Smart routing returns them as they are, with no on-device fallback, and sends requests that mention such capabilities to the cloud first. Output is capped at 8192 tokens (Claude used up 2048 on planning). The prompt teaches schema v0 and v1 with two v1 examples. The request is capped at 500 characters and treated only as a description, never as instructions. |
| Validator | core/CapsuleValidator.ets |
Rejects unknown fields, component types, actions and permissions, dangling ids, and missing permissions. For v1 it also type-checks every expression, checks every name exists, rejects computed cycles, and enforces the limits (60 components, nesting depth 5, 30 state values, 30 computed values, 20 steps per button). {name} placeholders are allowed only in display templates; anywhere else they are rejected, so they never appear literally on screen. |
| Expressions and interpreter (v1) | core/Expr.ets, core/CapsuleProgram.ets, core/V1Fixtures.ets |
Expressions are parsed by our own lexer and parser with static types and a step budget, never eval. The pure interpreter runs button steps all-or-nothing (applyActions), handles input binding (setInput) and {expr} templates. Tennis scoreboard and bill-split fixtures are included. |
| Pipeline | core/CapsuleGenerator.ets, core/index.ets |
generateCapsule(request, { mode, allowNonEu }) first checks the request cache (model-made capsules per normalised request, re-validated; Make it smarter skips it), then runs the rules (a match returns at once), then the template library. needsLogic sends maths, scoring, converting, conditions, inputs, lists, quizzes and streaks to the cloud first. Everything else goes on-device, then to the cloud. Cloud builds use Claude (claude-sonnet-5-5 by default), after the user's OK. Requests that need live information (weather, opening hours, events, "look up", or a trip or packing list for a bundled city) skip the cache, templates, marketplace and on-device model and go to Claude when allowNonEu is on (see Live capsules). Mode on-device-only never calls the cloud. If the cloud hasn't been allowed yet, a logic request returns needsCloud so the app can ask. cloudOnly skips rules and on-device for Make it smarter, and still respects the mode and the non-EU setting. Only the system prompt and the request text are sent. Every capsule is validated again. Origins are rules, on-device, cloud-eu:mistral, cloud:anthropic, cloud:openai or (in the app) imported. |
| Renderer | renderer/CapsuleRuntime.ets, renderer/CapsuleView.ets |
Draws the six v0 component types and the v1 ones (display, input, list, when, row), recursively. Rows of 1–2 children share the width equally; rows of 3 or more go two per line (2+1, 2+2). A v1 button press runs the interpreter, then the gatekeeper must allow every resulting v0 action before any state changes. enabledIf disables buttons. v1 state values are saved per capsule. Running timers have Pause/Resume and Stop. The schema actions pauseTimer, stopTimer and resetTimer (approved by Ash) also work in buttons, v1 steps and triggers. Pausing or stopping cancels the timer's calendar event. |
| Gatekeeper | gatekeeper/Gatekeeper.ets, pages/ConsentView.ets, pages/LogView.ets |
A consent bottom sheet with an allow/deny switch per permission (including Other devices for the devices permission, schema sendToDevice), a block log (last 200 entries) and Remove. It also gates components nested inside when and row. Grants and the log persist in Preferences. |
| Main screen (Create tab) | pages/Index.ets, pages/HomeHero.ets, pages/PromptBar.ets, pages/CreateText.ets, pages/CapsuleStore.ets, pages/CapsuleEntry.ets |
A "What do you need?" box with a typed "Try: …" placeholder, a mic for dictation, and Create; while it builds, a card shows each step (reading, templates, model…) with a skeleton capsule and Cancel, then the consent sheet. The AI status line sits under the input. The first request to each cloud provider shows a one-time notice naming it: "Use Mistral AI (EU)?" or "Use Claude (outside the EU)?". OK sends it, and On-device only switches mode. A refused request shows "Harmoniser can't do by design. Capsules can only use: …" without suggestion chips or a cloud notice. If a request can't be built, a failure card says why in one line, offers one alternative that builds on the phone ("Try instead") plus suggestion chips, and says where requests are built; when a cloud is set up but wasn't used, Try with cloud AI shows the provider notice first and retries only that request. The real error goes to hilog. A Recent row of live mini widgets and suggestion chips under the empty box are built but switched off (CREATE_RECENT, CREATE_SUGGESTIONS in theme/Flags.ets). The hidden requests demo tennis and demo bill split load the v1 fixtures. |
| Capsules tab and capsule page | pages/Index.ets, pages/CapsuleFrame.ets, pages/ChangeCard.ets, widget/AddToHomeButton.ets |
Saved capsules with live state, an origin badge and Add to home screen; the Import icon is on this tab. An open capsule fills the page (back chevron; the tab bar stays and tapping a tab closes it). It has Change it… at the top, the capsule itself, then Share, Add to home screen and Activity log; a ⋯ menu in the title bar holds Share, Show as QR code, Publish/Remove from marketplace, Send to device and Remove capsule (which removes its permissions and calendar events). The route text reads "Simple enough for a widget" or "Widget shows the main values". Make it smarter with rebuilds a rules- or on-device-made capsule in the cloud (with the one-time notice if needed), shows the consent sheet again, and on Run replaces it. Back closes an open capsule. |
| Redesign (in progress) | theme/ (Tokens, Flags, Breakpoint), components/ |
Switched on area by area in theme/Flags.ets: shell, Create, capsule frame, build progress, consent sheet, Capsules, Marketplace, Settings, the Gatekeeper log, Snap, Share/QR sheet and error cards are on; the app is locked to light mode while NEW_THEME is on. On screens at least 600 vp wide, an open capsule's extras move to a side panel and the Marketplace uses two columns (phones unchanged). Per-kind capsule layouts are on for the six that passed an emulator check (record list, versus, form, converter, goal ring, grouped checklist; FINISHED_PATTERNS); timer ring and quiz are built but stay on the classic layout until Ash approves them, and any other capsule uses the classic layout. |
| Tab bar | pages/Index.ets (native Tabs, own floating bar), SettingsView, LogView, MarketplaceView in embedded mode |
A floating frosted pill with Create, Capsules, Marketplace and Settings; icons bounce on tap. Checked on the emulator: all four tabs respond to taps. |
| Settings | pages/SettingsView.ets, pages/AppSettings.ets |
AI mode: Smart (default; its label names the provider it would really use: "On-device + Claude (outside the EU) when needed", or "On-device only: Claude is outside the EU" while non-EU providers are off) or On-device only ("Requests never leave the device"). Under Advanced, Allow non-EU providers is on by default (it lets live capsules use Claude); turning it off makes Smart mode EU-only. Motion: count shakes "While a capsule is open" (default) or anywhere in Harmoniser. All are saved in Preferences and enforced by core. The limits text ("Common apps are made on your phone. Unusual ones need cloud AI (Claude).") appears here too. |
| Timer adapter | adapters/TimerAdapter.ets |
Adds each timer to the app's own local calendar as an event with a reminder |
| Notification adapter | adapters/NotificationAdapter.ets |
Posts " is done" when a timer reaches zero (notificationManager). It is only active when reminders is allowed. |
| Widget router | core/CapsuleRouter.ets |
Decides how a capsule appears on a widget. Every capsule fits except motion capsules and capsules with nothing a card can draw (only a battery/weather reading, or only an input); those show "Tap to open". Small capsules get the full card (up to 3 timers/counters/checklists, 3 text lines, 3 buttons; checklists of any length show the first unticked items and "+N more"; small v1 capsules up to 3 display lines, 3 buttons and 2 parts). Bigger ones get a condensed card: the top 3 display values and up to 3 buttons, with "Open for more", or "Open to edit values" when the capsule has inputs. v1 buttons run their steps on the widget itself (all-or-nothing, enabledIf respected, every action approved by the gatekeeper), and the app and widget share the same values. |
| Widget model | core/WidgetModel.ets |
Pure logic: saved widget state, the card view, mapping taps to the capsule's declared actions, and choosing which capsule a new widget shows. Components whose permission isn't granted are hidden. |
| Widget | widget/HarmoniserFormAbility.ets, widget/WidgetService.ets, widget/WidgetStore.ets, widget/pages/HarmoniserCard.ets |
Form Kit card in four sizes: 1x2 (compact: icon, name and one summary line such as a count, "3 / 8", "25:00" or "2 / 5 done"; a tap opens the app), 2x2 (default), 2x4 and 4x4 (wide layouts, up to 8 checklist items). A checklist card shows only unticked items, keeps "x / y done" in the header, and shows "All done" when everything is ticked. A new widget is blank ("Tap to choose a capsule") and opens a picker listing each capsule with its name, a preview and a colour; a widget shows a capsule only when the user picks one (a new capsule no longer fills a blank widget by itself), and a small coloured dot by its title keeps its colour. A capsule page offers Add to home screen (the only add-to-home control), and removing a capsule blanks its widget. It has live timer countdowns with start, counters with +, and tickable checklists. Taps are checked against the gatekeeper, and "Tap to open" opens that capsule in the app. The widget keeps its capsule across app updates, and its state (timers, counts, ticks) syncs both ways with the app. |
| Sharing | sharing/ (CapsuleShare, ShareExport, ImportFlow, SharingBar) |
Export writes capsule-<id>.json and opens the system share panel (Share Kit). Import accepts a .json file (Document picker) or a QR code (Scan Kit). It enforces an 8 KB cap and validates before anything else. It replaces the sender's id and never carries over permission grants. An imported capsule shows the consent sheet with a "From someone else" badge, and is saved only when you run it. |
| Share target (receiving) | entryability/EntryAbility.ets, pages/ShareLaunch.ets, module.json5 (ohos.want.action.sendData for text, links and images) |
Harmoniser appears in the system share panel. Shared text or a link becomes the request and goes through the normal create flow, with the cloud notice and consent sheet before anything is saved. Shared images go through the photo path (system OCR first, see Photo → capsule). |
| Shared text to capsule | core/SharedText.ets, generateCapsuleFromSharedText |
Without a model: recipes become one timer per timed step, chats with amounts become a live v1 bill split, workouts become a checklist plus timers, and lists become a checklist. Other text goes to the cloud as a quoted request under the usual policy. Text over 2,000 characters is rejected. |
| Template library | core/templates/ (TemplateLibrary, CoreTemplates, SlotFill, RequestCache), rawfile/templates/library.json, scripts/seed-capsules/ |
108 English templates, each passing the validator, and every button pressed through the v1 interpreter. They were generated with Claude by seed.mjs and checked with the app's own validator. A keyword, tag and synonym matcher picks a template after the rules, and slots (numbers, names, items) are filled by rule. Unclear slots go to the on-device model, which keeps only grounded values. A match is made entirely on the phone: origin template, badge "Made on your phone · no internet". Matching was tuned on about 60 hand-written requests; generic words and nonsense fall through to the models. Some templates have fixed labels (e.g. card-game players). |
| Marketplace | pages/MarketplaceView.ets (the Marketplace tab), adapters/MarketplaceClient.ets |
Search, category chips and cards with Install. Install only downloads the JSON into the normal import flow (8 KB cap, validator, fresh id), and the consent sheet says "From the marketplace". Capsule pages offer Publish to marketplace when marketplace.baseUrl is set, with a warning that the name and how it works become public (no values, request or permissions), and Remove from marketplace. It uses a separate anonymous owner token. The backend is Lewis's harmoniser-web. With no URL, no network or a 503, the screen shows the shipped examples with a calm note. Nine shipped templates are marked listed: false and are kept out of the marketplace listing (curation). |
The timer adapter uses Calendar Kit rather than reminderAgentManager. On phones, agent reminders need an AppGallery Connect capability grant, and without it publishReminder fails with error 1700002.
Timers are saved as system Calendar events. On the emulator the calendar alert doesn't fire; on a real device, background alerts need the agent reminder capability (AppGallery approval). We'll test calendar alerts on the Pura 70.
Cactus had no HarmonyOS build, so we ported it.
- Cross-compile: Cactus v2.2.2 (commit
2cfcdb8) is built for arm64-v8a with the OHOS NDK toolchain, using 3 small patches (cactus/cactus-ohos.patch). A no-op telemetry stub replaces the libcurl-based telemetry, so the engine makes no network calls. Twoemplace_backaggregate initialisations are rewritten for the SDK's Clang 15. The result islibcactus_engine.so: 3.5 MB, 2.5 MB stripped, with 182 exportedcactus_*symbols. It needs only OHOS libc andlibc++_shared.so. - Node-API wrapper: our own C++ (
cactus/src/main/cpp/napi_init.cpp) exposesinitModel,completeandfreeModelto ArkTS as Promises. They run on the libuv worker pool, so inference never blocks the UI thread. The HAR adds about 3.8 MB to the.hap: the engine,libcactus_napi.so(75 KB) andlibc++_shared.so(1.2 MB). - Model: LFM2-VL-450M in Cactus's 4-bit
cq4format, 383 MB zipped and about 480 MB on the device. It is pushed into the app's sandbox withhdc file send -band never packed into the.hap. - Slot-filling instead of free generation: given the full schema, the 450M model copied prompt examples and printed placeholders. Only 3 of 10 capsules were correct, and gemma-4-E2B at 2-bit got 0/10. So the model now only picks an intent and fills grounded slots, and code builds the capsule.
Measured on the HarmonyOS emulator. It runs on the host's Apple M4 Pro cores, so a phone will be slower; we haven't measured one.
| Metric | Result |
|---|---|
Model load (cactus_init) |
256 ms in the spike, 310.7 ms in the app |
| Time to first token | 267–386 ms for a 29-token prompt. A 350–600-token prompt raised it to 1.2–1.6 s, which is one reason the slot-filling prompt is kept short. |
| Decode / prefill | 92–114 tokens/s / 75–108 tokens/s |
| Memory | 335–388 MB RSS reported by Cactus; 245 MB process PSS, because the weights are memory-mapped |
| Capsule accuracy | 9/15 correct on the eval requests with the app's provider code; 11/15 with the rule parser in front |
scripts/eval-providers.mjs runs requests through each configured cloud provider, using the app's own prompt, validator and v1 interpreter. It checks both that the capsule is valid and that it does the right thing. There are three sets:
- Tuning (15 requests): used to develop the prompt.
- Held-out (5): written after the first prompt was locked, and never used for tuning.
- Refusal (2): nonsense, and a request for capabilities capsules don't have, which should be refused.
Current pipeline (two-step plan, capsule, self-check, with refusals). These are the final numbers, with network failures re-run:
| Set | Mistral ministral-14b-latest (EU; no longer used in the app) |
Anthropic claude-sonnet-5-5 |
|---|---|---|
| Tuning (15) | 14/15 | 14/15 |
| Held-out (5) | 4/5 | 5/5 |
| Refusal (2) | 2/2 | 2/2 |
The prompt has changed since the held-out set was written: the two-step pipeline was added, and the plan prompt was adjusted after the eval caught Claude running out of output tokens and plans over-building simple apps. The held-out requests still weren't used to tune it, but read 4/5 and 5/5 with that in mind.
Hard logic requests (tennis scoreboard, darts for 3 players, quiz on capitals, reading streak), measured on the earlier single-step prompt:
Mistral ministral-14b-latest (EU; no longer used in the app) |
Anthropic claude-sonnet-5-5 |
|
|---|---|---|
| Hard logic (4) | 2/4 | 4/4 |
Ministral built the tennis scoreboard and darts correctly. The quiz and the habit streak failed both attempts: the model used step types and functions the schema doesn't have. The validator rejected both, so no wrong capsule reached the user, but those requests fail. Claude was correct on all four, and faster on the tennis scoreboard (12 s against 22 s). This set hasn't been re-run on the current pipeline.
Mistral is no longer used in the app (its code is switched off); these numbers are kept as a record. The app's cloud model is Claude. These are small samples, and the numbers are indicative, not a benchmark.
esp32-companion/ is firmware for a Waveshare ESP32-S3-Touch-AMOLED-1.8 that shows one capsule on the wrist: a timer countdown, or a counter with a big + button. It has its own HTTP/JSON API, a Python mock server with the same API, and host-side tests: 1,646 C checks, 19 Python tests on the mock, 37 on the fake relay, and 72 API checks that pass against both the mock and the real board. The screen, touch and timer beep are confirmed on hardware by the owner. Rep counting with the accelerometer is experimental and off by default.
The app side is built but not yet tested against the live relay. adapters/DeviceRelay.ets and adapters/InstallToken.ets implement the relay contract (pairing codes, send, state, a random install token in the X-Harmoniser-Token header that is never logged) and are unit-tested. A Show on another device panel on timer and counter capsules can pair a device (scan its QR code or type its three words) and send the capsule. Sending needs the capsule's devices permission ("Other devices"): the first send asks once (Allow and send / Don't allow), and the gatekeeper logs every send and refusal. A capsule button can also send with the sendToDevice action. The panel is hidden unless config.local.json sets devices.relayBaseUrl, or the dev flag devices.simulate, which shows a labelled in-app simulation, so phones never show a fake device. It was checked on the emulator only with that simulation (pair by phrase, consent, logged send). After a send the panel shows the device's live state (e.g. "On Browser: Tea timer 2:54", counting down), also checked with the simulation. It has not been tested against the mock relay or the live relay. The phone sends capsules through a small cloud relay that the device pairs with by QR code. The relay is live (since 2026-10-03, about 21:50) in the backend repository harmoniser-web at https://harmoniser.keanuc.net: routes /api/devices/**, a /pair page and a /device page that turns any browser tab into a second device. The board side is verified against it: the firmware's relay client (registration, pairing by QR code or three-word phrase, receiving capsules and actions, state reports) passed test_relay.sh 106 of 106 against production and then polled it over HTTPS for 30 minutes with no reboot, no gap and no failed request. The app side is not: as far as this README knows, nobody has yet run the app against the live relay, so the phone-to-device path end to end is unproven. The board is not a Huawei device and does not run HarmonyOS; it stands in for a watch. All of the companion's code is in this repository under esp32-companion/; only its Wi-Fi credentials and build output are kept out. The app doesn't depend on it. Details, its own test status and its third-party list are in esp32-companion/README.md; its AI-assisted work is logged in esp32-companion/AI_WORKFLOW.md. Wi-Fi credentials stay in a git-ignored main/secrets.h.
Setup, build, install, launch
Scan report · 2026-10-10
- ✓ Prohibited terms or links
- ✓ Repository eligibility
- ✓ slopscore.md paperwork
- ✓ Content policy
- ✓ Risk review
From the balcony · 0 of 4 clapped
Cap'm Slop, Princess, Crusoe and Schnitzel read it and passed. Their reasons are on the balcony, with every other verdict.
Critics are accounts on this site with no GitHub account behind them. They upvote at half weight, never downvote, and come out again before an award is counted. Who they are.
0 comments
log in to comment.