5 ポイント 投稿者 GN⁺ 2024-03-05 | 1件のコメント | WhatsAppで共有
  • 2020年に1型糖尿病と診断されたエンジニアが、血糖管理を障害対応のように扱うためにGoベースの個人モニタリングシステムを作成
  • Libreには公式のAPIやSDKがなく直接連携が難しかったが、Miao MiaoとTomatoアプリを経由して、2分ごとに血糖データを外部エンドポイントへ送る経路を見つけた
  • Goのecho serverとEncoreでのデプロイを使ってTomatoのリクエスト形式を確認し、SGV値を変換してGrafanaダッシュボードにリアルタイムの血糖ゲージとして表示
  • Telegramボットは食事・運動・インスリン注射のようなコンテキスト注釈を残し、5分ごとに実行されるcronは血糖が基準値を下回るとincident.ioのインシデントを作成
  • 通知、エスカレーション、レポートを組み込んだこのワークフローは、低血糖や測定の空白を一人で抱え込まないための個人健康オブザーバビリティシステムとして機能する

1型糖尿病を障害対応の問題として扱う

  • 1型糖尿病は、膵臓が炭水化物をエネルギーに変えるために必要なインスリンをほとんど、またはまったく作れなくなる自己免疫疾患
  • インスリンは注射で補う必要があり、投与量は食べるものやさまざまな変数によって変わる
  • 1型は生活習慣が原因で起こる病気ではなく、現時点で治療法はない
  • 血糖は継続的にモニタリングする必要がある
    • 高すぎる状態が長く続くと、臓器障害や寿命の短縮につながる可能性がある
    • 低すぎる状態は短時間でも致命的になりうる
    • 血糖が下がりすぎると安全に飲食できなくなり、他人の助けが必要になる場合がある
  • 食べ物や飲み物を見るたびに炭水化物量と必要なインスリン量を計算する必要があり、短い散歩のような運動でも体内で有効なインスリン量を考慮しなければならない
  • 血糖の可視性が失われる状況は、業務システムでオブザーバビリティがゼロになる状態に近いものとして扱える
    • 業務ならインシデントを宣言し、可視性が回復するまでクローズしない
    • 低血糖は何かがうまくいっていない最初のシグナルであり、即時対応が必要な状態

Libre、Miao Miao、Tomatoでデータ経路を見つける

  • 腕に装着するLibreデバイスは、スマートフォンを近づけると血糖を読み取れる
    • デバイスは2週間ごとに交換する必要がある
    • 血糖が特定の赤い線を下回ると、炭水化物を摂取する必要がある
    • 緑の範囲を外れると、インスリン投与などの対応を検討できる
  • 運動、体重増加、病気はインスリン感受性を変えるため、血糖管理は常に動く目標になる
  • Libreがエラーを表示すると、デバイスから血糖を読み取れず、危険レベルの通知も受け取れない
  • Libreはクローズドなデバイスなので、APIやSDKなしに直接データを取得するのは難しい
  • Miao MiaoはLibreの上に取り付けて約2分ごとにスキャンし、Tomatoアプリへ結果を送る
    • 標準のLibreアプリがエラーを表示しても、Miao Miao経由の経路ではデータを継続して配信する
    • 標準アプリは血糖の急激な変化を異常値と見なしてデータを送らないようで、ユーザーはそのようなデータも自分で判断したいと考えた
  • Tomatoアプリには、血糖を5分ごとにGoogle Calendarイベントとして配信する機能があり、Apple Watchのコンプリケーションで血糖を確認できた

Goのecho serverでTomatoのリクエストを確認する

  • Tomatoアプリの設定には、Nightscout用のデータ同期URL入力欄があった
  • Nightscout URLの代わりに自分で運用するWebサーバーのアドレスを入れると、ゲートウェイログに次のパスが現れた
/id/e1d67817-4591-4e8e-9bca-58a07a1087d8/api/v1/devicestatus
  • リクエスト本文を確認するため、Goで簡単なecho serverを書いた
  • デプロイと実行にはencore.devを使い、無料でモニタリングシステムを運用できた
// encore:api public raw method=POST path=/id/:id/api/v1/devicestatus
func Echo(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", req.Header.Get("Content-Type"))
    if _, err := io.Copy(w, req.Body); err != nil {
        http.Error(w, "Failed to echo request", http.StatusInternalServerError)
        return
    }
}
  • echo serverは、クライアントが送ったデータをそのまま返すサーバーで、ネットワーク問題の切り分けやアプリケーション動作のテストに役立つ
  • 同じ方法でほかのリクエストも確認し、血糖データを含むレスポンスを得た
