0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

薬飲み忘れ防止装置

0
Posted at

はじめに

薬の飲み忘れがはげしい。さらに飲んだかどうかの記憶があやしい。「あれ?きょう飲んだっけ?」。おくすりカレンダーが良いのかもしれないがちょっと恥ずかしい。

完成品

まず完成品から。こんなものを作ります。

要件

  • 定刻に通知
  • 服用有無
  • 電池駆動

要件的には大したことはないので簡単!

運用

  • 朝7時に通知開始(LED点滅)
  • 薬を飲む(薬箱で動作検知)
  • 通知停止(LED点滅停止)

薬箱で動作検知
ボタン押すとかLINE通知とかは嫌だ。
薬箱を動かす → 検知 → 薬を飲んだ!
※薬を飲む以外で薬箱をいじることはないんで。

これまでの服用動作となにも変わらないのがいい!いちいちボタン押すのはなしで。

LEDが点滅してなければ、今日はもう飲んだことが分かる!

電池駆動

これが一番大変!!

  • とにかく省電流
  • とにかくディープスリープにぶっ込む
  • 1年は電池交換なしで

部品一覧

部品 値段 個数 備考
MCU ATTINY44A-PU ¥210 1 秋月電子
リアルタイムクロック RX8900 ¥700 1 秋月電子
赤外線受信機 VS1838B ¥30 1 Amazon
ReedSwitch MKA-10110 ¥50 1 秋月電子
電池 CR20321 ¥100 1 Amazon
電池BOX - ¥55 1 Amazon
LED OSO5PA3133A-1MA1 ¥17 1 秋月電子
抵抗 1MΩ,2KΩ,10KΩ - 1 -
コンデンサー 0.1μF ¥10 1 秋月電子
ユニバーサル基板 AE-FRSK120-BB-TH-I ¥150 1 秋月電子

その他:ポリウレタン銅線、ICソケット、低ピンソケット、ネオジム磁石、ケース

MCU|ATTINY44A-PU

秋月電子引用

省電力ではありますが、ATmega328P(28Pin)(400円)の方が扱いやすいです。メモリが多いATTINY84A-PU(400円)でも可。ATTINY44A-PUは値段(210円)とサイズ(14Pin)で採用しました。

リアルタイムクロック|RX8900

秋月電子引用

省電力とアラームが付いているヤツというだけで採用。2019年に買ったのですが500円でした。今2025年は750円。
RX-8025NBが良かったんだけどライブラリが見つからなかったので不採用。次の機会に。

今回使用したRX8900ライブラリはこちらです。
https://github.com/citriena/RX8900RTC

赤外線受信機|VS1838B

リアルタイムクロックを使う場合、時刻合わせをする必要があります。その為に赤外線リモコンから4桁(HH:MM)の数字を送信し、このVS1838Bで受信させます。本装置の起動直後は、受信待ち。4桁受信ができればそれを現在時刻としてリアルタイムクロックの時刻合わせをする。
最初の時刻合わせのみに使うので、その時だけ電力を使うようにします。VCCにデジタルピンを使います。VS1838Bの電力はたかがしれてるのでデジタルピンからの電流で足ります。時刻合わせ開始時にDigitalWrite(VCC,HIGH)し、終了時にLOWする。時刻合わせ以外でVS1838Bに流れる電流を無くす。

赤外線リモコン

なんでも良いです。私は↓下のようなリモコンを何かのキットで持っていました。テレビのリモコンで構いません。使用するリモコンのコードを調べて書き換えてください。
CARMP3

    // ---------------------------------------
    // ⑥ コマンドとして使用できるのは「cmd」
    // ---------------------------------------
    switch(cmd) {
        //CARMP3
        case 0x30: num = 1; break; // 1
        case 0x18: num = 2; break; // 2
        case 0x7A: num = 3; break; // 3
        case 0x10: num = 4; break; // 4
        case 0x38: num = 5; break; // 5
        case 0x5A: num = 6; break; // 6
        case 0x42: num = 7; break; // 7
        case 0x4A: num = 8; break; // 8
        case 0x52: num = 9; break; // 9
        case 0x68: num = 0; break; // 0
        default: num = 0x0F; break;
    }

ReedSwitch|MKA-10110

秋月電子引用

リードスイッチは磁石によってON/OFFできるスイッチです。
今回は、薬箱をいじると取り付けた磁石が離れてコイツがOFFになる。

