Superb Garbages 2

千野純一(chinorin)のはてなダイアリーの続きです。

薬飲み忘れマシーン

・飲み忘れるのが目的ではないのだが、タイトルというものはこれくらい自由でよいのです。

脳出血既往につき、とにかく1日1回朝食後に薬を飲まなきゃいけないのだがもうひたすら忘れるというか、薬を飲むことに興味がなさすぎて薬を飲むという行動をしたこと自体全然覚えられない。「薬飲まなきゃ」と思った記憶やシートから薬を出した記憶だけはあるのだが、果たしてその記憶は10分前のことなのか、それとも前日なのか、まさか前日も飲んでないのかも全くわからず、最初は曜日対応のピルケース的なものを自作してやってたんだけど、すぐに紛失した。(⋯)

・とにかく薬を飲むことが大脳的に意味がないから、必ず自動行動にしちゃうんだよなあ。自動行動っていうのは造語で「わざわざ俺の意識がやらなくてよい行動を全部無意識に任せて、意識はその間別のことを考えてる」という現象。トイレ、風呂、歯磨き、たいていの食事、パネルでポン(最近またやらなくなったけど)、サーモンランなどの時間がこれに該当する。そして自動行動している間のその行動に関する記憶のほとんどは速やかに消滅する。正に「メシはまだかいのう」状態だ。

・というわけで、薬を飲んだ時間を記憶してくれるデバイス&仕組みを作った。機能としてはくだらない部類だとは思うし基礎技術的ではあるものの、仕組み的にはなかなか楽しげなものができたんじゃないかな?

・それが表題の「薬飲み忘れマシーン」である。ウェブサーバに設置したPerlCGIスクリプトと、Seeed社のマイコンボードXiao ESP32C3Wi-Fi機能を使ったデバイスで、最後に薬を飲んでからだいたいどれくらい経ったかを常にLEDで表示するというもの。最後に薬を飲んだ時間はウェブ上に記録し、新たに薬を飲んだときはデバイスのボタンを押せばPerlスクリプトが記録を更新してくれる。また、(デバイスWi-Fiパスワードとかの関係で自宅用なので)自宅にいないときはブラウザでCGIにアクセスすればボタンを押したのと同じ操作をすることができる。

・相変わらず誰も読まないと思うが、そのスクリプト、スケッチ、回路図を公開しておく。ChatGPT先生*1と相談しながらほとんどのソースを書いてもらったので色々無駄があるというか、もっと洗練させた設計にもできたんだが、まーとりあえずね。動けばいいのよ動けばの精神で。

・例によってソースコード/スケッチのライセンスはCC0著作権は主張しません。ご利用は自己責任で。


・まずPerlスクリプト。ファイル名はtime.cgiで、URLの末尾に ?mode=xxx という文字列をくっつけるいわゆるGETメソッドにより指定する4つのモードがある。

  • デフォルト:マイコン用、最後にボタンを押してから何時間経ったかという数字(0~99の範囲)だけを返すモード。
  • ?mode=reset:マイコン用、ボタンを押したときのモード。サーバが起動してからの秒数をtime.txtに書き込む。
  • ?mode=html_disp:ブラウザ用、数値表示モード。最後にボタンを押してから何時間経ったかという数字(0~99の範囲)と、最後にボタンを押してからの秒数(これは動作確認のための仕組み)、それからhtml_resetモードへのリンクを表示するHTMLを返す。
  • ?mode=html_reset:ブラウザ用、ボタンを押したときと等価のモード。サーバが起動してからの秒数をtime.txtに書き込み、その後html_dispモードのURLにリダイレクトする。
#!/usr/bin/perl
use strict;
use warnings;
use CGI;

# CGIオブジェクトの作成
my $cgi = CGI->new;

# モードの取得
my $mode = $cgi->param('mode') || '';

# time.txtのパス
my $time_file = 'time.txt';

# モードに応じた処理
if ($mode eq 'html_disp') {
    # HTMLを出力
    print $cgi->header('text/html; charset=UTF-8');
    print "<html><body>\n";

    # time.txtの内容を表示
    open(my $fh, '<', $time_file) or die "Cannot open $time_file: $!";
    my $time = <$fh>;
    close($fh);

    # 現在の時刻から経過時間と経過秒数を計算
    my $current_time = time;
    my $elapsed_hours = int(($current_time - $time) / 3600);
    my $elapsed_seconds = $current_time - $time;

    # 経過時間を表示
    print "経過時間: $elapsed_hours 時間<br>\n";

    # 経過秒数を表示
    print "経過秒数: $elapsed_seconds 秒<br>\n";

    # RESETリンクを表示
    my $reset_url = $cgi->url() . '?mode=html_reset';
    print "<a href=\"$reset_url\">RESET</a>\n";

    print "</body></html>\n";
} elsif ($mode eq 'html_reset') {
    # 現在のlocaltimeをtime.txtに書き込む
    my $current_time = time;
    open(my $fh, '>', $time_file) or die "Cannot open $time_file: $!";
    print $fh $current_time;
    close($fh);

    # 1秒待ってhtml_dispにリダイレクト
    print $cgi->redirect(-url => 'time.cgi?mode=html_disp', -status => 302, -delay => 1);
} elsif ($mode eq 'reset') {
    # 現在のlocaltimeをtime.txtに書き込む
    my $current_time = time;
    open(my $fh, '>', $time_file) or die "Cannot open $time_file: $!";
    print $fh $current_time;
    close($fh);

    # 1秒待ってtime.cgiにリダイレクト
    print $cgi->redirect(-url => 'time.cgi', -status => 302, -delay => 1);
} else {
    # time.txtの内容を取得
    open(my $fh, '<', $time_file) or die "Cannot open $time_file: $!";
    my $time = <$fh>;
    close($fh);

    # 現在の時刻から経過時間を計算
    my $current_time = time;
    my $elapsed_hours = int(($current_time - $time) / 3600);

    # 経過時間の範囲を制限
    $elapsed_hours = 99 if $elapsed_hours > 99;

    # HTTPヘッダを出力
    print $cgi->header('text/plain; charset=UTF-8');

    # 経過時間を出力
    print $elapsed_hours;
}

・次、Arduino言語による、Xiao ESP32C3用のスケッチ。

・ピン名の定数とか無意味に冗長だったりwifiConnectedという変数に全く意味なかったりするけど気にしない。こういうのっておそらくWi-Fiに接続できなかったときの云々とかが必要なんじゃないか(そしてそのためにwifiConnetcted変数があるんじゃないか)と思うが、まー部屋の中で使うし、必ず一発で繋がる前提である。なんか困ったらマイコンのリセットボタンを押す。w

・GPIOにはとにかくLED5個とボタンがひとつ接続する。A0のLEDはパイロットランプの役割で、普通に点滅させるの目障りだったので滑らかに明るくなったり暗くなったりするようにした。D1~D4のLEDは経過時間インジケータ。6時間ごとに1個ずつ点灯していき、24時間以降は全点灯する。なおこれらのLEDは起動時とかWi-Fi接続時とかに軽く電飾動作をする。

・起動時と、その後10分ごとにWi-Fiに接続してCGIを見に行く。デフォルトモードのCGIは数字だけを返すので、それを見て上記LEDを光らせる。D5に接続されたボタンが押されたらCGIのリセットモードにアクセスする。って感じ。

#include <WiFi.h>
#include <HTTPClient.h>

const char* ssid = "Wi-Fi SSID";
const char* password = "Wi-Fi Password";
const char* url1 = "http://hoge.jp/fuga/time.cgi?reset"; // resetモードのURL
const char* url2 = "http://hoge.jp/fuga/time.cgi"; // デフォルトモードのURL

const int pinD1 = D1;  // D1ピンの番号 6~11high
const int pinD2 = D2;  // D2ピンの番号  12~17high
const int pinD3 = D3;  // D3ピンの番号  18~23high
const int pinD4 = D4;  // D4ピンの番号  24以上 high
const int pinD5 = D5;  // D5ピンの番号  ボタン(pullup)
bool wifiConnected = false;  // Wi-Fi接続状態のフラグ

unsigned long blink_time,now,min10;
bool first=false;  // 初回起動時フラグ
bool litplus=true; 
int litduty;

void setup() {
  pinMode(pinD1, OUTPUT);
  pinMode(pinD2, OUTPUT);
  pinMode(pinD3, OUTPUT);
  pinMode(pinD4, OUTPUT);
  pinMode(pinD5, INPUT_PULLUP);

  digitalWrite(pinD1, LOW);
  digitalWrite(pinD2, LOW);
  digitalWrite(pinD3, LOW);
  digitalWrite(pinD4, LOW);

  now=millis();
  min10=now;
  blink_time=now;
}

