概要
iPhone を使って M5BALA を操作する、の4回目は Pythonista を使いBLE経由で M5BALA を操作します。iPhone を使ってBLE経由で M5BALA を動かす仕組みとしては前回の Blynk を使用する方法が手っ取り早いですが、お勉強ということで。
環境
- M5BALA+M5Stack Fire
- Pythonista3 v3.2
- iPhone6 iOS 12.1
-
Arduino 1.8.7 以下のライブラリを使用(Arduino IDEの「ライブラリを管理」からインストール可)
- M5Stack 0.2.4 - https://github.com/m5stack/M5Stack
- MPU6050_tockn 1.4.0 - https://github.com/tockn/MPU6050_tockn
- NeoPixelBus 2.3.4 - https://github.com/Makuna/NeoPixelBus
実行例
見た目はあまり変わりませんが、仕組みをいろいろ変えて、
— 稲澤祐一 (@inasawa) 2018年11月7日
「iPhoneを使ってM5BALAを操作する(Pythonista-BLE編)」https://t.co/eg7Vhz6v8U#M5Stack #M5BALA pic.twitter.com/ejFiXz0aRz
M5BALA制御用プログラム
以下のサンプルプログラムを一部修正して使用します。
https://github.com/m5stack/M5Bala/tree/master/src
Default_firmware.ino
M5Bala.cpp
M5Bala.h
Default_firmware.ino
: : :
// setup() の修正および BLE Setup の呼び出しを追加
void setup() {
: : :
//M5.setPowerBoostKeepOnは未定義のエラーになるのでコメントアウト
//M5.setPowerBoostKeepOn(false);
: : :
// BLE Setup
BLE_setup();
}
// loop() に BLE Loop の呼び出しを追加
void loop() {
: : :
// BLE Loop
BLE_loop();
: : :
}
// BLE関連の処理を追加
///////////////////
// Bluetooth LE //
///////////////////
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
BLEServer *pServer = NULL;
BLECharacteristic *pCharacteristic;
bool deviceConnected = false;
bool oldDeviceConnected = false;
int16_t m5bala_move = 0;
int16_t m5bala_turn = 0;
int16_t last_m5bala_move = 0;
int16_t last_m5bala_turn = 0;
#define LOCAL_NAME "M5BALA"
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "c44205a6-c87c-11e8-a8d5-f2801f1b9fd1"
#define CHARACTERISTIC_UUID "c442090c-c87c-11e8-a8d5-f2801f1b9fd1"
// Bluetooth LE Change Connect State
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
Serial.println("Device Connected");
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
Serial.println("Device Disconnected");
deviceConnected = false;
}
};
class MyCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pChar) {
std::string rxValue = pCharacteristic->getValue();
if (rxValue.length() == 4) {
int16_t move = *(int16_t *)&rxValue[0];
int16_t turn = *(int16_t *)&rxValue[2];
m5bala_move = move;
m5bala_turn = turn;
}
}
};
// Bluetooth LE initialize
void BLE_setup() {
// Create the BLE Device
BLEDevice::init(LOCAL_NAME);
// Create the BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Create the BLE Service
BLEService *pService = pServer->createService(SERVICE_UUID);
// Create a BLE Characteristic
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_WRITE);
pCharacteristic->setCallbacks(new MyCallbacks()); // コールバック関数を設定
// Start the service
pService->start();
// Start advertising
pServer->getAdvertising()->start();
//pServer->startAdvertising();
Serial.println("Start Advertising");
M5.Lcd.setCursor(220, 0*18); M5.Lcd.print("Not Connected");
M5.Lcd.setCursor(230, 1*18); M5.Lcd.print("Move :");
M5.Lcd.setCursor(230, 2*18); M5.Lcd.print("Turn :");
}
// Bluetooth LE loop
void BLE_loop() {
// disconnecting
if (!deviceConnected && oldDeviceConnected) {
M5.Lcd.setCursor(220, 0*18); M5.Lcd.print("Not Connected");
delay(500); // give the bluetooth stack the chance to get things ready
pServer->startAdvertising(); // restart advertising
Serial.println("Start Advertising");
oldDeviceConnected = deviceConnected;
m5bala_move = 0;
m5bala_turn = 0;
}
// connecting
if (deviceConnected && !oldDeviceConnected) {
M5.Lcd.setCursor(220, 0*18); M5.Lcd.print("Connected ");
// do stuff here on connecting
oldDeviceConnected = deviceConnected;
}
if (m5bala_move != last_m5bala_move) {
M5.Lcd.setCursor(275, 1*18); M5.Lcd.printf("%4d ", m5bala_move);
m5bala.move(m5bala_move);
last_m5bala_move = m5bala_move;
}
if (m5bala_turn != last_m5bala_turn) {
M5.Lcd.setCursor(275, 2*18); M5.Lcd.printf("%4d ", m5bala_turn);
if (m5bala_turn == 0) {
m5bala.stop();
} else {
m5bala.turn(m5bala_turn);
}
last_m5bala_turn = m5bala_turn;
}
}
Pythonista3プログラム
iPhoneの前後左右の傾きに応じて、左右の車輪を動かすための指示をBLE経由で送信する仕組みです。
m5bala_controller_ble.py
import motion
import time
import math
import ui
import cb
import struct
M5BALA_SERVICE_UUID = 'c44205a6-c87c-11e8-a8d5-f2801f1b9fd1'.upper()
M5BALA_CHARACTERISTIC_UUID = 'c442090c-c87c-11e8-a8d5-f2801f1b9fd1'.upper()
M5BALA_NAME = 'M5BALA'
class MyCentralManagerDelegate (object):
def __init__(self):
self.peripheral = None
self.data_characteristics = None
def did_discover_peripheral(self, p):
if p.name and M5BALA_NAME in p.name and not self.peripheral:
# Keep a reference to the peripheral, so it doesn't get garbage-collected:
print('+++ Discovered peripheral: %s (%s)' % (p.name, p.uuid))
self.peripheral = p
cb.connect_peripheral(self.peripheral)
view['status'].text = 'Detected'
def did_connect_peripheral(self, p):
print('*** Connected: %s' % p.name)
print('Discovering services...')
view['status'].text = 'Connected'
p.discover_services()
def did_fail_to_connect_peripheral(self, p, error):
print('Failed to connect')
view['status'].text = 'Failed'
def did_disconnect_peripheral(self, p, error):
print('Disconnected, error: %s' % (error,))
self.peripheral = None
self.data_characteristics = None
view['status'].text = 'Scanning'
cb.scan_for_peripherals()
def did_discover_services(self, p, error):
for s in p.services:
print(s.uuid)
if M5BALA_SERVICE_UUID in s.uuid:
print('M5BALA found')
p.discover_characteristics(s)
def did_discover_characteristics(self, s, error):
if M5BALA_SERVICE_UUID in s.uuid:
for c in s.characteristics:
if M5BALA_CHARACTERISTIC_UUID in c.uuid:
self.data_characteristics = c
def send_action(self, move, turn):
if self.peripheral and self.data_characteristics:
data = struct.pack('hh', move, turn)
#print('{:04x} {:04x} {:4d} {:4d}'.format(int(move) & 0xffff, int(turn) & 0xffff, int(move), int(turn)))
#print('{:4d} {:4d}'.format(move, turn))
self.peripheral.write_characteristic_value(self.data_characteristics, data, True)
def end_controller(sender):
global action_loop
sender.superview.close()
action_loop = False
view = ui.load_view()
view.present('sheet')
delegate = MyCentralManagerDelegate()
print('Scanning for peripherals...')
view['status'].text = 'Scanning'
cb.set_central_delegate(delegate)
cb.scan_for_peripherals()
motion.start_updates()
action_loop = True
# Keep the connection alive until the 'Stop' button is pressed:
try:
while action_loop:
roll, pitch, yaw = motion.get_attitude()
yaw = math.degrees(yaw)
pitch = math.degrees(pitch)
roll = math.degrees(roll)
view['yaw'].text = '{:.2f}'.format(yaw)
view['pitch'].text = '{:.2f}'.format(pitch)
view['roll'].text = '{:.2f}'.format(roll)
move = int(-max(-60, min(60, pitch)) / 60 * 100)
turn = int( max(-60, min(60, roll)) / 60 * 100)
view['move'].text = '{}'.format(move)
view['turn'].text = '{}'.format(turn)
delegate.send_action(move, turn)
time.sleep(0.5)
except KeyboardInterrupt:
pass
# Disconnect everything:
cb.reset()
Pythonista3 UI
m5bala_controller_ble.pyui
[
{
"nodes" : [
{
"nodes" : [
],
"frame" : "{{103, 27}, {60, 32}}",
"class" : "Label",
"attributes" : {
"name" : "label1",
"frame" : "{{82, 125}, {150, 32}}",
"uuid" : "5144134F-E68A-4023-A81E-A67824AF01CA",
"class" : "Label",
"alignment" : "right",
"text" : "方角",
"font_size" : 24,
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{171, 27}, {103, 32}}",
"class" : "Label",
"attributes" : {
"name" : "yaw",
"frame" : "{{82, 125}, {150, 32}}",
"uuid" : "F6954500-8FAB-4E42-B4A6-29A7865C50F9",
"class" : "Label",
"alignment" : "right",
"text" : "",
"font_size" : 24,
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{103, 67}, {60, 32}}",
"class" : "Label",
"attributes" : {
"name" : "label2",
"frame" : "{{82, 125}, {150, 32}}",
"uuid" : "01A7842C-1680-4655-9F09-F3B52A0721B6",
"class" : "Label",
"alignment" : "right",
"text" : "前後",
"font_size" : 24,
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{171, 67}, {103, 32}}",
"class" : "Label",
"attributes" : {
"name" : "pitch",
"frame" : "{{82, 125}, {150, 32}}",
"uuid" : "A73FDD85-DBEE-4857-9B9E-39B13FEEA553",
"class" : "Label",
"alignment" : "right",
"text" : "",
"font_size" : 24,
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{103, 107}, {60, 32}}",
"class" : "Label",
"attributes" : {
"name" : "label3",
"frame" : "{{82, 125}, {150, 32}}",
"uuid" : "A5EF17D2-3351-4AA3-B918-A82382B2DF4D",
"class" : "Label",
"alignment" : "right",
"text" : "左右",
"font_size" : 24,
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{171, 107}, {103, 32}}",
"class" : "Label",
"attributes" : {
"name" : "roll",
"frame" : "{{82, 125}, {150, 32}}",
"uuid" : "7744E0CF-F2CD-4985-AAD3-A68158E7F67D",
"class" : "Label",
"alignment" : "right",
"text" : "",
"font_size" : 24,
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{96, 160}, {67, 45}}",
"class" : "Label",
"attributes" : {
"font_size" : 32,
"frame" : "{{111, 149}, {150, 32}}",
"uuid" : "9B0F3B32-82ED-4C90-A99C-9E827DCB1DE6",
"class" : "Label",
"alignment" : "right",
"text" : "移動",
"name" : "label4",
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{171, 160}, {103, 45}}",
"class" : "Label",
"attributes" : {
"font_size" : 32,
"frame" : "{{111, 149}, {150, 32}}",
"uuid" : "4D458FEF-D620-49B5-A99E-DCA9B9988380",
"class" : "Label",
"alignment" : "right",
"text" : "",
"name" : "move",
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{96, 225}, {67, 45}}",
"class" : "Label",
"attributes" : {
"font_size" : 32,
"frame" : "{{111, 149}, {150, 32}}",
"uuid" : "E4C08BE2-1406-4855-8DCD-539F4E46676B",
"class" : "Label",
"alignment" : "right",
"text" : "回転",
"name" : "label5",
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{171, 225}, {103, 45}}",
"class" : "Label",
"attributes" : {
"font_size" : 32,
"frame" : "{{111, 149}, {150, 32}}",
"uuid" : "DC741DAD-FA01-43E7-B4C6-3FBD6699E467",
"class" : "Label",
"alignment" : "right",
"text" : "",
"name" : "turn",
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{71, 301}, {218, 32}}",
"class" : "Label",
"attributes" : {
"name" : "status",
"frame" : "{{111, 149}, {150, 32}}",
"uuid" : "42BB3ACF-D696-434B-A296-9EEEEC13DC27",
"class" : "Label",
"alignment" : "center",
"text" : "Status",
"font_size" : 24,
"font_name" : "<System>"
},
"selected" : false
},
{
"nodes" : [
],
"frame" : "{{140, 365}, {87, 37}}",
"class" : "Button",
"attributes" : {
"action" : "end_controller",
"border_width" : 1,
"frame" : "{{146, 149}, {80, 32}}",
"title" : "終了",
"uuid" : "0EC4234B-2C78-4812-89A4-236FB5384680",
"class" : "Button",
"corner_radius" : 8,
"font_bold" : true,
"name" : "button1",
"font_size" : 24
},
"selected" : false
}
],
"frame" : "{{0, 0}, {350, 460}}",
"class" : "View",
"attributes" : {
"border_color" : "RGBA(0.000000,0.000000,0.000000,1.000000)",
"enabled" : true,
"background_color" : "RGBA(1.000000,1.000000,1.000000,1.000000)",
"name" : "M5BALA Controller",
"tint_color" : "RGBA(0.000000,0.478000,1.000000,1.000000)",
"flex" : ""
},
"selected" : false
}
]