Preskočiť na obsah
Kristián Partl
Späť

Ako som vymenil Garmin váhu za lacnú Renpho a Raspberry Pi

Za Garmin Index S2 som zaplatil 150 eur a bol to najmenej užitočný kus môjho setupu. Stúpiš na váhu a čakáš. Občas sa meranie podarí. Inokedy stojíš v kúpeľni, nedeje sa nič, zídeš z váhy a skúsiš to o hodinu znova.

Vymenil som ju za Renpho ES-CS20M, ktorú som už doma mal. Stála asi 12 eur a o Garmin Connecte nevie vôbec nič. Medzi váhou a Garmin Connectom dnes stojí Raspberry Pi Zero 2W, ktoré beží od januára.

Súčasťou tej výmeny bol prepis skriptu z Pythonu do TypeScriptu. Myslel som si, že je to zmena jazyka.

Prvá verzia

Trinásteho januára. Tri Python súbory a šablóna konfigurácie. scale_scanner.py robil výpočet z bioimpedancie, scale_sync.py všetko ostatné: pripojenie cez BLE, odomknutie váhy, dekódovanie notifikácie, prihlásenie do Garminu, nahratie dát.

Fungovalo to prvý večer. Jedna váha, jeden cieľ, žiadna konfigurácia okrem môjho emailu a výšky.

Kontrakt medzi tými dvoma polovicami vyzeral takto:

garmin.add_body_composition(
    timestamp=None,
    weight=weight,
    percent_fat=metrics["Body Fat (%)"],
    percent_hydration=metrics["Water (%)"],
    bone_mass=metrics["Bone Mass (kg)"],
    muscle_mass=metrics["Muscle Mass (kg)"],
    metabolic_age=metrics["Metabolic Age"],
    body_mass_index=metrics["BMI"],
)

Tie kľúče sú texty určené na zobrazenie. "Body Fat (%)", aj s jednotkou a zátvorkou, vyrobené pre výpis do konzoly a potom prečítané späť ako názov poľa v API. Funguje to presne dovtedy, kým ten dict zapisuje jedna funkcia, číta ho druhá a obe máš otvorené na obrazovke naraz.

Zvyšok bol na tom rovnako. Odomykací príkaz bol literál vnútri pripájacej funkcie. Filter rámcov bol if data[0] != 0x10 priamo v handleri notifikácie. To, či je meranie hotové, držal globálny boolean s názvom sync_completed.

Ako skript, ktorý vyriešil môj problém, to bol dobrý kód.

Čo to zlomilo

Chcel som tie čísla aj inde než v Garmine. Spočiatku to bolo celé zadanie: jeden cieľ navyše vedľa toho, ktorý už fungoval.

Potom som si povedal, že takúto váhu má doma kopa ľudí a k dátam sa poriadne nedostane nikto z nich. Tak prečo z toho nespraviť open source projekt?

Kým to bol môj skript, druhý cieľ znamenal jednu funkciu navyše vedľa tej garminovskej. Niekto iný má ale inú váhu a chce dáta inde než ja. Medzi BLE rámcami a nahrávaním musí niečo existovať a musí to mať meno. V tom skripte nebolo kam to dať, lebo to dovtedy nikto nepotreboval.

Prepis

Ôsmeho februára som to prepísal do TypeScriptu. Tri nové súbory nahradili štyri pôvodné.

Sám pre seba som si tú zmenu vtedy pomenoval ako zavedenie adapter patternu. V tom pomenovaní nie je ani Python, ani TypeScript.

export interface ScaleAdapter {
  readonly name: string;
  readonly charNotifyUuid: string;
  readonly charWriteUuid: string;
  readonly unlockCommand: number[];
  readonly unlockIntervalMs: number;

  matches(peripheral: Peripheral): boolean;
  parseNotification(data: Buffer): ScaleReading | null;
  isComplete(reading: ScaleReading): boolean;
  computeMetrics(reading: ScaleReading, profile: UserProfile): GarminPayload;
}

Každé pole odtiaľ bolo v pôvodnej verzii už predtým. Len bolo rozsypané po troch súboroch ako literály, inline podmienky a jedna globálna premenná. Až keď som to napísal ako jednu deklaráciu, vzniklo miesto pre váhy, ktoré doma nemám.

