2022年8月26日金曜日

メカナムホイールのラジコンを分解してみた

 前回購入したメカナムホイールのラジコンを早速分解してみた。やっぱりオンオフ入力だけじゃ全然うまくコントロールできなくてすぐ飽きた…
改造してアナログ入力対応にしたいのでまずは調査。

裏側(スイッチがある方が裏)のプラスネジを6本取るとカバーが外れる。
作りは悪くない。というか思ったより良い作りをしていると思う。(改造ベースとして)
蓋を開けてもギアボックスが取れてきたりしないのでこの状態でも走れそう。
モーターは130モータが4つ。ギアボックスは前後で同じ設計のデュアルタイプのものが2つついてる。
バッテリは14500タイプのリチウムイオンバッテリだった。容量は500mAh。コネクタはPHコネクタっぽい。
JD-00742BBR-2213RX

制御基板の方は結構簡単な構造。モータドライバはMX1616Hが2つ。XC2455BってなってるICにはアンテナ出力がついていたり、モータドライバに直結されていたりしているので、2.4Ghzの無線内蔵のマイコンかな?SOT-23のICはXC2455B用の3.3Vレギュレータ。
分解する前は改造の際にモータドライバとしてDRV8833あたりを載せてもいいかなと思っていたんだけど、データシート見る限りはもともと乗っているMX1616Hがそのまま使えそうだ。MX1616Hの代わりにSA8301Sが2個乗ってるパターンもあるらしい。(VCCとVDD共通のIC)
MX1616HはMX1508とピンコンパチ。スペック的にも電子工作用に出回ってるMX1508やDRV8833に近いし、MX1508の情報がそのまま参考にできそう。MX1616とMX1616Lもあるようだけど何が違うんだろう…

電源スイッチはオフにすると充電端子に切り替わるように接続されていた。オンにすると充電器からは切り離される。充電回路はUSB端子の根本側についていた。

ちなみにモータの接続はM1が右後、M2が左後、M3が右前、M4が左前になってた。

モータドライバそのまま使えそうなので改造としてはXC2455Bを剥がして、各MX1616Hからケーブルを4本ずつ出せばいいのか。8本+GNDでモータ4個制御できそう。
ケーブルたくさん出すのが面倒なのでRoverCみたいにI2C経由でコントロールできるようにしたいなぁ。PWM8ch出せるお手軽なマイコンとかで…
まぁ8本出してきて全部ESP32に突っ込んでも良さそうだけど。

次回はついに基板を改造してみる。


2022年8月24日水曜日

小型ドローンSky Fight Xを買ってみた。

 昔やっていたクアッドコプター、登録するまでもないので最近解体したんだけど、おもちゃのドローンを買ってみようと思い立って買ってみた。
最初はHolyStoneのHS210あたりを狙っていたんだけど、同じサイズでSky Fight Xっていうのがヤフオクやメルカリに安く出ていたので、仲間内で募って複数台購入してみた。複数台割引で一人あたり920円だった。とても安い。

箱にぎっしり6台ほど詰まってきたが、ちゃんと交換用羽や日本語の説明書とかも人数分付属していた。バッテリは702025サイズの220mAhでコネクタはPHコネクタ。
中身はHS210やe010に似ている、モータがコネクタ式になっていたり、赤外線トランスポンダがついていた。事業用ドローンということもあって詳細は不明だけどドローンレースのタイム測定用とかに使うものかもしれない。DipスイッチでID変更できる感じだったし。
とりあえずトランスポンダは使わないので取り外した。重量は1gぐらいなんだけど、高輝度な赤外線LEDを積んでいるだろうからこれを外すだけでバッテリ消費が抑えられると思う。受信機がないと使い物にならないし。
気圧センサで高度を自動で保持してくれるんだけど、これも前にフライトコントローラーに対策していたようにスポンジを付けておいた。これで風が直接あたって値がふらつくのを防止できる気がする。
ちなみにレシーバはRF2500っていうICだった。e010と同じチップだと思う。プロポの方はRF2518なのでこれもe010と同じチップかも。もしかしてマルチプロトコルプロポとか使えたりしないかなぁ?プロポはブザーが少しうるさかったのでブザーの穴にテープを張っておいた。

