4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SpresenseAdvent Calendar 2024

Day 7

Spresense BLE1507 で画像を転送する

Last updated at Posted at 2024-12-07

SPRESENSE BLE CAMERA のシステム構成

Spresense メインボードとカメラとBLEを組み合わせれば、低消費電力無線カメラができそうです。ということで試しに作ってみました。BLEには、クレイン電子製BLE1507(BLE for Spresense)を活用します。

Spresense_BLE_Camera.jpg

SPRESENSE BLE CAMERA のプログラム

BLEライブラリ

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

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

カメラの制御

Arduinoのスケッチを以下に示します。Spresenseのカメラは begin() 関数で電源がオンになり、end() で電源がオフになります。できるだけ消費電力は抑えたいので、カメラで画像を撮影したらバッファにコピーしてカメラの電源をオフにしています。end()関数を呼ばれると撮影データも破棄されるのでコピーをしています。画像用メモリは、カメラ画像サイズ(1280x960)の2枚分(最大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 です。お使いのセントラル受信機によって、ディレイの数値の変更が必要かもしれません。

/****************************************************************************
 * 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"に保存します。

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'.
4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?