知的好奇心 for IoT

IoT関連の知的好奇心を探求するブログです

M5StickCでレッツダンベルカール!回数を画面に表示して楽しくトレーニングをしよう!

みなさん、運動してますか?トレーニングってなかなか続けるのが大変ですよね。

そこで、ちょっとしたトレーニングツールを作ってみました。

BLEビーコンで回数を飛ばすダンベルカールカウンターです!

f:id:IntellectualCuriosity:20200208085022j:plain

ESP32や慣性センサー、小型ディスプレイ、バッテリーなどを搭載したオールインワンのM5StickCを使いました。ベルトも付属しているんですよ。

 

カウントの仕組み

M5SticCに付いている慣性センサーの加速度を検出することで、ダンベルカールのカウントを行っています。

M5StickCを上に向けると加速度のY軸の値がプラスになります。

f:id:IntellectualCuriosity:20200208061556p:plain
逆に下に向けると加速度のY軸の値がマイナスになります。

f:id:IntellectualCuriosity:20200208061403p:plain

カウントは単純にY軸の値がプラスになった後にマイナスになったら1回としています。センサーが出力する値はマイナス1からプラス1までの小数点付きとなっていたので、扱いやすいように値を1,000倍して閾値を設定できるようにしました。

 

BLEビーコンで回数を飛ばす

M5StickCの無線通信手段はWi-FiBluetooth(ClassicおよびBLE)の3種類です。今回はなるべく簡単に、できれば設定を一切行わなくても使えることを目指して、BLEビーコンを使ってみることにしました。

これはnRF ConnectというBLEスキャナーでダンベルカールカウンタービーコンを表示したものです。

f:id:IntellectualCuriosity:20200208070852p:plain
BLEには複雑な規約があるようなのですが、とりあえず動作するものを作りたかったためテスト用のカンパニーID<0xFFFF>を利用しています。

赤枠で囲った部分の「0x010401」がデータで次のように定義しています。

  • 1バイト目:デバイスID
  • 2バイト目:シーケンシャル番号
  • 3バイト目:カウンター値

 

BLEについては次の記事を参考にさせていただきました。

 

回数の画面表示

サクッと回数を表示するものが欲しかったので、Raspberry PiでBLE制御用に「bluepy」、画面表示用に「pygame」というPythonモジュールを使って作りました。あと、ただ回数を表示するだけだとさみしかったので、「Open JTalk」を使って音声で回数を数えてくれるようにしています。

 

表示画面はこんな感じです。

f:id:IntellectualCuriosity:20200208081103p:plain

M5StickCのAボタンを押すと回数がリセットされて画面に「000」と表示され、「Open JTalk」のメイちゃんが「はじめ!」と、かけ声をかけてくれます。

その後は「いち」「に」と、ダンベルカールを行った回数を数え上げてくれます。

 

「Open JTalk」については次の記事を参考にさせていただきました。

 

プログラム

M5StickCはArduinoでコーディングしました。

DumbbellCurl.ino

/*
 * Dumbbell curl BLE beacon transmission program
 * February 6, 2020
 * By Hiroyuki ITO
 * http://intellectualcuriosity.hatenablog.com/  
 * MIT Licensed.
 */
#include <M5StickC.h>
#include <BLEDevice.h>
#include <BLEServer.h>

#define DEVICE_NAME   "DumbbellCurlCounter"  // Complete Local Name
#define DEVICE_ID     1                     // ID ranges from 0 to 255

// Global Objects and Variables
BLEAdvertising *pAdvertising; // Adverting Object
int counter = 0;              // Dumbbell Curl Counter
int state = 0;                // Accell State 0:down 1:up
int sequential = 0;           // Sequential number

// Creating Advertising Data
void setAdvertisementData() {
    std::string strData = "";
    strData += (char)0x06;        // Set Data Length
    strData += (char)0xff;        // Manufacturer specific data
    strData += (char)0xff;        // Test manufacture ID low byte
    strData += (char)0xff;        // Test manufacture ID high byte
    strData += (char)DEVICE_ID;   // Device ID
    strData += (char)sequential;  // Sequential number
    strData += (char)counter;     // Dumbbell Curl Counter
    
    BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
    oAdvertisementData.setName(DEVICE_NAME);
    oAdvertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | LE General Discoverable Mode
    oAdvertisementData.addData(strData);
    pAdvertising->setAdvertisementData(oAdvertisementData);
}

