月曜日, 10月 13, 2025
月曜日, 10月 13, 2025
- Advertisment -
ホームニューステックニュースM5Stack CoreS3 の カメラ映像を Bluetooth LE で送信する

M5Stack CoreS3 の カメラ映像を Bluetooth LE で送信する


KeyVisual

最近色々触り始めてた IoT、その中でも M5Stack CoreS3 を使って色々実験しています。

今回はその CoreS3 に内蔵されているカメラ映像を Bluetooth LE(以後 BLE)を介して iPhone へ送信するためのあれこれを実装したので備忘録がてらまとめようと思います。

実際に動作している動画↓

https://www.youtube.com/watch?v=CHJW-Q-wOJY

この記事では BLE とはなにかに軽く触れつつ、M5Stack CoreS3 から iPhone へ画像を転送するまでをコードを交えて解説していきます。

なお、今回のサンプルは GitHub にアップしてあるので実際に動かしてみたい人はこちらを参照ください。

https://github.com/edom18/M5StackCamera-BLE-Sample

Wikipedia から引用すると以下のように説明されています。

Bluetooth Low Energy (Bluetooth LE, BLE) とは、無線PAN技術である Bluetooth の一部で、バージョン 4.0 から追加になった低消費電力の通信モード。Bluetooth は Bluetooth Basic Rate/Enhanced Data Rate (BR/EDR) と Bluetooth Low Energy (LE) から構成される[1]。

従来からの BR/EDR と比較して、省電力かつ省コストで通信や実装を行うことを意図して設計されている。BR/EDR とは独立しており、互換性は持たないが、BR/EDR と LE の同居は可能である。もとの仕様はWibreeという名称で2006年にNokiaによって開発されたものであり[2]、これが2009年12月に Bluetooth Low Energy として Bluetooth 4.0 に統合された。

パーソナルコンピュータ(PC: Windows、macOS、Linuxなど)やモバイル端末(Androidデバイス、iPhone/iPad/Apple Watch[注釈 1]、Windows Phone、BlackBerryなど)において標準でBluetooth Low Energyに対応しており広く普及している。スポーツとフィットネス、医療、PC周辺機器[3]、ビーコン[4]などに利用されている。

BLE を扱う上で必要になる単語を簡単に示します。

  • セントラル
  • ペリフェラル
  • アドバタイジング

ペリフェラル

ペリフェラルは「周辺の」という意味がある英単語です。そのため IoT デバイスやセンサーなど、「周辺にあるデバイス」に対して使われる名前です。

セントラル

一方、セントラルはスマホなどが該当し、ペリフェラルデバイスを見つけて接続を行います。こうした構成により、例えば接続した温度センサーのペリフェラルデバイスから温度を受け取って処理する、といった構成になります。

アドバタイジング

BLE ではペリフェラルデバイス側が「アドバタイジング」を行います。アドバタイジングは「広告活動」などを意味する英単語で、要するにペリフェラルデバイスがセントラルデバイスへ自身を見つけてもらうための信号を発信することを指します。

BLE では下記のプロトコルスタックによって成り立っています。

プロトコル・スタック

今回はこの中でも GATT が重要です。

なお、BLE でやり取りされるパケットのフォーマットなどは以下の記事で詳しく解説されていたので興味がある方は見てみてください。

http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc212/doc21201.html

ATT(Attribute)

BLE のプロトコルの中で ATT(アトリビュート)は GATT で定義される最小のエンティティです。そのため、すべてのデータは ATT によって表現されます。

以下は BLE の仕様から引用した構造を示す図です。

Data構造

階層構造としては以下のようになります。

GATT(Generic Attribute) の階層データ構造

Profile
 ├── Service 
 │    ├── Characteristic 
 │    │    ├── Properties
 │    │    ├── Value
 │    │    ├── Descriptor 
 │    │    │    └── Value
 │    │    ├── Descriptor 
 │    │    │    └── Value
 │    │    ├── ..
 │    ├── Characteristic 
 │    │
 ├── Service 

