工作と競馬2

電子工作、プログラミング、木工といった工作の記録記事、競馬に関する考察記事を掲載するブログ

ESP32-WROVER-Eを使って、OV2640使用200万画素カメラを動かす

概要

ESP32-WROVER-Eを使って、OV2640使用200万画素カメラで撮ったUXGAの画像をクラウドストレージに送信できた。



背景と目的

以前、ESP-WROOM-32を使って、OV2640使用200万画素カメラを動かしたのだが、ESP-WROOM-32のRAM不足により最大解像度UXGAでの撮影がうまくいかなかった。そこで、RAMが増強されたESP32=WROVER-Eを使って、UXGAでの撮影を試す。



詳細

0. やること

UXGAで写真を撮影し、クラウド上にアップロードする。

1. ESP32-WROVER-E

ESP32-WROVER-Eは、こちらに書いた通り、ESP-WROOM-32にPSRAMと呼ばれる外付けのRAMが備わったもの。

2. 撮影

2.1 ピン設定で間違えてハマる

今回の撮影部分のプログラムは、以前の記事に挙げてあるesp_cameraのサンプルを少し修正したプログラムをそのまま使ったのだが、実はピン設定がおかしかったことに気づいたので修正。PWDN_GPIO_NUM、RESET_GPIO_NUMは接続なしとする必要があった。実は、この間違いに全く気付くまで、プログラムの途中でおかしな再起動が多発し、かなりの時間を費やしてしまった。再起動が起こったある箇所を修正すると、今度は影響がないはずの前方の箇所で再起動が発生するという不思議な現象が起きていた。結局、このあたりからヒントを得て、どうにか修正にたどり着いた。

// ピン設定
#define Y2_GPIO_NUM 32
#define Y3_GPIO_NUM 35
#define Y4_GPIO_NUM 34
#define Y5_GPIO_NUM 5
#define Y6_GPIO_NUM 39
#define Y7_GPIO_NUM 18
#define Y8_GPIO_NUM 36
#define Y9_GPIO_NUM 19
#define XCLK_GPIO_NUM 27
#define PCLK_GPIO_NUM 23
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 26
#define SIOD_GPIO_NUM 21
#define SIOC_GPIO_NUM 22
#define PWDN_GPIO_NUM -1 // 変更
#define RESET_GPIO_NUM -1 // 変更

2.2 カメラ設定

UXGAで撮るには、config.frame_sizeを変更。

  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  //                      for larger pre-allocated frame buffer.
  if (psramFound()) {
    Serial.println("PSRAM found.");
    config.frame_size = FRAMESIZE_UXGA; // UXGAを選択
    config.jpeg_quality = 5;
    config.fb_count = 1;
  } else {
   :

2.3 撮影部分

esp_camera_fb_get関数で実行するだけ。pixel_formatをPIXFORMAT_JPEG(サンプルそのまま)なので、fb->bufには、jpgのバイナリデータが格納される。

  // 撮影
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    return fb;
  }


3. アップロード

今回の送信先は、AWSのAPIGatewayのエンドポイント。JSONで受けて、バックエンドのLambdaでS3に保存する。なので、送信元はjpgバイナリデータをいったんbase64エンコードしてテキストに変換しJSONに格納する。(本当はapplication/octet-streamでバイナリを送った方がデータ量が少なくて済むが、クラウド側ができてしまっているので今回は不採用)

3.1 base64エンコード

extern "C" {
#include "libb64/cdecode.h"
#include "libb64/cencode.h"
}

以下のような関数を用意して呼び出して使う。

// base64エンコード
void base64_encode(uint8_t * packet, int packet_len, char * encoded) {
  
  base64_encodestate _state;
  base64_init_encodestate(&_state);
  int len = base64_encode_block((const char *) &packet[0], packet_len, &encoded[0], &_state);
  len = base64_encode_blockend((encoded + len), &_state);  

}

なお、呼び出し側で用意するencodedというエンコード結果格納用配列は、ps_callocを使ってPSRAM上にメモリ確保している。(またはps_malloc)UXGAのjpgバイナリは200kB程度があり、ESP32内部のRAMでは確保できないためだ。PSRAMがあると、できることが増えるというのを実感した。

  char * encoded = (char *) ps_calloc(size, sizeof(char));
  if (encoded == NULL) {
    Serial.printf("encoded=NULL!\n");
    while (1) {}
  }
  encoded[0] = '\0';
  Serial.printf("ps_calloc encoded at %x, size=%lu\n", &encoded[0], (unsigned long) size);

3.2 POST

実は、ここでもかなり悩まされてしまった。 ESP32でJSONをPOSTするために以前より自作して使用していた関数を今回も使ったところ、JSON BodyをWiFiClientSecure.printlnした直後に固まってしまった。いろいろ試行錯誤した結果、どうやらBodyのサイズが大きいと固まるらしく、Bodyを分割して複数回WiFiClientSecure->writeするとうまくいった。(POST POST自体を複数回行うという手もやってみたが、あまりに送信に時間がかかるのでボツ)

なので、以下のように、Bodyを分割してwriteするように関数を修正した。 なお、WiFiClientSecureのインスタンス化は、インスタンスをPSRAM上に確保するためにややこしい処理になっている。clientという変数は、WiFiClientSecureインスタンスではなくそのポインタ。そのせいで、各呼び出しが、client.***ではなく、client->になっている。(上述のBody送信後フリーズはが、WiFiClientSecureのインスタンスがRAM不足に起因すると思ってPSRAM上に確保してみたのだが、WiFiClientSecureのインスタンス自体はInternal RAMでも実は問題ない気もする。。。あとで余裕があれば試す。)

#include <WiFiClientSecure.h> // Wi-Fiに接続して、HTTPS通信するためのライブラリ

const int TIMEOUT_IN_MSEC = 60000; // タイムアウト[msec]

// POSTリクエストを送信する
// payload: 送信したいJSON Bodyのテキスト
void post_request(const char* host, const char* path, const char* payload) {

  // PSRAM上にインスタンスを確保するためのps_mallocとnew
  char * ptr = (char *) ps_malloc(sizeof(WiFiClientSecure));
  WiFiClientSecure* client = new (ptr) WiFiClientSecure;
  Serial.printf("client at %x\n", client);
  
  Serial.printf("Trying to connect to %s...\n", host);
  if (!client->connect(host, 443)) { // 接続にトライ
    // 接続失敗
    Serial.println("Connection failed!");
  } else {
    // 接続成功
    Serial.println("Connection success!");

    size_t contentLength = strlen(payload);
    Serial.printf("Content-Length: %d, Body=\"%c ... %c\"\n", contentLength, payload[0], payload[contentLength - 1]);
    
    // ヘッダ
    client->printf("POST %s HTTP/1.1\n", path); // POSTメソッド
    client->printf("Host: %s\n", host); // HTTP1.1で必須のヘッダ, アクセス先サーバーとしておく(そうしないとクロスドメインアクセスになる)
    client->println("Connection: close");
    client->println("Content-Type: application/json");
    client->printf("Content-Length: %d\n", contentLength);
    client->println(); // ヘッダの最後は空行
    
    // ボディ
    // 一度に大きなデータをprintfしたりwriteするとうまくいかない
    // mバイトに分割してwriteする
    int m = 1024;
    int i, j = 0;
    uint8_t * p = (uint8_t *) ps_malloc(m);
    while (true) {
      for (i = 0; i < m; i++) {
        p[i] = payload[i + j * m];
        if (i + j * m == contentLength - 1) break;
      }
      Serial.printf("j=%d, length=%d\n", j, i);
      client->write(p, m);
      client->flush();
      if (i + j * m == contentLength - 1) break;
      j++;
    }
    client->println();
    client->println(); // 空行を最後に

    // レスポンス待ち
    Serial.println("Waiting for reponse...");
    unsigned long curtime = millis();
    while (client->available() == 0) {
      if (millis() - curtime > TIMEOUT_IN_MSEC) {
        // タイムアウトのときは終了
        Serial.println(">>> Client Timeout !");
        client->stop();
        return;
      }
    }
    
    // レスポンス受信
    if (client->connected()) {
      while (client->available()) {
        char c = client->read();
        Serial.write(c); // 1文字ずつ出力
      }
      Serial.println("");
    }
    
    // 接続終了
    client->stop();
  }

}


4. 動作確認

無事、S3上に保存できた。



まとめと今後の課題

とにかく、ピン設定のミスと、Bodyの分割writeでハマってしまい非常に苦労した。ESP32-CAMのような出来上がっている基板を買えばもっとずっと楽だっただろう。とはいえ、ここまでたどり着けたので良しとする。


AWS IoT Device SDK v2でpublishするときのackCallbackとpacket idについて

概要

AWS IoT Device SDK v2でのpublishで、ackCallbackとpacket idについて旧版との違いがわかったのでメモした。



背景と目的

AWS IoT Device SDK v2を使う必要が出た。APIの仕様が旧バージョンから変わっているので、ちょっと動かして確認する。



詳細

0. 参考URL


1. インストール

pip install aws-iot-sdk

Windows PCでは、これだけでよかったが、他のあるLinux環境では、参考URLに書いてある通りwheelが取得できず、まず以下をやってからpipでのインストールが必要だった。

sudo apt-get update
sudo apt-get install cmake
sudo apt-get install libssl-dev


2. 動かして気づいたことをメモ

とりあえず、pubsubのサンプルを動かしてみた。

2.1 旧版のMQTTのClientオブジェクトに相当するもの

  • 旧: AWSIoTPythonSDK.MQTTLib.AWSIoTMQTTClient
  • v2: awscrt.mqtt.Connection

