Arduino で全自動体重記録計を作る

  • 78
    いいね
  • 6
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

世の中には、体重記録ダイエットなるものがあるようだ。
しかし、怠慢なプログラマ各位は体重の記録など当然自動化したくなるだろう。

既成品として、 WiFi 対応体重計というものがあり、これを購入すれば全自動で体重を記録できる。
しかし、値段も高く(10,000円 ~ 20,000円ほど)、機種によっては専用のWebサービスへの加入が必須だったり、そのサービスに月額料金がかかったり、データのエクスポート用APIがなかったりと課題も多い。

というわけで、今回は安価な全自動体重記録装置を自作することにする。

体重計

とはいうものの、人間の重量に耐えられるような測定器を自作するのは簡単ではない。1

一般的な体重計を改造して使うにも、以下の課題がある

  • 分解して内部配線を通る信号を直接監視するのは大掛かり
  • 一度分解したあと、体重測定の精度を保ったまま組み直せるかわからない

そこで、本体無改造で内部データを取れる体重計を探す必要がある。

今回は、要件を満たす体重計として、以下の機種を使う。
Amacon.co.jp: Hashy SALUTE(サルート) ワイヤレス体重計 (WH) BH-2581

この体重計は、本体と体重表示器が離れており、その間は赤外線でデータをやり取りしている。そのため、赤外線信号さえ解析すれば、本体に手を加えることなく体重値を読みだすことができる。また、実売2,000円程度と比較的安価だ。

この体重計を使うためには、赤外線信号のフォーマットを解析する必要がある。
そのため、本記事では赤外線信号の解析方法についても、手順ベースで紹介する。

部品と配線

以降のプログラムでは、赤外線センサの信号は、Arduino の2番ピンに接続したものとする。

01. 波形調査

以下のプログラムで波形の雰囲気を掴む。

01_signal.ino
#define PIN 2
#define LED 13

void setup() {
  pinMode(PIN, INPUT);
  pinMode(LED, OUTPUT);

  Serial.begin(115200);
}

void loop() {
  int pin = digitalRead(PIN);
  Serial.println(pin);
}

はじめに、体重計に乗らずにプログラムを実行すると、1ばかり出力される。

体重計に乗らずに測定した場合
1
1
1
1
1
1
1

次に、赤外線センサの遮蔽を外し、体重計に乗ってみる。
定期的に信号に0が混じり、赤外線をキャッチしていることを確認できた。

体重計に乗って測定した場合
1
1
1
0
0
1
1
0
0
1
1
0
0

赤外線センサのデータシートを確認すると、Active Low との記載があった。そのため、信号が出ているときは 0、信号がないときは 1 が表示される。
今回は、信号のありなしではなく、 センサが返す HIGH, LOW を基準に プログラムを実装する。

02. 波形調査 2

さて次に、波形の持続時間を調べてみる。
無信号時は HIGH なので、情報は LOW に乗っていると仮定して、LOW の持続時間を測ってみよう。

信号の持続時間を測るには、pulseIn 関数を使う。

02_pulse_low.cpp
#define PIN 2
#define LED 13

#define TIMEOUT_US 1000000

void setup() {
  pinMode(PIN, INPUT);
  pinMode(LED, OUTPUT);

  Serial.begin(115200);
}

void loop() {
  unsigned long pulse_us = pulseIn(PIN, LOW, TIMEOUT_US);

  /* 【注意】
     115200 bit/sec
     => 14400 byte/sec
     => 1/14400 sec/byte
     => 1 * 10^6 / 14400 usec/byte
     ≒ 70 usec/byte

     より、3文字+1改行文字の送信で 70 * (3+1) = 280us ほど使っている。
     計測対象の信号が 280us 以下の持続時間だと、信号を正しく読めない可能性がある。
     その場合、複数の計測データを配列などに書き込み、最後にまとめて表示すべき。

     今回は信号の持続時間が 500us あるという"答え"を利用して、プログラムを簡略化している。
   */

  Serial.println(pulse_us);
}
持続時間の計測(LOW)
514
514
518
510
482
513
482

