[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 サーバーのお話については。以下の記事を参考にしていただけたら幸いだ。

さくらの VPS の Ubuntu 24.04 + nginx で HTTPS ファイル転送システムを作ってみた

コード

環境に応じて修正する必要のある個所を 青字赤字 で記した。

青字: 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
$




コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください