99. A-tag-not-highly-recommended

ブラウザからBLEでラズパイとデータをやり取りする方法

Webの可能性を感じさせる画像
ブラウザ経由でBLEでラズパイと接続する方法についてまとめます.

ラズパイでBLEペリフェラルを立ち上げる

ラズパイの環境

私はpyblenoというライブラリを使ってペリフェラルを立ち上げることにしましたが,2021年6月現在,ペリフェラルからセントラルに値が変化したときに通知する「Notification」という機能が2018-11-13以降のラズベリーパイOSでは動作しません.公式ドキュメントによると,リナックスカーネルのBluetoothモジュールのバグが原因ということで,通知機能が必須な場合はラズベリーパイのOSを2018-11-13以前にする必要があります.従って,その場合rasbianのStrechやJessiになると思いますのでrasberry piは3B+を使う必要があります. 一方,私はラズベリーパイ4のBモデル(4G)を使ってこの記事を書いていますが,通知機能は使えなかったのでセントラルから定期的に読みに行くことにしてあまり問題は感じていません.

ライブラリをインストールする

sudo pip3 install pybleno

ソースコード

公式に置いてあるサンプルコードを参考に作成しました.main.pyとEchoCharacteristic.pyからなっています.
from pybleno import *
import sys
import signal
from EchoCharacteristic import *

print('bleno - echo');

bleno = Bleno()

def onStateChange(state):
   print('on -> stateChange: ' + state);

   if (state == 'poweredOn'):
     bleno.startAdvertising('echo', ['0000fff0-0000-1000-8000-00805f9b34fb'])
   else:
     bleno.stopAdvertising();

bleno.on('stateChange', onStateChange)
    
def onAdvertisingStart(error):
    print('on -> advertisingStart: ' + ('error ' + error if error else 'success'));

    if not error:
        bleno.setServices([
            BlenoPrimaryService({
                'uuid': '0000fff0-0000-1000-8000-00805f9b34fb',
                'characteristics': [ 
                    EchoCharacteristic('0000fff1-0000-1000-8000-00805f9b34fb')
                    ]
            })
        ])
bleno.on('advertisingStart', onAdvertisingStart)

bleno.start()
print ('Hit <ENTER> to disconnect')

if (sys.version_info > (3, 0)):
    input()
else:
    raw_input()

bleno.stopAdvertising()
bleno.disconnect()

print ('terminated.')
sys.exit(1)
from pybleno import Characteristic
import array
import struct
import sys
import traceback
import random

class EchoCharacteristic(Characteristic):
    
    def __init__(self, uuid):
        Characteristic.__init__(self, {
            'uuid': uuid,
            'properties': ['read', 'write', 'notify'],
            'value': None
          })
          
        self._value = array.array('B', [0] * 0)
        self._updateValueCallback = None
          
    def onReadRequest(self, offset, callback):
        try:
            print('EchoCharacteristic - %s - onReadRequest: value = %s' % (self['uuid'], [hex(c) for c in self._value]))
        except:
            print('error')
        #callback(Characteristic.RESULT_SUCCESS, self._value[offset:])
        callback(Characteristic.RESULT_SUCCESS, array.array('B',[20,90,50,100]))
    def onWriteRequest(self, data, offset, withoutResponse, callback):
        #global data
        #data += 1
        self._value = random.randint(1,10)#data
        print('write called')
        print(data,data[0],data.hex(),'data')
        #print('EchoCharacteristic - %s - onWriteRequest: value = %s' % (self['uuid'], [hex(c) for c in self._value]))
        """
        if self._updateValueCallback:
            print('EchoCharacteristic - onWriteRequest: notifying'); 
            self._updateValueCallback(self._value)
        """
        
        callback(Characteristic.RESULT_SUCCESS)
        
    def onSubscribe(self, maxValueSize, updateValueCallback):
        print('EchoCharacteristic - onSubscribe')
        
        self._updateValueCallback = updateValueCallback

    def onUnsubscribe(self):
        print('EchoCharacteristic - onUnsubscribe');
        
        self._updateValueCallback = None