- date: 1696171541297
sgV: 73
delta: 0
sysTime: 1696171541381
dateString: "2023-10-01T14:45:41.297Z"
_id: "dOUXaI8HcaulCGrQfxe23UE0"
type: "sgv"
device: "Tomato"
direction: "Flat"
  • sgVは血糖値で、英国で使う単位に合わせるため18で割った
  • レスポンスには日時とトレンド方向も含まれる
  • この呼び出しは2分ごとに発生する

Grafanaダッシュボードと血糖ゲージ

  • 受信した血糖値をGauge metricとして設定した
  • ゲージは上下する単一の数値を表すメトリクスで、メモリ使用量、同時リクエスト数、温度のような変動値の計測に使われる
  • 血糖値はnewValue / 18に変換して記録した
var BloodSugar = metrics.NewGauge[float64]{
    name: "blood_sugar",
    metrics.GaugeConfig{},
}
BloodSugar.Set(float64(newValue) / 18)
if err := insertReading(ctx, newValue); err != nil {
    rlog.Error(msg: "failed to insert blood sugar, proceeding", keysAndValues: "db_err", err)
}
  • 血糖データはベストエフォート方式でPostgresにも保存した
    • DB保存の失敗がほかのロジックまで失敗させないよう、ログだけ残して処理を続行する
  • Grafanaダッシュボードは、2分ごとに更新される血糖ゲージをリアルタイムで表示する
  • 目標血糖範囲は理想的には4から9の間
  • ダッシュボードを共有したり自宅にモニターを置いたりすれば、血糖状態をひと目で確認できる

Telegramボットで血糖のコンテキストを追加する

  • 血糖値だけでは十分ではなく、複雑なシステム監視と同じくコンテキストが重要
  • 食後すぐに血糖が上がっても、すでにインスリンを打っていれば、すぐに心配すべき状況ではないかもしれない
  • Grafanaは注釈をサポートしているが、ログインして直接注釈を残す方式は負担が大きい
  • Telegramボットを作り、メッセージを受け取るたびにWebhookで注釈を追加するよう構成した
// encore:api public raw method=POST path=/webhook
func Webhook(w http.ResponseWriter, req *http.Request) {
    body, err := io.ReadAll(req.Body)
    if err != nil {
        http.Error(w, "could not read request body", http.StatusBadRequest)
        return
    }
    defer req.Body.Close()

    var update Update
    err = json.Unmarshal(body, &update)
    if err != nil {
        http.Error(w, "could not unmarshal JSON", http.StatusBadRequest)
        return
    }
}
  • Webhookはリクエストを受け取り、メッセージを検証し、JSONをアンマーシャルしたうえで処理結果をTelegramへ返す
  • メッセージ検証後に整数をパースし、注釈サービスを呼び出す
  • 注釈サービスは活動内容に応じてタグを付ける
func getTags(ctx context.Context, activity string) []string {
    a := strings.ToLower(activity)
    switch a {
    case "walk", "run", "ran", "gym":
        return []string{"exercise"}
    case "eat", "ate":
        return []string{"food eaten"}
    case "inject":
        return []string{"inject"}
    default:
        return []string{"other"}
    }
}
  • Grafanaではタグごとの色を指定し、注釈クエリを設定できる
  • 食事、運動、インスリン注射のようなコンテキストがグラフ上に表示されると、本人やダッシュボードを見る人が血糖状況をより理解しやすくなる

低血糖をインシデントに変える

  • 血糖の可視性は確保できたが、通知がなければリアルタイム対応は難しい
  • 看護師から受けた指示に従い、血糖が4未満に下がったら処置を始める必要がある
  • 5分ごとに測定値を確認する小さなcronを書き、事前定義した下限より低ければインシデントをトリガーする
var _ = cron.NewJob(
    id: "monitor-blood",
    cron.JobConfig{
        Title: "monitor blood to check if there is reason to open an incident",
        Every: 5 * cron.Minute,
        Endpoint: BloodIncidentCron,
    },
)
if r < BloodLowerLimit {
    if err := triggerIncident(ctx); err != nil {
        return fmt.Errorf("failed to trigger an incident: %w", err)
    }
}
  • Telegramボットには、"i need help"メッセージで手動インシデントを開くコードも追加した
  • インシデントがトリガーされると、別のマイクロサービスがincident.ioにインシデントを作成する
  • incident.ioを選んだ理由は、Cloudflareの社内インシデントツールに最も近く、しかも完全にGoで書かれているから
payload := Payload{
    IdempotencyKey: idemKey.String(),
    Mode: incidentModeStandard,
    Name: fmt.Sprintf("Matt's blood sugar is currently #%s", blood),
    SeverityID: incidentSeverityCritical,
    Summary: "Matt's blood sugar is low!",
    Visibility: "public",
}
  • 実際のインシデントツールを使えば、エスカレーションポリシーを設定でき、インシデント状態で過ごした時間のようなレポートも見られる
  • incident.ioのワークフローではZapierに似たルールを構成でき、購読者へSMS通知を送ることもできる
  • 例のワークフローでは、インシデントが開いたときに本人へSMSを送り、20分間クローズされなければパートナーや兄弟姉妹へ自動エスカレーションする