出力データを見ると、LOW の持続時間は常に 500us で安定しているようだ。

さて、LOW は常に同じ長さだったので次は HIGH の持続時間を見よう。

02_pulse_high.cpp
#define PIN 2
#define LED 13

#define TIMEOUT_US 1000000

void setup() {
  pinMode(PIN, INPUT);
  pinMode(LED, OUTPUT);

  Serial.begin(115200);
}

void loop() {
  unsigned long pulse_us = pulseIn(PIN, HIGH, TIMEOUT_US);
  Serial.println(pulse_us);
}
持続時間の計測(HIGH)
984
547
1012
983
1010
510
517
1010
1003
507
540
520
1011
984
521
77653
510
985
544

これを見ると、出力結果はおよそ3分類できそうだ。

  • 500us
  • 1000us
  • 70000us

70000us (70ms) は、他2つと比べて明らかに長い。
おそらくここが信号の区切りだろう。
また、500us と 1000us が 0 と 1 のビットを表しているように思える。

まとめると

  • LOW の持続時間は常に 500us
  • HIGH の持続時間は 500us と 1000us
    • おそらくそれぞれ 0 と 1 に対応
  • 信号間の区切りとして HIGH が 70000us 持続
  • 信号がなくなれば、(赤外線センサが負論理なので) HIGH に戻る

03. 波形調査 3

さて、先の想定を元に、信号のビット長を調べてみよう。
70000us を区切りとして、500us と 1000us の HIGH パルスが何回現れるかカウントする。

03_count.ino
#define PIN 2
#define LED 13

#define TIMEOUT_US 30000

void setup() {
  pinMode(PIN, INPUT);
  pinMode(LED, OUTPUT);

  Serial.begin(115200);
}

void loop() {
  unsigned int count = 0;

  while (true) {
    unsigned long pulse = pulseIn(PIN, HIGH, TIMEOUT_US);
    if (pulse == 0) break;
    ++count;
  }

  if (count != 0) {
    Serial.println(count);
  }
}
信号のbit数測定
1919
39
39
39
39
39
39
39
39
39
39

これにより、以下の二種類のパターンが得られる。

  • はじめに1回 1919 bit
  • 以降繰り返し 39 bit

はじめの 1919bit はとりあえず無視しよう。
おそらくデータは 39bit 側に乗っている。2

04 bit 表示

今度は bit 列を表示してみる。

500us と 1000us の信号のどちらが 0 でどちらが 1 か、二通りの解釈がある。3

今回のプログラムでは、

  • 0: 1000us
  • 1: 500us

という割当を使う。4

04_bits.cpp
#define PIN 2
#define LED 13


#define TIMEOUT_US 30000

/*
   39bit 値を保存するため、uint32_t の変数を2つ使っている。
   Arduino 環境における uint64_t は罠が多いため避ける。
*/
int read_bits(uint32_t *bh, uint32_t *bl) {
  int count = 0;
  uint64_t ret = 0;

  while (true) {
    unsigned long pulse = pulseIn(PIN, HIGH, TIMEOUT_US);
    if (pulse == 0) break;

    uint32_t bit = pulse < 750 ? 1 : 0;
    if (count < 32) {
      bitWrite(*bh, 31 - count, bit);
    } else if (count < 64) {
      bitWrite(*bl, 63 - count, bit);
    }
    ++count;
  }

  return count;
}

void setup() {
  pinMode(PIN, INPUT);
  pinMode(LED, OUTPUT);

  Serial.begin(115200);
}

void loop() {
  uint32_t bh = 0;
  uint32_t bl = 0;

  int count = read_bits(&bh, &bl);

  /*
    シリアル通信に一定の時間がかかるため、表示処理の間に幾つかの信号を取り逃してしまう。
    また、不完全な信号を受信する可能性もある。
    今回は、目的の 39 bit を読めたときだけ表示することとする。
   */
  if (count == 39) {
    Serial.print(bh, BIN);
    Serial.print(' ');
    Serial.println(bl >> 25, BIN);
  }
}

