[Spresense] microSD に保存されたファイルを LTE 回線で Web サーバー (nginx) に HTTPS で転送するスケッチ
前書き
Spresense が撮影した画像は microSD に保存することを前提に、その保存された画像を LTE 回線を使ってインターネット上の Web サーバーに転送する。このスケッチを作って動作確認をした。
ハードウェア的には、
- Spresense メインボード
- HDR カメラボード
- LTE 拡張ボード (SIM カードと microSD カードを挿入)
となっている。
HTTP の PUT リクエストにより画像をアップロードするが、プロトコルは HTTPS を使用する。Web サーバーは、 さくらの VPS の Ubuntu 24.04 + nginx を準備した。独自ドメインも使っているので、この独自ドメイン用のサーバー証明書も Let’s encrypt で作っておいた。
このあたりの Web サーバーのお話については。以下の記事を参考にしていただけたら幸いだ。
コード
環境に応じて修正する必要のある個所を 青字 と 赤字 で記した。
青字: LTE ネットワークの APN 設定。僕は NURO モバイル のデータ専用 SIM カードを契約して使っているから APN は so-net.jp になり、ユーザー名もパスワードも nuro だ。
赤字: Spresense が TLS 通信時に使用するルート証明書のファイルパス (microSD に保存されている) や、アップロード先の Web サーバーの情報 (ホスト名、保存先ディレクトリ) と、ベーシック認証用のユーザー名とパスワードを Base64 にエンコードした文字列、と、アップロードするファイルが保存されているファイルパス (microSD に保存されている) だ。
TLS 通信で使用するルート証明書を microSD に保存する方法について「どうやってやるの?」と思われる方もいるかもしれない。 Spresense 公式ドキュメントに参考になる箇所 があります。
#include <ArduinoHttpClient.h> #include <LTE.h> #include <RTC.h> #include <SDHCI.h> #define APP_LTE_APN "so-net.jp" #define APP_LTE_USER_NAME "nuro" #define APP_LTE_PASSWORD "nuro" #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) #define ROOTCA_FILE "/certs/ISRG_Root_X1.cer" #define DEBUG_PRINT(x) Serial.print(x) #define DEBUG_PRINTLN(x) Serial.println(x) #define MAX_RETRIES 5 #define RETRY_DELAY 60000 // 60 seconds const char* server = "test-spresense.gadgets-today.net"; const char* putPath = "/uploads/cam01/"; const int port = 443; const char* encodedAuth = "aCyWXdlboNlPXSlc3e5VpVzIC0mOTL4QFqlbG6jZQ=="; const char* uploadFilePath = "/images/cam01-20240916-122803.jpg"; LTE lteAccess; LTETLSClient tlsClient; HttpClient client = HttpClient(tlsClient, server, port); SDClass theSD; String createPutPath(const char* basePath, const char* filePath) { String fileName = String(filePath); int lastSlash = fileName.lastIndexOf('/'); if (lastSlash != -1) { fileName = fileName.substring(lastSlash + 1); } return String(basePath) + fileName; } void printClock(RtcTime &rtc) { DEBUG_PRINTLN(String(rtc.year()) + "/" + String(rtc.month()) + "/" + String(rtc.day()) + " " + String(rtc.hour()) + ":" + String(rtc.minute()) + ":" + String(rtc.second())); } bool initializeLTE() { for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { if (lteAccess.begin() == LTE_SEARCHING) { if (lteAccess.attach(APP_LTE_RAT, APP_LTE_APN, APP_LTE_USER_NAME, APP_LTE_PASSWORD, APP_LTE_AUTH_TYPE, APP_LTE_IP_TYPE) == LTE_READY) { DEBUG_PRINTLN("LTE ネットワークに接続されました。"); return true; } } DEBUG_PRINTLN("LTE attach failed. Retrying..."); lteAccess.shutdown(); delay(RETRY_DELAY); } return false; } bool initializeSDCard() { if (!theSD.begin()) { DEBUG_PRINTLN("Failed to initialize SD card"); return false; } DEBUG_PRINTLN("microSD カードをマウントしました。"); return true; } bool setRTC() { RTC.begin(); unsigned long currentTime; int attempts = 0; while (0 == (currentTime = lteAccess.getTime())) { if (++attempts >= MAX_RETRIES) { DEBUG_PRINTLN("Failed to get time from network"); return false; } delay(1000); } DEBUG_PRINTLN("LTE ネットワークから日時を取得しました。"); RtcTime rtc(currentTime); printClock(rtc); RTC.setTime(rtc); DEBUG_PRINTLN("LTE ネットワークから取得した日時を RTC に設定しました。"); return true; } bool loadCACert() { File rootCertsFile = theSD.open(ROOTCA_FILE, FILE_READ); if (!rootCertsFile) { DEBUG_PRINTLN("Failed to open CA certificates file"); return false; } DEBUG_PRINTLN("microSD カードからルート証明書を読み込みました。"); tlsClient.setCACert(rootCertsFile, rootCertsFile.available()); rootCertsFile.close(); return true; } bool uploadFile() { File uploadFile = theSD.open(uploadFilePath, FILE_READ); if (!uploadFile) { DEBUG_PRINTLN("Failed to open file for reading"); return false; } DEBUG_PRINTLN("PUT でアップロードするファイルを microSD カードから開きました。"); String fullPutPath = createPutPath(putPath, uploadFilePath); size_t fileSize = uploadFile.size(); DEBUG_PRINTLN("PUT で 512 バイトごとにファイルを分割して転送します。"); client.beginRequest(); client.put(fullPutPath.c_str()); client.sendHeader("Authorization", "Basic " + String(encodedAuth)); client.sendHeader("Content-Length", String(fileSize)); client.sendHeader("Content-Type", "image/jpeg"); client.beginBody(); const size_t bufferSize = 512; uint8_t buffer[bufferSize]; size_t totalBytesUploaded = 0; while (uploadFile.available()) { size_t bytesRead = uploadFile.read(buffer, bufferSize); size_t bytesWritten = client.write(buffer, bytesRead); totalBytesUploaded += bytesWritten; DEBUG_PRINT("."); } uploadFile.close(); client.endRequest(); int statusCode = client.responseStatusCode(); String response = client.responseBody(); DEBUG_PRINT("\nResponse status code: "); DEBUG_PRINTLN(statusCode); DEBUG_PRINT("Response body: "); DEBUG_PRINTLN(response); return (statusCode == 201); } void setup() { Serial.begin(115200); while (!Serial) { ; } if (!initializeSDCard()) { return; } if (!initializeLTE()) { DEBUG_PRINTLN("LTE initialization failed"); return; } if (!setRTC()) { DEBUG_PRINTLN("RTC setting failed"); return; } if (!loadCACert()) { DEBUG_PRINTLN("Failed to load CA certificate"); return; } } void loop() { static bool uploadAttempted = false; if (!uploadAttempted) { if (uploadFile()) { DEBUG_PRINTLN("ファイルのアップロードが成功しました!"); } else { DEBUG_PRINTLN("ファイルのアップロードが失敗しました。"); } uploadAttempted = true; } delay(RETRY_DELAY); }
コードを動かした時のコンソール表示
以下のような感じになる。
ステータスコードが 201 でファイル転送に成功していることがわかる。
赤枠で囲んだ箇所は、開発環境 (Arduino IDE) に ArduinoHttpClient をインストールしたときの状況である。
プログラムの 1 行目で、#include <ArduinoHttpClient.h> を書いているが、開発環境に ArduinoHttpClient をインストールする必要があったのだ。
Web サーバー (nginx) 側のログ
nginx のアクセスログには以下のように記録される。
$ sudo grep spresense-iot-test /var/log/nginx/access.log
92.203.160.41 - spresense-iot-test [17/Sep/2024:23:52:59 +0900] "PUT /uploads/cam01/cam01-20240916-122803.jpg HTTP/1.1" 201 0 "-" "Arduino/2.2.0"
$
アップロードされたファイルの存在が確認できる。
$ pwd /home/spresense-iot-test/uploads/cam01 $ ls -l cam01-20240916-122803.jpg -rw-rw-r-- 1 www-data www-data 136544 9月 17 23:52 cam01-20240916-122803.jpg $