team M and Aの部屋

ゆるーい電子工作好きな夫婦のブログです。

micro:bitからBLE通信で送られたデータをWebブラウザで表示する

今更ながらBLE通信のお勉強のため、眠っていたmicro:bitを引っ張り出してきました。micro:bitとPCをBLE接続し、micro:bitからNotifyで送信されたデータをWebブラウザで取得するところまでできたので、備忘録として残します。

環境

micro:bitArduino IDEでプログラミングするための準備

以下のページを参考にしました。

sanuki-tech.net

Arduino IDEのボードマネージャーで「nRF5 boards」を追加します。この中にmicro:bitが含まれています。今回使うのはV1.5なので、「BBC micro:bit」を選択します。

 

BLEPeripheralライブラリの追加

BLE通信用に以下のライブラリを使用します。

github.com

  1. 上記リンクからzipファイルをダウンロード。
  2. Arduino IDEの[スケッチ]-[ライブラリをインクルード]-[ZIP形式のライブラリのインストール]をクリック。
  3. 1でダウンロードしたZIPファイルを選択。
  4. [ファイル]-[スケッチ例]で「BLEPeripheral」のサンプルを選択できることを確認。

 

LED表示用Screenライブラリの追加

BLEPeripheralと同様に下記のライブラリも追加します。

github.com

 

micro:bitソースコード (Arduino)

ベースのプログラムは下記を参考にさせてもらいました。

tomosoft.jp

動作はざっくり以下のようになっています。

  • Custom service (testService)を設定
  • Custom Characteristic (testReadWriteCharacteristic, testNotifyCharacteristic)を設定
  • LED表示
    • 接続前:ひし形(IconNames::Diamond)
    • 接続中:笑顔(IconNames::Happy)
    • 接続断:悲しい顔(IconNames::Sad)
  • 接続中かつボタンAを押しているとき、Notify発行

#include <bleperipheral.h>
#include <microbit_screen.h>
 
BLEPeripheral blePeripheral = BLEPeripheral();

// create service
//uuidgen
BLEService testService = BLEService("19b10010-e8f2-537e-4f6c-d104768a1214");
 
char defaultVal[] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 0x00};
 
BLECharacteristic testReadWriteCharacteristic = BLECharacteristic("19b10011-e8f2-537e-4f6c-d104768a1214",
                                       BLERead | BLEWrite,
                                       11);
BLECharacteristic testNotifyCharacteristic = BLECharacteristic("19b10012-e8f2-537e-4f6c-d104768a1214",
                                       BLERead | BLENotify,
                                       20);
char* receivedBuf = "          ";
 
void setup() {
  Serial.begin(115200);
  SCREEN.begin();
 
  pinMode(PIN_BUTTON_A, INPUT_PULLUP);
  pinMode(PIN_BUTTON_B, INPUT_PULLUP);
 
  blePeripheral.setDeviceName("BBC micro:bit");
  blePeripheral.setLocalName("BBC micro:bit");
 
  blePeripheral.setAdvertisedServiceUuid(testService.uuid());
  testReadWriteCharacteristic.setValue(defaultVal);
 
  blePeripheral.addAttribute(testService);
  blePeripheral.addAttribute(testReadWriteCharacteristic);
  blePeripheral.addAttribute(testNotifyCharacteristic);

  blePeripheral.setConnectionInterval(0x0006, 0x0028);
 
  // BLE init
  blePeripheral.begin();
  Serial.println(F("BLE HOGE Peripheral"));
}