データ

このプログラムで得られるビット列を幾つか例示してみる。

はじめに体重計に乗り、その後すぐ降りた時の信号列は次の通りである。

体重計をすぐ降りたとき
10101011000000000000000000000000 1010101
10101011000010000000000000000000 1011001
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101

一方、乗り続けた場合のデータは次のようになる。

体重計に乗り続けた場合
10101011100000000000000001111100 1010100
10101011100000000000000001111100 1010100
10101011100000000000000001111100 1010100
10101011100000000000000001111100 1010100
10101011100000000000000001111100 1010100
10101011100000000000000001111100 1010100
10101011100011000000000001111100 1011010
10101011100011000000000001111100 1011010
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100001000000000001111100 1010110
10101011100011000000000001111100 1011010
10101011100011000000000001111100 1011010
10101011100011000000000001111100 1011010
10101011100011000000000001111100 1011010
10101011100011000000000001111100 1011010
10101011100011000000000001111100 1011010
10101011100011000000000001111100 1011010

全体を眺めると、先頭 12 bit は常に 101010111000 固定のようだ。
また、次の 2 bit は

  • はじめは 00
  • 途中で 01
  • 重量が安定すると 11

という傾向が見て取れる。該当の2bit部分を区切って見ると分かりやすい。

先頭12bitと次の2bitを区切って表示
101010111000 00 000000000001111100 1010100
101010111000 00 000000000001111100 1010100
101010111000 11 000000000001111100 1011010
101010111000 11 000000000001111100 1011010
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 01 000000000001111100 1010110
101010111000 11 000000000001111100 1011010
101010111000 11 000000000001111100 1011010

次に、測定中に重量を変えてみる。
椅子に座って軽く体重をかけ、確定サインが見えたら足で体重計をより強く押してみる。

体重確定サインが出たあとで重量を変える
101010111000 11 000000000010000001 1011100       <--- 重量が安定した
101010111000 11 000000000010000001 1011100
101010111000 11 000000000010000001 1011100
101010111000 11 000000000010000100 1011110
101010111000 11 000000000010000100 1011110
101010111000 11 000000000010000100 1011110
101010111000 11 000000000010000100 1011110
101010111000 01 000000000010000100 1011010       <--- ここで負荷を増やした
101010111000 00 000000000011100000 110           <--- 桁数がずれる (後述)
101010111000 00 000000000011111101 10101
101010111000 00 000000000100010100 100000
101010111000 00 000000000100010100 100000
101010111000 00 000000000100010100 100000
101010111000 00 000000000100010101 100001

重量が安定したときに 11 だったビットが、重量変化で 00 に戻ったことが確認できる。

途中、桁数が 39bit でない列があるが、これは Arduino の print の仕様による。
2進数表示のときに、桁数を指定できないため、先頭にある0は消されてしまう。
そのため、以降はわかりやすたのために、先頭の0を手で補った出力を載せる。

補正前と補正後
# 補正前は、末尾 7bit が 3bit 分しか表示されない
10101011100000000000000011100000 110

# 末尾 7bit 部に0を補って桁を揃える
10101011100000000000000011100000 0000110

さて、次は重さを変えて信号の変化を見てみよう。
椅子に座って足で体重計を押し、信号を見ながら足にかける力を変える。

足にかける負荷を変えたとき
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101
10101011000000000000000000011000 1100001   <--- 力をかけ始めた
10101011100000000000000000111010 0110011
10101011100000000000000010010110 1100001
10101011100000000000000101110011 1010000
10101011100000000000001001111001 1010011
10101011100000000000001100000000 0010111
10101011100000000000001100000000 0010111
10101011100000000000001011010110 0000010
10101011100000000000001011101111 0001111
10101011100000000000001100001110 0011110
10101011100000000000001010100111 1101010  <--- 力を抜き始めた
10101011100000000000000110010100 1100000
10101011100000000000000010110001 1101110
10101011100000000000000001010010 0111111
10101011000000000000000000011100 1100011
10101011000000000000000000000000 1010101  <--- 完全に足を離した
10101011000000000000000000000000 1010101
10101011000000000000000000000000 1010101

