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

S-Shimizu が 2026年01月31日03時30分23秒 に編集

コメント無し

本文の変更

# 概要 猫のゴロゴロには、ストレスの解消や傷の回復といった効果があるといわれており、それはまさに癒しの魔法と言えるのではないでしょうか。 そんなゴロゴロの再現に挑戦してみました。 骨伝導スピーカを使って、猫のゴロゴロを音と振動で表現しています。 さらに、カメラで撮影した画像を元にねこまみれにするサービスを追加しました。 *本記事ではサーバ側の構成・動作については割愛します。 --- # 構成と実機動作 ごろごろを出すパペット部と、撮影を行うカメラ部で構成しています。 ![全体構成](https://camo.elchika.com/d6959ef9bbb8be7beddc6ba30db3e2c2a5836d33/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63643635626163622d613465632d343732632d383062662d3830666534333333613338652f35323235656362342d396238392d343764332d383230632d626163383530333339656561/) ## ごろごろ動作 1. センサで温湿度を計測 2. 環境センサのデータを、Spresenseを介してLTEでサーバに送信 3. (サーバ側動作:環境センサのデータを元に再生するべきファイルを選び、情報をサーバーにアップ) 4. サーバにアップされている情報を読み取り、その情報に従ったmp3ファイルを再生。 5. 骨伝導スピーカーを介して再生するので、音と振動となって出力される。 1~5を繰り返す ## ねこまみれ動作 1. パペットに内蔵されたタクトスイッチが押されると、MQTTをPublishする。 2. カメラは、MQTTサーバと定期的に交信し、MQTTリクエストに応じて撮影を行う。 3. 撮影した画像をサーバに送る 4. (サーバ側動作:送られてきた画像を元に生成AIで猫まみれになった画像を作成、HTMLで表示する) --- # 各部ハードウェア構成 ## パペット部 ### 使ったもの - Spresense メインボード - Spresense LTE拡張ボード - 温湿度センサユニット(BME280モジュール) - ステレオD級オーディオアンプモジュール(PAM8403使用)[https://eleshop.jp/shop/g/gF81125/] - microSDカード x1(猫のゴロゴロ音をmp3で保存しておきます) - LTE-MのSIM カード - 骨伝導スピーカー - その他筐体部品(トタンプレート、タミヤ製ユニバーサルプレートなど) - 猫型パペット(見た目) ### 構成 - SpresenseメインボードとLTE拡張ボードの組み合わせを使用 - 温湿度センサをSpresenseとSPIで接続、通信 - LTE拡張ボードのオーディオ端子から、D級アンプを介して骨伝導スピーカーに接続。 骨伝導スピーカーが貼りついたトタンプレートが振動し、振動と音でごろごろが出力されます。 ![パペット内部](https://camo.elchika.com/7c4ca9f70b46b9a9d7e6385abaa8246158c72b24/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63643635626163622d613465632d343732632d383062662d3830666534333333613338652f62636338623765322d366464342d343535632d623932302d643735306337663966346138/) ## カメラ部 ### 使ったもの - Spresense メインボード - Spresense LTE拡張ボード - SpresenseHDRカメラボード - LTE-MのSIM カード - 筐体(猫型、3Dプリンタで作成) ### 構成 - 電気的な構成はSpresenseメインボード、LTE拡張ボード、HDRカメラボードを仕様通り接続しています。 - 筐体を猫型して、首輪の位置にカメラレンズが来ることを意識しています。 ![カメラ構成](https://camo.elchika.com/4e3518841ddae8d45cd755c7fe94cff4bf31a543/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63643635626163622d613465632d343732632d383062662d3830666534333333613338652f38363962623163392d306665382d343761632d613335392d623939323063383631623664/) --- # ソフトウェア ## パペット部 パペット部のSpresenseはオーディオ再生、LTE通信、環境センサ動作、GPIO制御を並列で行う必要あるため、マルチコアを採用しました。 とはいえ、オーディオ再生とLTE通信はメインコアでしか動作できないので再生と再生の合間に通信を行うような処理にしています。 ### マルチコア処理 以下のようにコアを割り当てています。 メインコア:オーディオ+LTE制御 サブコア2:環境センサ サブコア3:GPS制御(屋内で使っていたので実装したものの情報拾えず結局使えていなかった・・) サブコア4:GPIO制御