飛ばした感じはめちゃめちゃ安定して飛ばすことができる。これは楽しい。一緒に購入した仲間もみんな結構ハマっているので良かった。みんな追加のバッテリが欲しくなっている。
使えそうなバッテリ探すか




2022年8月21日日曜日

メカナムホイールのラジコンを買ってみた

 メカナムホイールで遊びたくなったのでメカナムホイールを探していたんだけど、部品点数が多いからか地味にお高い。というわけでメカナムホイールを装着しているラジコンを探していたんだけど、2000円以下で売っているものを発見。メカナムホイール買うより安いのでポチってみた。

SQ2335-058SQ2335-058
DRIFT CAR EXTREME。メカナムホイールとは言っていない。
確かにメカナムホイールだとドリフトしてるような動きできるけど。まぁおもちゃだし。
SQ2335-058
説明書にはスタント用ドリフトリモコンカーってなってる。ちゃんと日本語の説明書だった。しかも変な日本語っぽくもなかったぞ…さすが定価は4000円以上するだけある。
そして本体は思ったより質感が良い。プロポはめっちゃしょぼい。

ラジコン本体はUSB充電できるバッテリー式。付属の充電器で充電中はUSBコネクタ部が赤く光る。充電完了で消える。
プロポは今どきの2.4Ghzタイプで乾電池が2本だった。自動ペアリングっぽい。
プロポは左のパッドは上下、右のパッドは左右にしか動かない。あとはLとRボタンにスタントボタン。全部タクトスイッチのデジタル入力。上下左右ぐらいはアナログが欲しかったなー。さすがおもちゃ。対象年齢が低いのもあって、電池蓋はネジ式である。

メカナムホイールは65mmタイプだった。60mmのやつがよく売っているけど、4個で3500円のことを考えるとホイール取り用にしてはいい買い物をした気がする。
電源を入れると青く光る。このラジコン、裏表どっちでも操作できるんだけどセンサーとかはついてないので裏になると操作が前後逆転する。
そしてやっぱりアナログ入力ほしいし、旋回がLとRボタンはとってもやりにくい。家の中で遊ぶのにいきなり全速全開は危ない。

まぁ少し遊んだら分解してみよう。多分これアナログスティックで操作できたらすごく楽しいと思う。ドリフトの定常円旋回みたいな動きは流石にデジタル入力だときつい。
Amazonで売っているOSOYOOのメカナムホイールキットは知り合いが購入したのを借りて、これまたコントローラアプリが微妙だったのでゲームパッドで遊べるようにソフトを変更してみたけどなかなか面白かった。

とりあえずこいつをゲームパッドで動くようにはしてみたいな。
次回は分解


2022年7月25日月曜日

タミヤの楽しい工作シリーズ

 夏と言ったら電子工作ということで久しぶりに梅沢無線に行ってきた。仲間内でやっている電子工作はみんな夏はアクティブなので冬にならないとなかなか進まないけど…
部品選定もぼちぼち進めようというわけで今回はタイヤを買ってみた。

昔からユニバーサルプレートと行ったらクローラを使っていたので今回はタイヤにしてみることに。クローラは旋回時にモータドライバに負荷がかかるので…
駆動形式は対向二輪にしようかなと。
駆動輪は懐かしの梵天丸と同じ、タミヤの楽しい工作シリーズのオフロードタイヤセット。受動輪はホームセンターで売っていたSTC-25 EMがTAMIYAのギヤボックスを使う上で高さがすごくちょうどいいんだけど、バック時に車体がブレるのでボールキャスタにしてみた。

梅沢無線の夏の20%オフセールで400円のところ320円に割り引かれた。
裏面の作例はユニバーサルプレートと同じような構成になっていた。
中身はタイヤとホイールがコンパクトに収納されている感じ。六角シャフトも付属してくる。一回分解してランナーを取ってから組み立てる感じ。
小学生のときよく遊んだなぁ…パワーを上げて坂を登ろうとするとタイヤとホイールの間で滑るので間に輪ゴムとかを入れて組んでいた記憶が…