なんとなく、末尾付近のビットが足にかけた重さに合わせて変化しているように見える。
先頭 16bit を削ってみよう。

先頭16bitを削ったもの
0000000000000000 1010101
0000000000000000 1010101
0000000000011000 1100001   <--- 力をかけ始めた
0000000000111010 0110011
0000000010010110 1100001
0000000101110011 1010000
0000001001111001 1010011
0000001100000000 0010111
0000001100000000 0010111
0000001011010110 0000010
0000001011101111 0001111
0000001100001110 0011110
0000001010100111 1101010  <--- 力を抜き始めた
0000000110010100 1100000
0000000010110001 1101110
0000000001010010 0111111
0000000000011100 1100011
0000000000000000 1010101  <--- 完全に足を離した
0000000000000000 1010101
0000000000000000 1010101

力の入れ具合に合わせてビットが伸びていることが見て取れる。

完全に足を離しても末尾 7bit は0にはならないことから、この部分は体重とは関係ないデータと思われる。
そこで、残った 16bit 分を10進数として表示してみよう。

05_weight.ino
// loop 関数以外は 04_bits.cpp と同じ

#define PIN 2
#define LED 13


#define TIMEOUT_US 30000

int read_bits(uint32_t *bh, uint32_t *bl) {
  int count = 0;
  uint64_t ret = 0;

  while (true) {
    unsigned long pulse = pulseIn(PIN, HIGH, TIMEOUT_US);
    if (pulse == 0) break;

    uint32_t bit = pulse < 750 ? 1 : 0;
    if (count < 32) {
      bitWrite(*bh, 31 - count, bit);
    } else if (count < 64) {
      bitWrite(*bl, 63 - count, bit);
    }
    ++count;
  }

  return count;
}

void setup() {
  pinMode(PIN, INPUT);
  pinMode(LED, OUTPUT);

  Serial.begin(115200);
}

void loop() {
  uint32_t bh = 0;
  uint32_t bl = 0;

  int count = read_bits(&bh, &bl);

  if (count == 39) {
    Serial.println(bh & 0xFFFF);
  }
}
データ
0
0
0
0
23
40
95
152
217
295
386
460
484
500
512
512
512

体重計付属の表示器と見比べると、ちょうど10倍された体重が kg で取得できていることがわかる。

プログラム化

以上のことを踏まえて、体重計から値を読み取るプログラムを設計する。
まずは状態遷移図を書く。

states.png

この図を元に、プログラムを書き出してみる。

weight.ino
#include <avr/sleep.h>

#define PIN 2
#define LED 13

#define SHORT_TIMEOUT 5000
#define LONG_TIMEOUT 1000000


void deepSleep() {
  set_sleep_mode(SLEEP_MODE_PWR_SAVE);

  // Arduino Uno では、以下のコードにより2番ピンの変化を受けてスリープから復帰できる
  attachInterrupt(0, wakeup, FALLING);
  digitalWrite(LED, LOW);
  delay(100);

  sleep_enable();
  sleep_mode();
  sleep_disable();

  detachInterrupt(0);
  digitalWrite(LED, HIGH);
}

void wakeup() {
  delay(100);
}

// 先の状態遷移図に対応する関数
// 新しい信号が来なくなったら、最後に読んだ体重値を return する
unsigned long receive() {
  uint32_t count = 0;
  uint32_t bits = 0;
  uint32_t ret = 0;

  while (true) {
    unsigned long pulse = pulseIn(PIN, HIGH, SHORT_TIMEOUT);
    if (pulse == 0) {
      if (count != 0) {
        /* 信号が 39bit かつ、体重が安定しているときに保存する。
      先頭 12 bit は固定で 0b101010110000 = 0xAB8
           安定を示す次の 2ビットが 11 、更に続く 2 bit は常に 0 である。
           よって、体重が安定したとき、信号の先頭 16 bit は 0xAB8C になる。
         */
        if ((count == 39) && (bits >> 16 == 0xAB8C)) {
          // SALUTE 体重計の測定上限は 150kg (1500) までなので、 11bit 取れば十分
          ret = bits & 0x7ff;
        }
        count = 0;
        continue;
      }

      pulse = pulseIn(PIN, HIGH, LONG_TIMEOUT);
      if (pulse == 0) {
        break;
      }
    }

    if (count < 32) {
      unsigned long b = pulse < 750 ? 1 : 0;
      bitWrite(bits, 31 - count, b);
    }
    ++count;
  }

  return ret;
}