-

### 音声再生(SD上のMP3をトラック管理

+

### オーディオ機能(メインコア) SDカード上のMP3をトラック管理し、Spresenseのオーディオ機能でループ再生します。

#### トラック定義 ```cpp static constexpr uint8_t kTrackCount = 6; 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", }; ``` SDカード直下にこれらファイル名で配置しています。 #### 再生状態の管理変数 ```cpp uint8_t g_playing = 0; // 現在再生中 uint8_t g_desired = 0; // 次に切り替えたい(曲末で反映) File g_mp3; ``` ポイントは **「今鳴っている曲 `g_playing`」と「次に鳴らしたい曲 `g_desired`」を分けている**ことです。 ループ中に `g_desired` が変わっても、**曲末までは切り替えず**安定動作を狙っています。

-

--- ### Audioの初期化と切替

+

#### Audioの初期化と切替

Spresense Audioは、運用中にエラーが出たり状態が崩れたりしやすいことがあるため、切替前に「止める→Ready→Codecロード→再開」という手順を共通化しています。

-

#### 出力先と音量設定

```cpp static void setOutputAndVolume() { theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, AS_SP_DRV_MODE_LINEOUT); theAudio->setVolume(volumeset[g_desired]); } ``` - `volumeset[]` 配列で **トラックごとに音量**を変えています - `g_desired` に合わせる設計なので、**次に鳴る曲の音量**が反映されます 音量を大きくしすぎると音が割れてしまうので、音が割れないギリギリを狙って調整しました。 #### MP3デコーダロード ```cpp static void loadMp3Decoder() { theAudio->initPlayer(AudioClass::Player0, AS_CODECTYPE_MP3, kBinPath, AS_SAMPLINGRATE_AUTO, AS_CHANNEL_STEREO); } ``` `kBinPath="/mnt/sd0/BIN"` に AudioライブラリのBINを配置しています。 #### 切替前に毎回“Ready”へ戻す ```cpp static void ensureReadyAndCodec() {

+

//<中略> MQTT処理 //

