みなさん、運動してますか?トレーニングってなかなか続けるのが大変ですよね。
そこで、ちょっとしたトレーニングツールを作ってみました。
BLEビーコンで回数を飛ばすダンベルカールカウンターです!
ESP32や慣性センサー、小型ディスプレイ、バッテリーなどを搭載したオールインワンのM5StickCを使いました。ベルトも付属しているんですよ。
カウントの仕組み
M5SticCに付いている慣性センサーの加速度を検出することで、ダンベルカールのカウントを行っています。
M5StickCを上に向けると加速度のY軸の値がプラスになります。
逆に下に向けると加速度のY軸の値がマイナスになります。
カウントは単純にY軸の値がプラスになった後にマイナスになったら1回としています。センサーが出力する値はマイナス1からプラス1までの小数点付きとなっていたので、扱いやすいように値を1,000倍して閾値を設定できるようにしました。
BLEビーコンで回数を飛ばす
M5StickCの無線通信手段はWi-FiとBluetooth(ClassicおよびBLE)の3種類です。今回はなるべく簡単に、できれば設定を一切行わなくても使えることを目指して、BLEビーコンを使ってみることにしました。
これはnRF ConnectというBLEスキャナーでダンベルカールカウンタービーコンを表示したものです。
BLEには複雑な規約があるようなのですが、とりあえず動作するものを作りたかったためテスト用のカンパニーID<0xFFFF>を利用しています。
赤枠で囲った部分の「0x010401」がデータで次のように定義しています。
- 1バイト目:デバイスID
- 2バイト目:シーケンシャル番号
- 3バイト目:カウンター値
BLEについては次の記事を参考にさせていただきました。
回数の画面表示
サクッと回数を表示するものが欲しかったので、Raspberry PiでBLE制御用に「bluepy」、画面表示用に「pygame」というPythonモジュールを使って作りました。あと、ただ回数を表示するだけだとさみしかったので、「Open JTalk」を使って音声で回数を数えてくれるようにしています。
表示画面はこんな感じです。
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を付けて実行してくださいね。