Spresense BLE1507 で画像を転送する

# SPRESENSE BLE CAMERA のシステム構成
Spresense メインボードとカメラとBLEを組み合わせれば、低消費電力無線カメラができそうです。ということで試しに作ってみました。BLEには、[クレイン電子製BLE1507(BLE for Spresense)](https://crane-elec.co.jp/products/vol-24/)を活用します。

![Spresense_BLE_Camera.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598183/a4f56c66-bb2b-074b-5160-f36de986e684.jpeg)


# SPRESENSE BLE CAMERA のプログラム
## BLEライブラリ
BLEのライブラリは次のものを使ってみました。ZIPでダウンロードし、Arduino IDE でライブラリを取り込んでください。

BLE1507_Arduino
https://github.com/TE-YoshinoriOota/BLE1507_Arduino

## カメラの制御
Arduinoのスケッチを以下に示します。Spresenseのカメラは begin() 関数で電源がオンになり、end() で電源がオフになります。できるだけ消費電力は抑えたいので、カメラで画像を撮影したらバッファにコピーしてカメラの電源をオフにしています。end()関数を呼ばれると撮影データも破棄されるのでコピーをしています。画像用メモリは、カメラ画像サイズ（1280x960）の２枚分（最大200kBx2）必要なので、メモリ設定は余裕をもって1152kBに設定してコンパイルしてください。

## 画像転送のプロトコル
画像は、"START_IMAGE"をプリアンブルとして出力し、その後、画像データを転送します。画像データの転送が終わったら”END_IMAGE”をポストアンブルとして出力します。ここで、一度に転送している画像データサイズは 20バイトです。これは、Spresense SDKの制限で20バイトに設定されているためです。

MTUサイズはログを見ると247Byteになっています。

```
negotiated MTU size(connection handle = 15) : 247
```

しかし、SDKの modules/include/bluetooth/ble_gatt.h の76行目が次のように定義されており、20バイトに制限されています。

```
#define BLE_MAX_CHAR_SIZE 20
```
どうもライブラリが BLE4.0 前提となっているようですね。この定義を変更して Arduinoライブラリを作ればサイズは増やせるかもしれませんが、SDK の kconfig で設定値がないのでうまくいくか今の段階では分かりません。今回は20バイトのままでコーディングしました。

## SPRESENSE のスケッチ
writeNotifyでデータを送信したあとは、delay(10) を入れないと途中で送信エラーが出てしまいます。送信先は Windows11 の PC です。お使いのセントラル受信機によって、ディレイの数値の変更が必要かもしれません。

```c++
/****************************************************************************
 * Included Files
 ****************************************************************************/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <Camera.h>
#include "BLE1507.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/
#define UUID_SERVICE  0x3802
#define UUID_CHAR     0x4a02

/****************************************************************************
 * ble parameters
 ****************************************************************************/
static BT_ADDR addr = {{0x19, 0x84, 0x06, 0x14, 0xAB, 0xCD}};
static char ble_name[BT_NAME_LEN] = "SPR-PERIPHERAL";

BLE1507 *ble1507;

uint8_t img_buf[200000];
size_t img_size;
uint8_t PREAMBLE[] = {'S','T','A','R','T','_','I','M','A','G','E'};
uint8_t POSTAMBLE[] = {'E','N','D','_','I','M','A','G','E'};

const size_t buf_size = 20;
uint8_t ble_buf[buf_size];

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

  theCamera.begin();
  theCamera.setStillPictureImageFormat(
    CAM_IMGSIZE_QUADVGA_H, CAM_IMGSIZE_QUADVGA_V, CAM_IMAGE_PIX_FMT_JPG);

  ble1507 = BLE1507::getInstance();
  ble1507->begin(ble_name, addr, UUID_SERVICE, UUID_CHAR);

  Serial.println("Take Picture");
  CamImage img = theCamera.takePicture();
  if (img.isAvailable()) {
    Serial.printf("img size: %d\n", img.getImgSize());
    Serial.printf("img width:  %d\n", img.getWidth());
    Serial.printf("img height: %d\n", img.getHeight());
    memcpy(img_buf, img.getImgBuff(), img.getImgSize());
    img_size = img.getImgSize();
  } else {
    Serial.println("Image is not available");
  }
  theCamera.end();
  bool isReady = false;
  while (isReady == false) {
    if (Serial.available()) {
      isReady = true;
    }
  }
  Serial.println("ready to send");
}

void loop() {
  ble1507->writeNotify(PREAMBLE, sizeof(PREAMBLE));
  Serial.println("Send PREAMBLE");

  uint8_t *img_ptr = &img_buf[0];
  size_t remain_size = img_size;
  size_t copy_size = buf_size;
  uint16_t send_times = 0;
  while(remain_size > 0) {
    memset(ble_buf, 0, sizeof(uint8_t)*copy_size);
    memcpy(ble_buf, img_ptr, sizeof(uint8_t)*copy_size);
    ble1507->writeNotify(ble_buf, sizeof(uint8_t)*copy_size);
    Serial.printf("[%d] Send image: %d\n", send_times++, copy_size);
    remain_size -= copy_size;
    if (remain_size < buf_size) { 
      copy_size = remain_size; 
    } else { 
      copy_size = buf_size; 
    }
    img_ptr += copy_size;
    delay(10);
  }

  ble1507->writeNotify(POSTAMBLE, sizeof(POSTAMBLE));
  Serial.println("Send POSTAMBLE");

  while(1);
}
```

# 画像受信側の Python プログラム
## 受信用 Python プログラム
受信はPythonで行っています。Spresenseとペアリングを行ったあとに、次のプログラムを動かしてください。デバイス名は”SPR-PERIPHERAL”です。こちらのプラグラムは Notify を受信したときに、受信データの中に”START_IMAGE”があったら、データ受信状態になり、次から受信したデータを"image_buffer"に追加していきます。受信データの中に"END_IMAGE"が含まれていたら、データ受信を終了し、画像ファイル "image.jpg"に保存します。

```python
import asyncio
import time
import struct
import argparse
import concurrent.futures
from bleak import BleakClient, BleakScanner
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.exc import BleakError

char_uuid = "4a02" # default

PREAMBLE = b"START_IMAGE"
POSTAMBLE = b"END_IMAGE"

image_buffer = bytearray()
is_receiving_data = False
start_time = 0

#
# this callback function receives data coming from BLE1507
#
def notification_handler(characteristic: BleakGATTCharacteristic, data:bytearray):
  global image_buffer, is_receiving_data, start_time

  if PREAMBLE in data:
    print("Preamble detected. Starting image reception.")
    is_receiving_data = True
    start_index = data.index(PREAMBLE) + len(PREAMBLE)
    image_buffer = bytearray(data[start_index:])
    start_time = asyncio.get_event_loop().time()

  elif POSTAMBLE in data:
    print("Postamble detected. Ending image reception.")
    end_index = data.index(POSTAMBLE)
    image_buffer.extend(data[:end_index])
    is_receiving_data = False
    end_time = asyncio.get_event_loop().time()
    duration = asyncio.get_event_loop().time() - start_time
    print(f"Duration: {duration}")
    if image_buffer:
      with open("image.jpg", "wb") as f:
        f.write(image_buffer)
        print("Image saved as 'image.jpg'.")
    else:
      print("No image data received")
    

  elif is_receiving_data:
    image_buffer.extend(data)
    #print(f"Received {len(data)} byte of image data.")

#
# connect to the base altimeter and handling notifications
#
async def connect_and_start_notify(address, name, uuid):

  global char_uuid

  if uuid:
    char_uuid = uuid

  while True:
    try:
      if address:
        device = await BleakScanner.find_device_by_address(address)
        if device is None:
          print(f"Could not find device with address : {address}")
        else:
          print(f"Device {address} found")
      else:
        device = await BleakScanner.find_device_by_name(name)
        if device is None:
          print(f"Could not find device with name : {name}")
        else:
          print(f"Device {name} found")

      async with BleakClient(device) as client:
        try:              
          if client.is_connected:
            print(f"Already connected to {device.name}")
            # activate the notification_handler 
            await client.start_notify(char_uuid, notification_handler)
            while True:
              await asyncio.sleep(1)
          else:
            try:
              print(f"Try to connect {address}")
              await asyncio.wait_for(client.connect(), timeout)
              if client.is_connected:
                print(f"Connected to {address}")
              else:
                print(f"Failed to connect {address}")

            except asyncio.TimeoutError:
              print(f"Timout retry to connect {address}")

        except Exception as e:
          print(f"Connection lost with {address} : {e}")
          print(f"Warning: Please ensure that {address} is properly paired") 

        finally:
          try:
            await client.stop_notify(char_uuid)
          except BleakError as e:
            print(f"Stop notify error: {e}")

    except BleakError as e:
      print(f"BleakError: {e}")
      print("(re)try to connect to {address}")

    except Exception as e:
      print(f"Unexpected error: {e}")
      print("Terminate this process")
      break

    await asyncio.sleep(1)


if __name__ == "__main__":
  parser = argparse.ArgumentParser()

  device_group = parser.add_mutually_exclusive_group(required=True)
  device_group.add_argument(
      "--name",
      metavar="<name>",
      help="the name of the buletooth device to connect to",
    )
  device_group.add_argument(
      "--address",
      metavar="<address>",
      help="the address of the buletooth device to connect to",
    )
  parser.add_argument("--uuid", type=str, default="4a02", help="the characteristic uuid for the altimeter devices")

  args = parser.parse_args()
  if args.address:
    print(f"the BLE1507 address is {args.address}")
  if args.name:
    print(f"the BLE1507 name is {args.name}")
  if args.uuid:
    print(f"the characteristic uuid is {args.uuid}")

  asyncio.run(connect_and_start_notify(args.address, args.name, args.uuid))

```

## 画像受信の実行結果
実行結果は次のようになります。受信時間が 180秒近くなので約3分。BLEなので消費電力は抑えられますが、ちょっと時間かかりすぎですね。BLE_MAX_CHAR_SIZE が200バイトになれば、かかる時間は10分の1の20秒弱になるかもしれないので、かなり電力を抑えることができそうです。少し調べてみたいと思います。

```
python .\bin_notify.py --name SPR-PERIPHERAL
the BLE1507 name is SPR-PERIPHERAL
the characteristic uuid is 4a02
Device SPR-PERIPHERAL found
Already connected to SPR-PERIPHERAL
Preamble detected. Starting image reception.
Postamble detected. Ending image reception.
Duration: 179.6413418999873
Image saved as 'image.jpg'.
```