void loop() {
  now=millis();

  // A0ピン呼吸
  if((now-blink_time)>50){
    if(litplus){
      litduty++;
    } else {
      litduty--;
    }
    if(litduty==60) litplus=false;
    if(litduty==0) litplus=true;

    analogWrite(A0, litduty);
    blink_time=now;
  }

  // D5ピンを監視
  if (digitalRead(pinD5) == LOW) {
    if (!wifiConnected) {
      analogWrite(A0, 100);
      digitalWrite(pinD1, HIGH);
      digitalWrite(pinD2, HIGH);
      digitalWrite(pinD3, HIGH);
      digitalWrite(pinD4, HIGH);
      // Wi-Fiに接続
      connectToWiFi();
      // URL1にリクエストを送信
      sendRequest(url1);
      // Wi-Fiを切断
      disconnectFromWiFi();
      // D1~D4ピンをLOWにする
      digitalWrite(pinD4, LOW);
      delay(300);
      digitalWrite(pinD3, LOW);
      delay(300);
      digitalWrite(pinD2, LOW);
      delay(300);
      digitalWrite(pinD1, LOW);
      delay(300);
      analogWrite(A0, 0);
    }
  } else {
    // 10分ごとにWi-Fiに接続
    if (((now-min10)>=600000)||!first) {
      first=true;
      analogWrite(A0, 100);
      delay(200);
      digitalWrite(pinD1,LOW);
      delay(200);
      digitalWrite(pinD2,LOW);
      delay(200);
      digitalWrite(pinD3,LOW);
      delay(200);
      digitalWrite(pinD4,LOW);
      delay(200);

      min10=now;
      if (!wifiConnected) {
        // Wi-Fiに接続
        connectToWiFi();
        // URL2にリクエストを送信
        int response = sendRequest(url2);
        // Wi-Fiを切断
        disconnectFromWiFi();

        digitalWrite(pinD1,HIGH);
        delay(200);
        digitalWrite(pinD2,HIGH);
        delay(200);
        digitalWrite(pinD3,HIGH);
        delay(200);
        digitalWrite(pinD4,HIGH);
        delay(200);
        digitalWrite(pinD1,LOW);
        delay(200);
        digitalWrite(pinD2,LOW);
        delay(200);
        digitalWrite(pinD3,LOW);
        delay(200);
        digitalWrite(pinD4,LOW);
        delay(200);

        // 数字に基づいて処理を行う
        if (response >= 6) digitalWrite(pinD1, HIGH);
        delay(200);
        if (response >= 12) digitalWrite(pinD2, HIGH);
        delay(200);
        if (response >= 18) digitalWrite(pinD3, HIGH);
        delay(200);
        if (response >= 24) digitalWrite(pinD4, HIGH);
        delay(200);
        }
      }
    }
  }


void connectToWiFi() {
  WiFi.begin(ssid, password);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.println("Connected to Wi-Fi");
  wifiConnected = true;
}

void disconnectFromWiFi() {
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  Serial.println("Disconnected from Wi-Fi");
  wifiConnected = false;
}

int sendRequest(const char* url) {

  HTTPClient http;
  
  // HTTPリクエストを開始
  http.begin(url);
  
  // GETリクエストを送信
  int httpResponseCode = http.GET();
  
  int response = 0;
  if (httpResponseCode == HTTP_CODE_OK) {
    // レスポンスの取得
    String payload = http.getString();
    response = payload.toInt();
  } else {
    Serial.print("HTTP Request failed with error code: ");
    Serial.println(httpResponseCode);
  }
  
  // リクエストの終了
  http.end();
  
  return response;
}

・そしてデバイスの回路図。実際は、ユニバーサル基板にピンソケットをはんだづけしてそこにピンヘッダ実装済Xiao ESP32C3やLEDが刺さる形にした。

・ま、単純なもんね。LEDは左から動作確認用、6時間経過、12時間経過、18時間経過、24時間経過のインジケータで、それぞれ緑,緑,黄,赤,自点滅の赤を使ったがこれらの色は何でもいい。

・その隣のボタンは薬飲んだ時に押す用。マイコン内でプルアップしているのでGNDに直接繋いでおり、つまりLOWでトリガー。

・右上のショットキーバリアダイオードは逆流防止用で5V端子から給電する場合は必須とのことだが、結局USB端子から給電しているのでこれも今のところ無意味だな。


・現状のいいところ:

  • 10分に1回軽くピカピカしてるのはがんばって仕事してる感じがしてよい。

・機能改善案:

  • やっぱり7セグかなんかで数字をそのまま表示した方がよくない?

  • CGIで書き込む時間は実時間にした方がいろいろ応用がきくんじゃない?

  • CGIで書き込む時間についてはログを残したり、間違えてボタンを押したときのためにキャンセルができたりした方がよくない?

  • ボタンを押したときにWi-Fiに接続するときのピカピカはやるが、もっとハッキリ「更新完了!」を主張するべき。(スケッチ書き換えるだけだから簡単にできるが)

・ そのうち作り直すかもしれないけど、まー要件は満たしており困っていることがあるわけでもないので、まー気が向いたときにマイコンのスケッチを書き換えるくらいかも。

*1:しかも3.5。GPT-4は喋るの遅いからめんどくさい