お次はボールキャスターセット。これも20%オフで320円だった。
金属製のボールがついたボールキャスターが2セット入ってるっぽい。これは初めて購入したとおもう。
説明書を見る限りだと標準の高さが25mmと35mmと組み立て方で変更できるっぽい。
そんでもってさらに取付方法によっては11mm、16mm、27mm、37mmと調整できるっぽい。

ギアボックスはそのまま使ったんじゃ面白くないしなーということでまだ動力源をどうしようか決めていな買ったのでユニバーサルプレートの高さが色々変更できるのは良いな~
仲間内で3Dプリンタを持っている人がいるのでお願いしてマウントとか作ってもらってもいいかなぁ

さてモータ選定しなければ…


2022年7月16日土曜日

BluepillのCANでフィルタを使ってみた。

 Bluepillは廉価でCANを使えて便利なんだけど、メッセージが増えると受信処理がけっこう大変なことになるのでフィルタを設定してみた。

フィルタの設定方法を探していたらライブラリなしでCANを使えるようなスケッチを公開してくださった方がいたのでこちらに切り替えてみた。CANフィルタの説明もしてくれてるのでわかりやすい。

しかしスケッチが長いのでsetupとloopをカットしてヘッダファイルとしてinoファイルに取り込むことに。

#include "stm32f103-can.h"
#include <EEPROM.h>

#define LED_BUILTIN PC13

#define Protocol_Version 0x01
#define Hw_Version_Major 0x00
#define Hw_Version_Minor 0x00
#define Hw_Version_Variant 0x00
#define Fw_Version_Major 0x00
#define Fw_Version_Minor 0x00
#define Fw_Version_Revision 0x00
#define Fw_Version_Unreleased 0x00

//#define DEBUG_CAN_RCV
#define NODE_ID_DEFAULT 0x005
#define NODE_BC 0x3F

uint16_t nodeID = 0x005;

//HardwareSerial Serial1(PA10, PA9);

typedef union {  //IEEE 754 Float
  uint8_t uint[8];
  float value[2];
} FLOAT_BYTE_UNION;

uint8_t eepromRead(uint8_t reqID) {
  uint8_t lastData = 0xFF;
  for (uint16_t i = 0; i < EEPROM.length(); i += 2) {
    uint8_t dataID = EEPROM.read(i);
    uint8_t data = EEPROM.read(i + 1);
    //Serial1.println(dataID);
    if (dataID == reqID) {
      lastData = data;
      //Serial1.print("EEPROM Read:");
      //Serial1.println(i);
    }
    if (dataID == 0xFF | dataID == 0x00) {
      break;
    }
  }
  //Serial1.print("EEPROM Data:");
  //Serial1.println(lastData);
  return lastData;
}

void eepromWrite(uint8_t reqID, uint8_t data) {
  uint16_t lastID = 0xFFFF;
  for (uint16_t i = 0; i < EEPROM.length(); i += 2) {
    uint8_t eepromId = EEPROM.read(i);
    if (eepromId == 0xFF | eepromId == 0x00) {
      lastID = i;
      //Serial1.print("EEPROM Write:");
      //Serial1.println(i);
      break;
    }
  }
  if (lastID == EEPROM.length()) {
    lastID = 0;
  }
  EEPROM.write(lastID, reqID);
  EEPROM.write(lastID + 1, data);
}

void toggleLED() {
  static bool led;
  led = !led;
  digitalWrite(LED_BUILTIN, led);
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial1.begin(115200);

  bool ret = CANInit(CAN_250KBPS, 0);  // CAN_RX mapped to PA11, CAN_TX mapped to PA12
  //bool ret = CANInit(CAN_500KBPS, 2);  // CAN_RX mapped to PB8, CAN_TX mapped to PB9
  //bool ret = CANInit(CAN_500KBPS, 3);  // CAN_RX mapped to PD0, CAN_TX mapped to PD1
  //bool ret = CANInit(CAN_1000KBPS, 0);  // CAN_RX mapped to PA11, CAN_TX mapped to PA12
  //bool ret = CANInit(CAN_1000KBPS, 2);  // CAN_RX mapped to PB8, CAN_TX mapped to PB9
  //bool ret = CANInit(CAN_1000KBPS, 3);  // CAN_RX mapped to PD0, CAN_TX mapped to PD1

  nodeID = eepromRead(1);
  if (nodeID >= 0x3F) {
    eepromWrite(1, NODE_ID_DEFAULT);
    nodeID = NODE_ID_DEFAULT;
  }

  CAN1->FMR |= 0x1UL;
  CAN1->FMR &= 0xFFFFC0FF;
  CAN1->FMR |= 0x1C << 8;
  CANSetFilter(0, 1, 0, 0, ((nodeID << 5) << 21), 0xFC000004);
  CANSetFilter(1, 1, 0, 0, ((NODE_BC << 5) << 21), 0xFC000004);
  CAN1->FMR &= ~(0x1UL);
}

