編集履歴一覧に戻る
droplet_of_starのアイコン画像

droplet_of_star が 2026年01月28日00時05分03秒 に編集

コメント無し

本文の変更

-

**■ ぬいぐるみが散歩の習慣化を後押し**

+

# **■ ぬいぐるみが散歩の習慣化を後押し**

++**課題:継続の難しさ**  散歩は健康に良いと分かっていても、一人で毎日続けるのは意外と難しいもの。モチベーションの維持が課題でした。++ ++**解決策:記憶を持つぬいぐるみ** お気に入りのぬいぐるみにデバイスを装着。 散歩中に位置と歩数を記録し、帰宅後に「楽しかったね!」と感想を話してくれます。ただの記録ではなく、感情を共有するパートナーになります。++

-

**■システム構成図**

+

# **■システム構成図**

BMI270とBLE拡張ボード、タクトスイッチの接続構成 ![取り付け方](https://camo.elchika.com/4303d37a5098c2a246870cd6a0396539d7511297/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31333132353036332d376636312d343831652d383438382d3566363766373339636561632f36303065383130652d363634362d343265312d393062632d626533666234646436393862/) **■使用したもの** Spresenceメインボード BLE for Spresence [SONY SPRESENSE用6軸加速度計ジャイロスコープセンサ(BMI270)](ssci.to/9870) タクトスイッチ ピンヘッダ(2pin) [シングルピンソケット](https://akizukidenshi.com/catalog/g/g103136/) 1セルLipo lipo充電器 配線、はんだ、はんだごて [PHコネクタ](https://akizukidenshi.com/catalog/g/g112802/) パソコン 書き込みケーブル

-

**■デバイス製作と実装**

+

# **■デバイス製作と実装**

-

**【1】回路の組み立て**

+

## **【1】回路の組み立て**

SpresenseにBLE拡張ボードを接続。ピンヘッダを2pinに換装し、タクトスイッチとLipoバッテリーを配線(Lipo バッテリーは必ず+-を確認し,ショートもさせないように気を付けながら使用、電圧落としすぎないように注意 ![キャプションを入力できます](https://camo.elchika.com/29c6fdcd5d08e1906688a965103b19c4cea402cd/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31333132353036332d376636312d343831652d383438382d3566363766373339636561632f63303261636663312d393932642d346563652d383161372d343761323361303166376661/)

-

**【2】専用バッグへ収納**

+

## **【2】専用バッグへ収納**

ぬいぐるみが背負うための小さなバッグを自作。 ![Spressenceケースの作製](https://camo.elchika.com/34af1baddfbff30ff9e8e21f847715ff3698394e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31333132353036332d376636312d343831652d383438382d3566363766373339636561632f39653562316335662d383135622d343336632d383037352d343761333630343030373864/)

-

**【3】装着・完成**

+

## **【3】装着・完成**

肩紐の長さを調整可能にし、様々なサイズのぬいぐるみに対応。お気に入りの子をIoT化完了! ![キャプションを入力できます](https://camo.elchika.com/867df1a1ed3b585edbc13007504bde01626e9b9c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31333132353036332d376636312d343831652d383438382d3566363766373339636561632f66313235313030382d653533332d346366372d396537322d363065373439353935343662/)

-

**■ ソフトウェア実装とUIデザイン**

+

# **■ ソフトウェア実装とUIデザイン**

-

**●Spresense ファームウェア**

+

## **●Spresense ファームウェア**

**データロギング:** 1分ごとにGNSS位置情報と、加速度センサ(BMI270)からの歩数を記録

-

**通信制御: **

+

**通信制御:**

タクトスイッチ押下でBLEでPCへ蓄積データを一括送信

-

**● PCアプリ機能 **

+

## **● PCアプリ機能**

![キャプションを入力できます](https://camo.elchika.com/ba71085646b668560e2f3259f6fecede7df71069/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31333132353036332d376636312d343831652d383438382d3566363766373339636561632f36366632336638612d633564642d346231352d613364302d353261626536613765333831/) **可視化:** 今日の歩数・累計歩数を表示し、散歩ルートを地図上にプロット

-

**逆ジオコーディング: **

+

**逆ジオコーディング:**

緯度経度から住所(市町村・町名)を特定 ([Webサービス by Yahoo! JAPAN](https://developer.yahoo.co.jp/sitemap/)) **AIによる「感想」生成: GPT-4.1-mini を活用** 散歩データ(時間、距離、場所)と、ぬいぐるみの設定(性格・口調)を組み合わせてプロンプトを作成。「今日は〇〇公園に行ったね!」といった自然な会話を生成します。 ![ぬいぐるみの写真や性格、口調などをユーザーが自由に設定](https://camo.elchika.com/f04672b4bc8287709f096e2cc23377ce73fffb76/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f31333132353036332d376636312d343831652d383438382d3566363766373339636561632f31613639303965302d663630322d346664392d613238612d336135323539376365363932/)

-

**■ 今後の展望**

+

# **■ 今後の展望**

・カメラ機能の追加 Spresenseカメラボードを追加し、思い出の写真ログも自動記録 (カメラが売り切れで買えなかったので) ・省電力化と長時間駆動 スリープ制御やサンプリング間隔の最適化でバッテリー持ちを改善(現在5時間程度) ・より正確な歩数計測

-

**■ プログラム**

+

# **■ プログラム**

● ぬいぐるみ側(Spresense) ```arduino: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; uint32_t step_at_gnss = 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) { Serial.println("test.txt:"); /* 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_writing("test"); Flash.remove("dir/gnss.csv"); 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_at_gnss; 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_at_gnss; 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"); ble_send_flash_file("dir/gnss.csv"); char msg[128]; //requestStep(); Serial.print(step_at_gnss); 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.print(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].t); //Serial.print(","); 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); //ble1507->writeNotify((uint8_t*)Msg, strlen(Msg)); delay(1); // Serial安定用 } Serial.println("=== END ==="); digitalWrite(LED1, LOW); } prevButton = nowButton; delay(5); } ``` https://gist.github.com/droplet-of-star/9b4b51a94fc4bbb479adba29cb454d62.js ● パソコン側 多いのでBLEの受信部のみ ```arduino: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() ```