void loop() {
  SCREEN.showIcon(IconNames::Diamond);
 
  // Blocking wait...
  BLECentral central = blePeripheral.central();
  if (central) {
    // central connected to peripheral
    Serial.print(F("Connected to central: "));
    Serial.println(central.address());

    while (central.connected()) {
      SCREEN.showIcon(IconNames::Happy);
      if (digitalRead(PIN_BUTTON_A) == LOW) {
        unsigned char temp[20] = {};
        uint32_t time_us = micros();
        memcpy(temp, &time_us, 4);
        for(int i = 4; i < 20; i++) {
          temp[i] = i;
        }
        if(testNotifyCharacteristic.canNotify()){
          testNotifyCharacteristic.setValue(temp, 20);
          Serial.println(time_us, DEC);
        } else {
          Serial.println("wait");
        }
      }
    }
    Serial.print(F("Disconnected from central: "));
    Serial.println(central.address());
    SCREEN.showIcon(IconNames::Sad);
    delay(2000);
  }
}

Webブラウザの準備

chromeを起動し、下記をURLのところに入力する。

chrome://flags/#enable-experimental-web-platform-features

以下の画像のように、「Experimental Web Platform features」を「Enabled」に変更する。

chromeを再起動する。

htmlファイルの作成

WebブラウザからBLE接続、Notifyデータの取得をするためのhtmlファイルを作成しました。普段はまったくコーディングすることのない不慣れなhtmlとjavascriptを色んなサイトを参考にさせてもらいながら書きました。

一応、動作するようになったものが下記です。(再接続処理は未完成)


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>BLE Test</title>
</head>

<body>
<form name="js">
  <div>
    <input type="button" value="接続" onclick="connect();"/>
    <input type="button" value="切断" onclick="disconnect();" />
  </div>
  <div>
    <p id="notify">Send</p>
    <p>
      <input type="text" name="y" value="" />
      <input type="button" value="送信" onclick="send();" />
    </p>
  </div>
  <div>
    <p id="notify">Notify</p>
    <p>
      <textarea cols="50" rows="10" id="notify_msg"></textarea>
    </p>
  </div>
</form>

<script>
var connect_try = true;

var ble_device;
var ble_server;
var gatt_service;
var send_characteristic;
var notify_characteristic;

//micro:bit BLE UUID
var TEST_SERVICE_UUID            = '19b10010-e8f2-537e-4f6c-d104768a1214';
var SEND_CHARACTERISTIC_UUID     = '19b10011-e8f2-537e-4f6c-d104768a1214';
var NOTIFY_CHARACTERISTIC_UUID   = '19b10012-e8f2-537e-4f6c-d104768a1214';

function connect() {
  navigator.bluetooth.requestDevice({
    filters: [{
      namePrefix: 'BBC micro:bit',
    }],
    optionalServices: [TEST_SERVICE_UUID]
  })
  .then(device => {
    ble_device = device;
    console.log("device", device);
    return device.gatt.connect();
  })
  .then(server =>{
    ble_server = server;
    console.log("server", ble_server)
    return ble_server.getPrimaryService(TEST_SERVICE_UUID);
  })
  .then(service => {
    gatt_service = service
    console.log("service", service)
    return gatt_service.getCharacteristic(NOTIFY_CHARACTERISTIC_UUID)
  })
  .then(chara => {
    console.log("Notify:", chara)
    notify_characteristic = chara;
    notify_characteristic.startNotifications();
    notify_characteristic.addEventListener('characteristicvaluechanged',onNotification);  
    return gatt_service.getCharacteristic(SEND_CHARACTERISTIC_UUID)
  })
  .then(chara => {
    alert("BLE接続が完了しました。");
    console.log("Send:", chara)
    send_characteristic = chara;
    // 切断時に実行される関数
    ble_device.addEventListener('gattserverdisconnected', onDisconnected);
    connect_try = true;
    return;
  })
  .catch(error => {
    alert("BLE接続に失敗しました。");
    console.log(error);
  });    
}

function onNotification(event) {
  let notify_element = document.getElementById('notify_msg');
  console.log(event.target.value);
  
  let timestamp = event.target.value.getUint32(0, littleEndian = true);
  notify_element.value = notify_element.value + timestamp + ":";

  for(let i = 4; i < event.target.value.byteLength; i++){
    let temp = event.target.value.getUint8(i);
    notify_element.value = notify_element.value + temp;
  }
  notify_element.value = notify_element.value + "\r\n";
}

function disconnect() {
  if (!ble_device || !ble_device.gatt.connected) return ;
  ble_device.gatt.disconnect();
  connect_try = false;
  alert("BLE接続を切断しました。")
}

function send() {
  temp = Uint8Array.from(document.js.y.value);
  console.log('y:' + temp);
  return (send_characteristic.writeValue(temp))
  .catch(error => {
    console.log('Error : ' + error);
    this.onError(error);
  });
}

function onDisconnected() {
  console.log('Disconnected');
  let timerId = setInterval(function() {
    if(connect_try){
      if(ble_device.gatt.connect()) {
        clearInterval(timerId);
        console.log('Connected');
      }
    } else {
      clearInterval(timerId);
    }
  }, 1000);
}
</script>    
</body>
</html>

下記が動作時の画面です。

「接続」ボタンをクリックすると、BLEデバイスを検索します。

Notifyが届くと、1行ずつ表示されます。最初の10桁の数字(:まで)はmicro:bitで付加したタイムスタンプ(us単位)です。どの程度の通信速度が出せるか調べてみたかったので、タイムスタンプ4byte+16byteの合計20byteを送信しています。

Notifyを連続送信させたところ、45msで20Byte×6回=120Byte送信できていたので、20kbpsくらい出ていた計算になります。ちょっとしたログを取るのに使えそうな感じ。

作品紹介:ミニそんたくん

そんたくんとは、新型コロナウイルスが流行する少し前に、なんとなく見つけたCO2センサで作ってみた作品。

tmanda.hatenablog.com

まさかこんなにCO2測定が世の中で流行るとは、、、

 

そのそんたくんのミニバージョンができました。

f:id:tmanda:20211211211305j:plain

幅70mm, 高さ50mm, 厚さ30mm

元祖そんたくんが150mm×120mm×30mmなのでかなり小さくなりました。

ミニでもそんたくんなので空気(CO2)濃度を測定して、高いか低いかほっぺの色でお知らせしてくれます。

f:id:tmanda:20211211211914j:plain

ESP8266も載せたので、CO2濃度をWiFiで飛ばすことができます。現在ThingSpeakを使っています。

元祖そんたくんにあった口はありません。スピーカーは入っているので、潜在能力的には音を出せるのですが、まだソフトを実装していません。

他には、お鼻がタクトスイッチになっていたり、SDカードも搭載していたりと元祖そんたくんにはなかった機能を追加しています。(ソフトはまだ・・・)

 

中身はこんな感じ。

f:id:tmanda:20211211212024j:plain

金色がCO2センサです。これはNDIR式なのでそれなりに良い精度で値を表示してくれます。MH-Z19Bという型番のもので、今までAliexpressで購入していたのですが、いつの間にか同シリーズのものが秋月でも売られるようになっていました。

akizukidenshi.com

 

ちなみにCO2センサはNDIR式の方がガスセンサ式よりもよいそうです。

参考: https://www.youtube.com/watch?v=sGHRCQmjrHQ&t=0s

 

ケースはいつもお世話になっているDMM 3Dプリントサービスで作りました。素材は一番安いナイロン白。結構いい精度で作ってもらえます。

 

今回の基板には、がじぇるねのGR-COTTONと同じRL78マイコンを実装したので、Arduinoコードがそのまま使えます。

www.renesas.com

 

小さくても高機能(になれる)ミニそんたくん、もうちょっとコードを書いてもっと能力を出せるようにしてあげないと。。。

フラックスの罠にはまる・・・

最近、QFP部品のはんだ付けに挑戦しました。

f:id:tmanda:20210619185333j:plain

 0.5mmピッチのRenesas製RL78マイコンです。

akizukidenshi.com

 

QFPのはんだ付けの動画をYoutubeでいろいろ見て、フラックスがあった方が良さそうだということで使いました!

f:id:tmanda:20210619185353j:plain

昔はんだごてを買ったときに、確かセットについていたフラックス。よくよく見ると、「プリント基板には使用できません」の文字が。。。

そんなことには気付かず、ジュージュー言わせながらはんだ付け完了!!

 

ちょっと濃度低いかもなと思いながら、消毒用アルコールで洗浄後、いざ、動作確認、、、

 

動かない!!

 

はんだブリッジは虫眼鏡でチェックしたのに、、、

念のため、はんだごてを当ててみると、ジューーーー・・・

まだフラックスが残っていたのか。

洗浄→動作確認→動かない→はんだごてジューーー→洗浄・・・

 

これを何度か何度か繰り返して、なんとかようやく動くようになりました。

QFPのICの間に入り込んで残ったものがなかなか取れなかったのかな。。。

 

そして気付いた。「プリント基板には使用できません」

 

Google先生にも教えてもらいました。フラックスには洗浄タイプと無洗浄タイプがあって、洗浄タイプはしっかり取りきらないといけないんですね。

 

そして手に入れた!無洗浄タイプでプリント基板用と書かれたフラックス!!

f:id:tmanda:20210619185403j:plain

 

このフラックスで作った2枚目の基板はあっさり動作確認完了。

めでたしめでたし。

作品紹介:ほっとぷれーと

玄関に置くほっとな鍵置きを作りました。


www.youtube.com

 

家に帰ってきて、ほっとぷれーとに鍵を置くと、

  • 家族の声で「おかえり」と言ってもらえる
  • LINEで帰宅したことを家族に伝える

お出かけするときに、ほっとぷれーとから鍵を取ると、

  • 家族の声で「いってらっしゃい」と言ってもらえる
  • LINEで出かけたことを家族に伝える

 

仕事で家にいないときに子供の帰宅を確認したり、単身赴任や一人暮らしの寂しさの紛らわしたり、遠くのおじいちゃんおばあちゃんを見守ったり、そんな風に使えるといいかもなぁと考えています。

 

鍵を置いたこと・取ったことの検知には、改札で使うICカードと同様の仕組みを使っています。ほっとぷれーとの中には、↓のFeliCaリーダーが入っています。

www.switch-science.com

 

f:id:tmanda:20210619180903j:plain

 

そして動画に出てきたアリクイとラッコのキーホルダーにはNFCタグのシールが貼ってあります。(白い丸いシールです)

f:id:tmanda:20210619180851j:plain

 

FeliCaリーダーでこの白いシールを読み取ることで、鍵を置いたり取ったりしたことを検知しています。 

 

接触って面白い!!

工具のきもち LINEスタンプ

工具のきもちを表現したLINEスタンプがデビューしました!

 

工具ってよく見ると1つ1つ顔が違いますよね。

そんな工具たちはいったいどんな気持ちで過ごしているんだろうと思いませんか。 

 

 ↓↓作品はこちら↓↓

 https://store.line.me/stickershop/product/11936314

f:id:tmanda:20200523225157p:plain 

工具といっても幅広いので、電子工作で身近なもの、

日常使うような工具に絞ってスタンプにしてみました。

ぜひ使ってみてくださいね!

ガーデニングはじめました

Stay homeということでガーデニングをはじめました。

種を植えてから2週間ほど経ち、双葉もだいぶ大きくなってきました。


Arduino (GR-ROSE) Robot Gardener

 

お出かけできるようになっときに備えて、水やりの自動化を目論見中です。
水を流す・止めるの制御には、サイフォンの原理を利用しています。水源のペットボトルをある高さで固定しておき

  • アームの先端が高いときは水が流れない
  • アームの先端が低いときは水が流れる

となるので、これなら弁とか蛇口とか難しいことを考える必要がなさそう!

サイフォン - Wikipedia

アーム&ペットボトルの固定とか実験用のビニール袋がかっこよくないとかまだまだ改良ポイントはいっぱい。

ロボットアームとArduino (GR-ROSE)で機構学のお勉強