つまり、ひとつの Profile の中に Service が複数存在し、さらにひとつのサービスに付き複数の Characteristic が、ひとつの Characteristic に複数の Descriptor が存在する、という感じです。

特に Characteristic は「特徴」と訳されるように、実装観点においては「ひとつの機能」と考えるといいと思います。今回の実装でも「iPhone から指示を受ける機能」と「JPEG データを送る機能」としてそれぞれひとつの Characteristic を定義しています。

Client Characteristic Configuration Descriptor (CCCD) について

Characteristic は機能を表しそのためのデータを保持します。また Characteristic は最低でもふたつの attribute を保持し、それぞれ Characteristic 宣言と Characteristic Value です。
そして Characteritic 宣言には他デバイスがどのようにアクセスできるかを示すプロパティがあります。例えば読み込み・書き込みや、データが変更された際の通知を受け取れるかなどの設定があります。

そしてそのプロパティに通知設定を許可している場合、CCCD と呼ばれる Descriptor に、通知の有効化を指示する必要があります。これを行わないと、機能として通知機能を持っていても、他デバイスに通知がされなくなってしまうため注意が必要です。

CCCD に 0x0001 を書き込むと notify 有効、0x0002 を書き込むと indicate 有効という意味になります。(indicate / notify については後述します)

大まかに仕組みを解説していきましたが、ここからは実際の実装に沿って解説を進めていきます。まずは BLE を使った接続のフローです。

前述したように、ペリフェラルデバイスがアドバタイジングを行い、セントラル側でその信号をキャッチし、探しているサービスを保持しているデバイスを見つけたらコネクションする、という流れになります。

アドバタイジング

まずは M5Stack CoreS3 側のアドバタイジングの実装部分を見ていきます。

アドバタイジング

#include 
#include 
#include 
#include 

#define SERVICE_UUID                 "FE56"
#define CONTROL_CHARACTERISTIC_UUID  "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define JPEG_CHARACTERISTIC_UUID     "c9d1cba2-1f32-4fb0-b6bc-9b73c7d8b4e2"
#define SERVER_NAME                  "M5CoreS3"


BLEServer *pServer = nullptr;
BLECharacteristic *pControlCharacteristic = nullptr;
BLECharacteristic *pJpegCharacteristic = nullptr;
bool deviceConnected = false;



void setupBle() {
  log("Init BLE");

  BLEDevice::init(SERVER_NAME);

  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID);

  pControlCharacteristic = pService->createCharacteristic(
    CONTROL_CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_WRITE |
    BLECharacteristic::PROPERTY_WRITE_NR |
    BLECharacteristic::PROPERTY_NOTIFY
  );
  pControlCharacteristic->setCallbacks(new ControlCallbacks());
  pControlCharacteristic->addDescriptor(new BLE2902());

  pJpegCharacteristic = pService->createCharacteristic(
    JPEG_CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_READ     |
    BLECharacteristic::PROPERTY_INDICATE
  );
  pJpegCharacteristic->addDescriptor(new BLE2902());

  pService->start();

  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->start();

  log("BLE ready");
}

BLE のためのライブラリを用いるとシンプルに構成することができます。 BLEDevice クラスを用いてサーバ、サービス、Characteristic をそれぞれ生成します。

それぞれの要素を生成するには、前述の階層構造がベースになります。つまり、「サーバ → サービス → Characteristic」という親子関係で生成するインスタンスが異なります。

また、前述の通り Characteristic を生成する際はそのプロパティを指定する必要があります。そしてもうひとつ重要なのが、通知を受け取る Characteristic には CCCD の Descriptor を追加する必要があります。

特定の Descriptor には固定の UUID が割り振られており、CCCD の場合は 0x2902 と決まっています。以下が CCCD を追加している部分です。

CCCD の追加

pJpegCharacteristic->addDescriptor(new BLE2902());

準備が整ったらアドバタイジングを開始します。その部分のコードを再掲すると以下の部分です。

アドバタイジング開始

BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->start();

これを実行することで BLE のアドバタイジングが開始されます。この情報を確認するためには iOS であれば BLE Scanner のようなアプリを使うことで対象のサービスがアドバタイジングされているか確認することができます。

https://apps.apple.com/jp/app/ble-scanner-4-0/id1221763603

コールバックの設定

アドバタイジングが開始されても、セントラル側が見つけて接続してもなにも処理が開始されません。(内部的に接続された状態になるだけです)
実際に有用な処理を行うためにはコールバックを設定して適切に処理する必要があります。

今回使用したライブラリではコールバックは、対象のクラスを継承して設定する形になっています。

以下はサーバのコールバックです。 onConnectonDisconnect をトラッキングします。
また、相互に通信する際の MTU の値の変更通知などもトラッキングすることができます。

サーバのコールバックの実装

class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer *server) override {
    Serial.println("connect");
    log("Connected a client.");
    deviceConnected = true;
  }

  void onDisconnect(BLEServer *server) override {
    Serial.println("=== BLE DISCONNECTED ===");
    log("Disconnected a client.");
    deviceConnected = false;
    BLEAdvertising *advertising = server->getAdvertising();
    if (advertising != nullptr) {
      advertising->start();
    }
  }

  void onMtuChanged(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) override {
    Serial.println("On MTU changed.");
    if (!param) {
      return;
    }
    uint16_t mtu = param->mtu.mtu;
    Serial.printf("MTU=%u\n", mtu);
  }
};

次のコールバックは ControlCharacteristic と名付けられた、セントラル側からのコマンド受信のための Characteristic のコールバックです。
ここでは、セントラルから SEND_JPEG というデータが送信されてきたらカメラ映像をキャプチャし、それを送り返す準備を行います。

ControlCharacteristic のコールバック

class ControlCallbacks : public BLECharacteristicCallbacks {
  void onRead(BLECharacteristic *characteristic) override {
    Serial.println("Control characteristic read.");
    String value = characteristic->getValue();
    log(value);
  }

  void onWrite(BLECharacteristic *characteristic) override {
    if (isSending) return;

    String value = characteristic->getValue();
    Serial.printf("Received control characteristic: %s\n", value.c_str());
    if (value == "SEND_JPEG") {
      Serial.println("Command received.");
      log("Command received.");
      prepareSendJpegToCentral();
    }
  }
};

次のコールバックは JpegCharacteristic のコールバックです。詳細は後述しますが、セントラルからの通知を受け取ったことを把握するために利用しています。

JpegCharacteristic のコールバック

class JpegCharacteristicCallbacks : public BLECharacteristicCallbacks {
  void onStatus(BLECharacteristic* characteristic, Status s, uint32_t code) override {
    Serial.printf("s=%d, code=%d\n", s, code);
    
    switch (s) {
      case Status::SUCCESS_INDICATE:
        needsWaitForConfirm = false;
        break;
    }
  }
};

送信処理

ControlCharacteristic にコマンドが届いたら実際に送信を開始します。画像データの準備、送信処理は以下です。

画像送信準備

void prepareSendJpegToCentral() {
  if (!deviceConnected || pJpegCharacteristic == nullptr) {
    Serial.println("No central");
    log("No central");
    return;
  }

  if (!checkCCCD()) {
    return;
  }

  
  Serial.println("Waiting for BLE connection to stabilize...");

  gJpegBuffer.clear();
  if (!captureFrameJPEG(gJpegBuffer, 80)) {
    Serial.println("Failed to capture camera image.");
    log("Failed to capture camera image.");
    return;
  }

  totalSize = static_castuint32_t>(gJpegBuffer.size());
  Serial.printf("JPEG size: %lu bytes\n", static_castunsigned long>(totalSize));

  if (pServer != nullptr) {
    const uint16_t connId = pServer->getConnId();
    const uint16_t peerMtu = pServer->getPeerMTU(connId);
    if (peerMtu > 0) {
      negotiatedMtu = peerMtu;
    }
  }

  payloadSize = negotiatedMtu > 3 ? negotiatedMtu - 3 : 20;
  if (payloadSize  20) {
    payloadSize = 20;
  }
  Serial.printf("MTU negotiated: %u, payload chunk: %u bytes\n",
                static_castunsigned>(negotiatedMtu),
                static_castunsigned>(payloadSize));

  needsSendHeader = true;
}

