ダイソー Bluetooth モバイルシャッターに特化したライブラリ

2022/05/31 08:58
大バグを修正して再掲しました。


DaisoBleButton.hpp

#ifndef _DAISO_BLE_BUTTON_HPP_
#define _DAISO_BLE_BUTTON_HPP_

#include <NimBLEDevice.h>
#include <nvs.h>
#include <functional>
#include <map>

static const char * nvs_name = "DaisoBleButton";
static const char * allow_devices [] = {"AB Shutter3"};
static const NimBLEUUID uuid_service((uint16_t)0x1812);
static const NimBLEUUID uuid_characteristic((uint16_t)0x2a4d);

class DaisoBleButton {
  public:
    static int begin() {
      NimBLEDevice::init("");

      uint8_t devCount = 0;
      uint32_t nvs_handle;
      if(!nvs_open(nvs_name, NVS_READONLY, &nvs_handle)) {
        for(;;devCount++) {
          char key[3];  //, buf[20];
          sprintf(key, "%02u", devCount);
          uint64_t address;
          if(nvs_get_u64(nvs_handle, key, &address))
            break;
      
          auto pClient = NimBLEDevice::createClient(NimBLEAddress(address));
          if(pClient) {
            pClient->setConnectTimeout(1);
            buttons[pClient] = new DaisoBleButton(devCount, pClient);
          }
        }
      }
      nvs_close(nvs_handle);
      return devCount;
    }
    
    static int paring(const int period = 10) {
      auto pClients = NimBLEDevice::getClientList();
      for(auto pClient : *pClients)
        pClient->disconnect();
    
      auto pBLEScan = NimBLEDevice::getScan();
      pBLEScan->setActiveScan(true);
    
      Serial.println("paring mode. wait 10 secs..");
      auto pScanResults = pBLEScan->start(period);
    
      int devCount = 0;
      uint32_t nvs_handle;
      if(!nvs_open(nvs_name, NVS_READWRITE, &nvs_handle)) {
        for (int i = 0; i < pScanResults.getCount(); i++) {
          auto advertisedDevice = pScanResults.getDevice(i);
          Serial.print("Found Device ");
          Serial.println(advertisedDevice.getName().c_str());
          for(auto allow : allow_devices) {
            if (!strncmp(advertisedDevice.getName().c_str(), allow, strlen(allow)) && advertisedDevice.haveServiceUUID())  {
              auto pClient = NimBLEDevice::createClient(advertisedDevice.getAddress());
              if(pClient && pClient->connect()) {
                if(!devCount)
                  nvs_erase_all(nvs_handle);
      
                uint64_t address = advertisedDevice.getAddress();
                char key[3];
                sprintf(key, "%02d", devCount);
                nvs_set_u64(nvs_handle, key, address);
                Serial.printf("added to list[%d]: %llu %s", devCount, address, advertisedDevice.getAddress().toString().c_str());
                Serial.println();
                devCount ++;
                pClient->disconnect();
                break;
              }
            }
          }
        }
      }  
    
      nvs_close(nvs_handle);
      return devCount;
    }

    static void handle() {
      auto pClients = NimBLEDevice::getClientList();
      for (auto pClient : *pClients) {
        if(pClient && !pClient->isConnected() && pClient->connect()) {
          pClient->getServices(true);
          auto pService = pClient->getService(uuid_service);
          auto pCharacteristics = pService->getCharacteristics(true);
          for (auto pCharacteristic : *pCharacteristics) {
            if(uuid_characteristic.equals(pCharacteristic->getUUID())) {
              pCharacteristic->subscribe(false,
                [](BLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
                  buttons[pRemoteCharacteristic->getRemoteService()->getClient()]->subscribe(pRemoteCharacteristic, pData, length, isNotify);
                }
              );
            }
          }
        }
      }
    }

    static std::function<void(DaisoBleButton *)> onClick_A;
    static std::function<void(DaisoBleButton *)> onClick_B;
    
  private:
    static std::map<NimBLEClient *, DaisoBleButton *> buttons;

    
  public:
    uint8_t getId() {return id;}
    NimBLEAddress getAddress() {return bleClient->getPeerAddress();}

  private:
    DaisoBleButton(uint8_t _id, NimBLEClient * _bleClient) : id(_id), bleClient(_bleClient) {;}
    
    void subscribe(BLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
      if(length == 2 && (pData[0] || pData[1])) {
        if(pData[0] == 0x01 && pData[1] == 0x00) {
          if(last_value[0] == 0x00 && last_value[1] == 0x28) {
            if(onClick_B)
              onClick_B(this);
          }
          else {
            if(onClick_A)
              onClick_A(this);
          }
        }
        last_value[0] = pData[0];
        last_value[1] = pData[1];
      }
    }
  
  private:
    NimBLEClient * bleClient;
    uint8_t last_value[2] = {0x00, 0x00};
    uint8_t id;
};

std::function<void(DaisoBleButton *)> DaisoBleButton :: onClick_A = nullptr;
std::function<void(DaisoBleButton *)> DaisoBleButton :: onClick_B = nullptr;
std::map<NimBLEClient *, DaisoBleButton *> DaisoBleButton :: buttons;