磁石を近づけるとONになり、離すとOFFになる(ノーマルクローズ)。
薬箱に磁石を張り付けておく。通常、薬箱と本装置が近距離状態でON、服用するときOFFになる。

ON状態が長いので、プルアップ抵抗を大きく1MΩにして消費電力を抑えています。
さらにプルアップ先をデジタルピンにしています。通知状態のときのみ検知できれば良いので、通知状態になった時このデジタルピンをHIGHにして電源として使っています。

問題発生

なんかリードスイッチがうまく機能しない。ネオジム磁石を近づけてもONにならない。ネオジム磁石は家にあった100均で買ってあったやつ。近づければONになるんでしょ?ぐらいに思ってた。けっこう強力なので磁力不足ってことはないしなーと思って調べた。

↓こういう丸形ネオジム磁石。上下平らな面にN極、反対側にS極がある。ふむふむ。まーそうだろうね。
image.png

引用元によると、動作パターンには水平タイプと垂直タイプがあり、これは垂直タイプになるようです。なんと↓この図をみるとド真ん中はOFF領域だそうだ。リードスイッチの構造を考えれば当たり前なんだけど深く考えないとハマりポイント。

なので、↓下の図のようにど真ん中はOFFで、ちょっとずらすとONです。

磁界の問題なのでネオジム磁石を立てて近づければド真ん中でもONになります(水平タイプになる)。

なので、今回はリードスイッチの端へ近づけるようにしましょう!!

それと、コイツは気を付けて扱わなと割れます。根本付近で曲げようとするとパリッと逝きます。1cm ぐらい横から曲げるようにしましょう。私は3個ほど割りました。運用時にはスミチューブで包みます。

引用元

電池|CR20321

電池駆動は決めていたのですが、どのタイプの電池にするかです。

ATTiny44(8MHz): 1.8V~5.5V
RX8900:2.5V~5.5V
VS1838B:2.7V~5.5V

となると、3Vは欲しいところです。

  • 単3電池x2
  • 単3電池x1 昇圧
  • エネループx2
  • CR20321x1

などの選択肢がありますが、容量的には単3電池x2が一番良いですが、乾電池は1.5Vを長時間維持ぜずすぐに1.2Vを切ってしまいます。2本でも2.5Vを維持できません。DCDCコンバーターで昇圧するのも良いですがコンバータ自体による消費がありますし配置とコストがある。エネループという選択もありですが、コスパ悪いですよね。CR20321は3V付近を持続してくれるのですが、容量が220mAh。CR20321x1個でなんとか1年もたないですかね?

LED|OSO5PA3133A-1MA1

秋月電子引用

低消費タイプです。制限抵抗2KΩにしました。普通のLEDでは点灯しないレベルです。少しでも省電力へ。
これでもLEDの点灯は長くできません。いちばん電力を喰います。なので点滅でできるだけ点灯時間を短くします。3秒おきに10ミリ秒だけ点灯させます。

ユニバーサル基板|AE-FRSK120-BB-TH-I

FRISKサイズのユニバーサル基板です。さらにブレッドボード配線パターン。

秋月電子引用

回路図

ブレッドボード試作

開発時の様子。下の赤いのがツマミ付きの磁石です。リードスイッチの端に付いているのが分かるかと。

仮計測

私の安物テスターですが、通常(スリープ)状態で2μAでした。期待通りに部品類の電源はOFFになっているようです。

捕らぬ狸の皮算用

いったん、Geminiに聞いてみました。ソースとか回路図とか渡して、「1日3時間の通知が継続するとしてどのくらいの稼働時間か?」

実測されたベース消費電流 2.0μA を適用した場合でも、1日3時間の通知が継続するという厳しい条件下で、理論的な稼働期間は約 3.17 年と推定されます。

ホントかよ!?
3秒おきに10msecしか点灯しないですからね。その3秒もスリープに入れてますし。

実装

黒いヒモがスミチューブで覆われたリードスイッチ
ケースは暫定的に100均ボックス
手のひらサイズ

当初は横に置く想定で上記の手のひらサイズにしたけど、上へ乗せる運用にしたため薬箱と同じ大きさのケースに変更。

実演

通知状態から薬箱を持ち上げて点滅が止まるところまでです。
※スマホを持ちながらの撮影だったのでぶれぶれですみません。

開発環境

  • Windows10
  • VSCode
  • PlatformIO

使用ライブラリ

ソース

PlatformIOですが、ArduinoIDEでも動くと思います。ピン番号だけ修正してください。

