2025年12月29日月曜日

ESP32-P4を使ってみた。

 ESP32-P4っていうESP32シリーズでも高性能なやつを見つけたので試しにポチってみた。WiFiがついてないけど400MHzデュアルコアと高性能っぽい。MIPIポートが付いていてカメラとディスプレイが使えたりH.264エンコーダに対応していたりと色々できそう。

WiFiモジュールがついたり、LANポートが付いたりしたモデルが出ているけど今回はとりあえずネットワークなしのESP32-P4-Picoにしてみた。WiFi付きはESP32-C6-MINI-1が乗ってるっぽくて、ESP32-C6-MINI-1自体は技適取ってるっぽい?(007-AN0136)

性能的にはカメラとかディスプレイを使ったようなプロジェクトを使うのに良さそうな感じで、M5Stack Tab5とかESP32-P4-EYEみたいなディスプレイやカメラを使った開発ボードが出ているみたい。ESP32-CAMよりだいぶ性能が良さそうなのでリアルタイムストリーミングとかもでより高FPSでできそうな気がする。

USB Type Cで接続するとUSB-Enhanced-SERIAL CH343として認識した。ドライバはWindows 11では勝手にインストールされた。

とりあえずArduino IDEが対応していたので前にESP32-C6で動かしたCANのスケッチを動かしてみた。ターゲットをESP32-P4に変更するだけでそのまま使えてしまった。ArduinoだとClockは360MHzが最大っぽい?(ESP32 Arduino Core3.3.1使用)ESP32-P4はTWAIコントローラを3つ積んでいるらしいので3ポート同時にCAN通信ができそう。

LEDは電源LEDしかついていないみたい。スピーカー用にES8311コーデックICが付いてるし、マイクもオンボードでついているみたい。カメラのコネクタはラズパイカメラがそのまま繋がるっぽいし。USBホストとかスピーカーを使う場合は別途ケーブルが必要みたいなんだけど、今回は付属してないバージョンを購入してみたのでコネクタを探してみようかな。

2025年12月28日日曜日

Xiaomi Redmi Note 5にAndroid 15を入れてみた。

 しばらく放置していたXiaomi Redmi Note 5だけども、カスタムROMでまだアップデートが続いてるやつがあるっぽいのでインストールしてみることに。新しいOSを入れるのにRecoveryも新しいものを入れる必要があった。

色々調べてみるとProject MatrixxというカスタムROMだとAndroid 15とAndroid 16のROMがwhyred用に公開されている模様。他にも何種類かあったんだけどまずは試しにMatrixxのAndroid 15を試してみることに。

RecoveryはTWRP 3.5.2_9-0が入っていたんだけども、そのまま新しいROMを焼こうとしても"this recovery does not support retrofit dynamic partitions"というエラーがでて書き込めなかった。whyredはA-onlyのようで、A/Bシステムアップデート対応のように使えるようなRetrofit Dynamic Partitionsに対応したRecoveryが必要っぽい。

whyredに対応したRetrofit Dynamic Partitions対応のRecoveryは何種類か存在しているようだったけど、今回はPitchBlack Recovery ProjectのRetrofit Dynamic Partitionsに対応した非公式ビルドをダウンロードして使ってみた。