void setup() {
  cli();
  pinMode(PIN, INPUT);
  pinMode(LED, OUTPUT);
  Serial.begin(115200);
  sei();

  Serial.println("");
  delay(100);

  digitalWrite(LED, LOW);
}

void loop() {
  uint32_t v;

  do {
    deepSleep();
    delay(100);
  } while ((v = receive()) == 0);
  Serial.println(v/10.0);
  delay(200);
}

測定プログラムのデーモン化

仕上げとして、Linux での自動起動設定を行う。

udev による Arduino の自動認識

まずは、Arduino Uno を USB 接続したときに、udev で自動認識する設定を書く。

/etc/udev/rules.d/90-arduino.rules
SUBSYSTEMS=="usb", ACTION=="add", ATTRS{idProduct}=="0043", ATTRS{idVendor}=="2341", GROUP="usb", TAG+="systemd", SYMLINK+="arduino/uno arduino/uno_$attr{serial}"

上記設定により、Arduino 接続時に以下のデバイスファイルが作成される。

  • /dev/arduino/uno
  • /dev/arduino/uno-(id)

systemd によるデーモン管理

体重通知プログラムを、以下設定でデーモン化する。

/etc/systemd/system/weight.service
[Unit]
Description=Arduino weight service
Requires=dev-arduino-uno.device
After=dev-arduino-uno.device

[Service]
Type=simple
ExecStart=/usr/bin/ruby /usr/local/bin/weight.rb
Restart=always

udev と連携する設定のため、Arduino を接続すると自動的にデーモンが起動する。

デーモン

実際に読み取るプログラムは、シリアル通信で数値を読むだけだ。
今回は Ruby で記述し zabbix へデータを送っている。

weight.rb
require 'serialport'

port = '/dev/arduino/uno'
bps = 115200

sp = SerialPort.new(port, bps)

value = 0

loop do
  begin
    value = sp.readline.chomp.to_f
    next if value.zero?
    # zabbix_sender コマンドでデータを送る
    # もちろん以下を変更し、どこに保存してもよい
    system("/usr/local/app/zabbix/bin/zabbix_sender -z localhost -p 10051 -s I -k weight -o '#{value}'")
  end
end

まとめ

本記事では、比較的安価なPC連動体重計を作成した。また、作成の過程で赤外線信号のフォーマットをどのように解析したかについて、大まかな流れをまとめた。
ESP8266 などの Arduino 互換環境がある WiFi デバイスを使えば、PC不要の既成品に近い構成にすることも可能だ。5

赤外線インタフェースを持つ家電は多い。この記事が、既存家電の自動化、便利化に少しでも役立てば幸いだ。


  1. 例えば、20kgまで測れるセンサで、100kgまで対応しようと思ったら、体重値のちょうど1/5の負荷がセンサにかかるようなギミックを作る必要がある。また、この手のセンサはキャリブレーションも難しいようだ。 

  2. 体重計に付属の表示器は、体重変化をリアルタイムで表示できる。よって、繰り返し送信される 39bit 側に体重値が乗っていると考えられる。 

  3. 0 と 1 の割当が動的に変わるようなプロトコルもありうるが、そんな複雑なことはしていないだろう。 

  4. 実はこの割当が正解であることが後でわかる。もちろん信号を調べるときは、両方のパターンでビット列を表示してみて考えるしかない。 

  5. 本記事はもともと ESP8266 Advent Calendar 2015 に投稿予定でした。遅くなってすみません……