theAudio->stopPlayer(AudioClass::Player0); theAudio->setReadyMode(); setOutputAndVolume(); loadMp3Decoder(); } ```

-

この関数が「安全に切替するための共通手順」です。

-

MQTT publish判定混ざっています(後述)

+

切り替えタイミングで必要応じてMQTT Publishいます

-

---

-

### ファイルを開く→再生を開始する(openTrack / startFromOpenFile)

#### ファイルオープン ```cpp static bool openTrack(uint8_t track) { if (track >= kTrackCount) track = 0; if (g_mp3) g_mp3.close(); g_mp3 = theSD.open(kTrackFiles[track]); ... } ```

-

- すでに開いている `g_mp3` は必ず close - 範囲外が来たら 0 丸め

+

- すでに開いている `g_mp3` は必ず closeするようします。

-

#### writeFrames失敗時はCodec再ロードしてリトライ

+

#### writeFrames失敗時処理

```cpp static bool startFromOpenFile() { g_mp3.seek(0); err_t err = theAudio->writeFrames(AudioClass::Player0, g_mp3); if (err != AUDIOLIB_ECODE_OK) { loadMp3Decoder(); err = theAudio->writeFrames(AudioClass::Player0, g_mp3); ... } delay(60); err = theAudio->startPlayer(AudioClass::Player0); ... } ```

-

- `writeFrames()` が失敗するケースを想定して **Codec再ロード→再試行** - `delay(60)` は環境依存“間”としてています

+

- `writeFrames()` が失敗するケースを想定して **Codec再ロード→再試行**処理をています

-

---

+

#### 曲末でのみ「切替」または「同曲リピート」

-

### 曲末でのみ「切替」または「同曲リピート」 曲末を検出するのはここです。

```cpp err_t ret = theAudio->writeFrames(AudioClass::Player0, g_mp3); if (ret == AUDIOLIB_ECODE_FILEEND) { if (g_desired != g_playing) { ok = switchTo(g_desired); } else { ok = repeatSame(); } sense_trigger = 1; } ``` - `AUDIOLIB_ECODE_FILEEND` のときだけ切替処理をする - 切替後に `sense_trigger=1` を立てて、次曲開始後のタイミングで計測&通信を行います。 ---

+

### センサ+LTE機能(メインコア) サブコアから取得したセンサ値をLTEを通してサーバにPOSTし、レスポンスを「次トラック番号」にします。

+

#### SubCoreからセンサ値を取得 ```cpp sensor_data_t BME280_get(void){ MP.Send(sndid, snddata, subcore); MP.RecvTimeout(1000); MP.Recv(&rcvid, &rcvdata, subcore); return *rcvdata; } ``` - `MP.Send()` で SubCore 側に要求を投げる - `MP.Recv()` で `sensor_data_t*` を受け取る #### HTTPでJSONを投げて、レスポンスを「次トラック番号」にする ```cpp int LTE_communication(float temp, float humi, float pres){ sprintf(sendData,"{\"key\": ... ,\"data\": { ... }}", temp, humi, pres); HttpClient http(client, server, port); http.post(path, contentType, sendData); int statusCode = http.responseStatusCode(); String response = http.responseBody(); return atoi(response.c_str()); } ``` - JSONをPOST - レスポンス本文を `atoi()` し、その整数を `g_desired` に入れる運用 (例:サーバが `"3"` を返したら次は3番トラック) - サーバ側の処理については割愛します

## MP3再生を曲末で切替しつつ、SubCoreセンサ取得+LTE(HTTP/MQTT)連携するスケッチ解説 Spresense(MainCore)で **SD上のMP3を再生**し、**曲の終端でのみトラックを切替**するプレイヤーを作ります。さらに、**SubCoreからBME280相当のセンサ値を取得**し、**LTEでHTTP送信して次に再生するトラック番号を受け取る**流れを入れています。加えて、**MQTT(TLS)で“shutter”をpublish**する処理も含めています。 - 音声:AudioライブラリでMP3再生(SDカード) - 切替:`g_desired` を更新し、**曲末で反映** - SubCore:`MP` を使ってセンサデータを受信 - LTE:HTTPでJSONをPOSTし、レスポンスを「次トラック番号」として採用 - MQTT:TLS接続で `spresense/test` に `shutter` を publish(retainあり) ---

-

## 全体構成(何がどこで動くか)

### MainCore(このスケッチ) - SDから `0_default.mp3` などを開いて再生 - `writeFrames()` をループで回して再生を継続 - `AUDIOLIB_ECODE_FILEEND`(曲末)になったら - `g_desired != g_playing` → 次の曲へ切替 - 同じなら同じ曲を開き直してリピート - 曲末のタイミングで `sense_trigger=1` にし、次ループでセンサ取得+通信を実施 ### SubCore(別途書き込みが必要) - `MP.Send/Recv` に応答し、`sensor_data_t{temperature,humidity,pressure}` を返す ---

-

### 再生ファイルとトラック管理

-

```cpp static constexpr uint8_t kTrackCount = 6;

-

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", }; ``` SD直下にこれらのファイル名で置きます。 MP3デコーダ用バイナリは SpresenseのAudio例と同様に `/mnt/sd0/BIN` を参照します。

---

-