tmanda.hatenablog.com で紹介したロボットアームを制御するための機構学のお勉強です。 f:id:tmanda:20200506103951p:plain ↑これの話です。

さっそくはじめていきましょう。

まずはロボットアームの関節角度を定義します。今回はロボットアームが垂直に立った状態を0度とします。
サーボモータの可動範囲は0度±135度) f:id:tmanda:20200506130807p:plain

ロボットアームの関節の位置(サーボモータの回転軸の位置)を計算します。 f:id:tmanda:20200509141301p:plain

今回はロボットアームの先端が水平に同じ高さになるように制御します。 f:id:tmanda:20200506131038p:plain 

「ロボットアームの先端が水平に同じ高さ」を数式で表すと次のようになります。 f:id:tmanda:20200509141308p:plain

①②の式を代入すると、 f:id:tmanda:20200509141354p:plain ここで黄色マークした式をこの後で使います。根本のモータを好きなように制御したときの他の2つのモータ角度を計算します。 f:id:tmanda:20200506131411p:plain

あとはプログラムにモータ角度をセットするコードを書くということになります。

※注意点
1. モータの可動範囲は実物のアームが干渉しない範囲になります。アーム同士がぶつからないようにしましょう。
2. 今回は高さを一定に保つ制御をしていますが、リンクが届かない範囲があります。以下の絵を参照。
f:id:tmanda:20200506135721p:plain

今回使用したArduino互換ボード「GR-ROSE」には近藤科学サーボモータを接続できるコネクタとライブラリが用意されているので、すぐに試すことができました。

www.renesas.com

ソースコードは以下です。

#include <ICS.h>

IcsController ICS(Serial1);
IcsServo servo0;
IcsServo servo1;
IcsServo servo2;
IcsServo servo3;

int theta0_pulse = 0;  //3500-11500
int theta1_pulse = 0;
int theta2_pulse = 0;
int theta3_pulse = 0;

bool motion_enable = false;
bool dir0 = false;
bool dir1 = false;

#define TH0_ZERO 7500
#define TH0_LIMIT_MIN 3500
#define TH0_LIMIT_MAX 11500

#define TH1_ZERO 7500
#define TH1_LIMIT_MIN 4500
#define TH1_LIMIT_MAX 10500

#define TH2_ZERO 7500
#define TH2_LIMIT_MIN 4500
#define TH2_LIMIT_MAX 10500

#define TH3_ZERO 7500
#define TH3_LIMIT_MIN 4500
#define TH3_LIMIT_MAX 10500


void setup() {

  Serial.begin(115200);
  pinMode(PIN_LED1, OUTPUT);

  ICS.begin();
  servo0.attach(ICS, 0x01);
  servo1.attach(ICS, 0x02);
  servo2.attach(ICS, 0x03);
  servo3.attach(ICS, 0x04);
  
  theta0_pulse = TH0_ZERO;
  theta1_pulse = servo1.getPosition();
  theta2_pulse = servo2.getPosition();
  theta3_pulse = servo3.getPosition();

  setPosition4axis(theta0_pulse, theta1_pulse, theta2_pulse, theta3_pulse);
  delay(1000);
  digitalWrite(PIN_LED1, HIGH);
}

