SPRESENSE BLE CAMERA のシステム構成
Spresense メインボードとカメラとBLEを組み合わせれば、低消費電力無線カメラができそうです。ということで試しに作ってみました。BLEには、クレイン電子製BLE1507(BLE for Spresense)を活用します。
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'.