IoTデバイスの作り方[Bluno Beetle BLE]

この記事では、BLE機能を持つマイコンにセンサーを繋げてIoTデバイスを作成、センサー値を自作スマホアプリで受け取れるところまで基礎的な内容を教えます。
提供するサンプルコードを改造することで様々なIoTデバイスが作成できるはずです！

# 準備するもの

<dl>
  <dt>iPhone(BLE機能を備える4s以降の機種）</dt>
  <dd>筆者がAndroid端末を使用したことがないため</dd>
  <dt>Mac</dt>
  <dd>アプリ開発にXcodeを使用するため</dd>
  <dt>Xcode</dt>
  <dd>アプリ開発用</dd>
  <dt>Arduino IDE</dt>
  <dd>マイコンのプログラミング用</dd>
  <dt>LightBlue</dt>
  <dd>BLEのUUIDを確認するため</dd>
  <dt>BLE機能を持つマイコン</dt>
  <dd>
  
 [Beetle BLE](https://www.switch-science.com/products/4312?srsltid=AfmBOoqNqkhuBNAFFl4nM1Ov-LwxHte25uaSNd4b_56reTn77HV1BsPnm)等の小型なものが望ましい</dd>
  <dt>お好みのセンサー</dt>
  <dd>
  
  [秋月電子](https://akizukidenshi.com/catalog/goods/search.aspx?search=x&keyword=%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC&search=search)のセンサー検索結果</dd>
</dl>

以下の記事ではBeetle BLEの使用を前提に説明

# マイコンの配線
VIN:電源の+線
GND:電源の-線、センサーのGNDピン
5V:センサーの電源ピン
A0:センサーの出力ピン


# Arduino IDEの準備
ツールのボードからArduino Unoを選択
書き込みポートはUSBを選択

# マイコンのプログラミング
Beetle BLEのボーレート:
115200bps

```ino:IoT_device.ino
int data = 0;

void setup() {
  Serial.begin(115200);
}

void loop() {
  uint16_t value = analogRead(A0);
  if (data == 0 && Serial.available() > 0) {
    data = Serial.read();
    if (data == 1) {
      Serial.write((uint8_t*)&value, sizeof(value));
      data = 0;
    }
  }
}
```
スマホから信号値１を受け取ると、アナログピンA0に繋がれたセンサーの値をスマホに返します。
# XcodeとiPhoneの準備
[Xcodeで作成したアプリを実機デバイスでテストする方法](https://www.ingenious.jp/articles/howto/xcode/xcode-actual-machine-test/)
上記記事を参考に準備してください

# アプリ実装
LightBlueでBLEのUUIDを調べます。

デバイス名:Bluno
Beetle BLEのUUID:
DFB0:サービスUUID(端末と接続するのに使用)
DFB1:Serial通信(データのやり取りで使用)
DFB2:AT Ctrl(今回は使わない)

```swift:ViewController.swift
import UIKit
import CoreBluetooth

class ViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {
    
    // MARK: - プロパティ
    var centralManager: CBCentralManager!
    var connectedPeripheral: CBPeripheral?
    var rxCharacteristic: CBCharacteristic?
    
    // MARK: - IBOutlet
    @IBOutlet weak var SSIDUILabel: UILabel!
    
    // MARK: - ライフサイクル
    override func viewDidLoad() {
        super.viewDidLoad()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }
    
    // MARK: - CBCentralManagerDelegate メソッド
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            scanBLEModule()
        case .poweredOff:
            SSIDUILabel.text = "Bluetoothがオフです"
            print("Bluetooth is Off")
        default:
            SSIDUILabel.text = "Bluetooth使用不能"
            print("Bluetooth state is not supported: \(central.state.rawValue)")
        }
    }
    
    func scanBLEModule() {
        let serviceUUID = CBUUID(string: "DFB0")
        centralManager.scanForPeripherals(withServices: [serviceUUID], options: nil)
        SSIDUILabel.text = "スキャン中..."
        print("Scanning for BLE devices...")
    }
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        print("Discovered device: \(peripheral.name ?? "Unknown")")
        
        if connectedPeripheral == nil {
            connectedPeripheral = peripheral
            connectedPeripheral?.delegate = self
            centralManager.connect(peripheral, options: nil)
            centralManager.stopScan()
            SSIDUILabel.text = "接続中: \(peripheral.name ?? "Unknown Device")"
        }
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("Connected to \(peripheral.name ?? "Unknown Device")")
        SSIDUILabel.text = "接続済み: \(peripheral.name ?? "Unknown Device")"
        peripheral.discoverServices([CBUUID(string: "DFB0")])
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        if let error = error {
            print("Disconnected with error: \(error.localizedDescription)")
        } else {
            print("Disconnected from \(peripheral.name ?? "Unknown Device")")
        }
        SSIDUILabel.text = "切断されました"
        connectedPeripheral = nil
    }
    
    // MARK: - CBPeripheralDelegate メソッド
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let error = error {
            print("Service discovery failed: \(error.localizedDescription)")
            return
        }
        
        if let services = peripheral.services {
            for service in services {
                if service.uuid == CBUUID(string: "DFB0") {
                    peripheral.discoverCharacteristics(nil, for: service)
                }
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let error = error {
            print("Characteristic discovery failed: \(error.localizedDescription)")
            return
        }
        
        if let characteristics = service.characteristics {
            for characteristic in characteristics {
                if characteristic.uuid == CBUUID(string: "DFB1") {
                    rxCharacteristic = characteristic
                    peripheral.setNotifyValue(true, for: characteristic)
                    print("RX Characteristic discovered and notifications enabled: \(characteristic)")
                }
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
            print("Failed to enable notifications: \(error.localizedDescription)")
            return
        }

        if characteristic.isNotifying {
            print("Notifications enabled for characteristic: \(characteristic)")
        } else {
            print("Notifications disabled for characteristic: \(characteristic)")
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
            print("Failed to receive data: \(error.localizedDescription)")
            return
        }
        
        if let data = characteristic.value {
            print("Data received: \(data as NSData)")
            processData(data)
        } else {
            print("No data received.")
        }
    }
    
    func processData(_ data: Data) {
        if data.count == 2 {
            let receivedValue = data.withUnsafeBytes { $0.load(as: UInt16.self) }
            print("Received 2-byte UInt16 value: \(receivedValue)")
            
            DispatchQueue.main.async {
                self.SSIDUILabel.text = "受信データ: \(receivedValue)"
            }
        } else {
            print("Invalid data size: \(data.count) bytes. Expected 2 bytes.")
            DispatchQueue.main.async {
                self.SSIDUILabel.text = "エラー: 不正なデータサイズ (\(data.count))"
            }
        }
    }
    
    // MARK: - デバイスへのデータ送信
    func sendCommand(value: Int) {
        guard let peripheral = connectedPeripheral,
              let characteristic = rxCharacteristic else {
            print("No connected device or RX characteristic.")
            return
        }
        
        var val = value
        let data = Data(bytes: &val, count: MemoryLayout<Int>.size)
        peripheral.writeValue(data, for: characteristic, type: .withResponse)
        print("Sent Int value: \(value)")
    }
    
    // MARK: - IBAction
    @IBAction func sendButtonTapped(_ sender: UIButton) {
        sendCommand(value: 1)
    }
}

```
ボタンを押すとセンサー値の要求信号値１をデバイスに送ります。
受け取ったセンサー値をSSIDUILabelに表示します。
[サンプルコードgitリンク](https://github.com/tyayaya/IoT_device)
サンプルコードを利用する場合は、Teamと[Bundle identifer](https://qiita.com/miyamotok0105/items/f24a4e10a2877ae70bc4)を入力してください。

![スクリーンショット 2024-11-29 23.10.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3947099/cad9accf-a9f1-3946-61fa-13ef4286222b.png)


# おわりに
I先輩へ
この内容で土壌湿度センサーを繋げば、1年前に依頼されていた観葉植物のためのIoTデバイスが作れるかと思います。