PBRP-whyred-4.0-20250601-2009-UNOFFICIAL-Erofs.zipをダウンロードしてきて、TWRPのMTPか、microSD経由でTWRPからzipをインストール。再起動してもう一回リカバリに入るとPitchBlack Recovery Projectが起動する。TWRPでUser[0]のパスワードを入れるように入れるように言われたけどとりあえず無視して書き込んだ(どうせOSを消すので…

あとはWipeのformatでyesと入力して実行するとすべてのOSデータがまっさらの状態に。

暗号化された状態になっていたので、再起動してもう一度PitchBlack Recovery Projectに入り直す。

MTPでMatrixx-v11.9.0-Official-whyred-Vanilla-202510191031.zipを転送してInstallしてみた。Installが終わったら再起動せずに戻って、NikGapps-core-arm64-15-20250716-signed.zipを追加でインストール。再起動してしまったらMatrixxのインストールからやり直さないと…(一回ミスった

ベースがVanillaだからかすごくサクサク動くー

これならまだ使えそうな気がするぞ。

2025年12月27日土曜日

AirPods Proの充電ケースのカバーが取れたので接着してみた。

 知り合いのAirPods Pro(第1世代)の充電ケースの蓋の接着が外れてしまったということで修理してみた。

こんな感じでバラバラになってしまったらしい…
6年ぐらい使ったらしいので接着剤が劣化してしまったのかな。

とりあえず接着してみることに。

接着剤はセメダインのスーパーXにしてみた。ケースの材質がわからないけどポリカーボネートっぽいなと言うことで。今回黒を持っていたので黒を使ったけど透明のほうが良いかも?ミスって余計なところについても目立たないし。

接着する順番も重要そうで、まずはケースのトップカバーから貼り付けることに。薄い磁力遮断プレートがインナーカバーのマグネットに張り付いてしまうので、これもちゃんとトップカバー側に接着しておく。

元あった接着剤は少し厚みがあって、これを剥がしてしまうと位置がズレたりしたら嫌なのでそのまま上から塗ってみた。コの字になるように塗られていた。
この接着剤は固まる前に位置の調整ができるのでこうやって蓋を閉じてズレがないか確認しておく。真ん中になるように接着すれば大丈夫そう。クランプするために養生テープを張っておいた。
最後に位置がずれないように慎重に開いてクランプで止めて1日放置。あまり強いクランプを使うと変形してしまうかもしれないので程よいやつがいいかも。
完全硬化したらお次はインナーカバーの方にも同じように元あったところに接着剤を塗って固定。こちらは位置決めが簡単そうなので一旦古い接着剤を剥がしてしまって同じところに塗ってみた。
クランプでいろいろ試してみたんだけど押しすぎたりと難しかったので結局タイラップで固定した。これでまた1日放置。

これでしばらく持つといいなぁ。あとから気がついたけど充電ケースって互換品も売ってるんだね。

2025年12月21日日曜日

Arduino IDEのDEADLINE_EXCEEDEDエラー対策

 Arduino IDE 2.3.7を入れた状態で、ESP32 Arduino Core 3.3.4をボードマネージャーからインストールしようとしたらDEADLINE_EXCEEDEDエラーが出てインストールできなくなってしまっていた。

Error: 4 DEADLINE_EXCEEDED: net/http: request canceled (Client.Timeout or context cancellation while reading body)
他のボードはインストールできるのになぜかESP32 Arduino Coreだけインストールできない。
DEADLINE_EXCEEDEDはどうやらタイムアウトっぽいので、Arduino IDEからみてインストールに時間がかかりすぎていて止まってるように見えてしまっているのかな?

ということでタイムアウトを伸ばせないか調べてみた。

調べてみるとどうやらarduino-cliのnetwork設定っぽくて、Linuxの設定方法ばっかり出てきたんだけど、ライブラリの方で問題が起きている人がいたっぽくてそこの設定がそのまま使えそうなのでやってみた。

C:\Users\<username>\.arduinoIDE\arduino-cli.yamlの中に設定が入っているようで、(<username>は自分がログオンしているWindowsのユーザー名)このファイルを編集していく。普通にCドライブから開いていっても隠しファイルとかじゃないので表示できる。

Arduino IDEを全部閉じた状態で、上のarduino-cli.yamlをメモ帳とかで開いて一番下の方に

network:
  connection_timeout: 300s

と入れて保存。

あとはArduino IDEをもう一度開いてインストールしてみると…

普通にインストールできるようになっていた。Windows 11でArduino IDEでDEADLINE_EXCEEDEDエラーが出ている人は試してみると良いかも。

2025年12月20日土曜日

drinkmate 620の炭酸ガス漏れ修理 その後

 この前ゴム部品を反対につけて治ったと思っていたdrinkmate 620なんだけど、やっぱり反対側が裂けているのでちょっと使っていたらまた少しずつ漏れるようになってきてしまった…
2週間しか持たなかった…

というわけで代わりにOリングを突っ込んでみようと思ってOリングを購入してみた。

EA423RB-3
P-3のOリングがサイズ的に良さそう。ということでAmazonでポチってみたんだけどエスコのOリングが10個で89円って値段間違ってない?

早速drinkmateのノズルのゴムパーツを外して代わりにOリングを突っ込んでみた。高さ的には3個必要かな?とおもったけど2個で大丈夫そう。

これでOリングを2個つけた状態で問題なく漏れなかった。周りのゴムはボロボロだけどあんまり関係ない模様。漏れを防止しているのはセンターの小さいゴム部品だけだったのね。

とりあえずこれでもれなくなったし、汎用部品になったのでまた壊れても安心だね。

2025年12月14日日曜日

USB-PD電源モジュールを使ってみた。

 USB PDトリガーデバイスの中でもスイッチで簡単に切り替えられるやつを見つけたので使ってみた。固定のほうが安全なパターンもあるけど、テストのときは切り替えられたほうが便利だし。

表面にはスイッチが3つついていてこれの組み合わせで電圧を変更できる。
裏側にはLEDがついている。PDSink (303PDSink01)っていうのがデバイス名なのかな。MRA193Aっても書いてあるけど。
そんでもってUSB PD+BC シンクコントローラのCH224Kが乗っている。

デフォルト設定だと全部がのスイッチがオフなので5Vが出力される。

5V: OFF-OFF-OFF
9V:ON-ON-ON
12V:ON-ON-OFF
15V:ON-OFF-OFF
20V:ON-OFF-ON

という感じで設定できるっぽい。USB PD充電器によっては12V対応していないのがあるのでその場合は12Vに設定しても9Vになってしまう。
電源を入れたままスイッチをいじるとリアルタイムで変更される。

端子台とスイッチがついてると実験用には地味に便利だなぁ。

ちなみにデバイスを接続する前には一旦電圧をテスターチェックをしたほうが良いと思う。

2025年12月7日日曜日

drinkmate 620の炭酸ガス漏れ修理

 drinkmate 620を2年半ぐらい使っているわけだけども、どうもボタンを押してから"キューッ"っていう音がするのが気になったので調べてみた。自動でガスが少し漏れるのでシューっていうのは元々していたんだけどもキューっていうゴム風船の空気を抜くような音は今までしていなかった。

炭酸ガス漏れというとボンベの方のパッキンの情報は多くて交換用が売っているんだけども、今回は炭酸水を作るときにだけ漏れてるので、インフューザーとヘッドの間から漏れていそうな。

ヘッドのところを下から覗いて見るとゴム部品がボロボロに…

回りのところはまぁ良いとして、先端のノズルについているゴム部品は先端が真っ二つに割れているのでここからガスが漏れているんじゃないかと…

先端のゴム部品を慎重に引き抜いてみるとこんな感じ。

形状がシンメトリなので先端が裂けてない方と入れ替えればいいんじゃね?って言うことで反対に入れてみた。

これだけで治ってしまった…

この部品だけ売って欲しいなぁ

小さいOリングを団子にするか、ゴムチューブを加工して作るか?インフューザーが入るところは5mmぐらい、一番太いところで5.8mmで、ノズルは3.2mmぐらい。長さは5mmぐらいなのでもしかしてグロメットとか加工して作れるかも?

ガスが減ってきていたので購入するか悩んでいたが、これでしばらくは使えそうなのでガスを買いに行こうかな。

2025年11月22日土曜日

KZ ZSN PRO 2を買ってみた。

 アリエクの11月11日セールでKZ ZSN PRO 2が安くなっていたので購入してみた。今までKZ ZNAを使っていたんだけども左右の音量に差が出てるような気がしたので…(他のイヤホンから付け替えたときに違和感があって気がついた)

KZ ZSN PRO 2は昔使っていたKZ ZSN PRO Xと同じ構成の進化系っぽいのでいいかなと。

箱が小さくなってる気がする。送料を抑えるための工夫かな?

スペックは裏側に書いてある。今回はマイク無しで1,390円だった。Amazonの半額以下とだいぶお安くなっている。
青いラインが入っているBlue-Blackにしてみた。ケーブルはZNAと同じ感じ。
イヤーピースはこんな感じ。
ケーブルに装着するときはいつも迷う。ミスると位相が逆になるので注意。

音質的にはZNAよりも女性ボーカルが出てくる感じで良い感じ。ZNAから切り替えると高音がすごい刺さってくる感じがあるけどこれはそのうち良くなるかな。

2025年11月9日日曜日

2208モータをセンサ付きFOC制御で回してみた。

 前回はSimpleFOCでOpenloopで回したんだけど、モータドライバ用アダプタを3Dプリントしてもらったのでセンサ付きFOCを試してみた。Openloopで回しただけなのにSimpleFOCで回したというタイトルはまずかったかな…

アダプタは今回もOnshapeで設計したのでフリープランなのでMars Power 2208とかで検索すれば出てくるかも。MT6701に付属のΦ4で2mm厚のマグネットをモータの軸に接着してMT6701と磁石の間が1.7mmぐらいになる感じにしておいた。

まずはアダプタをモータに装着して、その上から基板を装着する感じ。

他のモータ用に作った基板なので配線が外に出てしまったり大きさが微妙だったりするけど、そもそもこのモータの直径まで小さくできるかなぁ?

スケッチはこんな感じでとりあえずSimpleFOCで前回同様にキャリブレーションを行った。

#include "SPI.h"
#include "SimpleFOC.h"
#include "SimpleFOCDrivers.h"
#include "encoders/MT6701/MagneticSensorMT6701SSI.h"

#define MOTOR_POLE 7

#define CS1 PA15
#define SCK1 PB3
#define MISO1 PB4
#define MOSI1 PB5
#define DRV_EN PB12
#define TX3_SCL2 PB10
#define RX3_SDA2 PB11
#define CAN_RX PB8
#define CAN_TX PB9
#define V_CURR PA0
#define W_CURR PA1
#define U_CURR PA2
#define TEMP PA3
#define VOLTAGE PA4
#define LED_BUILTIN PC13


HardwareSerial Serial3(TX3_SCL2, RX3_SDA2);
SPIClass SPI_1(MOSI1, MISO1, SCK1);
MagneticSensorMT6701SSI sensor(CS1);
//InlineCurrentSense current_sense = InlineCurrentSense(0.01, -50 * 4, W_CURR, V_CURR, U_CURR);
InlineCurrentSense current_sense = InlineCurrentSense(0.01, -50 * 4, W_CURR, V_CURR, _NC);
BLDCMotor motor = BLDCMotor(MOTOR_POLE);
BLDCDriver6PWM driver = BLDCDriver6PWM(PA8, PB13, PA9, PB14, PA10, PB15, DRV_EN);  //W V U EN
float target_angle = 0;
Commander command = Commander(Serial3);


void toggleLED() {
  static bool state = false;
  state = !state;
  if (state) {
    GPIOC->BSRR = (1 << 13);
  } else {
    GPIOC->BSRR = (1 << (13 + 16));
  }
}

void doTarget(char* cmd) {
  command.scalar(&target_angle, cmd);
}

void doMotor(char* cmd) {
  command.motor(&motor, cmd);
}

void doAnalog(char* cmd) {
  if (cmd[0] == 'T') Serial3.println(analogRead(TEMP));
  else if (cmd[0] == 'V') {
    Serial3.print("Voltage: ");
    Serial3.println(analogRead(VOLTAGE) * 0.0032);
  }
};

void setup() {
  analogReadResolution(12);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  Serial3.setTx(TX3_SCL2);
  Serial3.setRx(RX3_SDA2);
  Serial3.begin(115200);
  SimpleFOCDebug::enable(&Serial3);

  sensor.init(&SPI_1);
  motor.linkSensor(&sensor);
  driver.voltage_power_supply = 8;
  driver.init();
  motor.linkDriver(&driver);
  current_sense.linkDriver(&driver);
  // control loop type and torque mode
  motor.torque_controller = TorqueControlType::foc_current;
  motor.controller = MotionControlType::velocity;
  motor.motion_downsample = 0.0;

  // velocity loop PID
  motor.PID_velocity.P = 1.5;
  motor.PID_velocity.I = 10.0;
  motor.PID_velocity.D = 0.0;
  motor.PID_velocity.output_ramp = 1000.0;
  motor.PID_velocity.limit = 2.0;
  // Low pass filtering time constant
  motor.LPF_velocity.Tf = 0.05;
  // angle loop PID
  motor.P_angle.P = 5.0;
  motor.P_angle.I = 0.0;
  motor.P_angle.D = 0.0;
  motor.P_angle.output_ramp = 0.0;
  motor.P_angle.limit = 200.0;
  // Low pass filtering time constant
  motor.LPF_angle.Tf = 0.0;
  // current q loop PID
  motor.PID_current_q.P = 1.1;
  motor.PID_current_q.I = 20.0;
  motor.PID_current_q.D = 0.0;
  motor.PID_current_q.output_ramp = 1000.0;
  motor.PID_current_q.limit = 5.0;
  // Low pass filtering time constant
  motor.LPF_current_q.Tf = 0.005;
  // current d loop PID
  motor.PID_current_d.P = 1.1;
  motor.PID_current_d.I = 15.0;
  motor.PID_current_d.D = 0.0;
  motor.PID_current_d.output_ramp = 1000.0;
  motor.PID_current_d.limit = 5.0;
  // Low pass filtering time constant
  motor.LPF_current_d.Tf = 0.02;
  // Limits
  motor.velocity_limit = 200.0;
  motor.voltage_limit = 5.0;
  motor.current_limit = 2.0;
  // sensor zero offset - home position
  motor.sensor_offset = 0.0;
  // general settings
  // motor phase resistance
  motor.phase_resistance = 1.06;
  // pwm modulation settings
  motor.foc_modulation = FOCModulationType::SpaceVectorPWM;
  motor.modulation_centered = 1.0;

  motor.useMonitoring(Serial3);

  motor.linkCurrentSense(&current_sense);
  current_sense.init();
  Serial3.print("current_sense.gain:\t");
  Serial3.print(current_sense.gain_a);
  Serial3.print(", ");
  Serial3.print(current_sense.gain_b);
  Serial3.print(", ");
  Serial3.println(current_sense.gain_c);
  Serial3.print("current_sense.offset_i:\t");
  Serial3.print(current_sense.offset_ia);
  Serial3.print(", ");
  Serial3.print(current_sense.offset_ib);
  Serial3.print(", ");
  Serial3.println(current_sense.offset_ic);

  motor.init();
  motor.sensor_direction = Direction::CCW;
  motor.initFOC();

  command.add('T', doTarget, "target angle");
  command.add('M', doMotor, "motor");
  command.add('A', doAnalog, "analog read");
  motor.monitor_downsample = 0;
  //motor.monitor_variables = 0;

  Serial3.println(F("Motor ready."));
  Serial3.println(F("Set the target angle using serial terminal:"));

  _delay(1000);
}

void loop() {
  motor.loopFOC();
  motor.move(target_angle);
  motor.monitor();
  command.run();
}

今回はインライン電流センサを2つに減らしてみたので使っていない層のピンアサインは"_NC"を指定している。ドキュメントだと2つのポートを指定しただけでも大丈夫そうに書いてあったんだけど初期化のときに"current_sense.offset_ia"が0になってしまって、bとcに値が入っていた。

とりあえず電流センサを2つに減らしても問題なく動いている。もともと2層だけのモータドライバも多いし。

SimpleFOC Studioでキャリブをしているけど9Vでも500mAぐらいしか流れないのでもっと電圧を高めたい感じ。しかしながらTMC6300が11Vまでという…
センサレスのときよりは断然トルクが上がっているのでギアボックスに組み込んでちゃんと回るかな。

2025年11月2日日曜日

2208モータをSimpleFOCで回してみた。

 知り合いが3Dプリントでジンバルモータ内蔵の流星ギアを作っていたみたいなんだけどトルクが全然無いので手でスタートしてやらないと回らないと…
ということでこの前作ったSimpleFOCモータドライバでセンサ付きで回してみることに。

モータはMars Powerの2208モータ。スペックとしては2208 80Tとしか書いてなくて電圧も2S-3Sとなってる。7.4V~11.1Vってことならこの前作ったモータドライバがちょうど良さそう。
ちなみに抵抗値は16Ω、114KVっぽい。ちょっと抵抗値が高いので電圧をかけないとトルクが出ないかも。

電流センサも2つで行けるだろうということで電流センサを2系統、CANトランシーバを装着しないやつをもう一枚作った。オペアンプをつけていないところのシャント抵抗の部分はジャンパした。

とりあえずモータにこの基板をつけるためのアダプタを設計しないといけないので磁気角度センサのMT6701はまだ実装していない。まずは動作確認してみる。

もともとモータのピンヘッダと合体できる設計なんだけど、今回はパッドからケーブルを出して接続。SimpleFOCでセンサなしでVelocity Open Loopで回してみた。
一応電流センサも動いてそう?ちゃんと回ってるけどこんな波形だっけ…
それにしてもOpenloopは発熱だけしてトルクがない感じがする。まだBack-EMFのフィードバックかけてるラジコン用ESCのほうがいいかも。
センサ付きFOC制御で回してみるのが楽しみ。

とりあえずモータとモータドライバのアダプタを設計して磁気アングルセンサ使えるようにしないと。

2025年10月22日水曜日

ESP32-CAMをWireGuard経由で使ってみた。

 ESP32でWireGuardが使えるライブラリがあるということで、ESP32-CAMもWireGuardを使ってNAT越えできるのではないかということで試してみた。

よくあるスマホのアプリ対応のWiFiカメラとかだとポート解放とか不要でユーザ登録すればカメラの映像を見れたりするけど、ESP32-CAMもモバイル回線とかのポート開放できないような場面で使えたら便利かも?ということで試してみた。前にUPnPでポートを開けてみたこともあったけど、そもそもグローバルIPがない環境だとこの方法も使えないし。

ESP32-CAM

久しぶりにESP32-CAMのスケッチをビルドするのでまずは環境を最新にしてビルドしてみた。ESP32 Arduino Coreは3.3.2でArduino IDEは2.3.5。前にESP32-CAMのESP32 Camera Driverを最新にしたときの手順で、ESP32 Camera Driver v2.1.3に更新してみた。
まずここで躓いたのがSCCB関連で、sccb-ng.cを削除したらうまくビルドできた。

最新の環境で前回のスケッチがビルドできるようになったので早速WireGuardを組み込んでみる。

とりあえずESP32のWireGuardライブラリをダウンロードしてきてサンプルをビルドしてみたんだけどまさかのArduino core for the ESP32 3.xに非対応という…

Pull requestに上がっていた3.x対応のESP32-WireGuardライブラリがあったのでこれをZipでダウンロードしてきて手動で追加した。テストでサンプルがビルドできたのでこのライブラリをESP32-CAMのスケッチに追加していく。

スケッチを全部貼ると長くなってしまうので前回のスケッチに追加した部分だけを紹介。

#define Tunnel

#ifdef Tunnel
#include <WireGuard-ESP32.h>
char private_key[] = "                                            ";  // [Interface] PrivateKey
IPAddress local_ip(10, 4, 0, 2);                                      // [Interface] Address
char public_key[] = "                                            ";   // [Peer] PublicKey
char endpoint_address[] = "     .f5.si";                      // [Peer] Endpoint
int endpoint_port = 51810;                                            // [Peer] Endpoint
static WireGuard wg;
#endif

まずは各種ライブラリをincludeしてるあたりにこんな感じで設定を追加。サーバ側はこの前設定したDD-WRTなんだけど、ESP32-CAM用にPeerを追加した。IPは同じセグメントで別なIPを割り振る感じ。Peerの設定はクライアントに割り振るIP以外ほぼコピーかな。ESP32ではQRコードではなくてQRコードの下にあるExport Peer Configボタンを押してコンフィグファイルをダウンロードする。そんでもってそのファイルの中のPrivateKeyってところとPublicKeyってところをコピーして上に貼り付け。DD-WRTの設定画面からじゃなくて、コンフィグファイルをダウンロードしてその中身ってのがポイント。

#ifdef Tunnel
  Serial.println("Adjusting system time...");
  configTime(9 * 60 * 60, 0, "ntp.jst.mfeed.ad.jp", "ntp.nict.jp", "time.google.com");

  struct tm timeInfo;
  if (getLocalTime(&timeInfo)) {
    Serial.print("Local Time  : ");
    Serial.printf("%04d-%02d-%02d ", timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);
    Serial.printf("%02d:%02d:%02d\n", timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
  }
#endif

WireGuardはNTPから時間を取得する必要があるのでNTPで時計を合わせる部分をsetup()の中のネットに繋がったあたりに追加。mDNSの上ぐらいに追加しておいた。

#ifdef Tunnel
  Serial.println("Initializing WG interface...");
  if (!wg.begin(
        local_ip,
        private_key,
        endpoint_address,
        public_key,
        endpoint_port)) {
    Serial.println("Failed to initialize WG interface.");
  }
#endif

そしてWireGuardに接続する部分をsetup()の中のserver.onの直前ぐらいに追加。これでWireGuardに接続されるとESP32の通信自体が全部WireGuardに流れる形。起動後10.4.0.2に接続すると普通にカメラの映像を確認できた。スタックも前回増やした状態でいい感じに安定して動いている。

ライブラリを使うことによって意外と簡単にESP32からWireGuard VPNに接続できちゃった。これならグローバルIPがなかったり、ポートが開けられない環境に設置して、サーバ側からポート変換で公開できちゃうので便利。お互いにNAT越えできない場合はTailscaleとか使えると便利そうだなぁ。

mDNSライブラリもそのまま動いてるみたいだけどWireGuardで接続している同セグメントからじゃないとmDNSでは解釈できないみたい。他のWireGuardのPeerからはうまく発見できた。IPだとWireGuard経由でも、ESP32-CAMがつながってるWiFiネットワークからでもどっちからもアクセスできた。

フレームレートの比較はどうやってやればよいのかな…

2025年10月19日日曜日

DD-WRTでWireGuardを使ってみた。

 DD-WRTでSoftEther VPNの設定がうまく行かなくなっていてからOpenVPNを設定して使っていたんだけども、WireGuardのほうが速いという噂を聞いたので試しにDD-WRTでWireGuardの設定をしてみて速度を比較してみた。

WireGuardの設定はOpenVPNとかSoftEtherVPNの設定画面とは別で、SetupのTunnelsから設定できる。DD-WRTのバージョンは62170以上が推奨みたい。

ここのAdd Tunnelを押すと各種Tunnelを追加できるようになっている。色々調べて設定してみたんだけど、どうしてもネットに繋がらなくなってしまうので最終的に公式フォーラムに登録してドキュメントを見ながら設定してみた。

Add Tunnelを押したら新規でTunnelが追加されるのでWireGuardを選択して設定していく。

Generate Keyボタンを押してLocal Public Keyを生成して、Advanced SettingsはEnableにして上のように設定する。MTUはインターネットのMTUからWireGuardのヘッダー分の60を引いた値にしておいた。サーバIPはドキュメント通りに10.4.0.1にしておいた。

とりあえず設定できたらSave & Applyで保存する。

次にクライアントの追加。

Add Peerボタンを押すと設定項目が追加されるのでPeerの設定をしていく。

Client Config FileをEnableにして、とりあえずクライアントのIPはドキュメント通りの10.4.0.6で設定。Peer Tunnel Endpointはサーバのアドレス設定ということで、外部からアクセスできるようにDDNSを設定した。DNSはとりあえずGoogle DNSを設定しておいた。
今回は1デバイス毎にPeerを設定しているのでAllowed IPsのサブネットマスクは/32にしておいた。/24とかにすると同一セグメントのPeer設定を増やすとうまく繋がらなかったり。

あとはMake Peer Configを押すとQRコードが生成される。
とりあえずこれでサーバ側の設定は完了。今回はiPhoneを接続したいのでiPhoneにWireGuardアプリをインストールしてQRコードをスキャンするだけでつながった。

OpenVPNの設定よりも簡単かも…

というわけで次に速度の測定をしてみた。

DD-WRT自体はOpenVPNもWireGuardもどっちもオンの状態で、同じ回線でiPhone側のVPNの切り替えだけでテスト。

まずはVPN不使用で回線のスピードをテスト

これがVPNでどこまで遅くなるのか…
OpenVPNはこんな感じ。何故かいつもダウンロードだけ遅いのよね。ルータの暗号化性能?
WireGuardはこんな感じ。結構速い。上りも下りも同じ速度が出ている
ちなみにルータの性能としてはBCM4708の800MHzデュアルコア、RAM512MBといったところ。
どっちも共存できてるので色んなところでつながるかテストしてみようかな。MTUも調整しないといけないかも。

ちなみにDD-WRTでやってるDNS広告ブロックもWireGuard経由で有効にできないかなといろいろ試してみたんだけど、PeerのDNS設定を10.4.0.1にしてDD-WRTのdnsmasq設定にWireGuardのセグメントを追加するだけ(listen-address=10.4.0.1)で行けた。そういえばOpenVPNの時もこんな設定にしていたな…

2025年10月12日日曜日

MediaMTXでラズパイカメラV1の映像をストリーミングしてみた。

 この前作ったダイレクトドライブなラジコンにカメラを搭載してFPVで操作してみたいと思って、LuckFox上でμStreamerを動かしたやつを載せてみたんだけど、このモータが結構早いのでもっと低遅延にしたい。

ということでもっと低遅延にできる方法が無いかどうか試してみることに。LuckFoxはビルドしてFlashに書き込んでっていうのが何回も往復するのが面倒なので、まずはRaspberry Pi Zero上で試してみることに。

久しぶりにRaspberry Pi Zeroを出してきたのでまずはOSをクリーンインストール。Raspberry Pi Imagerからセットアップして、WiFi設定までできるのは便利になったなぁ。Debian Trixieになった。

カメラ回りも少し仕様が変わっているみたい。raspi-configからの設定はいらなくて自動で認識するとか。

とりあえずセットアップが終わったRaspberry Pi Zeroに昔購入したRaspberry Pi Camera V1の互換品を取り付けて起動してみる。この前uStreamerで不安定だったリベンジ。

起動したら

rpicam-hello --list-cameras

と実行してみる。前のバージョンではlibcameraだった部分がrpicamに変更になっている感じかな?

Available cameras
-----------------
0 : ov5647 [2592x1944 10-bit GBRG] (/base/soc/i2c0mux/i2c@1/ov5647@36)
    Modes: 'SGBRG10_CSI2P' : 640x480 [58.92 fps - (16, 0)/2560x1920 crop]
                             1296x972 [46.34 fps - (0, 0)/2592x1944 crop]
                             1920x1080 [32.81 fps - (348, 434)/1928x1080 crop]
                             2592x1944 [15.63 fps - (0, 0)/2592x1944 crop]

こんな感じで差し込んだだけで認識してるみたい。互換品でもとりあえず問題なく自動で認識できた。確かに電源を入れたときに一瞬カメラのLEDが点灯したのでその時に確認してるのかな?

rpicam-jpeg -n --output test.jpg

次に静止画を撮影するコマンドで写真を撮影して、WinSCPでPCに転送して確認してみた。(SSHで作業しているため)
写真もバッチリ取れてるのでまずはカメラの動作確認はOK。

カメラの動作確認ができたのでMediaMTXをダウンロードして実行可能にする。MediaMTXはWebRTCを使ってカメラの映像をブラウザからストリーミングできる。

wget -P ./mediamtx https://github.com/bluenviron/mediamtx/releases/download/v1.15.1/mediamtx_v1.15.1_linux_armv6.tar.gz
cd mediamtx
tar xzf mediamtx_v1.15.1_linux_armv6.tar.gz
chmod +x mediamtx

ラズパイZeroWにはH264のハードウェアエンコーダが内蔵されてるのでせっかくなので使ってみようということでH264でWebRTCでストリーミングのテストをしてみる。同じフォルダのmediamtx.ymlを開いて一番下の方のpaths:っていうサンプルがあるところをまるっと下記のように置き換える。

paths:
  cam0:
    source: rpiCamera
    sourceProtocol: automatic
    rpiCameraWidth: 1296
    rpiCameraHeight: 972
    rpiCameraFPS: 30
    rpiCameraBitrate: 2000000
    rpiCameraCodec: hardwareH264

保存したらあとは./mediamtxでMediaMTXを起動するだけ。

ブラウザからhttp://raspberrypi.local:8889/cam0/にアクセスしてみるとカメラの映像が見れるようになってるはず。MediaMTX簡単で良いな…

早速遅延を図ってみた。前に使ったように画面にストップウォッチを表示して、その隣にストリーミングの映像を表示させてスクショを撮るという方法。

遅延は440msぐらい?ラズパイZero WはWiFiでルータに接続してる状態で測定。ハードウェアエンコーダとはいえH264に変換してるからかなぁ?
カメラの方が赤みがかっているのはレンズ改造のところに隙間ができていたからかな。隙間埋め無いと…

UVC対応のWebカメラでuStreamerを同じラズパイZero Wで試してみた。

とりあえず前と同じ感じで最新版をビルドした。

ustreamer -m MJPEG -f 30 -s 0.0.0.0 -p 8080 -r 1280x720

解像度はLogicool C270の解像度に合わせて近いところに合わせてみた。

そんでもって遅延を測定すると140msぐらい。ラズパイ上だと結構低遅延じゃん…
しかしこの解像度でもMJPEGだと帯域は8.5Mbpsぐらいあったので、H264の2Mbpsだと電波状況が悪いときは有利かも。

H264ハードウェアエンコード対応のWebカメラも試してみたいな。UVCのH264も実装が色々あるみたいでLinuxで使う場合はUVC1.5対応じゃないとちょっと大変そう。DJI Osmo Action 4とかもUSBカメラモードでH264に対応してるっぽいけど、UVC1.0なのでV4L2からそのまま取り出せなさそう

2025年10月5日日曜日

Windows 11へのアップグレードが31%で止まってしまった。

 そろそろWindows 10のサポートが終了するということで友人がWindows 11にアップグレードしようとしたところどうしてもアップグレードできないということで調べてみた。Windows Updateとかでも止まるらしいので最終手段のisoからインストールも試したみたいなんだけどどうしても毎回31%で止まってしまうらしい。
Windows 8のライセンスを使ってアップグレードでWindows 10をクリーンインストールしているんだけども、2023年9月27日からWindows 8のライセンスも使えなくなってしまったので再インストールしたらWindows 11のライセンスを購入しないといけない可能性もあるし…(ハードウェア変更無いので行けると思うけど。)
そもそも再インストール自体が面倒。

巷で噂のConexant SmartAudio HDデバイスは存在していないようだけれども…

ということでひとまずログファイルを確認してみることに。

C:\Windows\Panther

の中に「setupact.log」と「setuperr.log」というファイルが生成されていて、これがアップグレード時に残るログファイルらしい。

ログファイルは結構大きいけど、おそらく一番下の方だけを確認すれば良さそうな気がする。「setupact.log」の方を確認すると一番下の方でMIGのInfoがあったあとにMOUPGのErrorが出てログが途絶えていた。

2025-10-05 10:10:20, Info                  MIG        AddDriverFiles: Processing device: 4d36e96c-e325-11ce-bfc1-08002be10318
2025-10-05 10:10:20, Info                  MIG        AddDriverFiles: Processing driver: Sennheiser Communication Audio, Sennheiser, Sennheiser
2025-10-05 10:10:20, Info                  MIG        AddInfAndCatalog: Adding catalog file: C:\WINDOWS\system32\catroot\{f750e6c3-38ee-11d1-85e5-00c04fc295ee}\oem40.cat
2025-10-05 10:13:35, Info                  MOUPG  CInstallUI::ShowMessageBox: Showing MessageBox
2025-10-05 10:13:37, Info                  MOUPG  CInstallUI::ConfirmCanceled: User cancel confirmed
2025-10-05 10:13:37, Info                  MOUPG  CInstallUI::OnProgressChanged: Cancel is requested. Returning ERROR_REQUEST_ABORTED
2025-10-05 10:13:37, Error                 MOUPG  CInstallUI::OnProgressChanged(579): Result = 0x800704D3
2025-10-05 10:13:37, Error                 MOUPG  CSetupUIManager::OnProgressChanged(452): Result = 0x800704D3
2025-10-05 10:13:37, Error                 MOUPG  CSetupHost::OnProgressChanged(2531): Result = 0x800704D3
2025-10-05 10:13:37, Error                 MOUPG  CSetupManager::DlpManagerCallback(2341): Result = 0x800704D3
2025-10-05 10:13:37, Info                  MOUPG  Cancel of current task requested...
2025-10-05 10:13:37, Info                  MOUPG  Attempting to cancel current task...
2025-10-05 10:13:37, Info                  MOUPG  MoSetupPlatform: Calling SetupPlatform::INewSystem::RequestCancelOperations...
2025-10-05 10:13:37, Info                  MOUPG  Task cancel request returned: [0x0]
2025-10-05 10:13:37, Error                 MOUPG  SendCallbackMessage: [0x7] -> user callback returned 0x800704D3
2025-10-05 10:13:37, Error                 MOUPG  CDlpTask::Cancel(993): Result = 0xC1800108
2025-10-05 10:13:37, Info                  MOUPG  SendCallbackMessage: [0x7] -> Cancel request returned 0xC1800108

こんな感じのエラー。

この感じだと実はゼンハイザーのUSBヘッドホンが引っかかってる?

何度もアップデートが失敗しているのでUSBデバイスは抜いているんだけども、抜いていてもインストールしているドライバを移行する際にエラーで止まってしまってる模様。

ということでもう一度ゼンハイザーのUSBヘッドホンを差し込んでデバイスマネージャーからドライバを丸ごと削除してみる。

デバイスマネージャーのゼンハイザーのデバイスを右クリックしてデバイスのアンインストールを選択。

そんでもって出てきたダイアログで「このデバイスのドライバーソフトウェアを削除します。」にチェックを入れてアンインストール。とりあえずSennheiserのデバイス2つともこんな感じで削除。
削除したあとにUSBデバイスのスキャンが入ってしまうとまたインストールされてしまうので削除が終わったらUSBデバイスを抜く。

そんでもってWindows 11アップデートを掛けたら…

魔の31%を超えることができた。やっぱりWindowsのアップグレードはオーディオデバイスがネックになっているのかもしれないね。

EPOSのサイトにはドライバアンインストールツールでのアンインストール方法も公開されてるのでこれも参考にするとドライバをちゃんとアンインストールできるかもしれない。こっちのほうがデバイスマネージャーに表示されない部分も表示されるかもしれないので便利かも?

とりあえずWindows 11のアップグレードが止まってしまったらログファイルをみればなんとかなるかもしれないことがわかった。SetupDiagという、ログを見やすくする便利なツールもあるらしい。

ちなみに今回引っかかったゼンハイザーのGSP 350はアナログケーブルも市販(4極から3極も変換できるし)されてるようなので、こうやってドライバアップデートに引っかかるようだったら普通にアナログ化してオンボのRealtekとかで動かしても良さそう。7.1chのWindows 11対応のドングルに変更しても良いし。

2025年10月4日土曜日

ESP32-C6でCANを2ポート使ってみた。

 モータドライバをCAN対応で作ったので、ESP32からコントロールしていたんだけども、ESP32-C6はCANが2つあるのでモータドライバのID変更が面倒なときは2ポートそれぞれに同じIDのモータドライバ接続できたら便利なんじゃ?という意味不明なことをやってみたくて買ってみた。せっかくデイジーチェーンできる規格なのにもったいない。(ただ新しいESP32シリーズを試したかっただけ

Seeed Studio XIAO ESP32C6

というわけでESP32-C6が使いやすい状態になっているSeeed Studio XIAO ESP32C6にしてみた。M5Stack NanoC6だとIOが少なすぎるしね…

今までのESP32シリーズのCANはSJA1000互換実装だったんだけど、ESP32-C6はCANの実装が変わっているようで今までのライブラリが使えなさそう。ESP32版SLCANを作ったときみたいにIDFのTWAIドライバを直接使えば良さそうなんだけど、こちらもTWAI driver v2になってるっぽい?ESP32-C6はTWAI driver v2じゃないと動かないかも?

今回はArduino ESP32 v3.3.1を使用してみた。これならIDF5.5.1ベースなのでESP32-C6のDual TWAIにも対応しているはず…

#include <Arduino.h>
#include "esp_twai.h"
#include "esp_twai_onchip.h"

#define CAN0_TX GPIO_NUM_2
#define CAN0_RX GPIO_NUM_21
#define CAN1_TX GPIO_NUM_22
#define CAN1_RX GPIO_NUM_23
#define LED_BUILTIN 15

static twai_node_handle_t can0 = NULL;
static twai_node_handle_t can1 = NULL;
//CAN受信コールバック
static bool twai_rx_cb(twai_node_handle_t handle,
                       const twai_rx_done_event_data_t *edata,
                       void *user_ctx) {
  static uint8_t rx_buf[8];
  twai_frame_t frame = {
    .buffer = rx_buf,
    .buffer_len = sizeof(rx_buf),
  };
  if (twai_node_receive_from_isr(handle, &frame) == ESP_OK) {
    if (handle == can0) {
      digitalWrite(LED_BUILTIN, HIGH);
      Serial.print("[CAN0 RX] ");
    } else if (handle == can1) {
      digitalWrite(LED_BUILTIN, LOW);
      Serial.print("[CAN1 RX] ");
    }
    Serial.printf("ID=0x%X DLC=%d DATA=",
                  frame.header.id,
                  frame.header.dlc);
    for (int i = 0; i < frame.header.dlc; i++) {
      Serial.printf("%02X ", rx_buf[i]);
    }
    Serial.println();
  }
  return false;
}
//CAN初期化関数
esp_err_t CANbegin(
  gpio_num_t tx_gpio,
  gpio_num_t rx_gpio,
  uint32_t bitrate,
  twai_node_handle_t *out_node) {
  twai_onchip_node_config_t cfg = {
    .io_cfg = {
      .tx = tx_gpio,
      .rx = rx_gpio,
      .quanta_clk_out = GPIO_NUM_NC,
      .bus_off_indicator = GPIO_NUM_NC,
    },
    .bit_timing = {
      .bitrate = bitrate,
    },
    .tx_queue_depth = 5,
  };
  esp_err_t ret = twai_new_node_onchip(&cfg, out_node);
  if (ret != ESP_OK) {
    return ret;
  }
  twai_event_callbacks_t cbs = {
    .on_rx_done = twai_rx_cb,
  };
  ret = twai_node_register_event_callbacks(*out_node, &cbs, NULL);//受信コールバック
  if (ret != ESP_OK) {
    return ret;
  }
  ret = twai_node_enable(*out_node);
  if (ret != ESP_OK) {
    return ret;
  }
  return ESP_OK;
}
//CAN送信関数
esp_err_t CANsend(
  twai_node_handle_t node,
  uint32_t id,
  bool ext,
  bool rtr,
  const uint8_t *data,
  size_t len) {
  twai_frame_t frame = {
    .header = {
      .id = id,
      .ide = ext,
      .rtr = rtr,
    },
    .buffer = (uint8_t *)data,
    .buffer_len = len,
  };
  return twai_node_transmit(node, &frame, 100);
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(115200);
  delay(1000);
  esp_err_t ret = CANbegin(CAN0_TX, CAN0_RX, 250000, &can0);
  if (ret != ESP_OK) {
    Serial.printf("CAN begin failed: %s\n", esp_err_to_name(ret));
  }
  ret = CANbegin(CAN1_TX, CAN1_RX, 250000, &can1);
  if (ret != ESP_OK) {
    Serial.printf("CAN begin failed: %s\n", esp_err_to_name(ret));
  }
}

void loop() {
  static uint8_t buf0[8] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
  static uint8_t buf1[8] = { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 };

  static unsigned long last = 0;
  if (last + 1000 <= millis())  //ms周期
  {
    last = millis();  //次の実行のために時間更新
    if (can0) {
      if (CANsend(can0, 0x016, false, false, buf0, sizeof(buf0)) == ESP_OK) {
        Serial.println("CAN0 TX OK");
      }
    }
    if (can1) {
      if (CANsend(can1, 0x036, false, false, buf1, sizeof(buf1)) == ESP_OK) {
        Serial.println("CAN1 TX OK");
      }
    }
  }
}

SLCANを移植したときに使ったTWAI driver v1の感じでTWAI driver v2でtwai_driver_install_v2()を使ってみたんだけど、1ポートは初期化できるんだけど、もう一回別ポートを初期化するために呼び出すとエラーが出てしまった。なので2ポート使いたいときは"esp_twai_onchip.h"を使ってtwai_new_node_onchip()をしてやる必要がありそう。これだと2ポートともちゃんと初期化されて使用可能になった。

もはやArduinoでIDFのコード動かしている感じになってしまってArduino感がなくなってしまったので多少関数化をしておいた。GPIOの設定は"GPIO_NUM_2"みたいにgpio_num_t型で指定してやる必要があった。ライブラリにしてしまえばもうちょっとスッキリするかも。

ESP32-C6-CAN

CANトランシーバももちろん2セット必要なのでSN65HVD230を使用してみた。トランシーバーがちゃんと接続されていない場合はある程度送信したあとに"esp_twai: _node_queue_tx(580): node is bus off"が出た。CANトランシーバが正常に繋がっている場合はどっちも同時に送信できてそう。

受信の方もとりあえずIDFのリファレンスを参考にコールバックで受信できるように設定してある。受信するとCANポート番号とメッセージがシリアルコンソールで確認できる。とりあえずArduino環境で、CAN-BUSを2ポート同時に送信も受信も確認できた。

試しにM5Atom Liteでも試してみたけど(CANbeginを1回のみに変更)、こっちでもちゃんと使えたので他のESP32シリーズでもこの方法でTWAI driver v2でCAN通信が使えるっぽい。逆に言えばESP32-C6に対応させればTWAIが1ポートな今までのシリーズは大丈夫そうかな。ちなみにESP32-P4だとTWAIコントローラが3個入ってるのでCAN通信を同時に3ポート使えるっぽい。

2025年9月28日日曜日

タミヤのボールキャスターを試してみた。

 この前作った対向2輪型のラジコンなんだけど、高速で直進すると左右にゆらゆらするのでキャスターの制度が悪いからキャスターが振られてるんじゃないか?ということでボールキャスターに変更してみた。

金属製なので床が傷つくんじゃないかということで長らく放置していたタミヤのボールキャスター。
組み立ては超簡単。
キャスター用の高さ調整プレートはネジ穴を貫通にしてあるのでボールキャスターの高さ調整にも使用可能。ボールキャスターも35mmで組み立てたのでピッタリ。

実際に走らせてみるとやっぱりゆらゆらする。ということはSimpleFOCのPIDチューニングかもしれない。速度フィードバックで速度が一定になっていないのかなぁ。これはさらなるチューニングが必要かもしれない。
この前CAN経由でチューニングできるようにしたのでESP32側にちょっと変更できるようなしくみを入れようかな。

それはそうと、ボールキャスターになったことでフロントが軽くなってフロントが浮くようになってしまった。

重さはキャスターが36gでボールキャスターが12gだった。これはフロントが軽くなる。
キャスター変えただけでこんなに機敏に動くようになるなんて…
これジャイロつけたらずっとウィリーとかできそうな?

2025年9月27日土曜日

ESP32にLiDARを繋いでブラウザに表示してみた。

 前に購入した格安LiDARをESP32に繋いでブラウザでデータを表示するようにしてみた。購入したときにESP32に接続してデータ取得するところまではやってみたんだけど、今回はその取得できたデータをWebSocketでブラウザに送って可視化してみることに。

今回はM5AtomS3が余っていたのでこれに繋いでみることに。Groveコネクタは信号線が3.3Vだけど電源は5V取れるので好都合。GroveコネクタにLiDARが直結できるなんて便利かも。

色々試してみたんだけど、JSONでデータを送るとデータ量が多すぎてうまく360°分のデータが送れなかったので距離データだけをバイナリでWebSocketで送るようにしてみた。

今更気がついたんだけど、LDS-006って回転方向がCCWなのね…普通LiDARを手に持って回転させると画面の可視化データって反対回りに動くはずなんだけど同じ向きに回る。ということでそこもデータを取得するところで修正した。

#include <Arduino.h>
#include <WiFi.h>
#include <WebSocketsServer.h>
#include <WebServer.h>
#include <ESPmDNS.h>

#define RXD2 2
#define TXD2 1

// WiFi設定
const char* ssid = "SSID";
const char* password = "password";

WebServer server(80);  // Webサーバー port80
WebSocketsServer webSocket = WebSocketsServer(81);// WebSocketサーバー port81

// LDS-006 360点分の距離データ
volatile uint16_t lidar360[360];
volatile bool newData = false;

// LDS-006読み取りタスク
void taskLidarRead(void* pvParameters) {
  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
  delay(100);
  Serial2.print("startlds$");  // 起動コマンド

  uint8_t packet[22];
  int packetIndex = 0;

  while (1) {
    while (Serial2.available()) {
      uint8_t b = Serial2.read();
      if (packetIndex == 0 && b != 0xFA) continue;
      packet[packetIndex++] = b;
      if (packetIndex == 22) {
        if (packet[0] == 0xFA) {
          uint8_t block = packet[1];
          if (block >= 0xA0 && block <= 0xF9) {
            int baseAngle = (block - 0xA0) * 4;
            for (int i = 0; i < 4; i++) {
              int offset = 4 + i * 4;
              uint16_t dist = ((packet[offset + 1] & 0x3F) << 8) | packet[offset];
              int angle = baseAngle + i;
              if (angle >= 360) angle -= 360;
              int correctedAngle = (360 - angle) % 360;
              lidar360[correctedAngle] = dist;
            }
            newData = true;
          }
        }
        packetIndex = 0;
      }
    }
    vTaskDelay(1);
  }
}

// WebSocket送信タスク
void taskWebSocketSend(void* pvParameters) {
  while (1) {
    if (newData) {
      // 360点分の距離データを2バイトずつのバイナリで送信
      webSocket.broadcastBIN((uint8_t*)lidar360, 360 * 2);
      newData = false;
    }
    vTaskDelay(100);  // 100ms周期
  }
}

void taskHttpServer(void* pvParameters) {
  while (1) {
    server.handleClient();
    vTaskDelay(1);
  }
}

// WebSocketイベント
void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
  if (type == WStype_CONNECTED) {
    Serial.printf("WebSocket client #%u connected\n", num);
  }
}

#define index_html_gz_len 517
const uint8_t index_html_gz[] PROGMEM = {
  0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x55, 0x53, 0x5D, 0x6B, 0xDB, 0x30,
  0x14, 0x7D, 0x1F, 0xEC, 0x3F, 0xDC, 0x99, 0x41, 0xEC, 0x36, 0xB3, 0x9C, 0x8C, 0x85, 0x52, 0xDB,
  0x81, 0x2D, 0xED, 0x43, 0xA1, 0x65, 0x65, 0xE9, 0x18, 0x63, 0x0C, 0xAA, 0xD8, 0x4A, 0x2C, 0xA6,
  0x48, 0x45, 0xBA, 0x89, 0x63, 0x4A, 0x5E, 0xF6, 0x30, 0xF6, 0x97, 0xF6, 0x83, 0xC6, 0xFE, 0xC6,
  0x24, 0x7F, 0x24, 0xCE, 0x8B, 0xA5, 0xAB, 0x7B, 0xCE, 0xD5, 0x3D, 0x47, 0xBE, 0xC9, 0xAB, 0xAB,
  0x8F, 0xB3, 0x87, 0xAF, 0xF7, 0xD7, 0x50, 0xE0, 0x5A, 0x4C, 0x5F, 0xBE, 0x48, 0x0E, 0x2B, 0xA3,
  0xB9, 0x5D, 0x01, 0x12, 0xE4, 0x28, 0xD8, 0xF4, 0xF6, 0x6A, 0xFE, 0x26, 0x8A, 0x26, 0x70, 0xCB,
  0xB7, 0x2C, 0x21, 0xCD, 0x99, 0x85, 0x91, 0x16, 0x97, 0x2C, 0x54, 0x5E, 0xB9, 0x35, 0xA3, 0x72,
  0x4B, 0x0D, 0xF0, 0x3C, 0xF5, 0x04, 0xCF, 0xA9, 0x9E, 0xD5, 0xB1, 0x07, 0x25, 0xCF, 0xB1, 0x48,
  0xBD, 0x77, 0x51, 0xE4, 0x41, 0xC1, 0xF8, 0xAA, 0xC0, 0x26, 0x98, 0x26, 0xA4, 0xA1, 0x38, 0xB2,
  0xC9, 0x34, 0x7F, 0x42, 0xBB, 0xCB, 0x94, 0x34, 0x08, 0x6D, 0xAD, 0x14, 0x72, 0x95, 0x6D, 0xD6,
  0x4C, 0x62, 0xB8, 0x62, 0x78, 0x2D, 0x98, 0xDB, 0x7E, 0xA8, 0x6E, 0x72, 0x7F, 0xD0, 0xBB, 0x62,
  0x10, 0xC4, 0x07, 0x1E, 0xEE, 0x2C, 0xA9, 0x61, 0x3B, 0xCA, 0x4C, 0x49, 0x64, 0x3B, 0xF4, 0x07,
  0xE3, 0xBC, 0x87, 0x2A, 0x94, 0xFD, 0xA4, 0xB6, 0x31, 0x99, 0xAB, 0x32, 0x14, 0x2A, 0xA3, 0xC8,
  0x95, 0x0C, 0xDD, 0xB1, 0xA4, 0x6B, 0x76, 0xC0, 0x95, 0xAE, 0x03, 0xC9, 0x4A, 0xF8, 0xC2, 0x16,
  0x73, 0x95, 0xFD, 0x60, 0xE8, 0x3F, 0x96, 0xE6, 0x92, 0x90, 0xD7, 0xCF, 0x0E, 0xBB, 0xBF, 0xBC,
  0x18, 0x3D, 0xBA, 0xAA, 0xA5, 0x09, 0x17, 0x5C, 0x52, 0x5D, 0x3D, 0x54, 0x4F, 0xCC, 0x52, 0x3C,
  0xAA, 0x35, 0xAD, 0x16, 0x9B, 0xE5, 0x92, 0x69, 0xCF, 0xE6, 0x6B, 0x84, 0x92, 0x6B, 0x66, 0x0C,
  0x5D, 0x39, 0x80, 0xCF, 0xB6, 0x56, 0x48, 0x00, 0xE9, 0x14, 0x9E, 0x9D, 0xD3, 0xCD, 0x75, 0x0D,
  0xC1, 0xA6, 0xEB, 0x6C, 0x98, 0x53, 0xA4, 0xF1, 0x31, 0x9B, 0x73, 0x83, 0xEF, 0x5D, 0xDD, 0xB6,
  0xA7, 0xCF, 0x5C, 0xE2, 0x68, 0x52, 0x9F, 0xF8, 0x0D, 0x33, 0x88, 0x81, 0x10, 0x78, 0x3B, 0x89,
  0xFE, 0xFD, 0xFC, 0xF3, 0xF7, 0xF7, 0xAF, 0x9A, 0x8A, 0xBB, 0x30, 0x13, 0x8C, 0xEA, 0x4F, 0x2C,
  0x43, 0x3F, 0x1A, 0x46, 0xC3, 0xD6, 0x9B, 0xFA, 0x51, 0xBA, 0xA0, 0x79, 0x95, 0x20, 0xEE, 0x18,
  0x86, 0x6E, 0x99, 0x7F, 0x0C, 0x51, 0x53, 0x69, 0x04, 0x45, 0xE6, 0xF7, 0xC9, 0x64, 0x3C, 0x84,
  0x13, 0x3E, 0x19, 0x37, 0x94, 0xA5, 0xD2, 0xBE, 0x60, 0x08, 0x3C, 0x8D, 0x62, 0x9E, 0xD8, 0x76,
  0x62, 0x7E, 0x7E, 0x1E, 0xD4, 0x3A, 0x3B, 0x2D, 0x54, 0xAE, 0x84, 0xF3, 0x81, 0xC3, 0x19, 0xDC,
  0x51, 0x2C, 0xC2, 0xFB, 0x1B, 0x20, 0x30, 0xBA, 0x88, 0xE2, 0x3E, 0xCA, 0x59, 0x71, 0x50, 0xFD,
  0x8D, 0x7F, 0x3F, 0x8B, 0xC2, 0x51, 0x0C, 0x7D, 0x84, 0x7B, 0x6C, 0xDD, 0xD5, 0xC8, 0x94, 0xF1,
  0xEB, 0xC2, 0xC1, 0x49, 0x95, 0xAA, 0x8F, 0x31, 0x5C, 0x1E, 0x31, 0x2D, 0xCA, 0x2A, 0x5C, 0x72,
  0x21, 0xE6, 0x58, 0xD5, 0x3D, 0x79, 0x0B, 0xB1, 0x61, 0x5E, 0x7C, 0x9A, 0xAC, 0xED, 0xDB, 0x0D,
  0xA1, 0x1A, 0x82, 0x95, 0xDD, 0x0A, 0xDD, 0x77, 0x06, 0x69, 0x66, 0x50, 0xE9, 0xC6, 0xB2, 0x7D,
  0xEC, 0x66, 0xE3, 0xF0, 0x3B, 0x27, 0xA4, 0x1B, 0x0F, 0x52, 0xCF, 0xD7, 0x7F, 0x40, 0x2D, 0x47,
  0x49, 0x77, 0x03, 0x00, 0x00
};

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

  // WiFi接続
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("WiFi connected. IP: ");
  Serial.println(WiFi.localIP());
  MDNS.begin("esp32");

  // WebSocket
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);

  // タスク作成
  xTaskCreatePinnedToCore(taskLidarRead, "LidarRead", 4096, NULL, 2, NULL, 1);
  xTaskCreatePinnedToCore(taskWebSocketSend, "WebSocketSend", 4096, NULL, 1, NULL, 1);
  xTaskCreatePinnedToCore(taskHttpServer, "HttpServer", 4096, NULL, 1, NULL, 1);

  server.enableCORS();
  server.on("/", HTTP_GET, []() {
    server.sendHeader(F("Content-Encoding"), F("gzip"));
    server.send_P(200, "text/html", (const char*)index_html_gz, index_html_gz_len);
  });
  server.begin();
}

void loop() {
  webSocket.loop();  // WebSocketのループ
}

これでESP32を起動するとLiDARが回り始めて、http://esp.localにアクセスするとLiDARのデータがプロットされる。Webページのデータもスケッチにgzで圧縮して埋め込んでしまったんだけど、やっていることはWebSocketでデータを受け取ったやつをXY座標を三角関数で計算してCanvasでプロットしてるだけ。

こんな感じでiPhoneからアクセスしてもリアルタイムでデータが送られてくる。Androidの場合はmDNS非対応なのでIPアドレスでアクセスする必要があるかも。

中心線が無いと動きがわかりにくいので中心線とかグリッドとか入れておいたほうがいいかな?