“`

スマホやPCのブラウザをBLEセントラルにしてラズパイからデータを取得

フレームワークを用いずに素のHTMLとJavaScriptで実装する方法はこちら

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8"/>
  <title>AI App</title>

  <base href="/"/>

  <meta name="color-scheme" content="light dark"/>
  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
  <meta name="format-detection" content="telephone=no"/>
  <meta name="msapplication-tap-highlight" content="no"/>
</head>

<body>
  <h1>お知らせ</h1>
 <div id="text1">Hello BLE</div>
<button id="button1">READ</button>
<button id="button2">Write</button>

<script>

 function uint32ToArrayBuffer(n) {
   const view = new DataView(new ArrayBuffer(4));
   view.setUint32(0, n, false);
   return view.buffer;
 }
    
//ClickEvent
document.getElementById("button1").addEventListener("click", function(){
    console.log('hi');

    // 1.BLEデバイスをスキャンする
navigator.bluetooth.requestDevice({
  acceptAllDevices:true, // 全てのデバイスを対象にスキャンを実施する
  optionalServices:['0000fff0-0000-1000-8000-00805f9b34fb']
}).then(device => {

  // 2.デバイスに接続
  return device.gatt.connect();

}).then(server =>{

  // 3-1.「Service」を指定
  return server.getPrimaryService("0000fff0-0000-1000-8000-00805f9b34fb");

}).then(service =>{
    console.log('hikoko')

  // 3-2.「Characteristc」を指定
  return service.getCharacteristic("0000fff1-0000-1000-8000-00805f9b34fb");

}).then((characteristic)  => {
    console.log('hikoko2')

    return characteristic.writeValue(uint32ToArrayBuffer(15)).then(char => {
        console.log('write done',char)
    });
    
    return characteristic.readValue().then(char => {
        console.log('hikoko3',char,char.getUint8(0))
        console.log('hikoko4',char,char.getUint8(1))
        console.log('hikoko4',char,char.getUint8(2))
    });
    
/*
  const countUp = () => {
      console.log('unko');
      return characteristic.readValue().then(char => {
        console.log('hikoko3',char,char.getUint8(0))
        console.log('hikoko4',char,char.getUint8(1))
        console.log('hikoko4',char,char.getUint8(2))
      });
  }
  setInterval(countUp, 50);
  */
});

});

//ClickEvent
document.getElementById("button2").addEventListener("click", function(){
    console.log('hi button2');

    // 1.BLEデバイスをスキャンする
navigator.bluetooth.requestDevice({
  acceptAllDevices:true, // 全てのデバイスを対象にスキャンを実施する
  optionalServices:['0000fff0-0000-1000-8000-00805f9b34fb']
}).then(device => {

  // 2.デバイスに接続
  return device.gatt.connect();

}).then(server =>{

  // 3-1.「Service」を指定
  return server.getPrimaryService("0000fff0-0000-1000-8000-00805f9b34fb");

}).then(service =>{
    console.log('hikoko')

  // 3-2.「Characteristc」を指定
  return service.getCharacteristic("0000fff3-0000-1000-8000-00805f9b34fb");

}).then((characteristic)  => {
    console.log('hikoko2')

  //4.受信準備を行う
  return characteristic.startNotifications().then(char => {
    console.log('hikoko3',char)

    //5.受信したバイナリを解析、処理の実施
    characteristic.addEventListener('characteristicvaluechanged', (event) => {
        console.log(event.target.value,'event.target.value');
      // 「event.target.value」がDataView型で渡ってくるのでこれを解析

    });
  });
});


});

    
</script>
    </body>
</html>

Angularを用いる方法

GiuHubの方にまとめました. https://github.com/NP-Systems/demo-project-of-angular-ble/tree/main
Meditation Tools開発者
絹田 雅
複数の瞑想を学ぶことができるMeditation Toolsの開発者。 売上は人権段階を通じた寄附により社会をより良くすることに使われます。 利用はこちら
twitter-timeline