void loop() {
  if (CANMsgAvail()) {
    CAN_msg_t CAN_RX_msg;
    CANReceive(&CAN_RX_msg);
    uint16_t cmdID = uint16_t(CAN_RX_msg.id & 0x1F);
    uint16_t nodeIDrcv = CAN_RX_msg.id >> 5;
    CAN_msg_t CAN_TX_msg;
    memset(&CAN_TX_msg, 0, sizeof(CAN_msg_t));
    CAN_TX_msg.type = DATA_FRAME;
    CAN_TX_msg.format = STANDARD_FORMAT;
    CAN_TX_msg.id = ((nodeID << 5) | cmdID);
    if (nodeIDrcv == nodeID) {
      switch (cmdID) {
        // case 0x003:  //Get Motor Error
        //   break;
        // case 0x004:  //Get Encoder Error
        //   break;
        // case 0x005:  //Get Sensorless Error
        //   break;
        case 0x006:  //Set Axis Node ID
          if (CAN_RX_msg.type == DATA_FRAME) {
            uint32_t reqNodeID = CAN_RX_msg.data[0] | CAN_RX_msg.data[1] << 8 | CAN_RX_msg.data[2] << 16 | CAN_RX_msg.data[3] << 24;
            //Serial1.println(reqNodeID,HEX);
            if ((reqNodeID < 0x3F) && (reqNodeID != eepromRead(1))) {
              eepromWrite(1, reqNodeID);
              //Serial1.println("nodeID update");
            }
          }
          break;
        // case 0x009:  //Get Encoder Estimates
        //   break;
        case 0x012:  //Set Traj Accel Limits
          if (CAN_RX_msg.type == DATA_FRAME) {
            FLOAT_BYTE_UNION input;
            memcpy(input.uint, CAN_RX_msg.data, 8);
            //Serial1.println(input.value[0]);
            //Serial1.println(input.value[1]);
          }
          break;
        // case 0x014:  //Get IQ
        //   break;
        case 0x015:  //Get Temperature(FET IEEE 754 Float, Motor IEEE 754 Float)
          if (CAN_RX_msg.type == REMOTE_FRAME) {
            FLOAT_BYTE_UNION output;
            output.value[0] = (1.43 - analogRead(ATEMP) * 3.3 / 4095) / 0.0043 + 25;
            output.value[1] = analogRead(ATEMP);
            memcpy(CAN_TX_msg.data, output.uint, 8);
            CAN_TX_msg.len = 8;
            CANSend(&CAN_TX_msg);
          }
          break;
        // case 0x017:  //Get Vbus Voltage
        //   break;
        // case 0x01c:  //Get ADC Voltage
        //   break;
        default:
          break;
      }
    }
    if (CAN_RX_msg.type == REMOTE_FRAME) {
      switch (cmdID) {
        case 0x000:  //Get Version
          CAN_TX_msg.data[0] = Protocol_Version;
          CAN_TX_msg.data[1] = Hw_Version_Major;
          CAN_TX_msg.data[2] = Hw_Version_Minor;
          CAN_TX_msg.data[3] = Hw_Version_Variant;
          CAN_TX_msg.data[4] = Fw_Version_Major;
          CAN_TX_msg.data[5] = Fw_Version_Minor;
          CAN_TX_msg.data[6] = Fw_Version_Revision;
          CAN_TX_msg.data[7] = Fw_Version_Unreleased;
          CAN_TX_msg.len = 8;
          CANSend(&CAN_TX_msg);
          break;
        case 0x001:  //Heartbeat Message
          CAN_TX_msg.data[1] = nodeID >> 8;
          CAN_TX_msg.data[0] = nodeID;
          CAN_TX_msg.len = 8;
          CANSend(&CAN_TX_msg);
          break;
        default:
          break;
      }
    }
#ifdef DEBUG_CAN_RCV
    if (CAN_RX_msg.format == EXTENDED_FORMAT) {
      Serial1.print("Extended ID: 0x");
      if (CAN_RX_msg.id < 0x10000000) Serial1.print("0");
      if (CAN_RX_msg.id < 0x1000000) Serial1.print("0");
      if (CAN_RX_msg.id < 0x100000) Serial1.print("0");
      if (CAN_RX_msg.id < 0x10000) Serial1.print("0");
      if (CAN_RX_msg.id < 0x1000) Serial1.print("0");
      if (CAN_RX_msg.id < 0x100) Serial1.print("0");
      if (CAN_RX_msg.id < 0x10) Serial1.print("0");
      Serial1.print(CAN_RX_msg.id, HEX);
    } else {
      Serial1.print("Standard ID: 0x");
      if (CAN_RX_msg.id < 0x100) Serial1.print("0");
      if (CAN_RX_msg.id < 0x10) Serial1.print("0");
      Serial1.print(CAN_RX_msg.id, HEX);
      Serial1.print("     ");
    }

    Serial1.print(" DLC: ");
    Serial1.print(CAN_RX_msg.len);
    if (CAN_RX_msg.type == DATA_FRAME) {
      Serial1.print(" Data: ");
      for (int i = 0; i < CAN_RX_msg.len; i++) {
        Serial1.print("0x");
        Serial1.print(CAN_RX_msg.data[i], HEX);
        if (i != (CAN_RX_msg.len - 1)) Serial1.print(" ");
      }
      Serial1.println();
    } else {
      Serial1.println(" Data: REMOTE REQUEST FRAME");
    }
#endif
    toggleLED();
  }
}