レポートと今後の改善

  • インシデントツールを使うと、低血糖インシデントが時間とともにどれだけ発生したかをレポートで確認できる
  • 例のグラフはブログ用のデモデータで、実際のデータはありがたいことにもっと退屈な水準
  • 低血糖イベントが増加傾向なら、現在の治療方法が合っていないシグナルかもしれず、医師に相談する根拠になる
  • 現在または計画中の改善項目は次のとおり
    • 自動終了: 今はデータを確認するため手動でインシデントを閉じているが、閉じるのが遅れてエスカレーションされたことが何度かあるため、15分ほど安定した血糖が続けば自動終了できる
    • データとの対話: 何年分もの血糖データを保存しており、LLMを学習させて「なぜいつも午後3時に下がるのか?」のような質問をし、Google Calendarのような他のデータも併用したい
    • グラフ拡張: 現在のグラフはシンプルだが、糖尿病管理の成功を判断するhba1cのような指標を追加できる
    • 障害防止の強化: このシステムは重要なので、失敗をより多く防げるほどよい

コーディングで作った個人向け健康運用システム

  • 1型と診断された当初は、この疾患が人生を難しくするのではないかという恐れがあった
  • このプロジェクトは、1型の状態を理解し、管理・モニタリングする助けになっている
  • 血糖管理を分散システムのように扱い、オブザーバビリティ、通知、インシデント、エスカレーション、レポートを備えた個人運用システムにした
  • コーディング能力によって、1920年代までなら死の宣告だった病気の管理をある程度自動化できるようになった