折りたたみ(ここをクリック)
#include <Arduino.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <RX8900RTC.h>

//#define _TEST

// --- ピン定義 ※ATTiny44/84 PlatformIO(CounterClockwise) ---
#define LED_PIN 3           // LED出力ピン PA7
#define RTC_INT_PIN 2       // RTC割り込み入力ピン PB2(INT0) SCL:6(PA4) SDA:4(PA6)
#define REED_SW_PIN 5       // リードスイッチ割り込み入力ピン PA5(PCINT5)
#define REED_SW_ON 9        // リードスイッチ電源ピン PA1
#define IR_RECEIVE_ON 8     // IR Remocon Receive PIN VS1838B VCC PA2
#define IR_RECEIVE_PIN 7    // IR Remocon Receive PIN VS1838B recive PA3 [TODO]

// --- グローバルフラグ ---
volatile bool rtc_int_occurred = false;           // RTC割り込み発生フラグ
volatile bool reed_sw_int_occurred = false;       // リードスイッチ割り込み発生フラグ

unsigned long ir_timer_ms = 0;           // 時刻設定タイマー
bool is_notifying = false;               // 服用通知が必要な状態か (true=点滅中)
bool is_rtcInit = false;                 // 日時初期化済フラグ

// --- 定数設定 ---
const uint8_t FLASH_DURATION_MS = 10;           // LEDの点灯時間
const uint8_t NOTIFICATION_CYCLE_SEC = 3;       // 通知周期を 3秒 に設定(服用通知)
const uint8_t DAILY_ALARM_HOUR = 7;             // 毎日アラームを鳴らす時刻(時)服用通知
const uint8_t IR_TIMEOUT_SEC = 60;              // 時刻設定タイムアウト60秒

// --- 関数プロトタイプ ---
void deep_sleep();
void setup_rtc_fixed_timer(uint8_t sec);
void stop_rtc_fixed_timer();
void rtc_alarm_handler();
void enable_reed_interrupt();
void disable_reed_interrupt();
void set_next_daily_alarm(uint8_t hour, uint8_t minute);
void stop_notification();
void RTCInit(uint8_t ir_hour, uint8_t ir_min);
void startIR();
void ir_loop();
uint8_t irRecv();
void blink(uint8_t n);

// --- RTC ---
RX8900RTC rtc;

void setup()
{
    pinMode(LED_PIN, OUTPUT);
    pinMode(RTC_INT_PIN, INPUT_PULLUP);
    pinMode(REED_SW_ON, OUTPUT);
    pinMode(REED_SW_PIN, INPUT); // 外部でプルアップ(1MΩ)
    pinMode(IR_RECEIVE_ON, OUTPUT);
    pinMode(IR_RECEIVE_PIN, INPUT);

    // リードスイッチ割り込み無効化
    disable_reed_interrupt();

    // 3回点滅
    blink(3);

    startIR(); // 赤外線受信開始→時刻合わせ処理へ

    // RTC割り込み設定
    attachInterrupt(digitalPinToInterrupt(RTC_INT_PIN), rtc_alarm_handler, LOW); // FALLINGだとsleepで動作しない

    wdt_disable();
}

void loop()
{
    // 時刻合わせ中 IR受信中
    if (!is_rtcInit)
    {
        ir_loop();
        if (ir_timer_ms == 0)
        {
            ir_timer_ms = millis(); // 時刻合わせタイムアウトを計測開始
        }
        else if (millis() - ir_timer_ms >= (uint16_t)IR_TIMEOUT_SEC * 1000)
        {
            // 電源を入れなおすまで永久スリープ
            // 割り込み定義削除
            detachInterrupt(digitalPinToInterrupt(RTC_INT_PIN));
            disable_reed_interrupt();
            digitalWrite(IR_RECEIVE_ON, LOW); // IR Power OFF
            deep_sleep();
        }
        return;
    }

    // リードスイッチ割り込み発生時
    if (reed_sw_int_occurred)
    {
        blink(3);
        reed_sw_int_occurred = false;
        stop_notification();
    }

    // RTC割り込み発生時
    if (rtc_int_occurred)
    {
        rtc_int_occurred = false;

        // 2-A. アラームフラグをチェックし、クリアする
        if (rtc.alarm())
        {
            is_notifying = true;                            // 服用通知開始フラグセット
            setup_rtc_fixed_timer(NOTIFICATION_CYCLE_SEC);  // LED点滅用タイマーを起動
            enable_reed_interrupt();                        // リードスイッチ割り込み設定
        }

        // 2-B. 固定サイクルタイマーフラグをチェックし、クリアする
        if (rtc.fixedCycleTimer())
        {
            if (is_notifying)
            {
                blink(1); // LED点滅
            }
        }
    }

    // スリープモードの実行
    deep_sleep();
}