ポートのRemapとかもわかりやすかった。フィルタの設定はbank1にフィルタ、bank2にマスクみたいな感じ?例も上げてくれているので助かった。しかしブロードキャスト対応にするためにフィルタを追加するところで少しハマった。

CANSetFilterの前後でちゃんとレジスタをいじってやらないとだめっぽい。setupに1行だけいれてちゃんと動いていたので気が付かなかった…
ということでこれで特定のノードIDとブロードキャスト以外はフィルタができるように設定できた。

とりあえずこんな感じのスケッチだとデバイスノードID(上位ビット)を設定するだけで、そのデバイス宛のコマンド(下位ビット)を受け取るみたいな感じに使えるっぽい。

FIFO0しか使っていないのでFIFO1を使おうと思ったんだけど、フィルタ設定してちゃんとsFIFOMailBox[1]に入ってきているようだけど、CANReceive関数とかを変更しないと行けないので今回は処理能力がギリギリとかじゃなかったのでFIFO0のみを使ってみた。簡単にやるならCANReceiveをごそっとコピーしてもう一つFIFO1用の関数にしてしまっても良さそう。

ちなみに今回のサンプル、何個か持ってるBluepillの中このスケッチが動かないBluepillがあったんだけど、乗ってるICがGD32F103っていう互換チップだった。よく3Dプリンタの制御ボードではSTM32F103の代わりとして導入されていて、Klipperとかのカスタムファームウェアでも普通にSTM32でビルドしたバイナリが動くらしい。GD32F303っていうICを使った3Dプリンタ制御ボードもあるらしいけど、これもSTM32F103のバイナリが動くっぽい。(どちらもピン配まで一緒のdrop-in compatible)
ちなみに前に使ったeXoCANライブラリはCKS32F103っていうSTM32F103互換ICでも動くっぽいので試してみたらGD32F103でも動作した。もしかしたら303でも動くかも。
互換ICが乗ってるBluepillが増えてきてるようなので注意が必要だな…

2022年6月18日土曜日