Python má abstraktné triedy aj protokoly a ten istý tvar by vyjadril. Navyše som chcel, aby kontrola prebehla skôr, než sa kód vôbec spustí. Dekodér, ktorý z rámca prečíta zlé dva bajty, nevyhodí výnimku. Vráti číslo, a ak vyzerá vierohodne, nahrá sa. Kým si niekto všimne, že bolo zlé, je už v cudzej Garmin histórii aj s časovou pečiatkou a treba ho mazať ručne. Zlé typy sú jediná trieda tohto zlyhania, ktorú viem zachytiť bez toho, aby som vlastnil hardvér, na ktorom sa deje.

Deň nato pribudlo pätnásť ďalších adaptérov.

Typ pomenovaný podľa prvého konzumenta

Desiateho februára dostali merania druhý cieľ: MQTT, aby pristávali aj v Home Assistante.

Typ s dátami sa vtedy volal GarminPayload. Každé pole v ňom - telesný tuk, voda, kostná hmota, viscerálny tuk - je vlastnosť ľudského tela, nie API. Doménový typ som pomenoval podľa jeho prvého konzumenta a to nie je vidieť, kým sa neobjaví druhý.

Vyriešil som to takto:

/** Alias for GarminPayload - used by the exporter system. */
export type BodyComposition = GarminPayload;

Neprekrstil som ho, len aliasoval, vydal MQTT exportér a starý názov nechal ležať pod tým. Ten šev tam zostal týždne.

Dnes sa to volá BodyComposition a kontrakt exportéra má pole, ktoré prvá verzia nemohla predvídať:

export interface Exporter {
  readonly name: string;
  /** True when this exporter honours `context.timestamp`. */
  readonly supportsBackdate?: boolean;
  export(data: BodyComposition, context?: ExportContext): Promise<ExportResult>;
  healthcheck?(): Promise<ExportResult>;
}

Niektoré váhy si merania odložia, kým nikto nepočúva, a vysypú ich pri najbližšom pripojení. Tie merania majú vlastný čas. Iné platformy spätne datovaný záznam prijmú, ďalšie mu ticho dajú aktuálny čas, čo je horšie, než keby ho odmietli. Ten príznak pomenúva tento rozdiel a nebolo ho kde vyjadriť, kým exportér nemal vlastné rozhranie.

Čo ten projekt stál

Protokoly boli hotové. Väčšina prišla z openScale, ktorý túto prácu zbiera roky, a preportovať dekodér je otázka jedného poobedia.

Drahá bola šírka. Rozbehať to pre seba bol jeden večer. Pre človeka, ktorého váhu nevlastním, na stroji, na ktorý sa neviem prihlásiť, to zabralo mesiace: konfiguračná schéma, sprievodca, ktorý váhu nájde za teba, diagnostický príkaz na prípady, keď je problém Bluetooth a nie váha, Home Assistant add-on, ESP32 proxy pre tých, ktorých kúpeľňa nesusedí so serverom.

Žiadna z tých vecí nie je v zaujímavej časti kódu, ale bez nich by to nespustil nikto okrem mňa.

Čo beží u mňa

Pi Zero 2W s 512 MB RAM, spustené v standalone režime namiesto kontajnera. Docker funguje a na čokoľvek väčšie ho odporúčam, ale na tejto doske stojí démon dosť pamäte na to, aby to bolo cítiť, a ja chcem, aby to Pi robilo viac než jednu vec.

Index S2 som vrátil.

Projekt sa volá BLE Scale Sync, open source pod GPL-3.0.


Zdieľať článok:

Nové články e-mailom

Jeden e-mail, keď vyjde nový článok. Nič iné. E-maily píšem po anglicky.

Najprv ti príde potvrdzovací e-mail. E-maily za mňa posiela Buttondown (USA) a v každom je odkaz na odhlásenie jedným klikom. Ochrana súkromia

Radšej čítačka? Sleduj cez RSS


Nasledujúci článok
Prestal som otvárať schránku, aby som zistil, či niečo prišlo