概要
猫のゴロゴロ音には、ストレス解消や自然治癒力を高める効果があるといわれており、まさに「癒やしの魔法」と言えます。 本作品では、Spresenseと骨伝導スピーカを使用して、このゴロゴロの再現に挑戦しました。音だけでなく「振動」も再現することで、リアルな感触を目指しています。
さらに、Spresenseカメラボードと生成AIを組み合わせ、撮影した写真を「猫まみれ」にするサービスも追加実装しました。
この作品でできたこと
- センサ情報に応じてゴロゴロ音が切り替わる
- タクトスイッチでカメラを遠隔操作
主に使うもの
- Spresense+LTE
- BME280(SPI)
- 骨伝導スピーカ
- HDRカメラ
作品自体の紹介はProtopediaで掲載しておりますので、そちらもご覧ください。
https://protopedia.net/prototype/7051
*本記事ではサーバ側の構成・動作については割愛します。
構成と実機動作
ゴロゴロを出すパペット部と、撮影を行うカメラ部で構成しています。
本記事では詳細は割愛しますが、サーバ側ではOpenAIの生成AIで処理を行っています。(2025年8月時点の設計です)
- センサのデータを元にゴロゴロ音選択にgpt-4o-mini
- カメラ画像の文字化にgpt-40
- 文字化した内容からdall-e-3で画像を生成
ゴロゴロ動作
- センサで温湿度を計測
- 環境センサのデータを、Spresenseを介してLTEでサーバに送信
- (サーバ側動作:環境センサのデータを元に再生するべきゴロゴロ音を選び、情報をサーバにアップ)
- サーバにアップされている情報を読み取り、その情報に従ったmp3ファイルを再生。
- 骨伝導スピーカを介して再生するので、音と振動となって出力される。
1~5を繰り返す
ねこまみれ動作
- パペットに内蔵されたタクトスイッチが押されると、MQTTをPublishする。
- カメラは、MQTTサーバと定期的に交信し、MQTTリクエストに応じて撮影を行う。
- 撮影した画像をサーバに送る
- (サーバ側動作:送られてきた画像を元に生成AIでねこまみれになった画像を作成、HTMLで表示する)
各部ハードウェア構成
パペット部
使ったもの
- Spresense メインボード
- Spresense LTE拡張ボード
- 温湿度センサユニット(BME280モジュール)[https://ssci.to/2236]
- ステレオD級オーディオアンプモジュール(PAM8403使用)[https://eleshop.jp/shop/g/gF81125/]
- microSDカード x1(猫のゴロゴロ音をmp3で保存しておきます)
- LTE-MのSIM カード
- 骨伝導スピーカ
- その他筐体部品(トタンプレート、タミヤ製ユニバーサルプレートなど)
- 猫型パペット(見た目)
構成
- SpresenseメインボードとLTE拡張ボードの組み合わせを使用
- 温湿度センサはSpresenseとSPI通信にて接続
- LTE拡張ボードのオーディオ端子から、D級アンプボードを介して骨伝導スピーカに接続
- タクトスイッチをGPIOに接続、押されるとカメラのシャッターを切るためのMQTTをPublishします。
温湿度センサ
温湿度センサにはBME280を搭載したモジュール(https://ssci.to/2236)を採用、SPI通信にて接続します。
ここではLTE拡張ボードの端子に接続するため、SPI3を使用します。
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のオーディオ機能でループ再生します。
トラック定義
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カード直下にこれらファイル名で配置しています。
再生状態の管理変数
uint8_t g_playing = 0; // 現在再生中
uint8_t g_desired = 0; // 次に切り替えたい(曲末で反映)
File g_mp3;
ポイントは 「今鳴っている曲 g_playing」と「次に鳴らしたい曲 g_desired」を分けていることです。
ループ中に g_desired が変わっても、曲末までは切り替えず安定動作を狙っています。
Audioの初期化と切替
Spresense Audioは、運用中にエラーが出たり状態が崩れたりしやすいことがあるため、切替前に「止める→Ready→Codecロード→再開」という手順を共通化しています。
static void setOutputAndVolume() {
theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, AS_SP_DRV_MODE_LINEOUT);
theAudio->setVolume(volumeset[g_desired]);
}
volumeset[]配列で トラックごとに音量を変えていますg_desiredに合わせる設計なので、次に鳴る曲の音量が反映されます
音量を大きくしすぎると音が割れてしまうので、音が割れないギリギリを狙って調整しました。
MP3デコーダロード
static void loadMp3Decoder() {
theAudio->initPlayer(AudioClass::Player0,
AS_CODECTYPE_MP3,
kBinPath,
AS_SAMPLINGRATE_AUTO,
AS_CHANNEL_STEREO);
}
kBinPath="/mnt/sd0/BIN" に AudioライブラリのBINを配置しています。
切替前に毎回“Ready”へ戻す
static void ensureReadyAndCodec() {
//<中略> MQTT処理 //
theAudio->stopPlayer(AudioClass::Player0);
theAudio->setReadyMode();
setOutputAndVolume();
loadMp3Decoder();
}
この関数が「安全に切替するための共通手順」です。
切り替えのタイミングで必要に応じてMQTT Publishも行います。
ファイルオープン
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失敗時処理
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再ロード→再試行の処理を入れています。
曲末でのみ「切替」または「同曲リピート」
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通信で使用する変数を設定します
// 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からセンサ値を取得
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を投げて、レスポンスを「次トラック番号」にする
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()関数(Maincore)
以下の流れで実行しています
- SD begin(カードが入るまで待つ)
- Audio begin → 出力・音量設定 → MP3デコーダロード
- SubCore複数起動(SubCore2,SubCore4)
- LTE attach(失敗しても継続)
- MQTT接続
- トラック0を開いて再生開始
loop()関数(Maincore)
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)
#define BME280_CS_PIN 7 // SPI3_CS1(D07)想定
#define SPI_CLOCK 1000000 // 1MHz
- CSピンを自前で
digitalWrite()してSPI転送の前後でLOW/HIGHします - SpresenseのSPI3に接続しているので、
SPI3.begin()を使っています。
setup()関数(SubCore2)
マルチコアの開始とBME280の初期化、補正データの読み出しを行います
MPを開始(SubCore側)
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()関数(SubCore2)
測定→補正→MainCore要求を受けて返信を行います。
1回測定を開始(CTRL_MEASへ書く)
SPI3.transfer(CTRL_MEAS & 0x7F);
SPI3.transfer(0x25); // x1 + forced(1回測定) -> 測定後スリープ
delay(10);
0x25 により「forced mode」で1回測定し終えたらスリープに戻ります。
生データ読み出し(0xF7〜)
SPI3.transfer(0xF7 | 0x80);
for (i=0; i<8; i++) dac[i] = SPI3.transfer(0x00);
- 0xF7から8バイト読み、気圧/温度/湿度のadc値へ展開します
補正計算して実値へ
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からの要求を待って、返す
ret = MP.Recv(&msgid, &msgdata);
...
ret = MP.Send(msgid, &snddata);
MP.Recv()で「要求(msgid)」を受ける- 同じ
msgidを使ってMP.Send()で応答する、という形になっています
GPIO制御(SubCore4)
タクトスイッチ処理(方針)
- タクトスイッチをGPIO_03に接続し、
INPUT設定+プルアップします。 - タクトスイッチが押されたらMQTTをPublishするのですが、タクトスイッチ押下とMQTT Publishのタイミングが異なるため、GPIO_09をラッチさせ、MaincoreではGPIO_09を見て処理をします。
このため、Core間通信は不要としています。 - また、Maincore側の処理が終わるとMaincore側でGPIO_09をLowにするので、ここではHighでラッチする処理のみ実施しています。
void setup()
{
pinMode(3,INPUT_PULLUP);
pinMode(9,OUTPUT);
digitalWrite(9,LOW); //初期値設定
}
void loop()
{
int SW_raw = digitalRead(3);
if (SW_raw == 0){
digitalWrite(9,HIGH);
}
delay(100);
}
カメラ部 ソフトウェア
カメラ部は「MQTTSでシャッタートリガを受ける → カメラで撮影 → HTTPSでJPEGをアップロード → MQTTでACKを返す」の4段パイプラインです。
全体の状態(グローバル変数)
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の前提条件を作る)
TLS(MQTTS/HTTPS)でサーバ証明書を検証する場合、端末時刻が大きくずれていると検証に失敗しやすいです。そこでLTE attach直後にネットワーク時刻でRTCを合わせています。
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;
}
lte.getTime()が取れない環境もあるので、その場合は継続(ただしTLS失敗の可能性あり)という方針。
TLSルート証明書設定(MQTT/HTTPSで共通)
Let’s Encrypt系(ISRG Root X1)を setCACert() で入れて、サーバ証明書の検証を有効にしています。
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受信:トリガは軽量に、重い処理はloop側へ
MQTT接続&Subscribe
bool mqttConnectAndSubscribe() {
setupTLSRoot(tlsMqtt);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
mqttClient.setCallback(onMqttMessage);
mqttClient.setKeepAlive(60);
mqttClient.setBufferSize(512);
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):サーバログから疎通確認ができるようにしています。
受信コールバック(トリガ検出だけ)
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;
}
}
- ここでは 「shutter要求フラグを立てるだけ」 にしています。
撮影・TLS通信をコールバック内でやると、MQTT処理が詰まりやすくなるためです。
撮影 → HTTPSアップロード
撮影(1枚)
CamImage img = theCamera.takePicture();
if (!img.isAvailable()) { ... }
isAvailable()で撮影失敗を判定し、失敗ならACKにcapture_failedを返しています。
multipartの組み立て(key + file)
アップロードは multipart/form-data にして、フィールドを2つ送っています。
key: APIキー文字列(簡易認証)file: JPEG本体
bool httpsUploadJpeg(const uint8_t* img, size_t imgSize) {
const char* boundary = "SpresenseBoundary8c3f1aa55f";
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";
size_t contentLen = partKey.length() + partFileHeader.length() + imgSize + closing.length();
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();
http.write(partKey...);
http.write(partFileHeader...);
// JPEG本体は1KB単位で分割送信(RAM節約&安定化)
while (sent < imgSize) http.write(img + sent, n);
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回分まとめて実施します。
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の接続維持」と「撮影要求の実行」だけに絞っています。
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
#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);
}
パペット部 SubCore2
#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
#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);
}
カメラ部
// ====== 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接続&Subscribe
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);
}
投稿者の人気記事




-
S-Shimizu
さんが
前の金曜日の21:53
に
編集
をしました。
(メッセージ: 初版)
-
S-Shimizu
さんが
昨日の1:27
に
編集
をしました。
-
S-Shimizu
さんが
昨日の3:30
に
編集
をしました。
-
S-Shimizu
さんが
昨日の11:50
に
編集
をしました。
-
S-Shimizu
さんが
昨日の13:01
に
編集
をしました。
-
S-Shimizu
さんが
昨日の13:37
に
編集
をしました。
-
S-Shimizu
さんが
昨日の15:42
に
編集
をしました。
-
S-Shimizu
さんが
昨日の16:24
に
編集
をしました。
-
S-Shimizu
さんが
昨日の16:25
に
編集
をしました。
-
S-Shimizu
さんが
昨日の21:18
に
編集
をしました。
-
S-Shimizu
さんが
昨日の21:52
に
編集
をしました。
-
S-Shimizu
さんが
昨日の22:42
に
編集
をしました。
-
S-Shimizu
さんが
昨日の22:58
に
編集
をしました。
-
S-Shimizu
さんが
昨日の23:23
に
編集
をしました。
-
S-Shimizu
さんが
昨日の23:27
に
編集
をしました。
ログインしてコメントを投稿する