ESP32-CAMのWiFi自動再接続

 ESP32-CAM、お安いしバッテリつけてラジコンに乗せてFPVみたいにして遊ぶのも楽しいので、何人かの友人も使っているが(ESP-32S剥がすのも慣れた)WiFiルータを再起動すると自動で再接続してくれないのが不便なのでスケッチを修正してみた。

ベースのスケッチはマルチクライアント対応のものを使って、mDNSとSoftAP自動切り替え、SoftAP時のCaptivePortal、UPnPとかを盛り込んでいた。みんな色んな使い方しているので色々盛り込んだ感じ…
なんか問題あったときのファームウェアアップデートも面倒なのでOTAにも対応(ブラウザ上からbinファイルをアップロードする感じ)

本家のアップデートが止まっているようなので今回はついでにESP32 Arduino Core 2.0.4に対応させてみた。(そのままだと1.0.5か1.0.6じゃないとビルドできない)

バリエーションがあるんだけどいつもesp32-mjpeg-multiclient-espcam-driversの中のesp32-cam-rtosをベースに改良しているので、(いじっているのはinoファイルだけ)これで作業している前提でアップデート手順。

まずはこのフォルダ内の"esp32-cam-rtos.ino"ファイルと"camera_pins.h"の2つのファイルを残して他のファイルを削除。これ以外のファイルはESP32 Camera Driverなので今回はこれをアップデートするため、削除しておく感じ。

ESP32 Camera Driverから最新のカメラドライバをダウンロードしてくる。今回は2.0.1のZIPを使用した。ZIPをそのままエクスプローラで開いて"examples"フォルダと"test"フォルダを削除する。続いてesp32-camera-master/targetの中のesp32s2とesp32s3フォルダを削除。そんでもってこのZIPファイルをフォルダ構成なしで展開する。今回はWinRARを使って高度な設定でパスを無視にチェックを入れて解答することでいけた。

あとは解凍したファイルを"esp32-cam-rtos.ino"ファイルと"camera_pins.h"の2つのファイルと一緒のディレクトリにコピーしてビルドするだけ。これでArduino Core ESP32 2.0.4でもエラーなしでコンパイルすることができるようになった。

しかしArduino Core 2.0.4とCamera Driver 2.0.1の組み合わせだとメモリ不足でうまく起動しなくなってしまったのでxTaskCreatePinnedToCoreでマルチタスクを起動しているところの3箇所目、Start mainstreaming RTOS taskのスタックサイズを 2 * 1024 から 3 * 1024 に変更することでうまく動かすことができた。

とりあえずこれで最新の環境でビルドできるようになったところで、本題のWiFiのアクスポイントが再起動すると自動で再接続してくれない対策。おそらくESP32-CAMにかぎらず常時APに接続して運用しているESP32搭載デバイスには使える手法かも。

方法は何種類かあると思うんだけど、定期的にWiFi.status()でWiFiの接続情報を確認して再接続する方法と、WiFiEventのWiFi切断されたときのイベントを使用して再接続する方法があるらしい。今回はWiFiEventを使用してみた。

Arduino Core 2.0.xと1.0.xでちょっとイベント名が変わったりしているので注意かも。今回は2.0.4用。

まずはWiFi切断イベントがトリガされたときに読み込まれる関数を作成。

void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info){
  //digitalWrite(LED_BUILTIN, LOW);
  Serial.println("Disconnected from WiFi access point");
  Serial.print("WiFi lost connection. Reason: ");
  Serial.println(info.wifi_sta_disconnected.reason);
  Serial.println("Trying to Reconnect");
  WiFi.begin();
  //digitalWrite(LED_BUILTIN, HIGH);
}

これをスケッチの何処かに入れておく。
そんでもってsetupの何処かに

WiFi.onEvent(WiFiStationDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);

を入れる。(1行)この行が読み込まれた瞬間にESP32のWiFi切断イベントが有効になるのでWiFi接続後とかがいいかも。APモードでもWiFiに繋がってないときはトリガされてしまったので、SoftAPで起動するときは有効にしないような感じにしておいた。

一応これでAPを再起動しても何度かイベントが発生してしまうけど問題なくWiFiに再接続することができた。