だいたい同じようなメソッドが並んでいる。

2.2 publishメソッドは非同期

  • 旧版: publish=同期、publishAsync=非同期
  • v2: publish=非同期、publishAsync=ない

awscrt.mqtt.Connectionのpublishメソッドには、旧版のpublishAsyncの引数ackCallbackに相当するものがない。旧版のackCallbackはどうやって実装すればいいのか?

2.3 ackCallbackに相当するもの

こちらにある通り、publishメソッドの戻り値タプルの1つ目concurrent.futures.Futureオブジェクトなので、concurrent.futures.Futureオブジェクトのadd_done_callbackメソッドで、ackCallbackを割り当てればよい。

def ackCallback(f):
    # PUBACK受信後の処理を記述
    :

# publish
future, packetId = mqtt_connection.publish(...)

# 戻り値futureにackCallbackを割り当て
future.add_done_callback(ackCallback)
:

2.4 packetIdは旧版と同じではない?

旧版では、publishAsyncの戻り値packetIdは、ackCallbackの引数midと対応していた。publishごとにpacketIdがインクリメントされるので、どのpublishのACKかということが、midで判断できた。

v2では、APIドキュメントを見る限りpublish戻り値タプル2個目packet_idに見えるので、サンプルコードに以下を追記して動かしてみたが、publishごとにpacket_idが変化してくれない。ずっと1のまま。

def ackCallback(f):
    # f.result()にpacket_idが入っている
    print(f.result())

future, packetId = mqtt_connection.publish(...)
print(packetId) # publishのpacket_id
future.add_done_callback(ackCallback)

旧版でできたのにv2でできないのは、おかしい気がする。何か考え方間違えているのか? 自分でpublish時に管理用のカウンタみたいなものを用意するしかないのか?



まとめと今後の課題

旧版とv2の違いがわかった。packet idがpublishごとにインクリメントされないのがいまいち解せない。


Raspberry Pi Picoを初起動

概要

Raspberry Pi Picoを動かした。



背景と目的

最近、Raspberry Pi Picoと名乗るマイコンボードが出た。基板サイズはArduino Nanoくらい。RAMやクロック周波数を見ると性能は全然こちらのほうが高そう。そして550円とかなり安いので、とりあえず試したくなって買ってしまった。(ESP32など同価格帯でもっと高性能なおもちゃは手元にあるのだが・・・) というわけで、ひとまず動かしてみる。



0.実施環境

  • Windows 10 PCを使用
  • MicroPythonを使用

1.調達

スイッチサイエンスで売ると書いてあったので入荷通知を待っていたのだが、通知があった数時間後に売り切れになっていた。これは入手に手間取りそうだと思っていたが、その数日後、秋月のネットショップを何気なく見たら、普通に売っていた。というわけで、秋月で購入。


2.マニュアル類を読む


3.実物を触る

3.1 MicroPython本体を書き込む

Raspberry Pi Picoには、標準で用意されたMicroPythonインタプリタ以外に、こちらのような独自拡張したMicroPythonを作って入れることもできるが、それは必要になったらおいおいやるとして、今回はとりあえず標準のMicroPythonを使用する。

https://www.raspberrypi.org/documentation/pico/getting-started/

まず、上記から、.uf2というRaspberry Pi Pico用MicroPython本体をダウンロードする。

次に、マニュアルに従い、BOOTSELボタンを押しながら、USBケーブルを接続すると、RPI-RP2というストレージが表示された。uf2ファイルをドラッグアンドドロップすると、ストレージが消える。これでMicroPython本体は書き込めたらしい。

Tera Termでシリアルポートを開くと以下のようになった。とりあえず、動きそう。

3.2 LEDを点灯させる

SDKのドキュメントを見ながら、基板に載っているLEDをON/OFFしてみた。 Tera Termで直接入力したところ、LEDがON/OFFできた。成功。 GPIOを直接触るには、machineモジュールのPinを使う。Pinの引数modeにOUT/INなどを設定して入出力を設定する。オープンドレインやALTという周期動作をするモードもあるらしい。SDKのドキュメントには詳しく描いてあるので、そのうち試す。

from machine import Pin

led = Pin(25, mode=Pin.OUT)

# 点灯
led.on()

# 消灯
led.off()

3.3 MicroPython IDE Thonny

ターミナルに直接入力していてもしょうがないので、IDEでコードを作成してpyファイルを実行させたい。 Raspberry Pi Pico Python SDKのドキュメントにあるMicroPython IDE Thonnyを使うことにした。

https://thonny.org/

上記URLから、Windows用をダウンロードしてインストール。

マニュアルに従って、インタプリタの設定を行う。

いよいよ、コードを書く。

実行ボタンを押したところ、1秒周期で点滅した。成功。


まとめと今後の課題

Raspberry Pi Picoを動かした。次は、すぐに何かしたいわけではないが、ADCあたりを使ってみるか。