ここで行っているのは、カメラ画像の JPEG 化と、JPEG 画像のサイズおよび送信する際の Payload サイズなどを決めています。
準備が終わったら実際に送信します。

画像送信処理


void sendJpegToCentral() {
  const size_t chunk = std::min(payloadSize, gJpegBuffer.size() - offset);
  if (!sendChunk(pJpegCharacteristic, &gJpegBuffer[offset], chunk)) {
    Serial.printf("Chunk send failed at %u/%u (disconnected=%d)\n",
                  static_castunsigned>(chunkIndex + 1),
                  static_castunsigned>(totalChunks),
                  !deviceConnected);
    
    resetParams();
    return;
  }

  offset += chunk;
  chunkIndex += 1;
  if ((chunkIndex % 10) == 0 || chunkIndex == totalChunks || chunkIndex  3) {
    Serial.printf("Chunk %u/%u sent (%u bytes total)\n",
                  static_castunsigned>(chunkIndex),
                  static_castunsigned>(totalChunks),
                  static_castunsigned>(offset));
  }

  
  if (offset  gJpegBuffer.size() && deviceConnected) {
    return;
  }

  const bool transferComplete = (chunkIndex == totalChunks) && (offset == gJpegBuffer.size());
  if (!transferComplete) {
    Serial.printf("JPEG transfer incomplete (%u/%u chunks)\n",
                  static_castunsigned>(chunkIndex),
                  static_castunsigned>(totalChunks));
    
    resetParams();

    return;
  }

  Serial.println("Has been sent.");
  log("Has been sent");

  Serial.printf("JPEG %s %luB (%u/%u)\n",
              transferComplete ? "sent" : "partial",
              static_castunsigned long>(offset),
              static_castunsigned>(chunkIndex),
              static_castunsigned>(totalChunks));

  resetParams();
}


void sendHedaerToCentral() {
  
  uint8_t header[8];
  header[0] = 'J';
  header[1] = 'P';
  header[2] = 'E';
  header[3] = 'G';
  
  header[4] = (totalSize >> 24) & 0xFF;
  header[5] = (totalSize >> 16) & 0xFF;
  header[6] = (totalSize >> 8) & 0xFF;
  header[7] = totalSize & 0xFF;

  if (!sendChunk(pJpegCharacteristic, header, sizeof(header))) {
    Serial.println("Failed to send JPEG header");
    resetParams();
    return;
  }

  
  offset = 0;
  chunkIndex = 0;
  totalChunks = (gJpegBuffer.size() + payloadSize - 1) / payloadSize;

  needsSendHeader = false;
  isSending = true;
}


static bool sendChunk(BLECharacteristic *characteristic, const uint8_t *data, size_t length) {
  if (!deviceConnected) {
    Serial.println("sendChunk: device not connected");
    return false;
  }
  if (characteristic == nullptr) {
    Serial.println("sendChunk: characteristic is null");
    return false;
  }
  if (data == nullptr || length == 0) {
    Serial.println("sendChunk: invalid data");
    return false;
  }

  characteristic->setValue(const_castuint8_t*>(data), length);
  characteristic->indicate(); 

  if (!deviceConnected) {
    Serial.println("sendChunk: connection lost during notify");
    return false;
  }

  return true;
}

データの送信はまず、Characteristic に値を設定しそれを「通知」することで行います。
今回は indicate() 関数を用いて送信しています。

indicate と notify

