・飲み忘れるのが目的ではないのだが、タイトルというものはこれくらい自由でよいのです。
・脳出血既往につき、とにかく1日1回朝食後に薬を飲まなきゃいけないのだがもうひたすら忘れるというか、薬を飲むことに興味がなさすぎて薬を飲むという行動をしたこと自体全然覚えられない。「薬飲まなきゃ」と思った記憶やシートから薬を出した記憶だけはあるのだが、果たしてその記憶は10分前のことなのか、それとも前日なのか、まさか前日も飲んでないのかも全くわからず、最初は曜日対応のピルケース的なものを自作してやってたんだけど、すぐに紛失した。(⋯)
・とにかく薬を飲むことが大脳的に意味がないから、必ず自動行動にしちゃうんだよなあ。自動行動っていうのは造語で「わざわざ俺の意識がやらなくてよい行動を全部無意識に任せて、意識はその間別のことを考えてる」という現象。トイレ、風呂、歯磨き、たいていの食事、パネルでポン(最近またやらなくなったけど)、サーモンランなどの時間がこれに該当する。そして自動行動している間のその行動に関する記憶のほとんどは速やかに消滅する。正に「メシはまだかいのう」状態だ。
・というわけで、薬を飲んだ時間を記憶してくれるデバイス&仕組みを作った。機能としてはくだらない部類だとは思うし基礎技術的ではあるものの、仕組み的にはなかなか楽しげなものができたんじゃないかな?
・それが表題の「薬飲み忘れマシーン」である。ウェブサーバに設置したPerlのCGIスクリプトと、Seeed社のマイコンボードXiao ESP32C3のWi-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;
my $cgi = CGI->new;
my $mode = $cgi->param('mode') || '';
my $time_file = 'time.txt';
if ($mode eq 'html_disp') {
print $cgi->header('text/html; charset=UTF-8');
print "<html><body>\n";
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";
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') {
my $current_time = time;
open(my $fh, '>', $time_file) or die "Cannot open $time_file: $!";
print $fh $current_time;
close($fh);
print $cgi->redirect(-url => 'time.cgi?mode=html_disp', -status => 302, -delay => 1);
} elsif ($mode eq 'reset') {
my $current_time = time;
open(my $fh, '>', $time_file) or die "Cannot open $time_file: $!";
print $fh $current_time;
close($fh);
print $cgi->redirect(-url => 'time.cgi', -status => 302, -delay => 1);
} else {
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;
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";
const char* url2 = "http://hoge.jp/fuga/time.cgi";
const int pinD1 = D1;
const int pinD2 = D2;
const int pinD3 = D3;
const int pinD4 = D4;
const int pinD5 = D5;
bool wifiConnected = false;
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();
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;
}
if (digitalRead(pinD5) == LOW) {
if (!wifiConnected) {
analogWrite(A0, 100);
digitalWrite(pinD1, HIGH);
digitalWrite(pinD2, HIGH);
digitalWrite(pinD3, HIGH);
digitalWrite(pinD4, HIGH);
connectToWiFi();
sendRequest(url1);
disconnectFromWiFi();
digitalWrite(pinD4, LOW);
delay(300);
digitalWrite(pinD3, LOW);
delay(300);
digitalWrite(pinD2, LOW);
delay(300);
digitalWrite(pinD1, LOW);
delay(300);
analogWrite(A0, 0);
}
} else {
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) {
connectToWiFi();
int response = sendRequest(url2);
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.begin(url);
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に接続するときのピカピカはやるが、もっとハッキリ「更新完了!」を主張するべき。(スケッチ書き換えるだけだから簡単にできるが)
・ そのうち作り直すかもしれないけど、まー要件は満たしており困っていることがあるわけでもないので、まー気が向いたときにマイコンのスケッチを書き換えるくらいかも。