void loop() {
  if(Serial.available()){
    switch(Serial.read()){
      case 's':
        motion_enable = !motion_enable;
        break;
      case '0':
        motion_enable = false;
        theta0_pulse = TH0_ZERO;
        theta1_pulse = TH1_ZERO;
        theta2_pulse = TH2_ZERO;
        theta3_pulse = TH3_ZERO;
        break;
      default:
        break;
    }
  }
  
  if(motion_enable){
    int tmp;
    int dth = 50;

    if(theta0_pulse > (TH0_LIMIT_MAX - 400)){
      dth = 50 - (TH0_LIMIT_MAX - 400 - theta0_pulse)/8;
      if(dth < 5) dth = 5;
    } else if(theta0_pulse < (TH0_LIMIT_MIN + 400)) {
      dth = 50 - (TH0_LIMIT_MIN + 400 - theta0_pulse)/8;
      if(dth < 5) dth = 5;
    } else {
      dth = 50;
    }
    if(dir0){
      theta0_pulse += dth;
      if(theta0_pulse > TH0_LIMIT_MAX){
        theta0_pulse = TH0_LIMIT_MAX;
        dir0 = !dir0;
      }
    } else {
      theta0_pulse -= dth;
      if(theta0_pulse < TH0_LIMIT_MIN){
        theta0_pulse = TH0_LIMIT_MIN;
        dir0 = !dir0;
      }
    }//*/
    if(theta1_pulse > 8620){
      dth = 100 - (theta1_pulse-8620)/4;
      if(dth < 5) dth = 5;
    } else if(theta1_pulse < 6000) {
      dth = 100 - (6000 - theta1_pulse)/4;
      if(dth < 5) dth = 5;
    } else {
      dth = 100;
    }
    if(dir1){
      theta1_pulse += dth;
      if(theta1_pulse > 9000){
        theta1_pulse = 9000;
        dir1 = !dir1;
      }
    } else {
      theta1_pulse -= dth;
      if(theta1_pulse < 5620){
        theta1_pulse = 5620;
        dir1 = !dir1;
      }
    }//*/
    tmp = calc_theta2_pulse(theta1_pulse);
    if(tmp != 0) theta2_pulse = tmp;
    theta3_pulse = TH3_ZERO - ((theta1_pulse - TH1_ZERO) + (theta2_pulse - TH2_ZERO));
  }

  setPosition4axis(theta0_pulse, theta1_pulse, theta2_pulse, theta3_pulse);
  delay(25);
}

void setPosition4axis(int pos0, int pos1, int pos2, int pos3){
  if(pos0 < TH0_LIMIT_MIN){
    pos0 = TH0_LIMIT_MIN;
  } else if(pos0 > TH0_LIMIT_MAX){
    pos0 = TH0_LIMIT_MAX;
  }
  if(pos1 < TH1_LIMIT_MIN){
    pos1 = TH1_LIMIT_MIN;
  } else if(pos1 > TH1_LIMIT_MAX){
    pos1 = TH1_LIMIT_MAX;
  }
  if(pos2 < TH2_LIMIT_MIN){
    pos2 = TH2_LIMIT_MIN;
  } else if(pos2 > TH2_LIMIT_MAX){
    pos2 = TH2_LIMIT_MAX;
  }
  if(pos3 < TH3_LIMIT_MIN){
    pos3 = TH3_LIMIT_MIN;
  } else if(pos3 > TH3_LIMIT_MAX){
    pos3 = TH3_LIMIT_MAX;
  }
  Serial.print(servo0.setPosition(pos0)); Serial.write('\t');
  Serial.print(servo1.setPosition(pos1)); Serial.write('\t');
  Serial.print(servo2.setPosition(pos2)); Serial.write('\t');
  Serial.print(servo3.setPosition(pos3)); Serial.println();//*/
}


int calc_theta2_pulse(int theta1){
  const float l1 = 68.0f;
  const float l2 = 60.0f;
  const float l2_ref = 89.8f;
  const float pulse_to_rad = 135.0f*M_PI/(4000.0f*180.0f);
  const float rad_to_pulse = (4000.0f*180.0f)/(135.0f*M_PI);
  
  const float theta1_rad = (float)(theta1 - TH1_ZERO) * pulse_to_rad;
  const float p2_y = l1 * cos(theta1_rad);
  
  float theta2_rad;
  float cos_theta2 = (l2_ref - p2_y)/l2;
  
  if(cos_theta2 > 1.0f){
    return 0; //Error
  }
  theta2_rad = acos(cos_theta2) - theta1_rad;
  
  return ((theta2_rad * rad_to_pulse) + TH2_ZERO);
}