実は BLE には通信方法がふたつ用意されています。それが indicate()notify() です。Characteristic のプロパティ設定のところで以下のような記述があったことに気づいたでしょうか。

プロパティ設定

pJpegCharacteristic = pService->createCharacteristic(
  JPEG_CHARACTERISTIC_UUID,
  BLECharacteristic::PROPERTY_READ     |
  BLECharacteristic::PROPERTY_INDICATE
);

ここで指定している BLECharacteristic::PROPERTY_INDICATE がその設定です。これ以外に BLECharacteristic::PROPERTY_NOTIFY があり、どちらの通信を行うかで設定を変えます。

Indicate はセントラル側からの受信確認を求める通知です。今回のように確実にデータを届ける場合に有用な方法です。一方、 Notify は一方的に通知を行う形で、温度計の定期的な通知など通知を受け取れなくても大した問題にならない場合に適した方法です。

indicate と confirm

今回はデータ送信のため、受信確認が行える indicate() を使いました。BLE の仕様として、セントラル側からの受信確認信号である confirm を受信するまでは続くデータを送ってはいけないことになっています。そのため、画像送信 Characteristic のコールバックの onStatus() 関数をトラッキングし、 confirm を受信するまでは次を送らないようになっています。

以下は JpegCharacteristic のコールバックの再掲です。

コールバックでステータスの確認

class JpegCharacteristicCallbacks : public BLECharacteristicCallbacks {
  void onStatus(BLECharacteristic* characteristic, Status s, uint32_t code) override {
    Serial.printf("s=%d, code=%d\n", s, code);
    
    switch (s) {
      case Status::SUCCESS_INDICATE:
        needsWaitForConfirm = false;
        break;
    }
  }
}

loop 関数での送信

loop() 関数は常に呼ばれ続ける関数で、そのループ処理一度に付き 1 チャンク送るという形になっています。

loop 関数での送信

void loop() {
  

  if (needsSendHeader) {
    sendHedaerToCentral();
    return;
  }

  if (isSending) {
    sendJpegToCentral();
  }
}

実はここにハマりポイントがあって、自分が最初に実装した際はコールバック内で全チャンクを送る実装にしていました。しかしそれだと iPhone 側で一向に受信されませんでした。そこでふと思って loop() 関数に送信処理を移動したところ、無事に送信することができました。

送信処理は以上で終了です。次に、iOS 側の受信処理について見ていきましょう。

大まかな実装フローは以下です。

  1. CoreBluetooth フレームワークを利用する
  2. CBCentralManager を使ってコネクションを張る
  3. セントラル用のデリゲートを実装する
  4. ペリフェラル用のデリゲートを実装する

ペリフェラルのスキャンとコネクション

ペリフェラルがアドバタイジングしていることは前述した通りです。それをスキャンによって見つけます。

スキャン開始

private let serviceUUID = CBUUID(string: "FE56")