// Displaying and advertising counts
void advertise() {
  // Count display
  M5.Lcd.setCursor(20, 15);
  M5.Lcd.printf("%03d", counter);

  // BLE advertise
  pAdvertising->stop();
  delay(10);
  setAdvertisementData();
  pAdvertising->start(); 
  sequential++;
}

void setup() {
  // Initialize M5StickC
  M5.begin();
  M5.Lcd.setRotation(1);    // Landscape display
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(10);
  M5.MPU6886.Init();

  // Initialize BLE
  BLEDevice::init(DEVICE_NAME);
  BLEServer *pServer = BLEDevice::createServer();
  pAdvertising = pServer->getAdvertising();
  advertise();
}

void loop() {
  // Accelerometer variables
  float accX = 0;
  float accY = 0;
  float accZ = 0;

  M5.update();
  M5.MPU6886.getAccelData(&accX, &accY, &accZ);

  // Detect upward
  if (state == 0) {
    if (accY * 1000 > 500) {
      state = 1;
    }
  }

  // Detect downward
  if (state == 1) {
    if (accY * 1000 < -500) {
      state = 0;
      counter++;
      advertise();
    }
  }

  // Detect A button for Initialize
  if (M5.BtnA.isPressed()) {
    state = 0;
    counter = 0;
    advertise();
    delay(500);
  }
  
  delay(10);
}

Raspberry Pi側はPythonです。

DumbbellCurl.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Dumbbell curl BLE beacon reception program
# February 6, 2020
# By Hiroyuki ITO
# http://intellectualcuriosity.hatenablog.com/  
# MIT Licensed.

from bluepy.btle import DefaultDelegate, Scanner, BTLEException
import pygame
from pygame.locals import *
import sys
import subprocess

# Initialize Pygame
pygame.init()
pygame.display.set_caption("Dummbell Curl Counter")
screen = pygame.display.set_mode((1200, 800))
font = pygame.font.Font(None, 512) 

# Screen Display
def displayCount(count):
  screen.fill((0,0,0)) # black
  text = font.render(str(count).zfill(3), True, (255,255,255))
  screen.blit(text, [300, 180])
  pygame.display.update()

# BLE Scan Callback
class ScanDelegate(DefaultDelegate):
  def __init__(self):
    DefaultDelegate.__init__(self)
    self.lastseq = None       # last sequential number
    self.deviceFound = False  # found 'DumbbellCurlCounter' device
    self.count = 0            # count number

  def handleDiscovery(self, dev, isNewDev, isNewData):
    if isNewDev or isNewData:
      self.deviceFound = False
      # Analysis of advertising packets
      for (adtype, desc, value) in dev.getScanData():
        if not self.deviceFound:
          # Find 'DumbbellCurlCounter'
          if desc == 'Complete Local Name' and value == 'DumbbellCurlCounter':
            self.deviceFound = True
        else:
          # Found 'DumbbellCurlCounter'
          if desc == 'Manufacturer':    # Manufacture Specific
            seq = int(value[6:8], 16)   # Sequential number
            print('seq: %d' % seq)
            if seq != self.lastseq:     # Check new data
              self.count = int(value[8:10], 16)
              displayCount(self.count)  # Display count
              if self.count == 0:
                subprocess.call(['./jtalk.sh', '始め'])           # Speak '始め'
              else:
                subprocess.call(['./jtalk.sh', str(self.count)])  # Speak count
              print('count: %d' % self.count)
              self.lastseq = seq

def main():
  subprocess.call(['./jtalk.sh', 'ダンベルカール'])  # Speak 'ダンベルカール'
  scanner = Scanner().withDelegate(ScanDelegate())  # BLE Scan callback
  displayCount(0) # Display 000

  while True:
    # BLE scan
    try:
      scanner.scan(0.2) # timeout 0.2 seconds
    except BTLEException:
      MSG('BTLE Exception while scannning.')

    # Pygame event
    for event in pygame.event.get():
      if event.type == QUIT:
        pygame.quit()
        sys.exit()

if __name__ == "__main__":
  main()

BLEのスキャンを行うにはなぜかRoot権限が必要なため、DumbbellCurl.pyはsudoを付けて実行してくださいね。