43【ぬい活応援】記憶を持つぬいぐるみ~推しと散歩を一緒に楽しみ, あとで感想を話してくれる~
■ ぬいぐるみが散歩の習慣化を後押し
課題:継続の難しさ
散歩は健康に良いと分かっていても、一人で毎日続けるのは意外と難しいもの。モチベーションの維持が課題でした。
解決策:記憶を持つぬいぐるみ
お気に入りのぬいぐるみにデバイスを装着。
散歩中に位置と歩数を記録し、帰宅後に「楽しかったね!」と感想を話してくれます。ただの記録ではなく、感情を共有するパートナーになります。
推しと一緒に歩くことで散歩が楽しく変わります!
■システム構成図
■使用したもの
Spresenceメインボード
BLE for Spresence
SONY SPRESENSE用6軸加速度計ジャイロスコープセンサ(BMI270)
タクトスイッチ
ピンヘッダ(2pin)
シングルピンソケット
1セルLipo &lipo充電器
配線、はんだ、はんだごて
PHコネクタ
パソコン
書き込みケーブル
■デバイス製作と実装
【1】回路の組み立て
SpresenseにBLE拡張ボードを接続。ピンヘッダを2pinに換装し、タクトスイッチとLipoバッテリーを配線(Lipo バッテリーは必ず+-を確認し,ショートもさせないように気を付けながら使用、電圧落としすぎないように注意)
【2】専用バッグへ収納
ぬいぐるみが背負うための小さなバッグを自作。
【3】装着・完成
肩紐の長さを調整可能にし、様々なサイズのぬいぐるみに対応。お気に入りの子をIoT化完了!
■ ソフトウェア実装とUIデザイン
●Spresense ファームウェア
データロギング:
1分ごとにGNSS位置情報と、加速度センサ(BMI270)からの歩数を記録
通信制御:
タクトスイッチ押下でBLEでPCへ蓄積データを一括送信
● PCアプリ機能
可視化:
今日の歩数・累計歩数を表示し、散歩ルートを地図上にプロット
逆ジオコーディング:
緯度経度から住所(市町村・町名・建物名)を特定 (Webサービス by Yahoo! JAPAN)
AIによる「感想」生成: GPT-4.1-mini を活用
散歩データ(時間、距離、場所)と、ぬいぐるみの設定(性格・口調)を組み合わせてプロンプトを作成。「今日は〇〇公園に行ったね!」といった自然な会話を生成します。
■ 今後の展望
・カメラ機能の追加
Spresenseカメラボードを追加し、思い出の写真を撮れるようにする。(カメラが売り切れで買えなかったので)
・省電力化と長時間駆動
バッテリー持ちを改善(現在6時間程度)
・より正確な歩数計測
■ プログラム
● ぬいぐるみ側(Spresense)
spressence_walkapp(maincore)
#ifdef SUBCORE
#error "Core selection is wrong!!"
#endif
#include <GNSS.h>
#include "BLE1507.h"
#include "BMI270_Arduino.h"
#include <MP.h>
#include <File.h>
#include <Flash.h>
File myFile; /**< File object */
#define LOG_SWITCH_PIN 23
#define LOG_SWITCH_PIN2 24
/* ===== BLE設定 ===== */
#define UUID_SERVICE 0x3802
#define UUID_CHAR 0x4a02
int get_step = 1;
static unsigned long lastLogTime = 0;
//BT_ADDR addr;
static BT_ADDR addr = {{0x20, 0x84, 0x06, 0x14, 0xAB, 0xCD}};
static char ble_name[] = "SPR-GNSS";
static uint8_t mac_counter = 0;
bool led2_statas = 0;
BLE1507 *ble1507;
/* ===== GNSS ===== */
SpGnss Gnss;
SpNavData NavData;
/* ===== Gyro ===== */
BMI270Class BMI270;
struct bmi2_sens_float sensor_data;
/* ===== データバッファ ===== */
#define MAX_DATA 4096
struct GnssRecord {
char timeStr[32];
int sat;
bool fix;
double lat;
double lon;
int step;
} gnssData[MAX_DATA];
int head = 0; // 古いデータを上書きする位置
int tail = 0; // 未送信のデータ位置
/* ===== タイマー ===== */
unsigned long lastReadTime = 0;
bool firstMinute = true;
unsigned long lastAdvertise = 0;
//unsigned long lastNotifyTime = 0;
bool sendFlag = false; // 振動で送信するフラグ
float shakeThreshold = 20;//1.5; // G単位の振動閾値
bool sendingAll = false;
static uint8_t read_buf[128];
static uint16_t read_len = 0;
int msgid;
static void onWrite(struct ble_gatt_char_s *ble_gatt_char)
{
tail = 0;
sendingAll = true;
Serial.println("[BLE] Write trigger received");
}
/* ===== 設定 ===== */
#define LOG_SWITCH_PIN 24
#define ACC_LOG_SIZE 1000 // 50Hz × 40秒
#define ACC_LOG_INTERVAL 20 // ms (50Hz)
#define CMD_GET_STEP 1
const uint32_t STEP_MIN_INTERVAL = 300; // ms(誤検出防止)
float acc_lp = 0.0f;
const float LP_ALPHA = 0.8f; // 0.7〜0.85 推奨
/* ===== 歩数検出用 ===== */
float g = 0.0f;
float acc = 0.0f;
float acc_filt = 0.0f;
int nega =1;
float acc_prev1 = 0.0f;
float acc_prev2 = 0.0f;
float acc_lp_prev1 = 0.0f;
float acc_lp_prev2 = 0.0f;
const float THRESHOLD = 0.5; // ★要調整(G単位)
uint32_t step_count = 5;
uint32_t last_step_time = 0;
float a = 0.95f;
/* ===== BMI270 ===== */
/* ===== ログ構造体 ===== */
// struct AccLog {
// //unsigned long t;
// //float ax;
// //float ay;
// //float az;
// float g;
// float norm;
// float acc;
// float acc_filt;
// float acc_lp;
// };
// AccLog accLog[ACC_LOG_SIZE];
int accHead = 0;
bool logFilled = false;
/* ===== BMI270設定 ===== */
int8_t configure_sensor() {
int8_t rslt;
uint8_t sens_list[1] = { BMI2_ACCEL };
struct bmi2_sens_config config;
config.type = BMI2_ACCEL;
config.cfg.acc.odr = BMI2_ACC_ODR_200HZ;
config.cfg.acc.range = BMI2_ACC_RANGE_2G;
config.cfg.acc.bwp = BMI2_ACC_NORMAL_AVG4;
config.cfg.acc.filter_perf = BMI2_PERF_OPT_MODE;
rslt = BMI270.set_sensor_config(&config, 1);
if (rslt != BMI2_OK) return rslt;
return BMI270.sensor_enable(sens_list, 1);
}
void flash_writing(const GnssRecord& r) {
myFile = Flash.open("dir/gnss.csv", FILE_WRITE);
if (!myFile) {
Serial.println("error opening gnss.csv");
return;
}
// CSV 1行
myFile.print(r.timeStr);
myFile.print(",");
myFile.print(r.sat);
myFile.print(",");
myFile.print(r.fix);
myFile.print(",");
myFile.print(r.lat, 6);
myFile.print(",");
myFile.print(r.lon, 6);
myFile.print(",");
myFile.println(r.step);
myFile.close();
}
void flash_reading(){
/* Re-open the file for reading */
myFile = Flash.open("dir/gnss.csv");
if (myFile) {
/* Read from the file until there's nothing else in it */
while (myFile.available()) {
Serial.write(myFile.read());
}
/* Close the file */
myFile.close();
} else {
/* If the file didn't open, print an error */
Serial.println("error opening test.txt");
}
}
void ble_send_flash_file(const char* path) {
File f = Flash.open(path, FILE_READ);
if (!f) {
const char* err = "ERR: open failed\n";
ble1507->writeNotify((uint8_t*)err, strlen(err));
return;
}
const char* beginMsg = "=== BEGIN ===\n";
ble1507->writeNotify((uint8_t*)beginMsg, strlen(beginMsg));
delay(2);
const size_t CHUNK = 160; // 安全サイズ
uint8_t buf[CHUNK];
while (f.available()) {
int n = f.read(buf, CHUNK);
if (n <= 0) break;
ble1507->writeNotify(buf, n);
delay(1); // notify詰まり防止
}
f.close();
const char* endMsg = "\n=== END ===\n";
ble1507->writeNotify((uint8_t*)endMsg, strlen(endMsg));
}
/* ===== BMI270 設定 ===== */
void setup() {
Serial.begin(115200);
while (!Serial);
//MP.begin(1);
//pinMode(LED0, OUTPUT);
//pinMode(LED1, OUTPUT);
Serial.println("GNSS + BLE start");
/* GNSS初期化 */
Gnss.begin();
Gnss.select(GPS);
Gnss.start();
pinMode(LOG_SWITCH_PIN, INPUT_PULLUP);
pinMode(LOG_SWITCH_PIN2, INPUT_PULLUP);
/* BLE初期化 */
ble1507 = BLE1507::getInstance();
//ble1507->removeBoundingInfo();
//ble1507->pairing(); // Just Worksペアリングを設定(Bondingなし)
ble1507->beginPeripheral(ble_name, addr, UUID_SERVICE, UUID_CHAR);
ble1507->startAdvertise();
Serial.println("BLE advertising...");
ble1507->setWritePeripheralCallback(onWrite);
/* BMI270初期化 */
int8_t rslt = BMI270.begin(BMI270_I2C, BMI2_I2C_SEC_ADDR);
if (rslt != BMI2_OK) Serial.println("BMI270 init fail");
rslt = configure_sensor();
if (rslt != BMI2_OK) Serial.println("BMI270 config fail");
/* Create a new directory */
Flash.mkdir("dir/");
//Flash.remove("dir/gnss.csv");//Flash消去するか
// GnssRecord r;
// snprintf(r.timeStr, sizeof(r.timeStr),"2026-01-13 11:50:01");
// r.sat=10;
// r.fix=1;
// r.lat=35.68699;
// r.lon=139.734169;
// //requestStep();
// r.step = step_count;
// gnssData[head] = r;
// flash_writing(r);
// head = 1;
//flash_reading();
}
void loop() {
// ----- タイマーに応じてGNSS取得 -----
unsigned long now = millis();
unsigned long interval = firstMinute ? 300 : 60000; // 1秒 or 1分
if (now - lastReadTime >= interval) {
lastReadTime = now;
if (firstMinute && now > 60000) firstMinute = false;
Gnss.getNavData(&NavData);
GnssRecord r;
if (NavData.posDataExist) {
//requestStep();
// digitalWrite(LED1, HIGH);
snprintf(r.timeStr, sizeof(r.timeStr),"%04d-%02d-%02d %02d:%02d:%02d",
NavData.time.year, NavData.time.month, NavData.time.day,
NavData.time.hour, NavData.time.minute, NavData.time.sec);
r.sat = NavData.numSatellites;
r.fix = NavData.posDataExist;
r.lat = NavData.latitude;
r.lon = NavData.longitude;
r.step = step_count;
gnssData[head] = r;
flash_writing(r);
head = (head + 1) % MAX_DATA;
//if (head == tail) tail = (tail + 1) % MAX_DATA;
} else {
//digitalWrite(LED1, LOW);
//snprintf(r.timeStr, sizeof(r.timeStr), "NOFIX");
}
}
// ----- Read後の全ログ送信 -----
static bool prevButton2 = HIGH;
bool nowButton2 = digitalRead(LOG_SWITCH_PIN2);
//if (sendingAll) {
if (nowButton2 != 1 || sendingAll ) {
//digitalWrite(LED0, HIGH);
tail = 0;
Serial.println("button");
Serial.println(step_count);
ble_send_flash_file("dir/gnss.csv");
char msg[128];
//requestStep();
while(tail != head){
//if (tail != head) {
GnssRecord *r = &gnssData[tail];
//char msg[128];
if (r->fix) {
snprintf(msg, sizeof(msg),"%s,FIX,SAT=%d,LAT=%.6f,LON=%.6f step=%d",r->timeStr, r->sat, r->lat, r->lon, r->step);
} else {
snprintf(msg, sizeof(msg),"%s,NOFIX,SAT=%d",r->timeStr, r->sat);
}
//ble1507->writeNotify((uint8_t*)msg, strlen(msg));
Serial.println(msg);
//lastNotifyTime = millis();
tail = (tail + 1) % MAX_DATA;
delay(1); // ★重要:BLE安定化
}
sendingAll = false;
//digitalWrite(LED0, LOW);
}
int cmd;
if (millis() - lastLogTime >= ACC_LOG_INTERVAL) {
lastLogTime = millis();
if (BMI270.bmi2_get_sensor_float(&sensor_data) == BMI2_OK) {
float ax = sensor_data.acc.x;
float ay = sensor_data.acc.y;
float az = sensor_data.acc.z;
float norm = sqrt(ax*ax + ay*ay + az*az);
/* ===== 重力除去 ===== */
g = a * g + (1.0f - a) * norm;
acc = norm - g;
/* ===== 移動平均(3点)===== */
acc_filt = (acc + acc_prev1 + acc_prev2) / 3.0f;
acc_lp = LP_ALPHA * acc_lp + (1.0f - LP_ALPHA) * acc_filt;
//accLog[accHead].t = millis();
//accLog[accHead].ax = ax;
//accLog[accHead].ay = ay;
//accLog[accHead].az = az;
// accLog[accHead].norm = norm;
// accLog[accHead].g = g;
// accLog[accHead].acc = acc;
// accLog[accHead].acc_filt = acc_filt;
// accLog[accHead].acc_lp = acc_lp;
/* ===== ピーク検出 ===== */
/*
prev2 < prev1 > current なら prev1 がピーク
*/
if ( accHead > 4 && nega==1 && acc_lp_prev1 > THRESHOLD && acc_lp_prev1 > acc_lp_prev2 && acc_lp_prev1 > acc_lp &&
(millis() - last_step_time) > STEP_MIN_INTERVAL
) {
if (led2_statas==1){
led2_statas = 0;
}else{
led2_statas = 1;
}
//digitalWrite(LED2, led2_statas);
//Serial.println("COUNT++");
step_count++;
last_step_time = millis();
nega = 0;
}
if (nega==0 && acc_lp < 0){
nega = 1;
}
/* ===== 履歴更新 ===== */
acc_prev2 = acc_prev1;
acc_prev1 = acc;
acc_lp_prev2 = acc_lp_prev1;
acc_lp_prev1 = acc_lp;
/* ===== バッファ更新 ===== */
accHead++;
if (accHead >= ACC_LOG_SIZE) {
accHead = 0;
logFilled = true;
}
}
}
/* ===== ボタンで全ログ出力 ===== */
static bool prevButton = HIGH;
bool nowButton = digitalRead(LOG_SWITCH_PIN);
if (prevButton == HIGH && nowButton == LOW) {
Serial.println(step_count);
Serial.println("time_ms, acc, g,acc_filt, norm");
int count = logFilled ? ACC_LOG_SIZE : accHead;
int start = logFilled ? accHead : 0;
// for (int i = 0; i < count; i++) {
// int idx = (start + i) % ACC_LOG_SIZE;
// Serial.print(accLog[idx].acc, 4);
// Serial.print(",");
// Serial.print(accLog[idx].g, 4);
// Serial.print(",");
// Serial.print(accLog[idx].acc_filt, 4);
// Serial.print(",");
// Serial.print(accLog[idx].norm, 4);
// Serial.print(",");
// Serial.println(accLog[idx].acc_lp, 4);
// delay(1); // Serial安定用
// }
Serial.println("=== END ===");
//digitalWrite(LED1, LOW);
}
prevButton = nowButton;
delay(5);
}
● パソコン側
多いのでBLEの受信部のみ
BLEパソコン側
import asyncio
from bleak import BleakClient, BleakScanner
DEVICE_NAME = "SPR-GNSS"
CHAR_UUID = "00004a02-0000-1000-8000-00805f9b34fb"
ble_client = None
ble_loop = None
# ===== BLE Notify callback =====
def on_notify(sender, data):
try:
msg = data.decode(errors="ignore")
print(msg)
except Exception as e:
print("Parse error:", e)
# ===== BLE task =====
async def ble_task():
global ble_client, ble_loop
ble_loop = asyncio.get_running_loop()
print("Scanning BLE...")
devices = await BleakScanner.discover()
target = None
for d in devices:
if d.name == DEVICE_NAME:
target = d
break
if not target:
print("BLE device not found")
return
print("Connecting to", target.address)
async with BleakClient(target.address) as client:
ble_client = client
await client.start_notify(CHAR_UUID, on_notify)
print("Notify subscribed")
while True:
await asyncio.sleep(1)
# ===== Thread launch =====
def start_ble():
asyncio.run(ble_task())
if __name__ == "__main__":
start_ble()
● 歩数の計算方法


-
droplet_of_star
さんが
前の火曜日の23:58
に
編集
をしました。
(メッセージ: 初版)
-
droplet_of_star
さんが
前の水曜日の0:05
に
編集
をしました。
-
droplet_of_star
さんが
前の水曜日の0:06
に
編集
をしました。
-
droplet_of_star
さんが
前の水曜日の21:45
に
編集
をしました。
-
droplet_of_star
さんが
今日の0:14
に
編集
をしました。
-
droplet_of_star
さんが
今日の0:50
に
編集
をしました。
ログインしてコメントを投稿する