S-Shimizu が 2026年01月31日16時24分14秒 に編集
コメント無し
本文の変更
# 概要 猫のゴロゴロ音には、ストレス解消や自然治癒力を高める効果があるといわれており、まさに「癒やしの魔法」です。 本記事では、Spresenseと骨伝導スピーカを使用して、このゴロゴロ音の再現に挑戦しました。音だけでなく「振動」も再現することで、リアルな感触を目指しています。 さらに、Spresenseカメラボードと生成AIを組み合わせ、撮影した写真を「猫まみれ」にするサービスも追加実装しました。
- この作品でできたこと - センサ情報に応じてでゴロゴロ音が切り替わる - ボタンでカメラを遠隔操作 - 主に使うもの - Spresense+LTE - BME280(SPI) - 骨伝導スピーカ - HDRカメラ
作品自体の紹介はProtopediaで掲載しておりますので、そちらもご覧ください。 https://protopedia.net/prototype/7051
@[youtube](https://www.youtube.com/watch?v=_36-odUgAgo)
*本記事ではサーバ側の構成・動作については割愛します。 --- # 構成と実機動作 ゴロゴロを出すパペット部と、撮影を行うカメラ部で構成しています。 
本記事では詳細は割愛しますが、サーバ側ではOpenAIの生成AIで処理を行っています。(2025年8月時点の設計です) - センサのデータを元にゴロゴロ音選択にgpt-4o-mini - カメラ画像の文字化にgpt-40 - 文字化した内容からdall-e-3で画像を生成
## ゴロゴロ動作 1. センサで温湿度を計測 2. 環境センサのデータを、Spresenseを介してLTEでサーバに送信
3. (サーバ側動作:環境センサのデータを元に再生するべきファイルを選び、情報をサーバにアップ)
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級アンプを介して骨伝導スピーカに接続。 骨伝導スピーカが貼りついたトタンプレートが振動し、振動と音でゴロゴロが出力されます。   #### D級アンプボード D級アンプとして、ステレオD級オーディオアンプモジュール(PAM8403使用)[https://eleshop.jp/shop/g/gF81125/]を使用しました。ゲイン固定のため、Spresense側の出力を音がなるべく大きく、かつ音が割れないような状態になるようにソフト上で調整しています。 ## カメラ部 ### 使ったもの - Spresense メインボード - Spresense LTE拡張ボード - SpresenseHDRカメラボード - LTE-MのSIM カード - 筐体(猫型、3Dプリンタで作成) ### 構成 - 電気的な構成はSpresenseメインボード、LTE拡張ボード、HDRカメラボードを推奨通り接続しています。 - 筐体を猫型にして、首輪の位置にカメラレンズが来ることを意識しています。  --- # ソフトウェア ## パペット部 パペット部のSpresenseはオーディオ再生、LTE通信、環境センサ動作、GPIO制御を並列で行う必要あるため、マルチコアを採用しました。 とはいえ、オーディオ再生とLTE通信はMainCoreでしか動作できないので再生と再生の合間に通信を行うような処理にしています。 ### マルチコア処理 以下のようにコアを割り当てています。 MainCore:オーディオ+LTE制御 SubCore2:環境センサ SubCore3:GPIO制御 ### オーディオ機能(MainCore) 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の初期化と切替 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も行います。 #### ファイルオープン ```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するようにします。 #### 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再ロード→再試行**の処理を入れています。 #### 曲末でのみ「切替」または「同曲リピート」 ```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機能(MainCore) SubCore2から取得したセンサ値をLTEを通してサーバにPOSTし、レスポンスを「次トラック番号」にします。 #### ヘッダ部設定 HTTP通信およびMQTT通信で使用する変数を設定します ```cpp // LTE情報 #define APP_LTE_APN "apn_name" //replace with your apn name #define APP_LTE_USER_NAME "user" // replace with your username #define APP_LTE_PASSWORD "password" // replace with your password #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) ``` 使用するSIMカードの設定に従います。 ``` static const char server[] = "hostname"; // replace with server hostname static const char path[] = "/goro"; // replace with pathname static const int port = 80; //HTTP port static const char MQTT_HOST[] = "hostname"; // replace with server hostname static const int MQTT_PORT = 8883; // TLS static const char MQTT_TOPIC[] = "spresense/test"; // replace with pashname static const char MQTT_USER[] = "user"; // replace with server username static const char MQTT_PASS[] = "pass";// replace with server userpassword ``` #### ルート証明書 - `ISRG Root X1` を `setCACert()` に設定しています 各種サーバに接続するための設定を記述しています。 HTTPとMQTTで接続先が(同じサーバは使用していますが)異なるので、それぞれの変数を設定しておきます。 #### 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番トラック) - サーバ側の処理については割愛します #### MQTT 接続 まず、MQTTサーバに接続します ``` /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()); if (!mqtt.connect(cid, MQTT_USER, MQTT_PASS)) { return false; } return true; } void mqttEnsureConnected() { if (mqtt.connected()) return; while (!mqttConnect()) { Serial.println(F("Retry MQTT in 5s ...")); delay(5000); } } ``` #### MQTT 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"); } ``` MQTTサーバに接続後、シャッターを切る指令をPublishします #### スイッチ操作とMQTT 上述の`ensureReadyAndCodec()`関数内の処理 ``` if (mqtt_wait_counter == 0){ int SW = digitalRead(9); if (SW == 1){ sendShutter(); digitalWrite(9,LOW); mqtt_wait_counter = 5; } } else{ mqtt_wait_counter--; } ``` Subcore3で操作される`GPIO 9pin`の状態を見て、Highであれば`sendShutter()`を実行、MQTTをPublishします。 また、シャッターを切った後の画像処理に時間がかかるため、次にMQTTをPublishするには1分以上の経過を必要とするような処理にしています (音の再生に10秒近くかかることを利用しています) ### setup()関数 以下の流れで実行しています 1. SD begin(カードが入るまで待つ) 2. Audio begin → 出力・音量設定 → MP3デコーダロード 3. SubCore複数起動(SubCore2,SubCore4) 4. LTE attach(失敗しても継続) 5. MQTT接続 6. トラック0を開いて再生開始 ### loop()関数 ```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通信→次曲決定、という流れ ### センサ情報取得(SubCore2) #### BME280の接続(SPI3 + CS) ```cpp #define BME280_CS_PIN 7 // SPI3_CS1(D07)想定 #define SPI_CLOCK 1000000 // 1MHz ``` - CSピンを自前で `digitalWrite()` してSPI転送の前後でLOW/HIGHします - SpresenseのSPI3に接続しているので、`SPI3.begin()` を使っています。 #### setup()関数 マルチコアの開始とBME280の初期化、補正データの読み出しを行います ##### MPを開始(SubCore側) ```cpp MP.begin(); ``` SubCoreがMP通信可能な状態になります。 ##### BME280の動作設定(単発測定ベース) - `CONFIG(0xF5)` に `0x00` を書き込み - 単発測定 / フィルタなし / SPI 4線式 - `CTRL_MEAS(0xF4)` に `0x24` を書き込み - 温度・気圧 oversampling x1 / スリープ - `CTRL_HUM(0xF2)` に `0x01` を書き込み - 湿度 oversampling x1 ##### 補正係数(キャリブレーション)を読む BME280は生データ(adc_T/adc_P/adc_H)を、そのまま実値にできません。チップ内部の補正係数(dig_T*, dig_P*, dig_H*)を読み出して補正計算します。 - 0x88 から温度・気圧系の補正データを取得 - 0xE1 から湿度系の補正データを取得 - 取得したバイト列を `dig_T1..` などへ組み立てています コードは末尾の全体を参照ください #### loop()関数 測定→補正→MainCore要求を受けて返信を行います。 ##### 1回測定を開始(CTRL_MEASへ書く) ```cpp SPI3.transfer(CTRL_MEAS & 0x7F); SPI3.transfer(0x25); // x1 + forced(1回測定) -> 測定後スリープ delay(10); ``` `0x25` により「forced mode」で1回測定し終えたらスリープに戻ります。 ##### 生データ読み出し(0xF7〜) ```cpp SPI3.transfer(0xF7 | 0x80); for (i=0; i<8; i++) dac[i] = SPI3.transfer(0x00); ``` - 0xF7から8バイト読み、気圧/温度/湿度のadc値へ展開します ##### 補正計算して実値へ ```cpp pres_cal = BME280_compensate_P_int32(adc_P); temp_cal = BME280_compensate_T_int32(adc_T); humi_cal = bme280_compensate_H_int32(adc_H); pres = pres_cal / 100.0; temp = temp_cal / 100.0; humi = humi_cal / 1024.0; ``` - `t_fine` は温度補正で更新され、湿度・気圧補正でも使われます ##### MainCoreからの要求を待って、返す ```cpp ret = MP.Recv(&msgid, &msgdata); ... ret = MP.Send(msgid, &snddata); ``` - `MP.Recv()` で「要求(msgid)」を受ける - 同じ `msgid` を使って `MP.Send()` で応答する、という形になっています ### GPIO制御(SubCore4) #### タクトスイッチ処理(方針) - タクトスイッチをGPIO3に設定、プルアップします。 - タクトスイッチが押されたらMQTTをPublishするのですが、タクトスイッチ押下とMQTT Publishのタイミングが異なるため、GPIO9を使ってラッチさせます。 - このため、MP.Recv, MP.Sendは使用していません。 コードは末尾を参照してください。
---
## カメラ部
カメラ部では - サーバにMQTTメッセージが到着しているかを確認する。 - MQTTがサーバに到着していたらカメラで写真を撮影する - 撮影した画像をサーバに送る の処理を行っています。
カメラ部は「**MQTTSでシャッタートリガを受ける** → **カメラで撮影** → **HTTPSでJPEGをアップロード** → **MQTTでACKを返す**」の4段パイプラインです。
### 全体の状態(グローバル変数) ```cpp volatile bool gShutterRequested = false; // MQTT受信で立つ「撮影要求フラグ」 bool gBusy = false; // 撮影~アップロード中の再入防止 unsigned long gLastShotMs = 0; // 直近撮影時刻 const unsigned long MIN_INTERVAL_MS = 2000; // 連打防止(簡易デバウンス) ``` - `gShutterRequested`:MQTTのコールバックは「受信した」ことだけを伝え、重い処理(撮影やHTTPS)をその場でやらない設計です。 - `gBusy`:撮影とアップロードは時間がかかるので、処理中に追加トリガが来ても1回にまとめます。 - `MIN_INTERVAL_MS`:サーバ側の二重Publishや、ボタン連打相当の誤爆に対する保険です。
### 設定項目(各自の環境に合わせて変更)
#### LTE接続+RTC同期(TLSの前提条件を作る)
#### LTE(APN)
TLS(MQTTS/HTTPS)でサーバ証明書を検証する場合、**端末時刻が大きくずれていると検証に失敗**しやすいです。そこでLTE attach直後にネットワーク時刻でRTCを合わせています。
```cpp
#define APP_LTE_APN "apn_name" //replace with your apn name #define APP_LTE_USER_NAME "user" // replace with your username #define APP_LTE_PASSWORD "password" // replace with your password #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)
bool lteAttach() { if (lte.begin() != LTE_SEARCHING) return false; if (lte.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) { lte.shutdown(); return false; } RTC.begin(); unsigned long epoch = lte.getTime(); if (epoch > 1600000000UL) { RtcTime rt(epoch, 0); RTC.setTime(rt); } return true; }
```
#### HTTPSアップロード先
- `lte.getTime()` が取れない環境もあるので、その場合は継続(ただしTLS失敗の可能性あり)という方針。 #### TLSルート証明書設定(MQTT/HTTPSで共通) Let’s Encrypt系(ISRG Root X1)を `setCACert()` で入れて、**サーバ証明書の検証を有効**にしています。
```cpp
static const char HOST[] = "hostname"; // replace with server hostname static const int HTTPS_PORT = 443; static const char PATH[] = "/upload/image"; // replace with pathname
bool setupTLSRoot(LTETLSClient& cli) { cli.setCACert(ISRG_ROOT_X1_PEM); cli.setTimeout(30000); cli.setSendTimeout(30000); return true; }
```
- MQTT用 `tlsMqtt` と HTTPS用 `tlsHttp` の**2本のTLSクライアントを分けている**点が地味に重要です。 (1本を使い回すと、どちらかの接続状態に引きずられて詰まりやすい)
#### MQTT(MQTTS)ブローカー
#### MQTT受信:トリガは軽量に、重い処理はloop側へ #### MQTT接続&Subscribe
```cpp
static const char MQTT_HOST[] = "hostname"; // replace with server hostname static const int MQTT_PORT = 8883;
bool mqttConnectAndSubscribe() { setupTLSRoot(tlsMqtt);
static const char MQTT_TOPIC_SUB[] = "spresense/test"; static const char MQTT_TOPIC_ACK[] = "spresense/ack";
mqttClient.setServer(MQTT_HOST, MQTT_PORT); mqttClient.setCallback(onMqttMessage); mqttClient.setKeepAlive(60); mqttClient.setBufferSize(512);
static const char MQTT_USER[] = "user"; // replace with server username static const char MQTT_PASS[] = "pass"; // replace with server userpassword
snprintf(clientId, sizeof(clientId), "spresense-%lu", millis()); if (!mqttClient.connect(clientId, MQTT_USER, MQTT_PASS)) return false; mqttClient.subscribe("spresense/test"); mqttClient.subscribe("spresense/#"); // ズレ対策(保険) mqttClient.publish("spresense/ack", "online", true); return true; }
```
- `setBufferSize(512)`:受信payloadが大きいとデフォルトサイズで詰まることがあるため保険。 - `spresense/#` 購読:トピック設計が固まるまでのデバッグ性重視。ただし運用では購読範囲を絞る方が安全です。 - 起動時ACK(`online`):サーバログから疎通確認ができるようにしています。
### APIキー(共通鍵)
### 受信コールバック(トリガ検出だけ)
```cpp
static const char API_KEY[] = "techseeker-nekogorodoh";
void onMqttMessage(char* topic, byte* payload, unsigned int length) { String msg; for (unsigned int i = 0; i < length; i++) msg += (char)payload[i]; msg.trim(); if (msg.indexOf("shutter") >= 0) { gShutterRequested = true; } }
```
- HTTPS側のmultipartで `key` として送信します(簡易認証用途)
### ルート証明書 - `ISRG Root X1` を `setCACert()` に設定しています(Let’s Encrypt系の検証用)
- ここでは **「shutter要求フラグを立てるだけ」** にしています。 撮影・TLS通信をコールバック内でやると、MQTT処理が詰まりやすくなるためです。
---
## 4) 撮影 → HTTPSアップロード
#### MQTTメッセージ仕様(トリガ) - Publish:`spresense/test`(加えて`spresense/#`も購読) - ペイロード文字列に **`shutter` が含まれていれば撮影** - JSONでもOK(例:`{"cmd":"shutter"}`) - 余計な空白があっても `trim()` して判定
### 撮影(1枚) ```cpp CamImage img = theCamera.takePicture(); if (!img.isAvailable()) { ... } ``` - `isAvailable()` で撮影失敗を判定し、失敗ならACKに `capture_failed` を返しています。
ACKは `spresense/ack` に以下を送ります(retain=true): - 起動時:`online` - 成功:`ok` - 失敗:`ng` - 撮影失敗:`capture_failed`
### multipartの組み立て(key + file) アップロードは `multipart/form-data` にして、フィールドを2つ送っています。
#### HTTPSアップロード仕様(multipart/form-data) POST `https://<HOST>/upload/image`
- `key`: APIキー文字列(簡易認証) - `file`: JPEG本体
multipartの中身: - `key`: APIキー文字列 - `file`: JPEG(`photo.jpg`、`Content-Type: image/jpeg`)
```cpp bool httpsUploadJpeg(const uint8_t* img, size_t imgSize) { const char* boundary = "SpresenseBoundary8c3f1aa55f";
レスポンスはJSONを想定し、`result` フィールドがあれば表示します。
String partKey = "--<boundary>\r\n... name=\"key\" ...\r\n\r\n<API_KEY>\r\n"; String partFileHeader = "--<boundary>\r\n... name=\"file\"; filename=\"photo.jpg\" ...\r\n\r\n"; String closing = "\r\n--<boundary>--\r\n";
#### 実装上のポイント(このコードの工夫部分だけ) - **RTC同期(lte.getTime)** TLSで証明書検証するため、時刻ずれ対策として起動時にRTCを合わせます。
size_t contentLen = partKey.length() + partFileHeader.length() + imgSize + closing.length();
- **MQTT受信バッファ拡張** ```cpp mqttClient.setBufferSize(512); ``` 受信が256B超のケースで詰まるのを避ける保険。
HttpClient http(tlsHttp, HOST, HTTPS_PORT); http.beginRequest(); http.post(PATH); http.sendHeader("Content-Type", "multipart/form-data; boundary=..."); http.sendHeader("Content-Length", (int)contentLen); http.beginBody();
- **トピックずれ対策でワイルドカード購読** ```cpp mqttClient.subscribe("spresense/test"); mqttClient.subscribe("spresense/#"); ```
http.write(partKey...); http.write(partFileHeader...);
- **連打防止(簡易デバウンス)** ```cpp const unsigned long MIN_INTERVAL_MS = 2000; ``` `shutter`が短時間に複数回飛んでも2秒以内は無視。
// JPEG本体は1KB単位で分割送信(RAM節約&安定化) while (sent < imgSize) http.write(img + sent, n);
- **HTTPS送信後に明示的にstop** ```cpp tlsHttp.stop(); ``` セッション残りや接続資源の詰まりを避ける目的。
http.write(closing...); http.endRequest();
int status = http.responseStatusCode(); String body = http.responseBody();
tlsHttp.stop(); // 明示クローズ ... } ``` - **Content-Lengthを自前計算**している(multipartは地味にここでハマる) - JPEGを `CHUNK=1024` で分割送信(大きい画像でも安定しやすい) - `tlsHttp.stop()` で**明示的にセッションを閉じる**(LTE環境では接続資源が詰まって失速することがあるため) --- #### 1ショット処理 (doOneShot)関数で異常系も含めて完結させています 撮影要求が来たら、この関数が「撮影・アップロード・ACK」を1回分まとめて実施します。 ```cpp void doOneShot() { gBusy = true; gShutterRequested = false; CamImage img = theCamera.takePicture(); if (!img.isAvailable()) { mqttEnsureConnected(); if (mqttClient.connected()) mqttClient.publish(MQTT_TOPIC_ACK, "capture_failed", true); gBusy = false; return; } bool ok = httpsUploadJpeg(img.getImgBuff(), img.getImgSize()); mqttEnsureConnected(); if (mqttClient.connected()) mqttClient.publish(MQTT_TOPIC_ACK, ok ? "ok" : "ng", true); gLastShotMs = millis(); gBusy = false; } ``` - 「撮影失敗」と「アップロード失敗」をACKで分けているのは運用上便利です(障害切り分けが速い)。 - `gShutterRequested=false` を冒頭で落としているので、連続トリガが来ても「処理中に積まない(1回だけやる)」挙動になります。 #### loop:MQTTのポーリングと“フラグ駆動”の実行 メインループは「MQTTの接続維持」と「撮影要求の実行」だけに絞っています。 ```cpp void loop() { if (!mqttClient.connected()) { mqttConnectAndSubscribe(); } else { mqttClient.loop(); } if (gShutterRequested && !gBusy && (millis() - gLastShotMs > MIN_INTERVAL_MS)) { doOneShot(); } delay(10); } ``` - MQTTは `mqttClient.loop()` を回さないと受信できないので、ここが心臓部。 - 連打防止は `MIN_INTERVAL_MS` と `gBusy` の2段構えです。 ---- #### カメラ部の補足 - **retain=true の副作用**:`shutter` をretainでPublishすると、カメラが再接続した瞬間に「過去のshutter」を拾って撮影する可能性がある → 運用では「retainしない」か「payloadに時刻/nonceを入れて古い要求を捨てる」など - **部分一致トリガ**:`indexOf("shutter")` は誤爆しうるので、運用では完全一致やJSONパース推奨 - **TLSのための時刻同期**:LTE時刻が取れないとTLS失敗する場合があります ---
# コード ## パペット部 MainCore ```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 "apn_name" //replace with your apn name #define APP_LTE_USER_NAME "user" // replace with your username #define APP_LTE_PASSWORD "password" // replace with your password #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[] = "hostname"; // replace with server hostname 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----- <証明書データ・省略> -----END CERTIFICATE-----)"; LTE lteAccess; LTEClient client; LTETLSClient tlsCli; PubSubClient mqtt(tlsCli); // HTTP設定 char server[] = "hostname"; // replace with server hostname 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]; sense_trigger = 0; } pollSerial(); // デモ用オートローテーション(曲末で反映) if (millis() >= g_nextTick) { 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); } ``` ## パペット部 SubCore42 ```cpp #if (SUBCORE != 2) #error "Core selection is wrong!!" #endif #include <MP.h> #include <SPI.h> //アドレス指定 #define CONFIG 0xF5 #define CTRL_MEAS 0xF4 #define CTRL_HUM 0xF2 // SPI3設定 #define BME280_CS_PIN 7 // BME280のCS端子をSPI3_CS1 (D07)) に接続 #define SPI_CLOCK 1000000 // 1MHz struct sensor_data_int_t { int temperature; int humidity; int pressure; }; //気温補正データ uint16_t dig_T1; int16_t dig_T2; int16_t dig_T3; //湿度補正データ uint8_t dig_H1; int16_t dig_H2; uint8_t dig_H3; int16_t dig_H4; int16_t dig_H5; int8_t dig_H6; //気圧補正データ uint16_t dig_P1; int16_t dig_P2; int16_t dig_P3; int16_t dig_P4; int16_t dig_P5; int16_t dig_P6; int16_t dig_P7; int16_t dig_P8; int16_t dig_P9; unsigned char dac[26]; unsigned int i; int32_t t_fine; int32_t adc_P, adc_T, adc_H; void setup() { MP.begin(); //シリアル通信初期化 Serial.begin(115200); // CSピン設定 pinMode(BME280_CS_PIN, OUTPUT); digitalWrite(BME280_CS_PIN, HIGH); //SPI初期化 SPI3.begin(); //SPIを初期化、SCK、MOSI、SSの各ピンの動作は出力、SCK、MOSIはLOW、SSはHIGH SPI3.beginTransaction(SPISettings(SPI_CLOCK, MSBFIRST, SPI_MODE0)); //BME280動作確認 digitalWrite(BME280_CS_PIN, LOW); //SSピンの出力をLOW(0V)に設定 SPI3.transfer(0xD0 | 0x80); //出力データバイトを「気圧データ」のアドレスに指定、書き込みフラグを立てる for (i = 0; i < 26; i++) { dac[i] = SPI3.transfer(0x00); //dacにSPIデバイス「BME280」のデータ読み込み } digitalWrite(BME280_CS_PIN, HIGH); //SSピンの出力をHIGH(5V)に設定 for (i = 0; i < 26; i++) { Serial.print(dac[i]); //dacにSPIデバイス「BME280」のデータ読み込み } Serial.println(""); //BME280動作設定 digitalWrite(BME280_CS_PIN, LOW); //SSピンの出力をLOW(0V)に設定 SPI3.transfer(CONFIG & 0x7F); //動作設定 SPI3.transfer(0x00); //「単発測定」、「フィルタなし」、「SPI 4線式」 digitalWrite(BME280_CS_PIN, HIGH); //SSピンの出力をHIGH(5V)に設定 //BME280測定条件設定 digitalWrite(BME280_CS_PIN, LOW); //SSピンの出力をLOW(0V)に設定 SPI3.transfer(CTRL_MEAS & 0x7F); //測定条件設定 SPI3.transfer(0x24); //「温度・気圧オーバーサンプリングx1」、「スリープモード」 digitalWrite(BME280_CS_PIN, HIGH); //SSピンの出力をHIGH(5V)に設定 //BME280温度測定条件設定 digitalWrite(BME280_CS_PIN, LOW); //SSピンの出力をLOW(0V)に設定 SPI3.transfer(CTRL_HUM & 0x7F); //湿度測定条件設定 SPI3.transfer(0x01); //「湿度オーバーサンプリングx1」 digitalWrite(BME280_CS_PIN, HIGH); //SSピンの出力をHIGH(5V)に設定 //BME280補正データ取得 digitalWrite(BME280_CS_PIN, LOW); //SSピンの出力をLOW(0V)に設定 SPI3.transfer(0x88 | 0x80); //出力データバイトを「補正データ」のアドレスに指定、書き込みフラグを立てる for (i = 0; i < 26; i++) { dac[i] = SPI3.transfer(0x00); //dacにSPIデバイス「BME280」のデータ読み込み } digitalWrite(BME280_CS_PIN, HIGH); //SSピンの出力をHIGH(5V)に設定 dig_T1 = ((uint16_t)((dac[1] << 8) | dac[0])); dig_T2 = ((int16_t)((dac[3] << 8) | dac[2])); dig_T3 = ((int16_t)((dac[5] << 8) | dac[4])); dig_P1 = ((uint16_t)((dac[7] << 8) | dac[6])); dig_P2 = ((int16_t)((dac[9] << 8) | dac[8])); dig_P3 = ((int16_t)((dac[11] << 8) | dac[10])); dig_P4 = ((int16_t)((dac[13] << 8) | dac[12])); dig_P5 = ((int16_t)((dac[15] << 8) | dac[14])); dig_P6 = ((int16_t)((dac[17] << 8) | dac[16])); dig_P7 = ((int16_t)((dac[19] << 8) | dac[18])); dig_P8 = ((int16_t)((dac[21] << 8) | dac[20])); dig_P9 = ((int16_t)((dac[23] << 8) | dac[22])); dig_H1 = ((uint8_t)(dac[25])); digitalWrite(BME280_CS_PIN, LOW); //SSピンの出力をLOW(0V)に設定 SPI3.transfer(0xE1 | 0x80); //出力データバイトを「補正データ」のアドレスに指定、書き込みフラグを立てる for (i = 0; i < 7; i++) { dac[i] = SPI3.transfer(0x00); //dacにSPIデバイス「BM3280」のデータ読み込み } digitalWrite(BME280_CS_PIN, HIGH); //SSピンの出力をHIGH(5V)に設定 dig_H2 = ((int16_t)((dac[1] << 8) | dac[0])); dig_H3 = ((uint8_t)(dac[2])); dig_H4 = ((int16_t)((dac[3] << 4) + (dac[4] & 0x0F))); dig_H5 = ((int16_t)((dac[5] << 4) + ((dac[4] >> 4) & 0x0F))); dig_H6 = ((int8_t)dac[6]); delay(1000); //1000msec待機(1秒待機) } void loop() { int32_t temp_cal; uint32_t humi_cal, pres_cal; float temp, humi, pres; int ret; int8_t msgid; //BME280測定条件設定(1回測定後、スリープモード) digitalWrite(BME280_CS_PIN, LOW); //SSピンの出力をLOW(0V)に設定 SPI3.transfer(CTRL_MEAS & 0x7F); //測定条件設定 SPI3.transfer(0x25); //「温度・気圧オーバーサンプリングx1」、「1回測定後、スリープモード」 digitalWrite(BME280_CS_PIN, HIGH); //SSピンの出力をHIGH(5V)に設定 delay(10); //10msec待機 //測定データ取得 digitalWrite(BME280_CS_PIN, LOW); //SSピンの出力をLOW(0V)に設定 SPI3.transfer(0xF7 | 0x80); //出力データバイトを「気圧データ」のアドレスに指定、書き込みフラグを立てる for (i = 0; i < 8; i++) { dac[i] = SPI3.transfer(0x00); //dacにSPIデバイス「BME280」のデータ読み込み } digitalWrite(BME280_CS_PIN, HIGH); //SSピンの出力をHIGH(5V)に設定 adc_P = ((uint32_t)dac[0] << 12) | ((uint32_t)dac[1] << 4) | ((dac[2] >> 4) & 0x0F); adc_T = ((uint32_t)dac[3] << 12) | ((uint32_t)dac[4] << 4) | ((dac[5] >> 4) & 0x0F); adc_H = ((uint32_t)dac[6] << 8) | ((uint32_t)dac[7]); pres_cal = BME280_compensate_P_int32(adc_P); //気圧データ補正計算 temp_cal = BME280_compensate_T_int32(adc_T); //温度データ補正計算 humi_cal = bme280_compensate_H_int32(adc_H); //湿度データ補正計算 pres = (float)pres_cal / 100.0; //気圧データを実際の値に計算 temp = (float)temp_cal / 100.0; //温度データを実際の値に計算 humi = (float)humi_cal / 1024.0; //湿度データを実際の値に計算 sensor_data_int_t snddata; uint32_t msgdata; ret = MP.Recv(&msgid, &msgdata); if (ret < 0) { printf("subcore MP.Recv error = %d\n", ret); } memcpy(&snddata.temperature, &temp, sizeof(float)); memcpy(&snddata.humidity, &humi, sizeof(float)); memcpy(&snddata.pressure, &pres, sizeof(float)); ret = MP.Send(msgid, &snddata); if (ret < 0) { printf("subcore MP.Send error = %d\n", ret); } delay(1000); //1000msec待機(1秒待機) } //温度補正 関数 int32_t BME280_compensate_T_int32(int32_t adc_T) { int32_t var1, var2, T; var1 = ((((adc_T >> 3) - ((int32_t)dig_T1 << 1))) * ((int32_t)dig_T2)) >> 11; var2 = (((((adc_T >> 4) - ((int32_t)dig_T1)) * ((adc_T >> 4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14; t_fine = var1 + var2; T = (t_fine * 5 + 128) >> 8; return T; } //湿度補正 関数 uint32_t bme280_compensate_H_int32(int32_t adc_H) { int32_t v_x1_u32r; v_x1_u32r = (t_fine - ((int32_t)76800)); v_x1_u32r = (((((adc_H << 14) - (((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1_u32r)) + ((int32_t)16384)) >> 15) * (((((((v_x1_u32r * ((int32_t)dig_H6)) >> 10) * (((v_x1_u32r * ((int32_t)dig_H3)) >> 11) + ((int32_t)32768))) >> 10) + ((int32_t)2097152)) * ((int32_t)dig_H2) + 8192) >> 14)); v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4)); v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r); v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r); return (uint32_t)(v_x1_u32r >> 12); } //気圧補正 関数 uint32_t BME280_compensate_P_int32(int32_t adc_P) { int32_t var1, var2; uint32_t p; var1 = (((int32_t)t_fine) >> 1) - (int32_t)64000; var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * ((int32_t)dig_P6); var2 = var2 + ((var1 * ((int32_t)dig_P5)) << 1); var2 = (var2 >> 2) + (((int32_t)dig_P4) << 16); var1 = (((dig_P3 * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) + ((((int32_t)dig_P2) * var1) >> 1)) >> 18; var1 = ((((32768 + var1)) * ((int32_t)dig_P1)) >> 15); if (var1 == 0) { return 0; // avoid exception caused by division by zero } p = (((uint32_t)(((int32_t)1048576) - adc_P) - (var2 >> 12))) * 3125; if (p < 0x80000000) { p = (p << 1) / ((uint32_t)var1); } else { p = (p / (uint32_t)var1) * 2; } var1 = (((int32_t)dig_P9) * ((int32_t)(((p >> 3) * (p >> 3)) >> 13))) >> 12; var2 = (((int32_t)(p >> 2)) * ((int32_t)dig_P8)) >> 13; p = (uint32_t)((int32_t)p + ((var1 + var2 + dig_P7) >> 4)); return p; } ``` ## パペット部 SubCore3 ```cpp #if (SUBCORE != 3) #error "Core selection is wrong!!" #endif #include <MP.h> void setup() { int ret = 0; ret = MP.begin(); pinMode(3,INPUT_PULLUP); pinMode(9,OUTPUT); delay(100); digitalWrite(9,LOW); } void loop() { int SW_raw = digitalRead(3); if (SW_raw == 0){ digitalWrite(9,HIGH); } int SW = digitalRead(9); if (SW == 0){ ledOff(LED0); } if (SW == 1){ ledOn(LED0); } delay(100); } ``` ## カメラ部 ```cpp // ====== Spresense Cam → LTE MQTTS-triggered HTTPS Upload ====== #ifdef SUBCORE #error "Build for MainCore only." #endif #include <Arduino.h> #include <Camera.h> #include <LTE.h> #include <LTETLSClient.h> #include <ArduinoHttpClient.h> #include <Arduino_JSON.h> #include <RTC.h> #include <PubSubClient.h> // ===== LTE settings ===== #define APP_LTE_APN "apn_name" //replace with your apn name #define APP_LTE_USER_NAME "user" // replace with your username #define APP_LTE_PASSWORD "password" // replace with your password #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) // ===== HTTPS endpoint ===== static const char HOST[] = "hostname"; // replace with server hostname static const int HTTPS_PORT = 443; static const char PATH[] = "/upload/image"; // ===== MQTT (MQTTS) endpoint ===== static const char MQTT_HOST[] = "hostname"; // replace with server hostname static const int MQTT_PORT = 8883; static const char MQTT_TOPIC_SUB[] = "spresense/test"; // ← Flask側 publish先 static const char MQTT_TOPIC_ACK[] = "spresense/ack"; // ← 任意: 成否通知 static const char MQTT_USER[] = "spresense"; static const char MQTT_PASS[] = "spresense_password"; // ===== API Key (共通鍵) ===== static const char API_KEY[] = "techseeker-nekogorodoh"; // ISRG Root X1(PEM) static const char ISRG_ROOT_X1_PEM[] = R"(-----BEGIN CERTIFICATE----- <証明書データ> -----END CERTIFICATE-----)"; // ===== Camera settings ===== static const int CAM_W = CAM_IMGSIZE_VGA_H; // 640 static const int CAM_H = CAM_IMGSIZE_VGA_V; // 480 static const uint8_t JPEG_QUALITY = 75; // 1..100 // ===== Globals ===== LTE lte; LTETLSClient tlsMqtt; LTETLSClient tlsHttp; PubSubClient mqttClient(tlsMqtt); volatile bool gShutterRequested = false; bool gBusy = false; unsigned long gLastShotMs = 0; const unsigned long MIN_INTERVAL_MS = 2000; // 2秒の簡易デバウンス // ===== LTE attach ===== bool lteAttach() { Serial.println(F("[LTE] begin...")); if (lte.begin() != LTE_SEARCHING) { Serial.println(F("[LTE] begin failed")); return false; } Serial.println(F("[LTE] attach...")); if (lte.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(F("[LTE] attach failed")); lte.shutdown(); return false; } Serial.println(F("[LTE] READY")); // RTC同期(TLS検証安定化) RTC.begin(); unsigned long epoch = lte.getTime(); if (epoch > 1600000000UL) { RtcTime rt(epoch, 0); RTC.setTime(rt); Serial.println(F("[RTC] set by LTE network time")); } else { Serial.println(F("[RTC] LTE time unavailable; continuing…")); } return true; } // ===== Camera init ===== bool camInit() { Serial.println(F("[CAM] begin...")); CamErr err = theCamera.begin(); if (err != CAM_ERR_SUCCESS) { Serial.println(F("[CAM] begin failed")); return false; } err = theCamera.setStillPictureImageFormat(CAM_W, CAM_H, CAM_IMAGE_PIX_FMT_JPG); if (err != CAM_ERR_SUCCESS) { Serial.println(F("[CAM] setStillPictureImageFormat failed")); return false; } theCamera.setJPEGQuality(JPEG_QUALITY); Serial.println(F("[CAM] ready")); delay(200); return true; } bool setupTLSRoot(LTETLSClient& cli) { cli.setCACert(ISRG_ROOT_X1_PEM); cli.setTimeout(30000); cli.setSendTimeout(30000); return true; } // ===== multipart/form-data upload (with 'key' + 'file') ===== bool httpsUploadJpeg(const uint8_t* img, size_t imgSize) { const char* boundary = "SpresenseBoundary8c3f1aa55f"; String partKey = String("--") + boundary + "\r\n" "Content-Disposition: form-data; name=\"key\"\r\n\r\n" + String(API_KEY) + "\r\n"; String partFileHeader = String("--") + boundary + "\r\n" "Content-Disposition: form-data; name=\"file\"; filename=\"photo.jpg\"\r\n" "Content-Type: image/jpeg\r\n\r\n"; String closing = String("\r\n--") + boundary + "--\r\n"; size_t contentLen = partKey.length() + partFileHeader.length() + imgSize + closing.length(); HttpClient http(tlsHttp, HOST, HTTPS_PORT); http.beginRequest(); http.post(PATH); http.sendHeader("Accept", "application/json"); http.sendHeader("Connection", "close"); http.sendHeader("Content-Type", String("multipart/form-data; boundary=") + boundary); http.sendHeader("Content-Length", (int)contentLen); http.beginBody(); http.write((const uint8_t*)partKey.c_str(), partKey.length()); http.write((const uint8_t*)partFileHeader.c_str(), partFileHeader.length()); size_t sent = 0; const size_t CHUNK = 1024; while (sent < imgSize) { size_t n = min(CHUNK, imgSize - sent); http.write(img + sent, n); sent += n; } http.write((const uint8_t*)closing.c_str(), closing.length()); http.endRequest(); int status = http.responseStatusCode(); String body = http.responseBody(); Serial.printf("[HTTPS] status=%d, bodyLen=%d\n", status, body.length()); Serial.println(body); tlsHttp.stop(); // 明示的にクローズ if (status < 200 || status >= 300) return false; JSONVar j = JSON.parse(body); if (JSON.typeof(j) == "undefined") return false; if (j.hasOwnProperty("result")) { Serial.print(F("[JSON] result: ")); Serial.println((const char*)j["result"]); } return true; } // 変更: 受信メッセージの表示を強化、"shutter"を“部分一致”でも拾う void onMqttMessage(char* topic, byte* payload, unsigned int length) { Serial.print(F("[MQTT] Message on ")); Serial.print(topic); Serial.print(F(" len=")); Serial.println(length); String msg; msg.reserve(length + 1); for (unsigned int i = 0; i < length; i++) msg += (char)payload[i]; msg.trim(); Serial.print(F("[MQTT] payload: ")); Serial.println(msg); // "shutter" を含んでいればOK(JSONや余計な空白が混じっても反応) if (msg.indexOf("shutter") >= 0) { gShutterRequested = true; } } // publishトピックをワイルドカードでも捕捉(タイポ/サブトピックのズレ対策) bool mqttConnectAndSubscribe() { setupTLSRoot(tlsMqtt); mqttClient.setServer(MQTT_HOST, MQTT_PORT); mqttClient.setCallback(onMqttMessage); mqttClient.setKeepAlive(60); mqttClient.setBufferSize(512); // 受信が256B超で詰むのを予防(保険) char clientId[32]; snprintf(clientId, sizeof(clientId), "spresense-%lu", millis()); Serial.printf("[MQTT] connecting to %s:%d ...\n", MQTT_HOST, MQTT_PORT); bool ok = mqttClient.connect(clientId, MQTT_USER, MQTT_PASS); if (!ok) { Serial.printf("[MQTT] connect failed, rc=%d\n", mqttClient.state()); return false; } Serial.println(F("[MQTT] connected. subscribing...")); bool s1 = mqttClient.subscribe("spresense/test"); // 元のトピック bool s2 = mqttClient.subscribe("spresense/#"); // 念のためワイルドカード Serial.printf("[MQTT] subscribe s1=%d s2=%d\n", s1, s2); // 起動確認用にACKを一発(サーバログ/クライアントで見える) mqttClient.publish("spresense/ack", "online", true); Serial.println(F("[MQTT] published online ack")); return true; } void mqttEnsureConnected() { if (!mqttClient.connected()) { mqttConnectAndSubscribe(); } } // ===== 1ショット処理(撮影→HTTPSアップロード→ACK)===== void doOneShot() { gBusy = true; gShutterRequested = false; Serial.println(F("[CAM] takePicture()")); CamImage img = theCamera.takePicture(); if (!img.isAvailable()) { Serial.println(F("[CAM] capture failed")); mqttEnsureConnected(); if (mqttClient.connected()) mqttClient.publish(MQTT_TOPIC_ACK, "capture_failed", true); gBusy = false; return; } Serial.printf("[CAM] size : %d bytes\n", (int)img.getImgSize()); Serial.println(F("[HTTPS] upload...")); bool ok = httpsUploadJpeg(img.getImgBuff(), img.getImgSize()); Serial.println(ok ? F("Upload OK.") : F("Upload failed.")); // ACK publish(接続が切れていたら再接続してから) mqttEnsureConnected(); if (mqttClient.connected()) { mqttClient.publish(MQTT_TOPIC_ACK, ok ? "ok" : "ng", true); // retain=true は好みで } gLastShotMs = millis(); gBusy = false; } void setup() { Serial.begin(115200); while (!Serial) {} Serial.println(F("\n=== Spresense Cam → LTE MQTTS-triggered HTTPS Upload ===")); if (!camInit()) { Serial.println(F("FATAL: Camera init failed.")); while (1) delay(1000); } if (!lteAttach()) { Serial.println(F("FATAL: LTE attach failed.")); while (1) delay(1000); } // 事前にTLSルートCA設定だけ済ませる(HTTP側) setupTLSRoot(tlsHttp); // MQTT接続&購読 mqttConnectAndSubscribe(); } // 追加: 監視ログをもう少し(切断時の理由表示) void loop() { if (!mqttClient.connected()) { Serial.printf("[MQTT] disconnected, state=%d. reconnecting...\n", mqttClient.state()); mqttConnectAndSubscribe(); } else { mqttClient.loop(); } if (gShutterRequested && !gBusy && (millis() - gLastShotMs > MIN_INTERVAL_MS)) { doOneShot(); } delay(10); } ```