そういえばESP32-CAM、何台か購入していると基板の厚みが1.6と1.0があったり、RSTがピンヘッダに出ていたり、カメラの縦横が違かったり、カメラの画質も違うんだよねぇ…
カメラの画質に関してはレンズなのかねぇ(ただしレンズも互換性なかったりしている)
技適対応用のESP-WROOM-32Dもディスコンだし、ESP32-WROVER版とかあるとスッキリしていいんだけどねぇ


2022年6月11日土曜日

ESP32でLiDAR LDS-006からデータを取得してみる。

 この前ROSでLDS-006からデータを取得したとき、このLiDARはスペック的に1週5Hzぐらいのデータしか取れなかったのでESP32でも行けるんじゃないかということでやってみることに。

LDS-006は電源は5V何だけど信号ラインは3.3VレベルなのでESP32に直結できる。ESP32に接続して距離データを取得してみることに。

#include <Arduino.h>

#define RXD2 16
#define TXD2 17

// --- グローバル変数 ---
uint16_t lidar360[360];   // 1周分距離データ
uint32_t lastTime = 0;    // 前回受信時刻
uint16_t lastRpm = 0;     // 最後のRPM
bool newDataReady = false; // 1周分揃ったらtrue

QueueHandle_t lidarQueue;

struct LidarInfo {
  uint16_t rpm;
  uint32_t dt; // 前回からの時間差(ms)
};

void taskLidarRead(void *pvParameters) {
  uint8_t packet[22];
  int packetIndex = 0;
  static int count = 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 index = packet[1] - 0xA0;
          if (index <= 89) { // 0〜89ブロック
            // 4点ずつ距離データ取得
            for (int i = 0; i < 4; i++) {
              int offset = 4 + i * 4;
              uint16_t dist = ((packet[offset + 1] & 0x3F) << 8) | packet[offset + 0];
              int angle = index * 4 + i;

              if (angle < 360) {
                lidar360[angle] = dist;
              }
            }

            // パケット0でRPMとΔtを計算
            if (index == 0) {
              uint16_t speed = (packet[3] << 8) | packet[2];
              uint16_t rpm = speed / 64;

              uint32_t now = millis();
              uint32_t dt = (lastTime == 0) ? 0 : (now - lastTime);
              lastTime = now;
              lastRpm = rpm;

              LidarInfo info = { rpm, dt };
              xQueueSend(lidarQueue, &info, 0);
            }

            // 1周揃ったらフラグ
            if (index == 89) {
              newDataReady = true;
            }
          }
        }
        packetIndex = 0;
      }
    }
    vTaskDelay(1);
  }
}

void taskPcSend(void *pvParameters) {
  LidarInfo info;
  while (1) {
    if (xQueueReceive(lidarQueue, &info, portMAX_DELAY)) {
      Serial.print("RPM=");
      Serial.print(info.rpm);
      Serial.print("  Δt=");
      Serial.print(info.dt);
      Serial.println(" ms");

      // 1周分距離データが揃ったらカンマ区切りで送信
      if (newDataReady) {
        for (int i = 0; i < 360; i++) {
          Serial.print(lidar360[i]);
          if (i < 359) Serial.print(",");
        }
        Serial.println();
        newDataReady = false;
      }
    }
  }
}

void setup() {
  Serial.begin(115200);
  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);

  delay(100);
  Serial2.print("startlds$"); // LDS-006 起動

  lidarQueue = xQueueCreate(16, sizeof(LidarInfo));
  if (!lidarQueue) {
    Serial.println("Queue create failed!");
    while (1);
  }

  xTaskCreatePinnedToCore(taskLidarRead, "LidarRead", 4096, NULL, 3, NULL, 1);
  xTaskCreatePinnedToCore(taskPcSend, "PcSend", 4096, NULL, 1, NULL, 1);
}

void loop() {
  
}

最初はloopの中で全部やっていたんだけどデータの取りこぼしがあったので別タスクでやってみることにした。200ms周期でデータが取得できているのでぴったり5Hzで1周分のデータが揃う用になっていた。回転数は465rpmから470rpmをキープしている。

LDS-006のデータ更新周期は5Hzってことがわかったのであとは距離を測ってみないと。

あとはどうやってこのデータを処理すればよいのか…