### Audio初期化:出力先と音量、MP3デコーダロード

-

#### 出力と音量設定 ```cpp static void setOutputAndVolume() { theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, AS_SP_DRV_MODE_LINEOUT); theAudio->setVolume(volumeset[g_desired]); } ``` - 出力は `LINEOUT` を指定 - 音量は `volumeset[g_desired]`(トラックごとに変更) #### MP3デコーダ初期化 ```cpp static void loadMp3Decoder() { theAudio->initPlayer(AudioClass::Player0, AS_CODECTYPE_MP3, kBinPath, AS_SAMPLINGRATE_AUTO, AS_CHANNEL_STEREO); } ``` `/mnt/sd0/BIN` にデコーダが置かれている前提です。

---

-

### 「曲末切替」を安定させるための共通手順 `ensureReadyAndCodec()` 曲の切替時・リピート時に毎回同じ手順で「止める→Ready→再設定→デコーダ再ロード」を行います。 ```cpp static void ensureReadyAndCodec() { // (中略)MQTT publish チェック theAudio->stopPlayer(AudioClass::Player0); theAudio->setReadyMode(); setOutputAndVolume(); loadMp3Decoder(); } ``` このスケッチでは「切替時に変な状態が残って startPlayer が失敗する」ケースを潰すため、割と強めに初期化し直す方針です。 --- ### ファイルを開いて再生する(open → writeFrames → start) #### トラックを開く ```cpp static bool openTrack(uint8_t track) { if (g_mp3) g_mp3.close(); g_mp3 = theSD.open(kTrackFiles[track]); ... } ``` #### 開いたファイルを再生開始 ```cpp static bool startFromOpenFile() { g_mp3.seek(0); err = theAudio->writeFrames(AudioClass::Player0, g_mp3); if (err != OK) { loadMp3Decoder(); retry... } delay(60); err = theAudio->startPlayer(AudioClass::Player0); } ``` ポイント: - 先に `writeFrames()` を投げてから `startPlayer()` しています - `writeFrames()` が失敗したら **デコーダをロードし直してリトライ**します --- ### 曲末でだけ切替する `switchTo()` / `repeatSame()` 曲末に入った時だけ呼ばれます。 ```cpp static bool switchTo(uint8_t nextTrack) { ensureReadyAndCodec(); if (!openTrack(nextTrack)) return false; if (!startFromOpenFile()) return false; g_playing = nextTrack; return true; } ``` 同じ曲のリピートも同様に「開き直し」で統一しています。 --- ### シリアルで次トラック予約(即時切替ではない) ```cpp static void pollSerial() { while (Serial.available() > 0) { int c = Serial.read(); if ('0' <= c && c <= '5') { g_desired = c - '0'; } } } ``` - `g_desired` は「次にしたい曲」 - **曲末でのみ** `g_desired` が反映されます(再生途中でブツ切りしない) --- ### SubCoreからセンサ値を取得する `BME280_get()` ```cpp sensor_data_t BME280_get(void){ MP.Send(sndid, snddata, subcore); MP.RecvTimeout(1000); MP.Recv(&rcvid, &rcvdata, subcore); return *rcvdata; } ``` - `MP.Send()` で SubCore 側に要求を投げる - `MP.Recv()` で `sensor_data_t*` を受け取る想定 --- ### LTE:HTTPでJSONを投げて、レスポンスを「次トラック番号」にする ```cpp int LTE_communication(float temp, float humi, float pres){ sprintf(sendData,"{\"key\": ... ,\"data\": { ... }}", temp, humi, pres); HttpClient http(client, server, port); http.post(path, contentType, sendData); int statusCode = http.responseStatusCode(); String response = http.responseBody(); return atoi(response.c_str()); } ``` - JSONをPOST - レスポンス本文を `atoi()` し、その整数を `g_desired` に入れる運用 (例:サーバが `"3"` を返したら次は3番トラック) ---