1件のコメント

 
GN⁺ 2024-03-05
Hacker Newsのコメント
  • 糖尿病と16年間付き合ってきたが、筆者の雰囲気はとてもよく、尊敬できる。病気のことで悲しくなったり腹が立ったりしたときには、この記事を読み返して、すべてに対処できると確信していた今の気持ちを思い出せるといいと思う
    簡単なことではない。精神的にも実務的にもそうで、私の場合は何年ものあいだ「楽しく制御不能」に近い状態だった
    つい昨日も、糖尿病人生で初めて36時間目標範囲を維持できたのを見て、本当に泣いた。36時間ずっと100%緑色だった。もしかすると、ようやく分かってきたのかもしれない
    数年後、生活の質が60%ほど跳ね上がったのは、CGMにつないだマイクロポンプ、ほかの糖尿病患者との経験共有、NightScout VMを導入してスマホ・時計・デスクトップウィジェット・目覚まし時計・冷蔵庫のどこからでも血糖値とその推移を見られる環境、そしてドイツ的な精密さで炭水化物を数え、食前ちょうど15分前に正確にボーラスを打つようにしたおかげだった。こんな暮らし方は本当に嫌だが、効果はある
    耐えろ。気を緩めるな

  • 文体が本当に素晴らしく、読んでいて楽しかった。偶然にも私はペネトレーションテストの仕事をしているのだが、同僚の1人が1型糖尿病を患っている
    この記事の概要と、それによって生じる複雑さを見て、毎日そうした困難を乗り越えながら、それでもチームで最も親切で話しかけやすい人の1人であり続けている同僚に、より共感できるようになった

    • 糖尿病があっても普通の人生を送れるし、トップクラスのテニス選手にだってなれる: https://en.wikipedia.org/wiki/Alexander_Zverev#Personal_life
      ただ、この記事を読んで、そう生きるために必要な自己規律のレベルを本当に実感した
    • 「ペンテスティング」が糖尿病のペンをテストする仕事なら、一部はペンと紙でやるテストかもしれない、という冗談は言えなくもない
      もちろん、ペネトレーションテストのようにシステムを破って侵入しようとする仕事なのかもしれないが、その仕事をしているのか気になる
    • 良い雇用主に恵まれたようだ。多くの会社は病気のある従業員を採用しなかったり解雇したりするし、実際、職場で医療情報を共有しないよう助言されるのが一般的だ
      最近では、誰もが昇進すると予想していた従業員が昇進できなかった事例を読んだ。罪といえば、同僚に「車が壊れていて、しょっちゅう修理しなければならず疲れた」と短く話したことだけだった
      昇進するとある程度の通勤が必要になる予定だったが、マネージャーは「昇進させようとしていたが、車の調子が悪いと聞いた」と言ったそうだ
  • GopherConSGでMattがこの発表をしたあと、私たちは自分のデータを所有できていない問題について話した。データ提供者側、つまり血糖をモニタリングする会社が負うべき法的責任が多く、かなり複雑な話だという点が印象的で、大企業がオープン化をためらう理由も理解できる
    一方で、利用者が正当に自分のものであり、実際の行動に使えるデータにアクセスできないのは、かなり不当にも見える

    • この点はいつも理解できない。BICのペンを買うときに、そのペンによって生み出されるすべてのエントロピーがデータ作成者ではなく道具の製造者の所有物になるという必須ライセンス契約を結ばされるようなものだろうか。人がいなければ、そのデータ自体も存在しなかったはずなのに
  • 昨日、これに関連するものを1つ作った。ターミナルのステータスアイコンとして血糖データを表示するtmuxプラグイン
    私も1型糖尿病で、CGMとしてDexcomを使っており、今のところはその環境向けに動くようにしてあるが、ほかの機器にも対応し、機能もさらに増やしたい。興味があればここで見られる
    https://github.com/Cian911/tmux-xdrip

    • Dexcomが時計と直接同期してくれればいいのにと思う。何年も待っているが、たぶん時計のバッテリーをかなり消費するのだろう
    • tmuxの中で生きている1型糖尿病患者として、これは本当にすごい。できるだけ早く試してみたい
  • Miao Miaoのアイデアは興味深い。1型糖尿病患者として、Freestyle Libreは本当に人生を変えてくれた素晴らしい機器だが、ソフトウェアはかなりひどい
    1年以上前にバグを報告したのに、Abbottはいまだにバグだと認めていない。オンラインやGoogle Playのアプリレビューにも同じ報告がたくさんあるのにだ
    興味がある人や、Abbottに認めさせるために力を貸せる人のために言うと、LibreLinkをおやすみモードの例外アプリ一覧に入れても効果がなく、おやすみモードに入ると即座に「アラームを使用できない」という警告が出るバグだ。業務環境でおやすみモードを有効にすると、血糖アラームまで無効になってしまって不便だ

  • 少し関連する話だが、6か月前に閉塞性睡眠時無呼吸症と診断され、就寝中に一定圧の空気を送り込んで気道の閉塞を防ぐCPAP機器を使わなければならなくなった。1型糖尿病ほどミッションクリティカルではないが、これらの機器は予想以上に多くのログを残す
    この記事を読みながら、睡眠時無呼吸の分野でも、モニタリングやより重いケース向けに似たようなハックがあるのか気になった
    OSCARはすでに知っていて、本当にありがたく使っているが、それ以外に何があるのかも知りたい
    1: https://www.sleepfiles.com/OSCAR/

    • むしろ命に直結すると言っていいと思う。CPAPは私の人生を変えたし、長期間治療しない睡眠時無呼吸は心臓をはじめさまざまな健康問題を引き起こす
      睡眠検査で最悪だったときには、呼吸停止イベントが毎分48回ほど記録された。舌が気道に巻き込まれると、かなり呼吸しづらくなる
    • 機器が対応しているなら、https://home.sleephq.com/がこのデータをうまく扱ってくれる
  • 興味深いが、この分野のオープンソースソフトウェアであるNightscoutやxDripなどを見ていない理由があるのだろうか
    新しい問題ではなく、すでに多くの人が、より機能豊富でよく動く解決策で対処している

    • 見てはいた。自分がやろうとしていたことには、最初から作るほうが早そうだったし、楽しい学習体験でもあった
  • Scott Hanselmanは何年にもわたって1型糖尿病とさまざまな技術的ハックについて、ブログやYouTubeで取り上げてきた。以前は血糖値がターミナルプロンプトに表示されるようハックしていたこともある: https://www.youtube.com/watch?v=_meKUIm9NwA

  • 「この期間は機器で血糖値を測定できず、血糖値が危険なレベルに達しても通知を受け取れない。このエラーは最もストレスの高い瞬間に現れる癖があった」と、「調べてみると、メインアプリは血糖値が急変して外れ値のように見えるデータを送るのを嫌っているようだった」という箇所は、命に関わるバグのように聞こえる

    • ほとんどのセンサーはこういうふうに動く。センサーは常に正確だと想定されがちだが、実際にはそうではない。たとえば寝ている間にセンサーの上に体を乗せると、グルコースオキシダーゼストリップが体の内側にさらに押し込まれて、読み取り値が変わることがある
      厳密に言えば、CGMデータだけで治療の判断を下してはいけない
      Dexcomセンサーの場合、一部のアプリは生データを取得してこの過程を回避できる。Libre、少なくともLibre 3では、特定の状況でその値を誤読ではなく血糖値が急速に変化している最中だと判断した場合、あとからデータを補完しているのを見たことがある
  • 技術ベースではないが、糖尿病アラート犬もいる
    よく訓練された犬が、技術ベースのモニタリング・通知システムを補完して飼い主に警告するうえで大きな成果を上げていることを、最近学んでいる