kuroの覚え書き

96の個人的覚え書き

MAiX II DOCK

Amazonでなんか面白いもの売ってないかなと思って彷徨っていたらこれを見つけた。

wiki.sipeed.com

えーっとこれが600円?

中身。
KIOXIAの32GB microSDだけで元値超えてるんじゃない?
Linuxの走るM2スタイルのCPUボードに64MB DDR2メモリ、2Mピクセルのカメラ、SDカードスロット、1.3インチIPSモニタ、USB-OTG、無線LAN、3軸ジャイロ、マイク、スピーカーまでついている。

とりあえずそのままUSBをMacと繋いでみたが、何も起こらない。一応USBーシリアルの変換の応答はある。SDカードにOS入っているのかな?ということで挿入し、改めてUSB接続。
キタキタ。

��3����	-'�-�m�����)��Ӵ��)R��+��------run rc.preboot file-----
[?25lsetup console
[H[Jfbv - The Framebuffer Viewer
/home/res/logo.png
140 x 140
[H[J[?25h------run rc.modules file-----
------run rc.final file-----
Load mpp modules
insmod: can't insert '/lib/modules/4.9.118/videobuf2-core.ko': No such file or directory
insmod: can't insert '/lib/modules/4.9.118/videobuf2-memops.ko': No such file or directory
insmod: can't insert '/lib/modules/4.9.118/videobuf2-v4l2.ko': No such file or directory
load /etc/asound.conf ...
alsactl: set_control:1461: Cannot write control '2:0:0:codec trigger playback time value:0' : Operation not permitted
alsactl: set_control:1461: Cannot write control '2:0:0:codec trigger capture time value:0' : Operation not permitted
Starting app...
enable android usb
Initializing random number generator... done.
Starting network...
generate key
WARNING: Logging before InitGoogleLogging() is written to STDERR
I0101 00:00:06.624842   803 dup2SeldomUsedFd.c:20] [60D[60C<dup2SeldomUsedFdInit> gFdLock init
Successfully initialized wpa_supplicant
udhcpc: started, v1.27.2
udhcpc: sending discover
I0101 00:00:07.530875   803 mpi_sys.c:766] [60D[60C<AW_MPI_SYS_SetConf> kfctmpdir is [/tmp]
I0101 00:00:07.532697   803 mpi_sys.c:1195] [60D[60C<AW_MPI_SYS_Init> ISP init
I0101 00:00:07.532854   803 mpi_sys.c:1197] [60D[60C<AW_MPI_SYS_Init> ISP init done
I0101 00:00:07.536176   803 hwdisplay.c:83] [60D[60C<hw_display_init> [1;34m<hw_display_init:83> [90D[90C[0m
I0101 00:00:07.536359   803 hwdisplay.c:989] [60D[60C<hwd_init> [1;34m<hwd_init:989> [90D[90C[0m(hwd_init 989)
I0101 00:00:07.536538   803 hwdisplay.c:1044] [60D[60C<hwd_init> [1;34m<hwd_init:1044> [90D[90C[0mret[0][2,0]ch[2]lyl[0] init: enable[1], screenwin[0,0, 240x240], zorder[16], alpha[mode:0, value:255]
I0101 00:00:07.536736   803 alsa_interface.c:659] [60D[60C<alsaOpenMixer> open mixer:hw:0
I0101 00:00:07.606276   803 alsa_interface.c:721] [60D[60C<alsaOpenMixer> set player pa switch level 0
I0101 00:00:07.606596   803 alsa_interface.c:709] [60D[60C<alsaOpenMixer> set playback vol_val to value: 27
E0101 00:00:07.744445   803 video_buffer_manager.c:211] [60D[60C<VideoBufMgrCreate> Alloc 20 input frame buffers in list manager.
E0101 00:00:07.745197   803 VideoVirVi_Component.c:481] [60D[60C<VideoViSetViDevAttr> fps 20 nbufs 3
E0101 00:00:07.796672   803 video_buffer_manager.c:211] [60D[60C<VideoBufMgrCreate> Alloc 20 input frame buffers in list manager.
E0101 00:00:07.797082   803 VideoVirVi_Component.c:481] [60D[60C<VideoViSetViDevAttr> fps 20 nbufs 3
I0101 00:00:07.804659   803 hwdisplay.c:1244] [60D[60C<hwd_get_disp_type> [1;32m<hwd_get_disp_type:1244> [90D[90C[0mCurrent the  disp_type:0x1  tv_mode:0x0
I0101 00:00:07.804977   803 hwdisplay.c:1117] [60D[60C<hwd_layer_request_hlay> [1;34m<hwd_layer_request_hlay:1117> [90D[90C[0mhlay:0, zorder=0, cnt:2
I0101 00:00:07.805199   803 mpi_vo.c:1030] [60D[60C<AW_MPI_VO_SetVideoLayerAttr> ch[0]lyl[0]:dispRect changed, [0, 0, 320x240]->[0, 0, 240x240]
I0101 00:00:07.805321   803 hwdisplay.c:408] [60D[60C<hwd_layer_set_rect> [1;34m<hwd_layer_set_rect:408> [90D[90C[0mch[0]lyl[0]: screen_win[0,0, 240x240]
E0101 00:00:07.805822   803 vo.c:683] [60D[60C<vo_init> debuf create vo channel[0] success!
I0101 00:00:07.806197   889 Clock_Component.c:1109] [60D[60C<Clock_ComponentThread> ClockComp state[0x1]->Idle!
E0101 00:00:07.806368   803 vo.c:718] [60D[60C<vo_init> debuf create clock channel[0] success!
I0101 00:00:07.806529   889 cedarx_avs_counter.c:148] [60D[60C<avscounter_start> (f:avscounter_start, l:148) Avscounter status [pause]->[run], pauseDuration[0][0]ms
I0101 00:00:07.806711   803 hwdisplay.c:1117] [60D[60C<hwd_layer_request_hlay> [1;34m<hwd_layer_request_hlay:1117> [90D[90C[0mhlay:9, zorder=9, cnt:3
I0101 00:00:07.806860   803 mpi_vo.c:1030] [60D[60C<AW_MPI_VO_SetVideoLayerAttr> ch[2]lyl[1]:dispRect changed, [0, 0, 320x240]->[0, 0, 240x240]
I0101 00:00:07.806959   803 hwdisplay.c:408] [60D[60C<hwd_layer_set_rect> [1;34m<hwd_layer_set_rect:408> [90D[90C[0mch[2]lyl[1]: screen_win[0,0, 240x240]
I0101 00:00:07.807058   803 mpi_vo.c:1130] [60D[60C<AW_MPI_VO_SetVideoLayerAlpha> video layer alpha changed, [0, 128]->[0, 25]
E0101 00:00:07.808746   803 vo.c:454] [60D[60C<CreateVoUiLayer> create vo channel[0] success!
udhcpc: sending discover

〜中略〜

I0101 00:00:16.854119   890 mpi_vo.c:508] [60D[60C<VideoRenderEventHandler> KeyFrameDecoded, pts[0]us
E0101 00:00:16.854372   890 vo.c:286] [60D[60C<VoUiCallbackWrapper> debuf vo report rendering start
done


BusyBox v1.27.2 () built-in shell (ash)

------run profile file-----
 _   .-')      ('-.            ) (`-.
( '.( OO )_   ( OO ).-.         ( OO ).
 ,--.   ,--.) / . --. /  ,-.-')(_/.  \_)-.
 |   `.'   |  | \-.  \   |  |OO)\  `.'  /
 |         |.-'-'  |  |  |  |  \ \     /\
 |  |'.'|  | \| |_.'  |  |  |(_/  \   \ |
 |  |   |  |  |  .-.  | ,|  |_.' .'    \_)
 |  |   |  |  |  | |  |(_|  |   /  .'.  \
 `--'   `--'  `--' `--'  `--'  '--'   '--'
   __   _
  / /  (_)__  __ ____ __ ------------------------
 / /__/ / _ \/ // /\ \ /  sipeed.com (Neptune)
/____/_/_//_/\_,_//_\_\  ------------------------

root@sipeed:/# PaUnixThread_New: Waited for 0.00035175 seconds for stream to start

〜後略〜

うんLinuxだね。

エッジAI?
どう使うかちょっと調べて遊んでみよう。
ちなみに在庫2個だったんで2個とも確保。いま見てみたら普通は9600円で売っているらしい。9を入力し損ねたんかも。

M5Stack ESP32CAM-PSRAM

さらに気をよくしてM5Stack ESP32CAM-PSRAMも試してみる。こちらは入手したもののArduinoで全然使い方が分からずに放置していたのだが。
まず、このボードはESP32-WROVER。メモリはPSRAM,FLASHとも4MBと半分しかない。カメラはOV2640なんだけど魚眼がついている。(このカメラが曲者だった。)

というわけでボードとしてはESP32 Wrover Kitを選択することになるのだが、camera_pin.hでどのカメラユニットとして設定すればいいのかがどこにも書かれていない。
結局
m5-docs
このページのピン情報をcamera_pin.hと1個ずつ見比べてやったところ、どうやらCAMERA_MODEL_M5STACK_V2_PSRAMかもしくはCAMERA_MODEL_M5STACK_WIDEということになりそうだLEDピンがないので前者の方が近いのかな。

で、これらの設定で他のカメラと基本同じプログラムを書き込んでみたのだが、
E (1118) camera: Camera probe failed with error 0x105(ESP_ERR_NOT_FOUND)
Camera init failed with error 0x105
という表示がシリアルに出て、全く認識してくれない。なのでほかのカメラ設定を念の為手当たり次第に試してみたのだがどれも同じ反応。
試しにカメラユニットをOV2640の標準画角のレンズのものに付け替えてみたら、なんかあっけなくCAMERA_MODEL_M5STACK_V2_PSRAMで認識して、普通に画像が撮れた。
つまりついていた広角レンズのカメラユニットが壊れているっぽい。以前逆にXIAOの方にこのカメラをつけてみたときには普通に写っていたのでどっかのタイミングで断線してしまったか何かなのだろう。というのもこのユニットはレンズ部分が不釣り合いにデカくて、フレキケーブルだけで支えていることに結構不安があったのだ。まさに不安的中。カバンに入れて持ち歩いているうちにどっか接触が悪くなってしまったに違いない。要注意だわ。
追記:よくよくみてみるとフレキケーブルが端から2ミリほど切れていた。こりゃあかんわな。ちなみにこのカメラユニットのレンズはいわゆるM12マウントなのでレンズは他に流用できそうだけど、ちぎれたフレキケーブルとCCDはもったいないけど捨てるしかないな。

ま、とりあえずカメラ(ESP基板)は使えることがわかったからよしとしよう。しかし、XIAOの方が普通に使えそう、ということがわかったので、こっちをわざわざ使う意味は完全になくなったわけだ。特に広角レンズが死んでしまった今となっては。

M5 Timer Camera X再び

XIAO ESP32S3 senseの運用がうまくいったのに気をよくして、M5 Timer Camera Xの方も上手く使えないかと思い、もう一度引っ張り出してきた。
そして以前作っていたファームウェアを書き込もうとしたらどういうわけかエラーが出てコンパイルができない。
以前使ってみた時と変わっているのはMacintelMacbook AirからM1のMacbook Proになっている点だけど、この環境構築がうまくいっていないんだろうかということで構築し直してone by oneで確認してみる。

まずM5 Timer Camera Xを使うためにArduino IDEのboard managerでM5のライブラリを入れる。
お作法に従って、
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json


Preferenceにコピーしてやり、BOARDS MANAGERタブでM5と入れて検索し、出てくる”M5Stack by M5Stack”をインストールしてやる。

今回入ったバージョンは2.1.1であった。
次にTimer-CAMのライブラリをLIBRALY MANAGERタブでTimer-CAMで検索すると"Timer-CAM by M5Stack"が見つかる。

以前試してみていた時は最新バージョンが0.0.3だったけど最新バージョンのArduinoIDEでインストールできないというバグがあった。今回1.0.0とベータじゃなくなった感じのバージョンになっていて、インストールも問題がないようだった。

早速なんかプログラムを入れてみよう。
まずボードとしてM5Stack-Timer-CAMを選ぶ。

ここで気になったのだが、M5TimerCAMというハイフンの入っていない選択肢もあるということ。

こちらを選んでいいものかどうか、現時点では判断がつかない。M5Stackのオフィシャルページではそんなことは書かれていないのだが、これって以前にインストールしていた何かの残骸だったりするんだろうか。

とりあえずお約束のファイル>Examples>01.Basics>Blink

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second
}

このボードではLED_BUILTINはIO2とライブラリで定義されている。
無事LEDが1秒間隔で点滅した。とりあえずボードは認識できているね。
次にTimer-CAMライブラリで入ったexampleも試しておく。

#include "M5TimerCAM.h"

void setup() {
    TimerCAM.begin();
}

void loop() {
    for (int16_t i = 0; i < 255; i++) {
        TimerCAM.Power.setLed(i);
        vTaskDelay(pdMS_TO_TICKS(10));
    }

    for (int16_t i = 255; i >= 0; i--) {
        TimerCAM.Power.setLed(i);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

#include "M5TimerCAM.h"
でライブラリを呼び出している。
こちらも特に問題なくLEDがpwmで明暗を繰り返している。
環境構築には問題なさそうだな。じゃあどうして以前作ったプログラムはコンパイルできないんだろうね。

どうも
# include battery.h
# include bm8563.h
が使えないらしい。どうなっているんだろうか。

M5TimerCAM.h

#ifndef _M5_TIMER_CAM_H_
#define _M5_TIMER_CAM_H_

#include "./utility/Power_Class.h"
#include "./utility/RTC8563_Class.h"
#include "./utility/Camera_Class.h"
#include "esp_camera.h"

namespace m5 {
class M5TimerCAM {
   private:
    /* data */
   public:
    void begin(bool enableRTC = false);
    Power_Class Power;
    RTC8563_Class Rtc;
    Camera_Class Camera;
};
}  // namespace m5
extern m5::M5TimerCAM TimerCAM;

#endif

なるほどbattery.h,bm8563.hともになくなって、Power_Class.h、RTC8563_Class.hに変わったというわけか。
とりあえずこの2つはincludeせず#include "M5TimerCAM.h"だけ入れてやればいけそう。

#include "M5TimerCAM.h"
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "Base64.h"

const char *ssid     = "your_SSID";
const char *password = "SSID_passward";

const char* myDomain = "script.google.com";
String myScript = "/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec"; //Replace with your own url
String myFilename = "filename=M5Camera.jpg";
String mimeType = "&mimetype=image/jpeg";
String myImage = "&data=";

int waitingTime = 10; //Wait 10 seconds to google response.

int shootingIntervalSec = 600;   // 撮影間隔 (秒)

void setup() {
  Serial.begin(115200);
  delay(10);

  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);

  WiFi.mode(WIFI_STA);

  Serial.println("");
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);  

  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }

  Serial.println("");
  Serial.println("STAIP address: ");
  Serial.println(WiFi.localIP());

  Serial.println("");

  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 10000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_UXGA;  // UXGA|SXGA|XGA|SVGA|VGA|CIF|QVGA|HQVGA|QQVGA
  config.jpeg_quality = 5;
  config.fb_count     = 1;
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; //CAMERA_GRAB_LATEST;

  esp_err_t err = esp_camera_init(&config);

  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    delay(1000);
    ESP.restart();
  }

  sensor_t * s = esp_camera_sensor_get();
  //initial sensors are flipped vertically and colors are a bit saturated
  s->set_vflip(s, 1);//flip it back
  s->set_brightness(s, -1);//up the blightness just a bit OV3660をOV2640に近づけるセッティング
  s->set_saturation(s, 2);//lower the saturation OV3660をOV2640に近づけるセッティング
  s->set_denoise(s, 1);//OV3660をOV2640に近づけるセッティング
  s->set_contrast(s, 1);//OV3660をOV2640に近づけるセッティング
  //drop down frame size for higher initial frame rate
  // s->set_framesize(s, FRAMESIZE_SXGA);
  delay(1000);
}

void loop() {
  saveCapturedImage();
  Serial.printf("Waiting for %u sec.\n", shootingIntervalSec);
//  delay(shootingIntervalSec * 1000);    // 次の撮影まで待つ。スリープしない時。  
  Serial.printf("esp_sleep_enable_timer_wakeup: %d\n", esp_sleep_enable_timer_wakeup((uint64_t)shootingIntervalSec * 1000ULL * 1000ULL)); //単位はマイクロ秒  スリープする時
  esp_deep_sleep_start();// スリープする時
}

void saveCapturedImage() {
  Serial.println("Connect to " + String(myDomain));
  WiFiClientSecure client;

  client.setInsecure();
  if (client.connect(myDomain, 443)) {
    Serial.println("Connection successful");

    camera_fb_t * fb = esp_camera_fb_get(); //最初の1枚は捨てる
    esp_camera_fb_return(fb); //最初の1枚は捨てる
    fb = esp_camera_fb_get();  //こちらを保存する
    if(!fb) {
      Serial.println("Camera capture failed");
      delay(1000);
      ESP.restart();
      return;
    }
    Serial.printf("frame buffer size: %u x %u\n", fb->width, fb->height);
    
    Serial.println("Step 1: calicurating data size...");    // Base64とurlencodeされたデータのサイズを数える。

    int index = 0;
    uint8_t *p = fb->buf;
    int rest = fb->len;
    int base64EncodedSize = 0;
    int urlencodedSize = 0;
    while (rest > 0)
    {
      char output[2048 +1];    // 一度に出力するBase64化されたデータを入れるバッファ (base64_encode()が末尾にヌルを入れるので、1バイト追加。)
      int srcLen = rest > 1536 ? 1536 : rest;   // このサイクルでエンコードする元データサイズ(最大はバッファの 3/4 のサイズ)
      int encLen = base64_encode(output, (char *)p + index, srcLen);   // Base64エンコードする。
      base64EncodedSize += encLen;
      if (encLen > 0) {
        String str = urlencode(String(output));   // URLエンコードする。
        urlencodedSize += str.length();
      }
      index += srcLen;
      rest -= srcLen;
    }
    Serial.printf("frame buffer size: %u\n", fb->len);
    Serial.printf("after Base64 encoding: %u\n", base64EncodedSize);
    Serial.printf("frame buffer size: %u\n", urlencodedSize);

    Serial.println("Step 2: Sending a captured image to Google Drive.");
    String Data = myFilename + mimeType + myImage;    // POSTで送られるデータの先頭部分。これの後に画像をBase64化したものが続く。
    client.println("POST " + myScript + " HTTP/1.1");
    client.println("Host: " + String(myDomain));
    client.println("Content-Length: " + String(Data.length() + urlencodedSize));    // ここでデータの長さを書く必要があるので、Step 1 が必要。
    client.println("Content-Type: application/x-www-form-urlencoded");
    client.println();
    client.print(Data);

    index = 0;
    p = fb->buf;
    rest = fb->len;
    Serial.printf("Estimated cycle: %u\n", rest / 1536);
    while (rest > 0 && client.connected())
    {
      char output[2048 +1];    // 一度に出力するBase64化されたデータを入れるバッファ (base64_encode()が末尾にヌルを入れるので、1バイト追加。)
      int srcLen = rest > 1536 ? 1536 : rest;   // このサイクルでエンコードする元データサイズ(最大はバッファの 3/4 のサイズ)
      int encLen = base64_encode(output, (char *)p + index, srcLen);    // Base64エンコードする。
      if (encLen > 0) {
        String str = urlencode(String(output));   // URLエンコードする。
        client.write((uint8_t *)(str.c_str()), str.length());   // データを送信する。
        index += srcLen;
        rest -= srcLen;
        Serial.print(".");
      }
    }
    Serial.println();
    client.flush();

    // char *input = (char *)fb->buf;
    // char output[base64_enc_len(3)];
    // String imageFile = "";
    // for (int i=0;i<fb->len;i++) {
    //   base64_encode(output, (input++), 3);
    //   if (i%3==0) imageFile += urlencode(String(output));
    // }
    // String Data = myFilename+mimeType+myImage;

    esp_camera_fb_return(fb);

    Serial.println("Send a captured image to Google Drive.");

    // client.println("POST " + myScript + " HTTP/1.1");
    // client.println("Host: " + String(myDomain));
    // client.println("Content-Length: " + String(Data.length()+imageFile.length()));
    // client.println("Content-Type: application/x-www-form-urlencoded");
    // client.println();

    // client.print(Data);
    // int Index;
    // for (Index = 0; Index < imageFile.length(); Index = Index+1000) {
    //   client.print(imageFile.substring(Index, Index+1000));
    // }

    Serial.println("Waiting for response.");
    long int StartTime=millis();
    while (!client.available()) {
      Serial.print(".");
      delay(100);
      if ((StartTime+waitingTime * 1000) < millis()) {
        Serial.println();
        Serial.println("No response.");
        //If you have no response, maybe need a greater value of waitingTime
        break;
      }
    }
    Serial.println();   
    while (client.available()) {
      Serial.print(char(client.read()));
    }  
  } else {         
    Serial.println("Connected to " + String(myDomain) + " failed.");
  }
  client.stop();
}

//https://github.com/zenmanenergy/ESP8266-Arduino-Examples/
String urlencode(String str)
{
    String encodedString="";
    char c;
    char code0;
    char code1;
    char code2;
    for (int i =0; i < str.length(); i++){
      c=str.charAt(i);
      if (c == ' '){
        encodedString+= '+';
      } else if (isalnum(c)){
        encodedString+=c;
      } else{
        code1=(c & 0xf)+'0';
        if ((c & 0xf) >9){
            code1=(c & 0xf) - 10 + 'A';
        }
        c=(c>>4)&0xf;
        code0=c+'0';
        if (c > 9){
            code0=c - 10 + 'A';
        }
        code2='\0';
        encodedString+='%';
        encodedString+=code0;
        encodedString+=code1;
        //encodedString+=code2;
      }
      yield();
    }
    return encodedString;
}

とりあえず超適当な画像だが取得してGoogleDriveに保存まで成功。

XIAO ESP32S3 senseがネットワークに繋がりにくい件



ESP32でwifi接続できてOV2640のカメラを搭載した小さい基板であるXIAO ESP32S3 senseをたくさん購入してタイムラプスを撮りまくるはずだったのだが、この基板、どういうわけか無線LANへの接続がうまくいかないことが多い。多いというのは合計8台買ってみたのだが、スッキリとつながるのは2台だけで、残りは全然繋がらなかったり、つながったり繋がらなかったり、ととても不安定だった。
ペラっとしたアンテナが付属しており、このアンテナをコネクタに接続しないと基板自体にはアンテナパターンがついていないため、まず電波をキャッチできそうにない。なので、このコネクタの接触が悪いのか、そもそも基板の品質がいまいちなのか判断できずお蔵入りしかけていたのだけれど、最近有力な情報を知った。
XIAO ESP32S3がWiFiに繋がらなかったので調べてみたら、ハードの問題である可能性が高いことがわかった - 知的好奇心 for IoT
こちらのブログエントリーによるとどうやら無線LANの出力をあえて下げてやる必要があるらしい。
そんなことよく気がついたな、と言う感じだ。普通繋がらなかったら電波が弱いからできるだけ上げようと思うだろうに。
ESP32のWiFiライブラリのうちのWiFiGeneric.hに

typedef enum {
    WIFI_POWER_19_5dBm = 78,// 19.5dBm
    WIFI_POWER_19dBm = 76,// 19dBm
    WIFI_POWER_18_5dBm = 74,// 18.5dBm
    WIFI_POWER_17dBm = 68,// 17dBm
    WIFI_POWER_15dBm = 60,// 15dBm
    WIFI_POWER_13dBm = 52,// 13dBm
    WIFI_POWER_11dBm = 44,// 11dBm
    WIFI_POWER_8_5dBm = 34,// 8.5dBm
    WIFI_POWER_7dBm = 28,// 7dBm
    WIFI_POWER_5dBm = 20,// 5dBm
    WIFI_POWER_2dBm = 8,// 2dBm
    WIFI_POWER_MINUS_1dBm = -4// -1dBm
} wifi_power_t;

このように定義されているらしいのだが、先のエントリーによると
tx power > 70以上にすると繋がらなくかるらしい。
で、デフォルトはmax powerになっているからtx power = 78 (19.5 dBm)

というわけで

void connectToWiFi() {
  // Wi-Fiに接続する
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  WiFi.setTxPower(WIFI_POWER_13dBm);
  // Wi-Fiに接続待ち
  Serial.println("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connected");
  Serial.print("STAIP address: ");
  Serial.println(WiFi.localIP());
  Serial.println("");
}

という感じに明示的にWIFI出力を13 dBmくらいに抑えてやるとなんということでしょう、8台全部普通につながるようになったとさ。

秋月電子で売っている小さいケース(タカチ PB3-2-3)にピッタリ。発泡ウレタンボードでレンズボードを作成してはめ込むととても安定する。

他にもこの方のブログは有益で、ESP32のカメラでよく起こる変な色かぶり(緑っぽかったり赤っぽかったり、起動して最初に撮影したフレームがだいたいおかしい)の原因と対策だとかOV3660がOV2640 よりハイスペックなはずなのにESP32につなぐとなんだかボンヤリした画像になる件の対策だったりが紹介されていた。とても役に立ったので拡散しておく。

Slurmでノードのstateがdrainedになってしまうとき

サブシステムをクラスタ化しようとしてちょっとハマった。
メインシステムはほぼ同じスペックで各ノード256GBメモリを積んでおり、特に問題なくSlurmがインストールできて

NODELIST       NODES PARTITION       STATE CPUS    S:C:T MEMORY TMP_DISK WEIGHT AVAIL_FE REASON              
node3-a6      1     work*        idle 48     1:24:2 256000        0      1   (null) none                
node4-a6      1     work*        idle 48     1:24:2 256000        0      1   (null) none                
node5-a6      1     work*        idle 48     1:24:2 256000        0      1   (null) none

こんな感じ。
ところが寄せ集めのサブシステムではなんだかうまくいかず、

NODELIST       NODES PARTITION       STATE CPUS    S:C:T MEMORY TMP_DISK WEIGHT AVAIL_FE REASON              
node6-x2110      1     work*     drained 8       1:4:2  32000        0      1   (null) Low RealMemory      
node7-r620        1     work*        idle 32      2:8:2  96000        0      1   (null) none                

このようにLowRealMemoryというエラーがついてstateがdrainedとなってしまう。
問題のノードの実メモリ

$ cat /proc/meminfo 
MemTotal:       32583320 kB
MemFree:        31144288 kB
MemAvailable:   31471072 kB
Buffers:            4664 kB
Cached:           686380 kB
SwapCached:            0 kB
Active:           177208 kB
Inactive:         918404 kB
Active(anon):       2884 kB
Inactive(anon):   425188 kB
Active(file):     174324 kB
Inactive(file):   493216 kB
Unevictable:       25072 kB
Mlocked:           21984 kB
SwapTotal:       8241148 kB
SwapFree:        8241148 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:        411780 kB
Mapped:           235504 kB
Shmem:             19008 kB
KReclaimable:      53572 kB
Slab:             124156 kB
SReclaimable:      53572 kB
SUnreclaim:        70584 kB
KernelStack:        7296 kB
PageTables:        23696 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    24532808 kB
Committed_AS:    2863272 kB
VmallocTotal:   34359738367 kB
VmallocUsed:       84148 kB
VmallocChunk:          0 kB
Percpu:             5248 kB
HardwareCorrupted:     0 kB
AnonHugePages:    116736 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
FileHugePages:         0 kB
FilePmdMapped:         0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
DirectMap4k:      202340 kB
DirectMap2M:     6031360 kB
DirectMap1G:    27262976 kB

ということで32GBなので32000 (MB)として/etc/slurm/slurm.confで

# COMPUTE NODES
NodeName=node6-x2110 CPUs=8 Sockets=1 CoresPerSocket=4 ThreadsPerCore=2 RealMemory=32000 State=UNKNOWN
NodeName=node7-r620 CPUs=32 Sockets=2 CoresPerSocket=8 ThreadsPerCore=2 RealMemory=96000 State=UNKNOWN
PartitionName=work Nodes=ALL OverSubscribe=FORCE Default=YES MaxTime=INFINITE State=UP

というふうに設定したのだが。

slurmd -Cというコマンドでどういうふうに認識されているかを見る方法があるということなので

$ slurmd -C
NodeName=node6-x2110 CPUs=8 Boards=1 SocketsPerBoard=1 CoresPerSocket=4 ThreadsPerCore=2 RealMemory=31819
UpTime=0-00:53:14

おやおや?31819と認識されているよ?

ってことで実際に載せているメモリよりちょっと小さめに設定しておくのが吉らしい。
RealMemory=30000
としてやり、

$ sudo systemctl stop slurmctld
$ sudo systemctl stop slurmd
$ sudo systemctl start slurmd
$ sudo systemctl start slurmctld

さらに

$ sudo scontrol reconfigure
$ sudo scontrol update nodename=node6-x2110 state=resume

としてやってようやく

$ sinfo -N -l

NODELIST       NODES PARTITION       STATE CPUS    S:C:T MEMORY TMP_DISK WEIGHT AVAIL_FE REASON              
node6-x2110      1     work*     idle 8       1:4:2  30000        0      1   (null) none                
node7-r620       1     work*     idle 32      2:8:2  95000        0      1   (null) none 

無事使えるようになった。

ネットワークの冗長化

サーバとして売られているPCにはたいていネットワークコネクタ(NIC)が2つ以上ついている。
場合によっては2つのNICを使ってネットワークをブリッジさせる目的に使われるが、多くは冗長化によるネットワークの安定を図る目的で使われていると思う。
チーミングやボンディングと呼ばれる手法だな。これはあくまで片方のNICの具合が悪くなってももう片方で正常に通信できるようにしておくことが目的なので、ネットワークの帯域が広がったりはしない。そういう設定もあるらしいが。

$ sudo dnf install -y teamd

AlmaLinux8.9ではサーバとしてインストールしたら最初からインストールされている。

NICチーミングを設定するには、/etc/sysconfig/network-scripts/ディレクトリに設定ファイルを作成する。
例えば、ifcfg-team0という名前のファイルを作成する。

$ sudo nano /etc/sysconfig/network-scripts/ifcfg-team0

DEVICE=team0
DEVICETYPE=Team
ONBOOT=yes
TEAM_CONFIG='{"runner": {"name": "activebackup"}}'

そしてイーサネットケーブルを繋いでいたNICに割り当てていたIPをここに設定する。

IPADDR=10.0.1.10
PREFIX=24
GATEWAY=10.0.1.1
DNS1=8.8.8.8

など。

そしてもともとのNICの設定ファイルを開く。

$ sudo nano /etc/sysconfig/network-scripts/ifcfg-<NIC1>

DEVICE=<NIC1>
ONBOOT=yes
TEAM_MASTER=team0
DEVICETYPE=TeamPort

もともと書かれていた設定とかぶるところだけ書き換えで、その他はそのまま残しておく

NICのUUIDをもとにHDDをマウントしたりすることがあるので、team0にUUIDをつけておくとよい。

$ sudo uuidgen

これで生成された文字列を

$ sudo nano /etc/sysconfig/network-scripts/ifcfg-team0

UUID= <文字列>

と足しておく。
これで再起動すればOK。

Slurmの計算ノードのSTATEがdownになったとき

Slurmを運用していて、原因不明で計算ノードが動かなくなった。
sinfoで確認すると

# sinfo
PARTITION AVAIL  TIMELIMIT  NODES  STATE NODELIST
work*        up   infinite      1   idle kk3
work*        up   infinite      2   down kk4,kk5

みたいな感じ。
それぞれのノードでslurmdのステータスを確認しても

# systemctl status slurmd
● slurmd.service - Slurm node daemon
   Loaded: loaded (/usr/lib/systemd/system/slurmd.service; enabled; vendor pres>
   Active: active (running) since Sun 2024-01-07 11:33:23 JST; 1 weeks 1 days a>
 Main PID: 2023 (slurmd)
    Tasks: 1
   Memory: 5.3M
   CGroup: /system.slice/slurmd.service
           └─2023 /usr/sbin/slurmd -D

のようにactiveだ、と言われる。


対処法。
管理ノード(slurmctldの動いているノード)で

$ sudo scontrol update nodename=kk4 state=resume
$ sudo scontrol update nodename=kk5 state=resume

とする。

bowtie2-RSEM

bowtie2によるローカルアライメントを使い、Poly-Tプライマーで作成したcDNAライブラリのRNA-seq解析を行う。

まずはbowtie2をインストールする。

$ git clone https://github.com/BenLangmead/bowtie2.git
$ cd bowtie2/
$ make
$ sudo make install

次にリファレンスのインデックスを作成する。

$ cd PATH/TO/reference
$ mkdir target_ref_bowtie2index
$ bowtie2-build --threads 40 ./genome_assembly.fasta ./target_ref_bowtie2index/genome_assembly

ゲノムのサイズにもよるがマルチスレッドを使ってもかなりの時間を要する。


ローカルアライメントオプションを入れてマッピングしてみる

bowtie2 -p 40 -x [リファレンスゲノムのインデックスへのパス] -U [RNA-seqデータのファイル.fastq.gz] --local -k 1 -S [出力SAMファイル名.sam] 2> mapping_report.txt

標準ではエラー出力にしかレポートが出力されないため、バッチ処理等で大量にまとめて処理するとレポートが取れない。なので2>でファイルにリダイレクトしている。
また-k 1オプションにより、1つのリードが複数箇所にマッピングされるときは最良の1箇所だけを残す選択をしている。(これは必須ではない)

このあと生成したSAMをsamtoolsでBAMに変換、ソート、インデックス作成までは問題なく実施できた。
これをRSEMで発現解析させたいのだが、まだ上手く行っていない。

追記
どうもうまく行かない。
結局bowtie2で前もってゲノムにマッピングしておいて、それをRSEMで作成したtranscriptデータベースを使って発現解析をするという手順に問題がありそうだ。最初からbowtie2のtranscriptインデックスを作成してそちらにマッピング〜発現解析までをRSEMで一括して行うのが良さそう。

まずRSEMでtranscriptデータベースを作成する。

$ rsem-prepare-reference -p 40 --bowtie2 --bowtie2-path /usr/local/bin --gff3 genes.gff3 genome.fa rsem_db/transcript_db

ポイントとしては--bowtie2-pathはあくまでbowtie2のあるディレクトリのパスであり、bowtie2まで入れないこと。このコマンドでRSEM用の発現解析用データベースと一緒にbowtie2のマッピング用インデックスも生成される。

そしてbowtie2でのマッピングもRSEMの中でやってしまう。

$ rem-calculate-expression -p 40 --bowtie2 --bowtie2-path /usr/local/bin   input_reads_1.fastq.gz  --bowtie2-options "--local"  rsem_db/transcript_db output_prefix

と思ったが、RSEMでは--localオプションを扱えないらしい。上のようなオプションの付け方は無効だ。そのままend-to-endで解析すると結果としてマップ率が非常に低くなってしまうため、トリミングの時点で3’末端を削っておく必要がある。(以下の例では50base削った)

fastp --trim_tail1 50 -w 40 -i  input_reads_1.fastq.gz -o  input_reads_1_trim50.fastq.gz -q 20 -l 20 -j input_reads_1_trim_report50.json -h input_reads_1_trim_report50.html

そのうえで、

$ rsem-calculate-expression -p 40 --bowtie2 --bowtie2-path /usr/local/bin input_reads_1_trim50.fastq.gz  rsem_db/transcript_db output_prefix 2> mapping_report.txt

レポートのリダイレクトも有効だ。

マッピングレートは--localオプションを付けると90%以上であったが、トリミングしてのend-to-endアライメントではせいぜい60%くらいにしかならなかった。

さて、これでとりあえずリードカウントができることがわかったが、少しまだ問題が残る。
上記パイプラインをコマンドラインで順番に処理する上では問題がないのだが、クラスタサーバでslurmを使ったジョブスケジューリングで自動運転した場合、RSEM内部で呼び出しているsamtoolsがうまく見つけてもらえない。bowtie2は明示的にパスを与えているのだが、samtoolsにはそのオプションがなく、処理が止まってしまうようだ。

なので、やはりbowtie2によるマッピングはRSEM内部で処理せず、個別に実施した上で、RSEMではリードカウントのみ実施してもらうのが良さそうだ。

そこで、

$ bowtie2 -p 40 -x rsem_db/transcript_db -U input_reads_1_trim50.fastq.gz -S output_prefix.sam 2> output_prefix_mapping_report.txt
$ samtools view -@ 40 -bS output_prefix.sam > output_prefix.bam
$ samtools sort-@ 40 output_prefix.bam -o output_prefix_sort.bam
$ samtools index output_prefix_sort.bam
$ rsem-calculate-expression -p 40 --alignments output_prefix_sort.bam rsem_db/transcript_db output_prefix

というパイプラインにしてみたのだが、
どういうわけか

rsem-parse-alignments rsem_db/transcript_db output_prefix.temp/output_prefix output_prefix.stat/output_prefix output_prefix.transcript_sort.bam 1 -tag XM
Read LH00220:78:22H5YFLT3:7:1284:39149:5518: RSEM currently does not support gapped alignments, sorry!

"rsem-parse-alignments rsem_db/transcript_db output_prefix.temp/output_prefix output_prefix.stat/output_prefix output_prefix.transcript_sort.bam 1 -tag XM" failed! Plase check if you provide correct parameters/options for the pipeline!

というエラーが出る。どうもbowtie2は厳密にギャップアライメントを排除することが難しく、素のbowtie2のデフォルトオプションとRSEM内部でやっているオプションが違うらしい。
RSEMのヘルプの‐‐bowtie2オプションの説明にヒントが有った。

--bowtie2
        Use Bowtie 2 instead of Bowtie to align reads. Since currently RSEM
        does not handle indel, local and discordant alignments, the Bowtie2
        parameters are set in a way to avoid those alignments. In
        particular, we use options '--sensitive --dpad 0 --gbar 99999999
        --mp 1,1 --np 1 --score-min L,0,-0.1' by default. The last parameter
        of '--score-min', '-0.1', is the negative of maximum mismatch rate.
        This rate can be set by option '--bowtie2-mismatch-rate'. If reads
        are paired-end, we additionally use options '--no-mixed' and
        '--no-discordant'. (Default: off)

つまり

--sensitive --dpad 0 --gbar 99999999 --mp 1,1 --np 1 --score-min L,0,-0.1

というオプションがデフォルトで入っているらしい。
なので上のパイプラインの1行目を

$ bowtie2 --sensitive --dpad 0 --gbar 99999999 --mp 1,1 --np 1 --score-min L,0,-0.1 -p 40 -x rsem_db/transcript_db -U input_reads_1_trim50.fastq.gz -S output_prefix.sam 2> output_prefix_mapping_report.txt

としてやることでRSEM内部での動作と同等にしてやることが可能となった。

今後ローカルアライメントを受け付けるリードカウントについては検討の余地があると思う。そもそもbowtie2を使いたかったのもローカルアライメントが使えるからであり、それが使えないならSTARでマッピングしても同じようなものだから。bowtie2を内部で実施しないなら、そもそもRSEMを使う必然性も低く、これにかわるリードカウント方法を考えてもいいように思う。RSEMのカウントが比較的正確だ、というのもあるけど。

Flask2.0.0(3.0.0)のBlueprintの使い方

Flask1系から2系にアップデートしたとき、blueprintの使い方がちょっと変わっていてトラブったので覚書。


manage.py   ## 2系以降は不要
sqlite.db
app----------+
             |-__init__.py
             |-config.py
             |-models.py
             |-views---------+
             |               |-home.py
             |               |-tools.py
             |
             |-templates-----+
             |               |-index.html
             |               |.........
             |-static           

こういう構成だとして
1系のときは
__init__.py

from flask import Flask

app = Flask(__name__)
app.config.from_object('app.config')

中略

#Blueprint
from app.views import home, tools
                    
app.register_blueprint(home.app)
app.register_blueprint(tools.app)

app/home.py

from functools import wraps
from flask import request, redirect, url_for, render_template, flash, g, Blueprint
from flask import session as fl_session
from ..models import User
from app import app, Session
import sys
sys.setrecursionlimit(100000)

app = Blueprint('home', __name__)

################################################################################
def login_required(f):
	@wraps(f)
	def decorated_view(*args, **kwargs):
		if g.user is None:
			return redirect(url_for('login', next=request.path))
		return f(*args, **kwargs)
	return decorated_view

@app.before_request
def load_user():
	user_id = fl_session.get('user_id')
	if user_id is None:
		g.user = None
	else:
		g.user = Session().query(User).get(fl_session['user_id'])

@app.errorhandler(404)
def page_not_found(e):
	return render_template('404.html'), 404

@app.errorhandler(500)
def internal_server_error(e):
	return render_template('500.html'), 500

##############################################################################
#アプリルート
@app.route('/')
def index():
	return render_template('index.html')
##############################################################################
#ログイン・アウト管理
@app.route('/login', methods=['GET', 'POST'])
def login():
	if request.method == 'POST':
		user, authenticated = User.authenticate(Session().query, request.form['email'], request.form['password'])
		if authenticated:
			fl_session['user_id'] = user.id
			flash('You were logged in')
			return redirect(url_for('index'))
		else:
			flash('Invalid email or password')
	return render_template('login.html')

@app.route('/logout')
def logout():
	fl_session.pop('user_id', None)
	flash('You were logged out')
	return redirect(url_for('index'))

このような感じであったが、
2系以降では

__init__.py

from flask import Flask

app = Flask(__name__)
app.config.from_object('app.config')

中略

#Blueprint
from app.views.home import home_bp
from app.views.tools import tools_bp

app.register_blueprint(home_bp)
app.register_blueprint(tools_bp)

home.py

from functools import wraps
from flask import request, redirect, url_for, render_template, flash, g, Blueprint
from flask import session as fl_session
from ..models import User
from app import app, Session
import sys
sys.setrecursionlimit(100000)

home_bp = Blueprint('home', __name__)

################################################################################
def login_required(f):
	@wraps(f)
	def decorated_view(*args, **kwargs):
		if g.user is None:
			return redirect(url_for('home.login', next=request.path))
		return f(*args, **kwargs)
	return decorated_view

@home_bp.before_request
def load_user():
	user_id = fl_session.get('user_id')
	if user_id is None:
		g.user = None
	else:
		g.user = Session().query(User).get(fl_session['user_id'])

@home_bp.errorhandler(404)
def page_not_found(e):
	return render_template('404.html'), 404

@home_bp.errorhandler(500)
def internal_server_error(e):
	return render_template('500.html'), 500

##############################################################################
#アプリルート
@home_bp.route('/')
def index():
	return render_template('index.html')
##############################################################################
#ログイン・アウト管理
@home_bp.route('/login', methods=['GET', 'POST'])
def login():
	if request.method == 'POST':
		user, authenticated = User.authenticate(Session().query, request.form['email'], request.form['password'])
		if authenticated:
			fl_session['user_id'] = user.id
			flash('You were logged in')
			return redirect(url_for('home.index'))
		else:
			flash('Invalid email or password')
	return render_template('login.html')

@home_bp.route('/logout')
def logout():
	fl_session.pop('user_id', None)
	flash('You were logged out')
	return redirect(url_for('home.index'))

このようになる。
一度書き換えてしまえばこの方が分類がはっきりしてわかりやすい書き方になっていると思う。1系の書き方ではappがあっちにもこっちにも出てきて繋がりが分かりにくかったので。

1系の書き方のままでも2系の大方の動作には問題がないのだが、Loginまわりが軒並み動かなくなるので、書き換えたほうがいいと思う。おそらくurl_forの書き方が変わっているのが原因だと思う。

なお2系ではFlask_scriptによる python manage.py runserverによる起動はできないので、manage.pyは使わない。flask runコマンドで起動する。

Flaskアプリでbotのアクセスを受けたときに拒否する

Flaskのアクセスログを見ていると半分以上がbotのアクセスであった。さらに、過去に生成して、時間経過のために消去した一時ファイルを参照していたりするので、ことごとくエラーになり、無駄な処理をさせられまくっていたので特にしつこくアクセスしてくるIPは一括でブロックすることにした。

from flask import Flask, request, abort

app = Flask(__name__)

# ブロックしたいIPアドレスのプレフィックスを指定
BLOCKED_IP_PREFIXES = ['85.208.96.', '185.191.171.']

@app.before_request
def block_ip():
    # アクセス元IPアドレスを取得
    remote_ip = request.remote_addr

    # ブロックされたIPアドレスの場合は403 Forbiddenを返す
    for prefix in BLOCKED_IP_PREFIXES:
        if remote_ip.startswith(prefix):
            abort(403)

# 通常のルート
@app.route('/')
def index():
    return 'Welcome to the site!'

if __name__ == '__main__':
    app.run(debug=True)

こんな感じ。
とりあえず403をすぐ打ち返し、スクリプトに入らないようにした。これで多少はトラフィックが減ってくれるといいのだが。でもこれはきっといたちごっこになるんだろうな。