### MQTT(TLS):shutter を publish(retainあり) TLSクライアントにルート証明書(ISRG Root X1)を設定して MQTT over TLS (8883) に接続します。 - `mqttEnsureConnected()`:未接続なら接続し直す - `sendShutter()`:`payload="shutter"` を `MQTT_TOPIC` に publish このスケッチでは `ensureReadyAndCodec()` 内で一定周期に `digitalRead(9)` を見て `sendShutter()` を呼ぶ仕組みが入っています。 --- ### 12. setup():SD / Audio / SubCore / LTE / MQTT / 初回再生 重要な流れ: 1. SD begin(カードが入るまで待つ) 2. Audio begin → 出力・音量設定 → MP3デコーダロード 3. SubCore複数起動(2,3,4) 4. LTE attach(失敗しても継続) 5. MQTT接続 6. トラック0を開いて再生開始 --- ### loop():再生継続 → 曲末検出 → 切替 or リピート → 次回センサ送信予約 ```cpp err_t ret = theAudio->writeFrames(AudioClass::Player0, g_mp3); if (ret == AUDIOLIB_ECODE_FILEEND) { if (g_desired != g_playing) switchTo(g_desired); else repeatSame(); sense_trigger = 1; } ``` - `writeFrames()` が回り続けることで再生が進む - 曲末コードが返ったら切替処理 - その後 `sense_trigger=1` を立て、**次のループ冒頭で**センサ取得→HTTP通信→次曲決定、という流れ ###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 #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"; 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() { theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, AS_SP_DRV_MODE_LINEOUT); theAudio->setVolume(volumeset[g_desired]); } 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(); digitalWrite(9,LOW); mqtt_wait_counter = 5; } } else{ mqtt_wait_counter--; } theAudio->stopPlayer(AudioClass::Player0); theAudio->setReadyMode(); setOutputAndVolume(); loadMp3Decoder(); } // 指定トラックを開く(失敗時は 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); 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_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]; sprintf(sendData,"{\"key\": \"techseeker-nekogorodoh\",\"data\": {\"gps\": \"35.00000, 135.00000\",\"temperature\": \"%f\",\"humidity\": \"%f\",\"pressure\": \"%f\"}}" ,temp,humi,pres); 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); delay(1500); 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(); // ひとまず接続しておく // 初期トラック → 再生開始 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(); g_desired = LTE_communication(rcvdata->temperature,rcvdata->humidity,rcvdata->pressure); 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) { g_logThrottle = millis(); if(sense_trigger == 1 ){ *rcvdata = BME280_get(); g_desired = LTE_communication(rcvdata->temperature,rcvdata->humidity,rcvdata->pressure); sense_trigger = 0; } } // 曲末のみで切替 or リピート if (ret == AUDIOLIB_ECODE_FILEEND) { 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; // 次ループに入ったとき、測定を行う } 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` --- ## 10. ハマりどころ / 注意点 - AudioとCameraとMQTT処理の同時実行で詰まりやすい点(バッファ、タスク、メモリ) - SDアクセス競合(撮影保存と音声読み出しが同時の場合) - MQTT再接続処理(ネットワーク瞬断時の復帰) - 撮影中に連打されたときのbusy制御(キュー or 無視 or 上書き) ※実際に遭遇した項目があれば箇条書きで追記してください。 --- ## 11. 改善案(今後やりたいこと) - 画像をMQTTで通知する(サイズ制約があるため別経路推奨、など) - HTTP/FTP/S3等にアップロードしてURLだけMQTT通知 - 音声を状態ごとに切り替え(接続/撮影成功/失敗) - コマンドをJSON化してパラメータ(解像度、連写、保存先)を指定 --- ## 12. 参考リンク - (Spresense公式ドキュメント) - (MQTTクライアント/ブローカ) - (使用ライブラリ) ※元のProtopedia記事や関連ページも、必要ならここに追加。