// RTCからの割り込み (INT0)
void rtc_alarm_handler()
{
    rtc_int_occurred = true;
}

// --- 割り込み処理関数 (ISR) ---
// リードスイッチからの外部割り込み - 薬箱検知
ISR(PCINT0_vect) {
    reed_sw_int_occurred = true;
}


// RTC内部の固定サイクルタイマーを3秒周期で設定
void setup_rtc_fixed_timer(uint8_t sec)
{
    stop_rtc_fixed_timer(); // 念のため

    rtc.setFixedCycleTimer(sec, SECOND_UPDATE);
    rtc.fixedCycleTimerInterrupt(ENABLE);
}

// RTC内部の固定サイクルタイマーを停止
void stop_rtc_fixed_timer()
{
    rtc.fixedCycleTimerInterrupt(DISABLE);
    rtc.disableFixedCycleTimer();
}

// リードスイッチ (PCINT5) の割り込みを有効化
void enable_reed_interrupt()
{
    digitalWrite(REED_SW_ON, HIGH); // ReedSwitch Power ON
    delay(5);                       // 電源立ち上がり待ち(調整可)

    PCMSK0 |= (1 << PCINT5);        // マスクを先にセット
    (void)PINB;                     // 現在のピン状態を読み捨て(ラッチ)
    GIFR |= (1 << PCIF0);           // 未処理のピンチェンジフラグをクリア
    GIMSK |= (1 << PCIE0);          // グループの割り込みを有効化
}

// リードスイッチ (PCINT5) の割り込みを無効化
void disable_reed_interrupt()
{
    // PCINT5 のマスクをクリア
    PCMSK0 &= ~(1 << PCINT5);
    // グループ内に有効なマスクが無ければ PCIE0 をクリア
    if (PCMSK0 == 0) {
        GIMSK &= ~(1 << PCIE0);
    }
    digitalWrite(REED_SW_ON, LOW); // ReedSwitch Power OFF
}

// Power Down Mode
void deep_sleep()
{
    ADCSRA &= 0b01111111;
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_enable();
    sei();
    sleep_cpu();
    sleep_disable();
    ADCSRA |= 0b10000000;
}

void set_next_daily_alarm(uint8_t h, uint8_t m)
{

#ifdef _TEST
    //----------------------------------
    // RTCに初期時刻をセット(デバッグ用)現在時刻をアラーム時刻の30秒前にする
    tmElements_t tm;
    tm.Hour = h-1;
    tm.Minute = 59;
    tm.Second = 30;
    tm.Day = 1;      // Dummy
    tm.Month = 1;    // Dummy
    tm.Year = 2025 - 1970; // Dummy
    rtc.write(tm); // set IR receive time to RTC
    //----------------------------------
#endif

    // 毎日同じ時刻にアラームを設定
    rtc.setWeekAlarm(m, h, SUN | MON | TUE | WED | THU | FRI | SAT);

    rtc.alarmInterrupt(ENABLE);

}

// 服用検知により通知を停止
void stop_notification()
{
    is_notifying = false;
    stop_rtc_fixed_timer();
    digitalWrite(LED_PIN, LOW);
    disable_reed_interrupt();

    // 停止したら、次の服用時刻アラームを設定
    set_next_daily_alarm(DAILY_ALARM_HOUR, 0);
}

void RTCInit(uint8_t ir_hour, uint8_t ir_min)
{
    // RTC
    tmElements_t tm;
    tm.Hour = ir_hour;
    tm.Minute = ir_min;
    tm.Second = 0;
    tm.Day = 1;      // Dummy
    tm.Month = 1;    // Dummy
    tm.Year = 2025 - 1970; // Dummy
    rtc.write(tm); // set IR receive time to RTC

    stop_rtc_fixed_timer();

    // 初回起動時またはリセット時、アラームを設定する
    set_next_daily_alarm(DAILY_ALARM_HOUR, 0);

    is_rtcInit = true;
}

// IR
byte count = 0, data[4];
// 赤外線受信処理開始
void startIR() {
    is_rtcInit = false;
    digitalWrite(IR_RECEIVE_ON, HIGH); // IR Power ON
}