#endif


使用例

/*
 * ダイソーモバイルシャッターリモコン
 *
 * IO0: HIGH→LOW/ペアリングモードに入り、10秒間の間に接続成功したデバイスを保存する
 */
 
#include "DaisoBleButton.hpp"

void setup() {
  Serial.begin(115200);
  pinMode(0, INPUT_PULLUP);

  DaisoBleButton::onClick_A = [] (DaisoBleButton * button) {
    Serial.println("button A");
  };

  DaisoBleButton::onClick_B = [] (DaisoBleButton * button) {
    Serial.println("button B");
  };
  
  if(!DaisoBleButton::begin()) {
    DaisoBleButton::paring();
    ESP.restart();
  }
}

void loop() {
  DaisoBleButton::handle();
  
  if(digitalRead(0) == LOW && DaisoBleButton::paring() > 0)
    ESP.restart();

  delay(1);
}

 ペアリングモード(デフォルト10秒間)の間に接続された ダイソーモバイルシャッター の情報を NVS に記録し、次回からはその接続のみを受け付けます。
 GPIO0 を押すと改めてペアリングモードに入り、ペアを登録し直すことが出来ます。
 ESP32-DevBoard 以外の、例えば M5Stack 等では GPIO0 ではなく備わってる物理スイッチに変更するといいでしょう。

LINEWORKS API 2.0 C# で AccessToken を得る方法 [Newtonsoft.Json を使わない版]

 NET 5.0 ~ は、「ImportFromPem」というメソッドが備わっているため、LINEWORKS が提供している PEM 形式の private-key をそのまま読み込める。

System.Collections.Generic.Dictionary<string, string> GetAccessToken(System.IO.FileInfo privateKey, string service_account, string client_id, string client_secret, params string[] scopes)
{
    using (var rsa = System.Security.Cryptography.RSA.Create())
    {
        rsa.ImportFromPem(System.IO.File.ReadAllText(privateKey.FullName));

        var descriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
        {
            Issuer = client_id,
            Claims = new System.Collections.Generic.Dictionary<string, object>() { ["sub"] = service_account },
            SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa), "RS256"),
            IssuedAt = System.DateTime.UtcNow,
            Expires = System.DateTime.UtcNow.AddMinutes(60),
        };

        var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
        var pv = new System.Collections.Specialized.NameValueCollection()
        {
            ["assertion"] = handler.WriteToken(handler.CreateJwtSecurityToken(descriptor)),
            ["grant_type"] = @"urn:ietf:params:oauth:grant-type:jwt-bearer",
            ["client_id"] = client_id,
            ["client_secret"] = client_secret,
            ["scope"] = string.Join(",", scopes),
        };

        using (var wc = new System.Net.WebClient())
        {
            var resText = System.Text.Encoding.ASCII.GetString(wc.UploadValues(@"https://auth.worksmobile.com/oauth2/v2.0/token", pv));
            return System.Text.Json.JsonSerializer.Deserialize<System.Collections.Generic.Dictionary<string, string>>(resText);
        }
    }
}

※「System.IdentityModel.Tokens.Jwt」を nuget しておくこと


 NET Framework ~ 4.8 だと XML 形式の private-key しか読めないので、いったん NET 5.0 ~ を使って XML に変換するプログラムを書いて変換しておく。

void PEM2XML(System.IO.FileInfo pemFile)
{
    using (var rsa = System.Security.Cryptography.RSA.Create())
    {
        rsa.ImportFromPem(File.ReadAllText(pemFile.FullName));
        File.WriteAllText(pemFile.FullName + ".xml", rsa.ToXmlString(true));
    }
}

 そのうえで、「ImportFromPem」の代わりに「FromXmlString」を使う。

System.Collections.Generic.Dictionary<string, string> GetAccessToken(System.IO.FileInfo privateKey, string service_account, string client_id, string client_secret, params string[] scopes)
{
    using (var rsa = System.Security.Cryptography.RSA.Create())
    {
        rsa.FromXmlString(System.IO.File.ReadAllText(privateKey.FullName));

        var descriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
        {
            Issuer = client_id,
            Claims = new System.Collections.Generic.Dictionary<string, object>() { ["sub"] = service_account },
            SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa), "RS256"),
            IssuedAt = System.DateTime.UtcNow,
            Expires = System.DateTime.UtcNow.AddMinutes(60),
        };

        var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
        var pv = new System.Collections.Specialized.NameValueCollection()
        {
            ["assertion"] = handler.WriteToken(handler.CreateJwtSecurityToken(descriptor)),
            ["grant_type"] = @"urn:ietf:params:oauth:grant-type:jwt-bearer",
            ["client_id"] = client_id,
            ["client_secret"] = client_secret,
            ["scope"] = string.Join(",", scopes),
        };

        using (var wc = new System.Net.WebClient())
        {
            var resText = System.Text.Encoding.ASCII.GetString(wc.UploadValues(@"https://auth.worksmobile.com/oauth2/v2.0/token", pv));
            return System.Text.Json.JsonSerializer.Deserialize<System.Collections.Generic.Dictionary<string, string>>(resText);
        }
    }
}