private func beginScanning() {
  guard shouldScan else { return }
  central.scanForPeripherals(withServices: [serviceUUID],
                                  options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
}

スキャン結果などはすべてデリゲートメソッドを実装することで実現します。
CBCentralManager を生成する際に以下のようにデリゲートを自身に設定しています。

デリゲート設定

private lazy var central: CBCentralManager = {
  CBCentralManager(delegate: self, queue: centralQueue)
}()

デリゲートの実装の詳細についてはここでは割愛します。ここではペリフェラルが見つかった際の処理のみ記載します。コード全文は GitHub のリポジトリを参考にしてください。
以下のように、ペリフェラルが見つかったら、これまたデリゲートを設定した上で central.connect() を呼んでペリフェラルへの接続を試みます。

CBCentralManagerDelegate

extension BLEJpegSample: CBCentralManagerDelegate {
  public func centralManager(_ central: CBCentralManager,
                             didDiscover peripheral: CBPeripheral,
                             advertisementData: [String: Any],
                             rssi RSSI: NSNumber) {
    updateStatus("Found \(peripheral.name ?? "M5CoreS3")")
    shouldScan = false
    central.stopScan()
    updateState(.connecting)
    targetPeripheral = peripheral
    peripheral.delegate = self
    central.connect(peripheral, options: nil)
  }
}

ペリフェラルからのデータ受信

以下に、ペリフェラル向けのデリゲート処理の一部を抜粋します。最初の処理はペリフェラル上の Characeteristic について処理しています。

大事な点として、前述したように jpegCharaceristic の場合に peripheral.setNotifyValue(true, for: jpegPeripheral) を実行して CCCD に値を書き込む必要がある点に注意してください。これにより CCCD に 0x00010x0002 の値が書き込まれます。

CBPeripheralDelegate

extension BLEJpegSample: CBPeripheralDelegate {
  public func peripheral(_ peripheral: CBPeripheral,
                        didDiscoverCharacteristicsFor service: CBService,
                        error: Error?) {
  if let error = error {
    updateState(.error)
    updateStatus("Characteristic discovery failed: \(error.localizedDescription)")
    return
  }
  guard let chars = service.characteristics else { return }
  for characteristic in chars {
    switch characteristic.uuid {
    case controlCharacteristicUUID:
      controlCharacteristic = characteristic
    case jpegCharacteristicUUID:
      jpegCharacteristic = characteristic
      peripheral.setNotifyValue(true, for: characteristic)
      updateStatus("Subscribing to JPEG")
    default:
      continue
    }
  }

  public func peripheral(_ peripheral: CBPeripheral,
                         didUpdateValueFor characteristic: CBCharacteristic,
                         error: Error?) {
    if let error = error {
      updateStatus("Update error: \(error.localizedDescription)")
      return
    }
      
    guard let data = characteristic.value else { return }
    if characteristic.uuid == jpegCharacteristicUUID {
      handleJpegNotification(data)
    }
  }
}

setNotifyValuetrue を設定することによってペリフェラルからの通知が届くようになります。そして後半のデリゲートメソッド didUpdateValueFor:error によってペリフェラルからのデータが受信されます。今回はここで、チャンクデータを蓄積し、最後のデータを受信したら画像化する、という方法で実装しています。

受信データのハンドリング

データ受信処理を解説します。受信したデータ( Data )には、初回はヘッダが含まれます。ヘッダは 8 byte構成で、最初の 4 byte には JPEG の文字が格納されており、続く 4 byte に画像サイズが格納されています。

ヘッダの受信とパース

private func handleJpegNotification(_ data: Data) {
  print("[BLEJpegSample] Received notification with \(data.count) bytes")
    
  
  if expectedImageLength == nil {
    headerBuffer.append(data)
      
    
    let requiredHeaderBytes = 8
    if headerBuffer.count  requiredHeaderBytes {
      updateStatus("Receiving header (\(headerBuffer.count)/\(requiredHeaderBytes))")
      return
    }

    
    let header = headerBuffer.prefix(requiredHeaderBytes)
    let signature = Data(header.prefix(4))
    guard signature == Data("JPEG".utf8) else {
      updateStatus("Unexpected JPEG header")
      resetTransferState()
      return
    }

    
    let lengthBytes = header.suffix(4)
    var length: UInt32 = 0
    for byte in lengthBytes {
      length = (length  8) | UInt32(byte)
    }
    let expectedCount = Int(length)
    expectedImageLength = expectedCount
    let signatureString = String(data: signature, encoding: .ascii) ?? "JPEG"
    print("[BLEJpegSample] Header signature=\(signatureString) expected=\(expectedCount) bytes")
    updateStatus("Header received: \(expectedCount) bytes")

    let remainder = headerBuffer.dropFirst(requiredHeaderBytes)
    headerBuffer.removeAll(keepingCapacity: false)
    if !remainder.isEmpty {
      transferBuffer.append(contentsOf: remainder)
      print("[BLEJpegSample] Appended remainder \(remainder.count) bytes")
    }

    if let expected = expectedImageLength {
      
      if transferBuffer.count >= expected {
        finalizeTransfer()
      }
      else {
        let received = transferBuffer.count
        print("[BLEJpegSample] After header remainder -> \(received)/\(expected) bytes")
        updateStatus("Receiving JPEG (\(received)/\(expected))")
        scheduleTransferCompletion()
      }
    }
    count = 0
    return
  }
  
}

ヘッダをパースし、受信予定の画像サイズが判明したらそれを設定して、続くデータ受信に備えます。
以下は続くデータが受信された際に、バッファにデータを貯めていく部分の処理です。

受信データの蓄積

private func handleJpegNotification(_ data: Data) {
  

  
  

  transferBuffer.append(data)
  guard let expected = expectedImageLength else {
    scheduleTransferCompletion()
    return
  }

  count += 1
    
  let received = transferBuffer.count
  print("[BLEJpegSample] Chunk(\(count)) received, total \(received)/\(expected)")
  if received >= expected {
    finalizeTransfer()
  }
  else {
    updateStatus("Receiving JPEG (\(received)/\(expected))")
    scheduleTransferCompletion()
  }
}

画像化

すべてのデータを受信し終えたら最後にそれを画像化して画面に表示しします。

画像化

private func finalizeTransfer() {
  guard let expected = expectedImageLength,
        transferBuffer.count >= expected else {
    print("[BLEJpegSample] finalizeTransfer skipped; buffer=\(transferBuffer.count) expected=\(expectedImageLength ?? -1)")
    return
  }
    
  let jpegSlice = transferBuffer.prefix(expected)
  let jpegData = Data(jpegSlice)
  let hasValidHeader = jpegData.starts(with: [0xFF, 0xD8])
  let hasValidFooter = jpegData.suffix(2) == Data([0xFF, 0xD9])

  chunkTimeoutWorkItem?.cancel()
  chunkTimeoutWorkItem = nil
  headerBuffer.removeAll(keepingCapacity: false)
  transferBuffer.removeAll(keepingCapacity: false)
  expectedImageLength = nil

  guard hasValidHeader, hasValidFooter else {
    print("[BLEJpegSample] JPEG markers invalid header=\(hasValidHeader) footer=\(hasValidFooter)")
    updateStatus("JPEG invalid (missing markers)")
    DispatchQueue.main.async {
      self.lastJpegData = nil
#if canImport(UIKit)
      self.lastImage = nil
#endif
    }
    return
  }

  DispatchQueue.main.async {
    self.lastJpegData = jpegData
#if canImport(UIKit)
    self.lastImage = UIImage(data: jpegData)
#endif
    self.statusMessage = "JPEG received (\(jpegData.count) bytes)"
  }
  print("[BLEJpegSample] JPEG transfer complete (\(jpegData.count) bytes)")
}

ヘッダから取得した期待するデータサイズ分バッファから切り出し、そのデータを UIImage(data:) を利用して UIImage 化します。このクラスは ObservableObject なので、これを利用している SwiftUI 側で変化を検知し、データが更新されたらそれを UI に表示しています。

画像の表示

@StateObject private var ble = BLEJpegSample()



if let image = ble.lastImage {
    Image(uiImage: image)
        .resizable()
        .scaledToFit()
        .frame(maxHeight: 240)
        .border(.gray)
}

以上で BLE を介した JPEG 画像の転送についての実装の解説は終わりです。

iPhone 同士とかだともう少しハマりどころは少なかったと思いますが、M5Stack などを利用すると細かなハマりポイントがあるので色々と苦労しますね。
ただ、ハードがあると動いたときの嬉しさもひとしおです。

今回の実装は、実は M5Stack のカメラ映像をスマホに投げて、それをさらに AI に解析させる、みたいなことを想定して作っていました。つまりスマホを母艦にして色々動かしてみる、というのをやろうかなと。

今後は IoT x AI について色々やっていく予定なので、またなにか実装したら記事を書こうと思います。



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -