iPhoneを使ってM5BALAを操作する（Pythonista-BLE編）

#概要

iPhone を使って M5BALA を操作する、の４回目は Pythonista を使いBLE経由で M5BALA を操作します。iPhone を使ってBLE経由で M5BALA を動かす仕組みとしては前回の Blynk を使用する方法が手っ取り早いですが、お勉強ということで。

##環境

- M5BALA+M5Stack Fire
- [Pythonista3 v3.2](http://omz-software.com/pythonista/)
- iPhone6 iOS 12.1
- [Arduino 1.8.7](https://www.arduino.cc/en/Main/Software) 以下のライブラリを使用（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

##実行例

iPhone画面表示例
![IMG_1881.png](https://qiita-image-store.s3.amazonaws.com/0/133064/78be845f-0e8d-0385-52e3-b984ced3ac83.png)

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">見た目はあまり変わりませんが、仕組みをいろいろ変えて、<br>「iPhoneを使ってM5BALAを操作する（Pythonista-BLE編）」<a href="https://t.co/eg7Vhz6v8U">https://t.co/eg7Vhz6v8U</a><a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://twitter.com/hashtag/M5BALA?src=hash&amp;ref_src=twsrc%5Etfw">#M5BALA</a> <a href="https://t.co/ejFiXz0aRz">pic.twitter.com/ejFiXz0aRz</a></p>&mdash; 稲澤祐一 (@inasawa) <a href="https://twitter.com/inasawa/status/1060162121994911744?ref_src=twsrc%5Etfw">2018年11月7日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

#M5BALA制御用プログラム

以下のサンプルプログラムを一部修正して使用します。

https://github.com/m5stack/M5Bala/tree/master/src
Default_firmware.ino
M5Bala.cpp
M5Bala.h

```cpp: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経由で送信する仕組みです。

```python: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

```json: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
  }
]
```