// 赤外線受信ループ中処理
// 受信毎に1回点滅 4回目受信後 成功:1回点滅 エラー:2回点滅→やり直し
void ir_loop() {
    byte ir_hour = 0, ir_min = 0;
    uint8_t num = irRecv();
    if(num != 0x0F) {
        data[count] = num;
        count++;
        blink(1);
        if(count==4) {
            ir_hour = data[0]*10 + data[1];
            ir_min  = data[2]*10 + data[3];
            blink(1);
            if(ir_hour < 24 && ir_min < 60) { // success
                // set time !!
                digitalWrite(IR_RECEIVE_ON, LOW); // IR Power OFF
                RTCInit(ir_hour, ir_min);
                return;
            } else { //error
                blink(2);
                count = 0;
            }
        }
    }
}

// ---- パルス長(状態=LOW/HIGH)を計測 ----
uint32_t readPulse(bool state) {
    uint32_t t = micros();

    // 状態が変わるまで待つ
    while (digitalRead(IR_RECEIVE_PIN) == state) {
        // タイムアウト(50ms)
        if ((micros() - t) > 50000) return 0;
    }

    return micros() - t;
}

// ---- NECフォーマット赤外線信号の受信 ----
uint8_t irRecv()
{
    uint8_t num = 0x0F;
    // ---------------------------------------
    // ① スタートBIT(9ms LOW)
    // ---------------------------------------
    uint32_t lowStart = readPulse(LOW);
    if (lowStart < 8500 || lowStart > 9500) {
        return num;  // NECではない
    }

    // ② スタートBIT後のスペース(4.5ms HIGH)
    uint32_t highSpace = readPulse(HIGH);
    if (highSpace < 4000 || highSpace > 5000) {
        return num;
    }

    // ---------------------------------------
    // ③ ビット32個読み取る
    // ---------------------------------------
    uint32_t data = 0;

    for (uint8_t i = 0; i < 32; i++) {

        // ---- ビットの LOW は固定(560us) ----
        uint32_t bitLow = readPulse(LOW);
        if (bitLow < 400 || bitLow > 700) {
            return num; // エラー
        }

        // ---- HIGH の長さで 0 と 1 を判定 ----
        uint32_t bitHigh = readPulse(HIGH);

        if (bitHigh > 1500 && bitHigh < 2000) {
            // 1 ビット(約 1.7ms)
            data = (data << 1) | 1;
        }
        else if (bitHigh > 300 && bitHigh < 800) {
            // 0 ビット(約 560us)
            data = (data << 1);
        }
        else {
            return num; // エラー
        }
    }

    // ---------------------------------------
    // ④ データ32bitの構造
    // ---------------------------------------
    uint8_t addr      = (data >> 24) & 0xFF;
    uint8_t addr_inv  = (data >> 16) & 0xFF;
    uint8_t cmd       = (data >>  8) & 0xFF;
    uint8_t cmd_inv   = (data >>  0) & 0xFF;

    // ⑤ 簡易チェック(反転が一致するか)
    if ((addr ^ addr_inv) != 0xFF) return num;
    if ((cmd  ^ cmd_inv) != 0xFF) return num;

    // ---------------------------------------
    // ⑥ コマンドとして使用できるのは「cmd」
    // ---------------------------------------
    switch(cmd) {
        //CARMP3
        case 0x30: num = 1; break; // 1
        case 0x18: num = 2; break; // 2
        case 0x7A: num = 3; break; // 3
        case 0x10: num = 4; break; // 4
        case 0x38: num = 5; break; // 5
        case 0x5A: num = 6; break; // 6
        case 0x42: num = 7; break; // 7
        case 0x4A: num = 8; break; // 8
        case 0x52: num = 9; break; // 9
        case 0x68: num = 0; break; // 0
        default: num = 0x0F; break;
    }
    return num;
}

void blink(uint8_t n) {
    for(uint8_t i=0;i<n;i++) {
        digitalWrite(LED_PIN, HIGH);delay(FLASH_DURATION_MS);
        digitalWrite(LED_PIN, LOW);delay(100);
    }
}

おわりに

3ヶ月後ぐらいに経過報告をここに書きたいと思います。

この装置は実は三代目。先代まではATmega328pでしたが移植しました。またATtiny44で何か作りたいと思います。

このあとに、RX-8025NBのライブラリを作成してみました。四代目はこれを使えたらと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?