S-Shimizu が 2026年01月30日21時53分07秒 に編集
初版
タイトルの変更
猫魔法「ごろごろ」
タグの変更
SPRESENSE
MQTT
カメラ
LTE-M
メイン画像の変更
記事種類の変更
製作品
本文の変更
# 1. 概要 猫のゴロゴロには、ストレスの解消や傷の回復といった効果があるといわれており、それはまさに癒しの魔法と言えるのではないでしょうか。 そんなゴロゴロの再現に挑戦してみました。 骨伝導スピーカを使って、猫のゴロゴロを音と振動で表現しています。 さらに、カメラで撮影した画像を元にねこまみれにするサービスを追加しました。 *本記事ではサーバ側の構成・動作については割愛します。 --- # 2. 構成と実機動作 ごろごろを出すパペット部と、撮影を行うカメラ部で構成しています。  # 2.1 ごろごろ動作 1. センサで温湿度を計測 2. 環境センサのデータを、Spresenseを介してLTEでサーバに送信 3. (サーバ側動作:環境センサのデータを元に再生するべきファイルを選び、情報をサーバーにアップ) 4. サーバにアップされている情報を読み取り、その情報に従ったmp3ファイルを再生。 5. 骨伝導スピーカーを介して再生するので、音と振動となって出力される。 1~5を繰り返す # 2.2 ねこまみれ動作 1. パペットに内蔵されたタクトスイッチが押されると、MQTTをPublishする。 2. カメラは、MQTTサーバと定期的に交信し、MQTTリクエストに応じて撮影を行う。 3. 撮影した画像をサーバに送る 4. (サーバ側動作:送られてきた画像を元に生成AIで猫まみれになった画像を作成、HTMLで表示する) --- # 3. 各部ハードウェア構成 ## 3. 1 パペット部 ### 3.1.1 使ったもの - Spresense メインボード - Spresense LTE拡張ボード - 温湿度センサユニット(BME280モジュール) - ステレオD級オーディオアンプモジュール(PAM8403使用)[https://eleshop.jp/shop/g/gF81125/] - microSDカード x1(猫のゴロゴロ音をmp3で保存しておきます) - LTE-MのSIM カード - 骨伝導スピーカー - その他筐体部品(トタンプレート、タミヤ製ユニバーサルプレートなど) - 猫型パペット(見た目) ### 3.1.2 構成 - SpresenseメインボードとLTE拡張ボードの組み合わせを使用 - 温湿度センサをSpresenseとSPIで接続、通信 - LTE拡張ボードのオーディオ端子から、D級アンプを介して骨伝導スピーカーに接続。 骨伝導スピーカーが貼りついたトタンプレートが振動し、振動と音でごろごろが出力されます。  ## 3.2カメラ部 ### 3.1.1 使ったもの - Spresense メインボード - Spresense LTE拡張ボード - SpresenseHDRカメラボード - LTE-MのSIM カード - 筐体(猫型、3Dプリンタで作成) ### 3.1.2 構成 - 電気的な構成はSpresenseメインボード、LTE拡張ボード、HDRカメラボードを仕様通り接続しています。 - 筐体を猫型して、首輪の位置にカメラレンズが来ることを意識しています。  --- # 4. ソフトウェア ## 4.1 パペット部 パペット部のSpresenseはオーディオ再生、LTE通信、環境センサ動作、GPIO制御を並列で行う必要あるため、マルチコアを採用しました。 とはいえ、オーディオ再生とLTE通信はメインコアでしか動作できないので再生と再生の合間に通信を行うような処理にしています。 ### 4.1.1 マルチコア処理 以下のようにコアを割り当てています。 メインコア:オーディオ+LTE制御 サブコア2:環境センサ サブコア3:GPS制御(屋内で使っていたので実装したものの情報拾えず結局使えていなかった・・) サブコア4:GPIO制御 ### 4.1.2. オーディオ制御 ### 4.1.2. LTE制御 ###4.1.3. 環境センサ ###4.1.4. GPIO制御 ## 4. 2 カメラ部 カメラ部では - サーバにMQTTメッセージが到着しているかを確認する。 - MQTTがサーバに到着していたらカメラで写真を撮影する - 撮影した画像をサーバに送る の処理を行っています。 ## 7. コード(貼り付け枠) ### 7.1 全体(メイン) ```cpp // ここにメインコードを貼り付け ``` ### 7.2 オーディオ再生部(必要なら分割して貼る) ```cpp // ここにAudio関連コードを貼り付け ``` ### 7.3 MQTT受信・コマンド処理部 ```cpp // ここにMQTT関連コードを貼り付け ``` ### 7.4 カメラ撮影部 ```cpp // ここにCamera関連コードを貼り付け ``` ### 7.5 設定値(SSID/ブローカ/トピック等) ```cpp // ここに設定値(環境依存)を貼り付け ``` ```main.cpp // ====== Spresense MP3 Switch Debug (LTE/SubCore optional) ====== #ifdef SUBCORE #error "This sketch is for MainCore. Build target is wrong." #endif // ---- Feature flags ------------------------------------------------ #define USE_LTE 1 // 1: 実機LTE使用, 0: 使わない(ビルドから除外) #define USE_SUBCORE 1 // 1: 実機SubCore使用, 0: 使わない(ビルドから除外) // ---- Includes ----------------------------------------------------- #include <Arduino.h> #include <SDHCI.h> #include <Audio.h> #if USE_LTE #include <ArduinoHttpClient.h> #include <LTE.h> #include <LTEClient.h> #include <Arduino_JSON.h> #include <LTETLSClient.h> #include <PubSubClient.h> #endif #if USE_SUBCORE #include <MP.h> struct sensor_data_t { float temperature; float humidity; float pressure; }; #endif // ---- Constants ---------------------------------------------------- static constexpr uint8_t kTrackCount = 6; static constexpr uint32_t kChangeInterval = 10000; // 10s auto-rotate (デモ用) static constexpr uint8_t kVolume = 120; // 0dB=100, 120=+12dB static constexpr char kBinPath[] = "/mnt/sd0/BIN"; static const char* kTrackFiles[kTrackCount] = { "0_default.mp3", "1_neoki_new.mp3", "2_kitai_new.mp3", "3_kyukei.mp3", "4_asobo.mp3", "5_kyoumi.mp3", }; // ---- Globals ------------------------------------------------------ SDClass theSD; AudioClass* theAudio = nullptr; File g_mp3; uint8_t g_playing = 0; // 現在再生中 uint8_t g_desired = 0; // 次に切り替えたい(曲末で反映) uint32_t g_nextTick = 0; // オート切替タイミング uint32_t g_logThrottle = 0; // writeFrames ログ間引き uint8_t fumi_fumi_speed = 100; //100をマックス、10をミニマムに? int subcore = 2; /* Communication with SubCore1 */ int volumeset[6] = { -110, -100, -110, -110, -100, -110, }; bool sense_trigger = 0; // sense_trigger = 1 で測定+通信 int mqtt_wait_counter = 5; // // ---- LTE (optional) ---------------------------------------------- #if USE_LTE #define APP_LTE_APN "iijmobile.biz" #define APP_LTE_USER_NAME "mobile@iij" #define APP_LTE_PASSWORD "iij" #define APP_LTE_IP_TYPE (LTE_NET_IPTYPE_V4V6) #define APP_LTE_AUTH_TYPE (LTE_NET_AUTHTYPE_CHAP) #define APP_LTE_RAT (LTE_NET_RAT_CATM) // MQTT ブローカ(証明書は Let’s Encrypt 例) static const char MQTT_HOST[] = "xn--28jc7e4f771s.jp"; static const int MQTT_PORT = 8883; // TLS static const char MQTT_TOPIC[] = "spresense/test"; static const char MQTT_USER[] = "spresense"; static const char MQTT_PASS[] = "spresense_password"; // ISRG Root X1(Let’s Encrypt)----- 必要なら差し替え ----- static const char ISRG_ROOT_X1_PEM[] = R"(-----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE-----)"; LTE lteAccess; LTEClient client; LTETLSClient tlsCli; PubSubClient mqtt(tlsCli); char server[] = "xn--28jc7e4f771s.jp"; char path[] = "/goro"; int port = 80; // --------------------------------------------------------------- #endif // ---- Helpers ------------------------------------------------------ static void setOutputAndVolume() { Serial.println("setoutput loopin"); theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, AS_SP_DRV_MODE_LINEOUT); Serial.println("setPlayerMode"); //theAudio->setVolume(-140); //kvolume (-1020 ~ 120) :-102dB ~ 12dB theAudio->setVolume(volumeset[g_desired]); //kvolume (-1020 ~ 120) :-102dB ~ 12dB Serial.print(volumeset[g_desired]); Serial.println(" volume set complete!"); } static void loadMp3Decoder() { theAudio->initPlayer(AudioClass::Player0, AS_CODECTYPE_MP3, kBinPath, AS_SAMPLINGRATE_AUTO, AS_CHANNEL_STEREO); } // 完全に「Ready + Codecロード」まで戻す共通手順 static void ensureReadyAndCodec() { // この段階でMQTTを発行チェックと発行 if (mqtt_wait_counter == 0){ int SW = digitalRead(9); if (SW == 1){ //sendShutter(); //LTE使用しないためコメントアウト digitalWrite(9,LOW); mqtt_wait_counter = 5; } } else{ mqtt_wait_counter--; } Serial.print("mqtt_wait_counter = "); Serial.println(mqtt_wait_counter); theAudio->stopPlayer(AudioClass::Player0); Serial.println("stopplayer"); theAudio->setReadyMode(); Serial.println("setready"); setOutputAndVolume(); Serial.println("setoutput"); loadMp3Decoder(); Serial.println("loaddecoder"); } // 指定トラックを開く(失敗時は false) static bool openTrack(uint8_t track) { if (track >= kTrackCount) track = 0; if (g_mp3) g_mp3.close(); g_mp3 = theSD.open(kTrackFiles[track]); if (!g_mp3) { Serial.print("ERROR: open failed -> "); Serial.println(kTrackFiles[track]); return false; } Serial.print("open: "); Serial.println(kTrackFiles[track]); return true; } // 開いているファイルの先頭から再生を開始 static bool startFromOpenFile() { err_t err; g_mp3.seek(0); err = theAudio->writeFrames(AudioClass::Player0, g_mp3); if (err != AUDIOLIB_ECODE_OK) { Serial.printf("writeFrames failed (%d) -> reload codec\n", err); loadMp3Decoder(); err = theAudio->writeFrames(AudioClass::Player0, g_mp3); if (err != AUDIOLIB_ECODE_OK) { Serial.printf("writeFrames retry failed (%d)\n", err); return false; } } delay(60); // 環境依存の短い待ち err = theAudio->startPlayer(AudioClass::Player0); //UART_transfar(); if (err != AUDIOLIB_ECODE_OK) { Serial.printf("startPlayer failed (%d)\n", err); return false; } return true; } // トラック切替(曲末時にのみ使用) static bool switchTo(uint8_t nextTrack) { ensureReadyAndCodec(); if (!openTrack(nextTrack)) return false; if (!startFromOpenFile()) return false; g_playing = nextTrack; return true; } // 同一トラックのリピート(「巻き戻し」ではなく開き直しで統一) static bool repeatSame() { ensureReadyAndCodec(); if (!openTrack(g_playing)) return false; return startFromOpenFile(); } // 0..5 のシリアルコマンドを取り込む(次回曲末で反映) static void pollSerial() { while (Serial.available() > 0) { int c = Serial.read(); if ('0' <= c && c <= '5') { g_desired = static_cast<uint8_t>(c - '0'); Serial.print("Requested next track: "); Serial.println(g_desired); } } } // ---- センサデータ取得 -------------------------------------------------------- sensor_data_t BME280_get(void){ int ret_sub2; int8_t sndid = 100; /* user-defined msgid */ uint32_t snddata =10; int8_t rcvid; int8_t msgid; sensor_data_t *rcvdata; ret_sub2 = MP.Send(sndid, snddata, subcore); if (ret_sub2 < 0) { printf("MP.Send error = %d\n", ret_sub2); } MP.RecvTimeout(1000); // ret_sub1 = MP.Recv(&msgid, &received_data, 1); ret_sub2 = MP.Recv(&rcvid, &rcvdata, subcore); if (ret_sub2 < 0) { printf("MP.Recv error = %d\n", ret_sub2); } return *rcvdata; } // ---- データ送信&受信 -------------------------------------------------------- #if USE_LTE int LTE_communication(float temp, float humi, float pres){ char sendData[500]; Serial.print(temp); Serial.print(", "); Serial.print(humi); Serial.print(", "); Serial.print(pres); Serial.println(""); sprintf(sendData,"{\"key\": \"techseeker-nekogorodoh\",\"data\": {\"gps\": \"35.00000, 135.00000\",\"temperature\": \"%f\",\"humidity\": \"%f\",\"pressure\": \"%f\"}}" ,temp,humi,pres); //,rcvdata->temperature,rcvdata->humidity,rcvdata->pressure); String contentType = "application/json"; HttpClient http(client, server, port); http.beginRequest(); http.post(path, contentType, sendData); http.endRequest(); // read the status code and body of the response int statusCode = http.responseStatusCode(); String response = http.responseBody(); Serial.print("Status code: "); Serial.println(statusCode); Serial.print("Response: "); Serial.println(response); return atoi(response.c_str()); } #endif // ---- UART送信 -------------------------------------------------------- void UART_transfar(void){ char jsonBuffer[100]; sprintf(jsonBuffer, "{\"track\": %d, \"speed\": %d}", g_desired, fumi_fumi_speed); Serial2.println(jsonBuffer); } // ---- MQTT ----------------------------------------------------------- #if USE_LTE bool mqttConnect() { tlsCli.setCACert(ISRG_ROOT_X1_PEM); tlsCli.setTimeout(30000); mqtt.setServer(MQTT_HOST, MQTT_PORT); mqtt.setKeepAlive(60); mqtt.setBufferSize(512); char cid[32]; snprintf(cid, sizeof(cid), "sender-%lu", millis()); Serial.printf("[MQTT] connect %s:%d ...\n", MQTT_HOST, MQTT_PORT); if (!mqtt.connect(cid, MQTT_USER, MQTT_PASS)) { Serial.printf("[MQTT] connect NG rc=%d\n", mqtt.state()); return false; } Serial.println(F("[MQTT] connected")); return true; } void mqttEnsureConnected() { if (mqtt.connected()) return; while (!mqttConnect()) { Serial.println(F("Retry MQTT in 5s ...")); delay(5000); } } // ---------- シャッター指令を publish ---------- void sendShutter() { mqttEnsureConnected(); const char payload[] = "shutter"; bool ok = mqtt.publish(MQTT_TOPIC, (const uint8_t*)payload, strlen(payload), true); // retain = true Serial.printf("[PUB] shutter %s\n", ok ? "OK" : "NG"); } #endif // ---- Setup -------------------------------------------------------- void setup() { Serial.begin(115200); //Serial2.begin(115200); delay(1500); Serial.println(); Serial.println("=== Spresense MP3 Switch Debug (LTE/Sensor-less) ==="); pinMode(9,OUTPUT); // SD while (!theSD.begin()) { Serial.println("Insert SD card with /BIN and MP3 files (0..5)."); delay(500); } // Audio theAudio = AudioClass::getInstance(); theAudio->begin(); theAudio->setRenderingClockMode(AS_CLKMODE_NORMAL); setOutputAndVolume(); loadMp3Decoder(); #if USE_SUBCORE { int subcore = 2; int rc = MP.begin(subcore); if (rc < 0) Serial.printf("MP.begin(%d) error=%d\n", subcore, rc); else Serial.println("SubCore started."); subcore = 3; rc = MP.begin(subcore); if (rc < 0) Serial.printf("MP.begin(%d) error=%d\n", subcore, rc); else Serial.println("SubCore started."); subcore = 4; rc = MP.begin(subcore); if (rc < 0) Serial.printf("MP.begin(%d) error=%d\n", subcore, rc); else Serial.println("SubCore started."); } #else Serial.println("SIM: SubCore disabled."); #endif #if USE_LTE Serial.println("LTE: attaching..."); if (lteAccess.begin() != LTE_SEARCHING) { Serial.println("LTE begin failed, continue without HTTP."); } else if (lteAccess.attach(APP_LTE_RAT, (char*)APP_LTE_APN, (char*)APP_LTE_USER_NAME, (char*)APP_LTE_PASSWORD, APP_LTE_AUTH_TYPE, APP_LTE_IP_TYPE) == LTE_READY) { Serial.println("LTE attached."); } else { Serial.println("LTE attach failed, continue without HTTP."); lteAccess.shutdown(); } #else Serial.println("SIM: LTE disabled."); #endif //mqttConnect(); // ひとまず接続しておく LTE接続しないためコメントアウト // 初期トラック → 再生開始 g_playing = 0; g_desired = 0; if (!openTrack(g_playing) || !startFromOpenFile()) { Serial.println("FATAL: initial start failed."); while (1) { delay(1000); } } g_nextTick = millis() + kChangeInterval; Serial.println("Ready. Auto-rotate every 10s; or type 0..5 in Serial to request next track."); } // ---- Loop --------------------------------------------------------- void loop() { sensor_data_t *rcvdata; //trigger = 1 の時に測定・通信を行う// if(sense_trigger == 1 ){ *rcvdata = BME280_get(); Serial.print(rcvdata->temperature); Serial.print(", "); Serial.print(rcvdata->humidity); Serial.print(", "); Serial.print(rcvdata->pressure); Serial.println(""); //g_desired = LTE_communication(rcvdata->temperature,rcvdata->humidity,rcvdata->pressure); //Serial.print("returned no = "); //Serial.println(g_desired); char sendData[500]; g_desired = random(0,6); Serial.print("Next track_No = "); Serial.println(g_desired); sense_trigger = 0; } pollSerial(); // デモ用オートローテーション(曲末で反映) if (millis() >= g_nextTick) { //g_desired = static_cast<uint8_t>((g_desired + 1) % kTrackCount); g_nextTick += kChangeInterval; Serial.print("[Auto] Next desired track: "); Serial.println(g_desired); } // 再生継続 err_t ret = theAudio->writeFrames(AudioClass::Player0, g_mp3); // ログ間引き(多すぎるとUARTが詰まる環境がある) if (millis() - g_logThrottle > 1000) { Serial.printf("writeFrames -> %d\n", ret); g_logThrottle = millis(); if(sense_trigger == 1 ){ *rcvdata = BME280_get(); Serial.print(rcvdata->temperature); Serial.print(", "); Serial.print(rcvdata->humidity); Serial.print(", "); Serial.print(rcvdata->pressure); Serial.println(""); //g_desired = LTE_communication(rcvdata->temperature,rcvdata->humidity,rcvdata->pressure); LTE使用しないためコメントアウト g_desired = random(1,6); //g_desired++; //if (g_desired == 6){ // g_desired = 1; //} //fumi_fumi_speed = random(10,100); Serial.print("returned no = "); Serial.println(g_desired); Serial.print("Next track_No = "); Serial.println(g_desired); sense_trigger = 0; } } // 曲末のみで切替 or リピート if (ret == AUDIOLIB_ECODE_FILEEND) { Serial.println("== File END =="); bool ok = false; if (g_desired != g_playing) { Serial.printf("Switch track: %u -> %u\n", g_playing, g_desired); ok = switchTo(g_desired); } else { Serial.println("Repeat same (re-open)."); ok = repeatSame(); } if (!ok) { Serial.println("ERROR: restart failed. Will retry next loop..."); // 失敗時も loop を継続し、次ループで再試行 } sense_trigger = 1; // 次ループに入ったとき、測定を行う } //Serial.println("loop end"); delay(50); } ``` --- ## 8. 使い方 ### 8.1 セットアップ手順 1. 配線・接続(カメラ、音声出力、SD、ネットワーク) 2. 音声ファイルを所定のパスへ配置(例:`/sound/xxx.wav`) 3. スケッチ書き込み 4. シリアルモニタでログ確認 5. MQTTで撮影トリガを送って動作確認 ### 8.2 MQTTで撮影命令(例) - (コマンド例はあなたのtopic/payloadに合わせて差し替え) 例: - topic:`spresense/camera/cmd` - payload:`shot` --- ## 9. 動作結果(写真・ログ) - 外観写真(装置全体) - 撮影した画像の例 - シリアルログ例(MQTT受信→撮影→保存→音再生 など) (画像を貼る欄として残してあります) --- ## 10. ハマりどころ / 注意点 - AudioとCameraとMQTT処理の同時実行で詰まりやすい点(バッファ、タスク、メモリ) - SDアクセス競合(撮影保存と音声読み出しが同時の場合) - MQTT再接続処理(ネットワーク瞬断時の復帰) - 撮影中に連打されたときのbusy制御(キュー or 無視 or 上書き) ※実際に遭遇した項目があれば箇条書きで追記してください。 --- ## 11. 改善案(今後やりたいこと) - 画像をMQTTで通知する(サイズ制約があるため別経路推奨、など) - HTTP/FTP/S3等にアップロードしてURLだけMQTT通知 - 音声を状態ごとに切り替え(接続/撮影成功/失敗) - コマンドをJSON化してパラメータ(解像度、連写、保存先)を指定 --- ## 12. 参考リンク - (Spresense公式ドキュメント) - (MQTTクライアント/ブローカ) - (使用ライブラリ) ※元のProtopedia記事や関連ページも、必要ならここに追加。