M5Stack CoreS3 の カメラ映像を Bluetooth LE で送信する
はじめに
最近色々触り始めてた IoT、その中でも M5Stack CoreS3 を使って色々実験しています。
今回はその CoreS3 に内蔵されているカメラ映像を Bluetooth LE(以後 BLE)を介して iPhone へ送信するためのあれこれを実装したので備忘録がてらまとめようと思います。
実際に動作している動画↓
この記事では BLE とはなにかに軽く触れつつ、M5Stack CoreS3 から iPhone へ画像を転送するまでをコードを交えて解説していきます。
なお、今回のサンプルは GitHub にアップしてあるので実際に動かしてみたい人はこちらを参照ください。
Bluetooth LE について
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 の用語について
BLE を扱う上で必要になる単語を簡単に示します。
- セントラル
- ペリフェラル
- アドバタイジング
ペリフェラル
ペリフェラルは「周辺の」という意味がある英単語です。そのため IoT デバイスやセンサーなど、「周辺にあるデバイス」に対して使われる名前です。
セントラル
一方、セントラルはスマホなどが該当し、ペリフェラルデバイスを見つけて接続を行います。こうした構成により、例えば接続した温度センサーのペリフェラルデバイスから温度を受け取って処理する、といった構成になります。
アドバタイジング
BLE ではペリフェラルデバイス側が「アドバタイジング」を行います。アドバタイジングは「広告活動」などを意味する英単語で、要するにペリフェラルデバイスがセントラルデバイスへ自身を見つけてもらうための信号を発信することを指します。
BLE の通信について
BLE では下記のプロトコルスタックによって成り立っています。
今回はこの中でも GATT が重要です。
なお、BLE でやり取りされるパケットのフォーマットなどは以下の記事で詳しく解説されていたので興味がある方は見てみてください。
ATT(Attribute)
BLE のプロトコルの中で ATT(アトリビュート)は GATT で定義される最小のエンティティです。そのため、すべてのデータは ATT によって表現されます。
以下は BLE の仕様から引用した構造を示す図です。
階層構造としては以下のようになります。
Profile
├── Service <UUID>
│ ├── Characteristic <UUID>
│ │ ├── Properties
│ │ ├── Value
│ │ ├── Descriptor <UUID>
│ │ │ └── Value
│ │ ├── Descriptor <UUID>
│ │ │ └── Value
│ │ ├── ..
│ ├── Characteristic <UUID>
│ │
├── Service <UUID>
つまり、ひとつの 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 の接続フロー
大まかに仕組みを解説していきましたが、ここからは実際の実装に沿って解説を進めていきます。まずは BLE を使った接続のフローです。
前述したように、ペリフェラルデバイスがアドバタイジングを行い、セントラル側でその信号をキャッチし、探しているサービスを保持しているデバイスを見つけたらコネクションする、という流れになります。
アドバタイジング
まずは M5Stack CoreS3 側のアドバタイジングの実装部分を見ていきます。
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
#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"
/* ================ BLE State ================ */
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 を追加している部分です。
pJpegCharacteristic->addDescriptor(new BLE2902());
準備が整ったらアドバタイジングを開始します。その部分のコードを再掲すると以下の部分です。
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->start();
これを実行することで BLE のアドバタイジングが開始されます。この情報を確認するためには iOS であれば BLE Scanner のようなアプリを使うことで対象のサービスがアドバタイジングされているか確認することができます。
コールバックの設定
アドバタイジングが開始されても、セントラル側が見つけて接続してもなにも処理が開始されません。(内部的に接続された状態になるだけです)
実際に有用な処理を行うためにはコールバックを設定して適切に処理する必要があります。
今回使用したライブラリではコールバックは、対象のクラスを継承して設定する形になっています。
以下はサーバのコールバックです。 onConnect
や onDisconnect
をトラッキングします。
また、相互に通信する際の 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
というデータが送信されてきたらカメラ映像をキャプチャし、それを送り返す準備を行います。
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
のコールバックです。詳細は後述しますが、セントラルからの通知を受け取ったことを把握するために利用しています。
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;
}
// Wait a bit to ensure BLE connection is stable after CCCD write
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_cast<uint32_t>(gJpegBuffer.size());
Serial.printf("JPEG size: %lu bytes\n", static_cast<unsigned 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_cast<unsigned>(negotiatedMtu),
static_cast<unsigned>(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_cast<unsigned>(chunkIndex + 1),
static_cast<unsigned>(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_cast<unsigned>(chunkIndex),
static_cast<unsigned>(totalChunks),
static_cast<unsigned>(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_cast<unsigned>(chunkIndex),
static_cast<unsigned>(totalChunks));
// 送信失敗時はすべてをリセット
resetParams();
return;
}
Serial.println("Has been sent.");
log("Has been sent");
Serial.printf("JPEG %s %luB (%u/%u)\n",
transferComplete ? "sent" : "partial",
static_cast<unsigned long>(offset),
static_cast<unsigned>(chunkIndex),
static_cast<unsigned>(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_cast<uint8_t*>(data), length);
characteristic->indicate(); // 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 チャンク送るという形になっています。
void loop() {
// ===== 前略 =====
if (needsSendHeader) {
sendHedaerToCentral();
return;
}
if (isSending) {
sendJpegToCentral();
}
}
実はここにハマりポイントがあって、自分が最初に実装した際はコールバック内で全チャンクを送る実装にしていました。しかしそれだと iPhone 側で一向に受信されませんでした。そこでふと思って loop()
関数に送信処理を移動したところ、無事に送信することができました。
iOS 側の実装
送信処理は以上で終了です。次に、iOS 側の受信処理について見ていきましょう。
大まかな実装フローは以下です。
-
CoreBluetooth
フレームワークを利用する -
CBCentralManager
を使ってコネクションを張る - セントラル用のデリゲートを実装する
- ペリフェラル用のデリゲートを実装する
ペリフェラルのスキャンとコネクション
ペリフェラルがアドバタイジングしていることは前述した通りです。それをスキャンによって見つけます。
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()
を呼んでペリフェラルへの接続を試みます。
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 に 0x0001
か 0x0002
の値が書き込まれます。
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)
}
}
}
setNotifyValue
で true
を設定することによってペリフェラルからの通知が届くようになります。そして後半のデリゲートメソッド 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)
// 今回のサンプルではヘッダは 8bytes 想定
let requiredHeaderBytes = 8
if headerBuffer.count < requiredHeaderBytes {
updateStatus("Receiving header (\(headerBuffer.count)/\(requiredHeaderBytes))")
return
}
// 今回のサンプルではヘッダの頭に「JPEG」が付与されているのでそれを確認する
let header = headerBuffer.prefix(requiredHeaderBytes)
let signature = Data(header.prefix(4))
guard signature == Data("JPEG".utf8) else {
updateStatus("Unexpected JPEG header")
resetTransferState()
return
}
// ヘッダの後半 4 バイトは画像データサイズ
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 について色々やっていく予定なので、またなにか実装したら記事を書こうと思います。
Discussion