ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【esp32/3Dプリンタ】3DプリンタとM5 Atom Liteでミニ四駆ラジコンを作ってみた

 過去の記事でPS3コントローラとESP32でBLE通信をやりました。

 最近になって3Dプリンタを使える機会があったので、N番煎じになりますが「一回ミニ四駆ラジコン作ってみるか」と思い作ってみました。
 割と試行錯誤しながら作った結果、いくつか失敗したところもあったのでそれも含めて記事にしようと思った次第です。


 では、始めます。


1:設計コンセプト
 まずは作る上でどのようなコンセプトにするかを決めました。ブレッドボード上での簡単な回路実験などから、今回は以下のようにコンセプトを決めました。

1:車体自体(ミニ四駆で言うシャーシ部分)を3Dプリンタで自作する
2:できる限り汎用的な設計にする
3:電池で動くようにする

 これらのコンセプトを元に作っていくようにしました。


2:回路設計とプログラム
 コンセプトは決まったので、まずは電池とマイコンでモータを制御する回路を実験しつつ設計していきました。

 当初はモータを1つにしサーボモータを動かして前輪をステアリングさせることを考えていましたが、色々と難しそうだったので「モータ2つでそれぞれ左右のタイヤを制御する」という方式を採用することにしました。

 ブレッドボード上で色々と実験した結果、使用する部品と回路、プログラムは以下のようになりました。

  • 回路部品一覧
部品名 個数 備考
ATOM Lite 1 マイコン
AE-XCL102D503CR-G 1 5V出力昇圧DCDCコンバータ
DRV8835 モータドライバモジュール 1 モータドライバ
ユニバーサル基板AE-D1 1 ユニバーサル基板
スライドスイッチ SS-12D00G3 1 電源スイッチ
ピンヘッダ 1×40 1 ATOM Lite固定用
DCモータ 2 ノーマルモータ
単4電池ボックス(2本) 1 電池ボックス
XHコネクタ2Pセット 1 電源用コネクタ
EHコネクタ2Pセット 2 モータ用コネクタ
  • 回路図


  • プログラム

・M5Atom_ps3_rc.ino

/**
 * PS3コントローラでモータ制御
 */
#include "M5Atom.h"
#include <Servo.h>
#include <Ps3Controller.h>
#include "Drv8835.h"

/* ピン定義 */
const int A_IN_1 = 22;
const int A_IN_2 = 19;
const int B_IN_1 = 33;
const int B_IN_2 = 23;

/* チャンネルの定義 */
const int A_IN_1_CHANNEL = 1;
const int A_IN_2_CHANNEL = 2;
const int B_IN_1_CHANNEL = 3;
const int B_IN_2_CHANNEL = 4;

const int SPEED_VALUE    = 255; // PWMで入力する値(MAXは255)
const int LED_BRIGHTNESS = 10;  // LEDの明るさ
const int DELAY_TIME     = 200; // 待ち時間

/* 状態 */
const int CAR_COAST      = 0; // 空転
const int CAR_FORWARD    = 1; // 前進
const int CAR_REVERSE    = 2; // 後進
const int CAR_RIGHT_TURN = 3; // 右回り
const int CAR_LEFT_TURN  = 4; // 左回り
const int CAR_BRAKE      = 5; // ブレーキ

Drv8835 motor = Drv8835(); // モータドライバ

void setup() {
  // 本体初期化(UART有効, I2C無効, LED有効)
  M5.begin(true, false, true);
  Ps3.begin("xx:xx:xx:xx:xx:xx");  // M5AtomのMacアドレス

  Serial.begin(115200);
  showMsgLed("Pairing Ready...", 255, 0, 0); // LED赤色

  // ペアリング待ち
  while (!Ps3.isConnected()){
    delay(1000);
  }

  // モータドライバの準備
  motor.setAin(A_IN_1, A_IN_2, A_IN_1_CHANNEL, A_IN_2_CHANNEL);
  motor.setBin(B_IN_1, B_IN_2, B_IN_1_CHANNEL, B_IN_2_CHANNEL);

  // ペアリングできたらLEDを青に
  showMsgLed("Pairing OK", 0, 0, 255);

  Serial.println("--- setup end ---");
}

void loop() {
  M5.update();  //本体のボタン状態更新

  // 車の状態を取得
  int car_status = getCarStatus();

  if (Ps3.data.button.circle)  {
    // 〇ボタン押下時(前進)
    Serial.println("push Circle");
    if (car_status == CAR_REVERSE || car_status == CAR_RIGHT_TURN || car_status == CAR_LEFT_TURN) {
      // 前進以外でモーターが動いている時は一度全部のモータを止める
      motor.brakeA();
      motor.brakeB();
      delay(DELAY_TIME);
    }
    // 前進
    motor.forwardA(SPEED_VALUE);
    motor.forwardB(SPEED_VALUE);
  } else if (Ps3.data.button.r1) {
    // R1ボタン押下時(右回り)
    Serial.println("push R1");
    if (car_status == CAR_FORWARD || car_status == CAR_REVERSE || car_status == CAR_LEFT_TURN) {
      // 右回り以外でモーターが動いている時は一度全部のモータを止める
      motor.brakeA();
      motor.brakeB();
      delay(DELAY_TIME);
    }
    // 右回り
    motor.forwardA(SPEED_VALUE);
    motor.coastB();
  } else if (Ps3.data.button.l1) {
    // L1ボタン押下時(左回り)
    Serial.println("push L1");
    if (car_status == CAR_FORWARD || car_status == CAR_REVERSE || car_status == CAR_RIGHT_TURN) {
      // 左回り以外でモーターが動いている時は一度全部のモータを止める
      motor.brakeA();
      motor.brakeB();
      delay(DELAY_TIME);
    }
    // 左回り
    motor.coastA();
    motor.forwardB(SPEED_VALUE);
  } else if (Ps3.data.button.triangle){
    // △ボタン押下時(逆転)
    Serial.println("push Triangle");
    if (car_status == CAR_FORWARD || car_status == CAR_RIGHT_TURN || car_status == CAR_LEFT_TURN) {
      // 逆転以外でモーターが動いている時は一度全部のモータを止める
      motor.brakeA();
      motor.brakeB();
      delay(DELAY_TIME);
    }
    // 逆転
    motor.reverseA(SPEED_VALUE);
    motor.reverseB(SPEED_VALUE);
  } else if (Ps3.data.button.cross) {
    // ×ボタン押下時(ブレーキ)
    Serial.println("push Cross");
    motor.brakeA();
    motor.brakeB();
  } else {
    // ボタンを押していない時は空転
    motor.coastA();
    motor.coastB();
  }

  delay(100);
}

// Serial文字列の送信とLEDの色を変更する関数
void showMsgLed(String s, uint8_t r, uint8_t g, uint8_t b) {
  M5.dis.setBrightness(LED_BRIGHTNESS); // LEDの光量(0-255)
  M5.dis.drawpix(0, getDispColor(r, g, b));
  Serial.println(s);
}

// FastLEDに設定する色を取得する関数
CRGB getDispColor(uint8_t r, uint8_t g, uint8_t b) {
  return (CRGB)((r << 16) | (g << 8) | b);
}

// 車の状態を取得する関数
int getCarStatus() {
  int statusA = motor.getStatusA();
  int statusB = motor.getStatusB();

  if (statusA == Drv8835::FORWARD && statusB == Drv8835::FORWARD) {
    // A:正転、B:正転=前進
    return CAR_FORWARD;
  } else if (statusA == Drv8835::REVERSE && statusB == Drv8835::REVERSE) {
    // A:逆転、B:逆転=逆転
    return CAR_REVERSE;
  } else if (statusA == Drv8835::FORWARD && statusB == Drv8835::COAST) {
    // A:正転、B:逆転=右回り
    return CAR_RIGHT_TURN;
  } else if (statusA == Drv8835::COAST && statusB == Drv8835::FORWARD) {
    // A:逆転、B:正転=左回り
    return CAR_LEFT_TURN;
  } else if (statusA == Drv8835::BRAKE && statusB == Drv8835::BRAKE) {
    // A:ブレーキ、B:ブレーキ=ブレーキ
    return CAR_BRAKE;
  } else {
    // 上記以外:空転
    return CAR_COAST;
  }
}


・Drv8835.h

/**
 * DRV8835モータドライバのクラス
 */

class Drv8835 {

  public:
    static const int COAST   = 0;
    static const int FORWARD = 1;
    static const int REVERSE = 2;
    static const int BRAKE   = 3;

    Drv8835();
    void setAin(int in1, int in2, int c_in1, int c_in2);
    void setBin(int in1, int in2, int c_in1, int c_in2);
    void forwardA(uint32_t pwm);
    void forwardB(uint32_t pwm);
    void reverseA(uint32_t pwm);
    void reverseB(uint32_t pwm);
    void brakeA();
    void brakeB();
    void coastA();
    void coastB();
    int getStatusA();
    int getStatusB();
    

  private:
    const int LEDC_TIMER_BIT = 8;   // PWMの範囲(8bitなら0〜255、10bitなら0〜1023)
    const int LEDC_BASE_FREQ = 490; // 周波数(Hz)
    const int VALUE_MAX = 255;      // PWMの最大値

    int a_status = COAST;
    int a_in1 = 0,   a_in2 = 0;
    int a_c_in1 = 0, a_c_in2 = 0;

    int b_status = COAST;
    int b_in1 = 0,   b_in2 = 0;
    int b_c_in1 = 0, b_c_in2 = 0;
};

Drv8835::Drv8835() {
  // コンストラクタ
}

void Drv8835::setAin(int in1, int in2, int c_in1, int c_in2) {
  // AINのピン番号とチャンネルを設定
  a_in1 = in1;
  a_in2 = in2;
  a_c_in1 = c_in1;
  a_c_in2 = c_in2;

  pinMode(a_in1, OUTPUT); // IN1
  pinMode(a_in2, OUTPUT); // IN2

  // ピンのセットアップ
  ledcSetup(a_c_in1, LEDC_BASE_FREQ, LEDC_TIMER_BIT);
  ledcSetup(a_c_in2, LEDC_BASE_FREQ, LEDC_TIMER_BIT);

  // ピンのチャンネルをセット
  ledcAttachPin(a_in1, a_c_in1);
  ledcAttachPin(a_in2, a_c_in2);
}

void Drv8835::setBin(int in1, int in2, int c_in1, int c_in2) {
  // BINのピン番号とチャンネルを設定
  b_in1 = in1;
  b_in2 = in2;
  b_c_in1 = c_in1;
  b_c_in2 = c_in2;

  pinMode(b_in1, OUTPUT); // IN1
  pinMode(b_in2, OUTPUT); // IN2

  // ピンのセットアップ
  ledcSetup(b_c_in1, LEDC_BASE_FREQ, LEDC_TIMER_BIT);
  ledcSetup(b_c_in2, LEDC_BASE_FREQ, LEDC_TIMER_BIT);

  // ピンのチャンネルをセット
  ledcAttachPin(b_in1, b_c_in1);
  ledcAttachPin(b_in2, b_c_in2);
}

void Drv8835::forwardA(uint32_t pwm) {
  // AOUTを正転
  if (pwm > VALUE_MAX) {
    pwm = VALUE_MAX;
  }
  ledcWrite(a_c_in1, pwm);
  ledcWrite(a_c_in2, 0);
  a_status = FORWARD;
}

void Drv8835::forwardB(uint32_t pwm) {
  // BOUTを正転
  if (pwm > VALUE_MAX) {
    pwm = VALUE_MAX;
  }
  ledcWrite(b_c_in1, pwm);
  ledcWrite(b_c_in2, 0);
  b_status = FORWARD;
}

void Drv8835::reverseA(uint32_t pwm) {
  // AOUTを逆転
  if (pwm > VALUE_MAX) {
    pwm = VALUE_MAX;
  }
  ledcWrite(a_c_in1, 0);
  ledcWrite(a_c_in2, pwm);
  a_status = REVERSE;
}

void Drv8835::reverseB(uint32_t pwm) {
  // BOUTを逆転
  if (pwm > VALUE_MAX) {
    pwm = VALUE_MAX;
  }
  ledcWrite(b_c_in1, 0);
  ledcWrite(b_c_in2, pwm);
  b_status = REVERSE;
}

void Drv8835::brakeA() {
  // AOUTをブレーキ
  ledcWrite(a_c_in1, VALUE_MAX);
  ledcWrite(a_c_in2, VALUE_MAX);
  a_status = BRAKE;
}

void Drv8835::brakeB() {
  // BOUTをブレーキ
  ledcWrite(b_c_in1, VALUE_MAX);
  ledcWrite(b_c_in2, VALUE_MAX);
  b_status = BRAKE;
}

void Drv8835::coastA() {
  // AOUTを空転
  ledcWrite(a_c_in1, 0);
  ledcWrite(a_c_in2, 0);
  a_status = COAST;
}

void Drv8835::coastB() {
  // BOUTを空転
  ledcWrite(b_c_in1, 0);
  ledcWrite(b_c_in2, 0);
  b_status = COAST;
}

int Drv8835::getStatusA() {
  // Aのステータスを返す
  return a_status;
}

int Drv8835::getStatusB() {
  // Bのステータスを返す
  return b_status;
}

 回路については詳しくないので色々と間違いや足りないところもあると思いますが、ブレッドボード上で2つのモータをマイコンで制御できたのでこれで進めることにしました。


3:自作シャーシの設計
 回路がひとまず決まったので、この回路に合うように自作シャーシを設計していきました。

 まずはモータボックス部分からやろうということで試行錯誤と微調整を繰り返し、最終的には以下のモデルになりました。

  • モータボックスモデル(モータとギア有)


  • モータボックスモデル(モータとギアなし)

 2つ穴を開けているのはギア部分を隠すようなカバーを取り付けるための穴です。実際にそのカバーも最後にモデルを作成しました。



 モータボックスができたので次は実際にモータボックスのギアと噛み合ってそれを車輪に伝えるギアボックス部分を作成していきました。

 ここでは汎用性も考え、同じものを2つ作成することでうまく動くようにしたいのとモータボックスとはパチ組みできる形にしたいと思いました。
 こちらも試行錯誤と微調整を繰り返した結果、以下のモデルになりました。

  • ギアボックス(ギア有)


  • ギアボックス(ギアなし)


  • ギアボックス(モータボックス有)


 次に2つのギアボックスを繋ぐボディ部分を作成していきました。
 ユニバーサル基板やギアボックスを固定するためのナットを入れる穴やネジ穴、プロペラシャフトが跳ねた時に押さえつける突起部分などを試行錯誤しつつ、最終的に以下のモデルになりました。
 ちなみに電池ボックスは下側に配置し、裏から電池をセットするようにしています。

  • ボディ(ギアボックス有)


  • ボディ(ギアボックスなし)


 これでモデルが完成したので、あとはこれらを3Dプリンタで印刷しました。ボディ以外は全て2つずつ印刷しました。

 あとは回路以外の不足している部品などを購入しました。

 シャーシ側として購入したものは以下になります。

  • シャーシ部品一覧
部品名 個数 備考
M3 六角ナット 7 各種固定用。ナットの厚みは約2mm
M3×15ネジ 10本入 1 ユニバーサル基板固定用。実際に使うのは2本
M3×8ネジ 185本入 1 ギアボックス固定用。実際に使うのは4本
60mm 中空スレンレスシャフト 2本入 1 六角シャフト。切断して使う
ミニ4駆 アルミシャフトストッパー 4個 1 六角シャフト切断後の固定用
M2 ステンレスワッシャー 100個入り 1 実際に使うのは4つでタイヤ用
大径ナローホイルタイヤ 1 タイヤ
ARシャーシ セッティング ギヤセット 2 プロペラシャフトとギア用
M3 5mm スペーサー 100個入 1 実際に使うのは2つ
銅箔テープ 1 モータボックス用


 モータボックスには銅箔テープを貼り付けてその先に導線をはんだ付けしました。こうすることでモータ自体の交換もできるようにしました。


4:組み立てと動作確認
 ユニバーサル基板をはんだ付けし、3Dプリンタで出力した部品と購入した部品を組み合わせた完成形は以下のようになりました。



 ギアは比較的力の出る赤とうす茶色の組み合わせにしました。

 実際に動かしてみると、ちゃんとPS3コントローラとペアリングが成功した後、〇ボタンで前進、△ボタンで後進はできていました。
 しかしR1やL1ボタンで方向転換しようとすると遅いながらも方向転換できる時もありますが、ガリガリ音を立ててギアが空転して動かない場合もありました。

 おそらくモータボックスとギアボックスとのギアのかみ合わせが悪いのが原因かと思っていますが、そうなると色々なところの再設計が必要になるので今後の課題ということで今回は終わりにすることにしました。

 一応動作確認の動画も貼り付けておきます。


 以上が3DプリンタとM5 Atom Liteでミニ四駆ラジコンを作ってみた内容になります。

 3Dモデル自体を公開してもよいのですが、素人なのであまり綺麗に作れていないのと現時点で課題が残っているものを公開してもどうかという思いがあります。
 また3Dプリンタの機種や精度、個体差によっても出力結果が違ってくる(ミニ四駆程度の大きさであれば0.3mmのずれでも結構大きい)のでモデルを出力してもうまく動く保証はないため特に意味はないと考え公開しないようにしています。

 割と時間をかけて今回のミニ四駆ラジコンを作ってみましたが、ミニ四駆の精度に改めて驚かされましたし、モデリングの微調整などかなり勉強にはなりました。

 また時間があれば再設計をしたり、当初の目的だったステアリング制御のものを作ってみたいと思います。


・参考資料

【esp32】M5 Atom LiteでPS3コントローラの値を取得

 ゲーム機のコントローラでマイコンを制御してみたいと思い立ち色々と調べたところ、割と簡単にesp32でPS3のコントローラの値を取得できることがわかったので今回はその備忘録になります。

 また今回の開発環境はWindowsのみになるので注意してください。


 では、始めます。


0:esp32とPS3コントローラの準備
 今回esp32のマイコンとしてAtom Liteを使用しました(※他のesp32系のボードでは本記事のソースコードがそのままでは動かない可能性もあるので注意してください)。

 また前提としてArduino IDEとesp32、Atomを扱える状態にまでしておく必要があります。

 Atom Matrixの方ですが、過去に環境構築してサンプルを動かした記事があるのでそちらを参照してください。


 PS3コントローラ(Bluetooth対応)とPS3コントローラと接続するUSBケーブルを準備します。
 Bluetooth対応PS3コントローラは中古ショップなどで2000円しないぐらいで購入できると思います。

 PS3はモバイルバッテリーなどからは充電できないようで、PCなどのUSBポートから充電するようにしてください。


1:本体のMACアドレスを取得する
 まずはesp32とPS3をペアリングしなければBluetoothでの通信はできないので、esp32のMACアドレスを確認します。

 以下のプログラムをAtom Liteに書き込みます。

・M5Atom_macadrs.ino

#include "M5Atom.h" // 適宜書き換える(今回はM5Atom Lite)

void setup() {
  Serial.begin(115200);
  Serial.println("-----------------------------");
  uint8_t btmac[6];
  esp_read_mac(btmac, ESP_MAC_BT);
  Serial.printf("[Bluetooth] Mac Address = %02X:%02X:%02X:%02X:%02X:%02X\r\n", btmac[0], btmac[1], btmac[2], btmac[3], btmac[4], btmac[5]);
}

void loop() {

}


 このスケッチを書き込んでからシリアルモニタを開いてAtom Liteのリセットボタンを押下すると、以下のようにMACアドレスが表示されるのでその値をメモ/コピーしておきます。


2:PS3コントローラにペアリングしたいMACアドレスを書き込む
 PS3のコントローラのペアリングは、コントローラ側に予めペアリング先のMACアドレスを設定しておく必要があります。

 まずは、PC側の書き込みツールをインストールします。

 次のページを開きます。

 赤枠部分をクリックします。

 ダウンロード先を選択してクリックします。ここではUKを選択しました。

 クリックするとダウンロードページが開き、数秒待つと自動的にexeファイルがダウンロードされます。

 ダウンロードしたインストーラを起動し「Next」をクリックします。

 ダウンロード先を指定して「Next」をクリックします。デフォルトのままで問題ないかと思います。

 スタートメニューのショートカットもデフォルトのままで「Next」をクリックします。

 設定確認の画面になるので「Install」をクリックしてインストールを開始します。

 インストールが完了したら「Finish」をクリックします。

 これで書き込むツールのインストールは完了です。

 インストール後、PCとPS3コントローラをUSBケーブルで接続します。

 その状態でWindowsの検索欄に「sixaxis」と入力して出てきた「SixaxisPairTool」を選択してツールを起動させます。

 初回起動時には以下のようにドライバのインストールが始まる場合があるので、しばらく待ちます。

 ドライバのインストール後、以下のような画面になるので「Change Master」と書かれている方に1で確認したMACアドレスを入力し「Update」ボタンをクリックします。

 すると「Current Master」と「Change Master」の値が同じになります。

 これでPS3コントローラへのMACアドレスの書き込みは完了になります。


3:Arduino IDEPS3コントローラのライブラリをインストールする
 次にArduino IDEPS3コントローラのライブラリをインストールします。

 以下のGithubページにアクセスします。

 ページ右上にある「Code」をクリックし、出てきた中にある「Download ZIP」をクリックしてzipファイルをダウンロードします。

 Arduino IDEを開いてメニューの「スケッチ」→「ライブラリをインクルード」→「ZIP形式のライブラリをインストール」を選択します。

 先ほどダウンロードしたzipファイルを指定して「開く」をクリックします。

 これでPS3コントローラをArduinoで扱えるようになりました。


4:Bluetoothのサンプル
 準備ができたので、実際にBluetoothの通信ができるかを確認します。

 Atom Liteに以下のスケッチを書き込みます。

※「Ps3.begin("xx:xx:xx:xx:xx:xx");」のところは各自のMACアドレスに適宜変更してください

・M5Atom_ps3con_test.ino

/**
 * M5Atom Lite PS3 controller
 */
#include "M5Atom.h"
#include <Ps3Controller.h>

const int LED_BRIGHTNESS = 100;

// analogスティックの値を保持する変数
uint8_t b_lx = 0, b_ly = 0, b_rx = 0, b_ry = 0;

void setup() {
  // 本体初期化(UART有効, I2C無効, LED有効)
  M5.begin(true, false, true);
  Ps3.begin("xx:xx:xx:xx:xx:xx");  // M5AtomのMacアドレスに変更

  Serial.begin(115200);
  showMsgLed("Pairing Ready...", 255, 0, 0); // LED赤色

  // ペアリング待ち
  while (!Ps3.isConnected()){
    delay(1000);
  }

  // ペアリングできたらLEDを青に
  showMsgLed("Pairing OK", 0, 0, 255);
}


void loop() {
  M5.update();  //本体のボタン状態更新

  if (Ps3.data.button.start) { showMsgLed("start", 0xF0, 0xF8, 0xFF); }     // start    :AliceBlue
  if (Ps3.data.button.select){ showMsgLed("select", 0x99, 0x66, 0xCC); }    // select   :Amethyst

  if (Ps3.data.button.l1)    { showMsgLed("button L1", 0xFA, 0xEB, 0xD7); }      // button L1 :AntiqueWhite
  if (Ps3.data.button.l2)    { showMsgLed("button L2", 0xFF, 0x7F, 0x50); }      // button L2 :Coral
  if (Ps3.data.button.l3)    { showMsgLed("analog L3 push", 0x64, 0x95, 0xED); } // analog L3 :CornflowerBlue

  if (Ps3.data.button.r1)    { showMsgLed("button R1", 0x00, 0xFF, 0xFF); }      // button R1 :Aqua
  if (Ps3.data.button.r2)    { showMsgLed("button R2", 0xFF, 0xF8, 0xDC); }      // button R2 :Cornsilk
  if (Ps3.data.button.r3)    { showMsgLed("analog R3 push", 0x00, 0x00, 0x8B); } // analog R3 :DarkBlue

  if (Ps3.data.button.up)   { showMsgLed("up", 0x7F, 0xFF, 0xD4); }         // ↑ :Aquamarine
  if (Ps3.data.button.right){ showMsgLed("right", 0xF0, 0xFF, 0xFF); }      // → :Azure
  if (Ps3.data.button.down) { showMsgLed("down", 0xF5, 0xF5, 0xDC); }       // ↓ :Beige
  if (Ps3.data.button.left) { showMsgLed("left", 0xFF, 0xE4, 0xC4); }       // ← :Bisque

  if (Ps3.data.button.cross)   { showMsgLed("cross", 0x8A, 0x2B, 0xE2); }    // X :BlueViolet
  if (Ps3.data.button.square)  { showMsgLed("square", 0xA5, 0x2A, 0x2A); }   // □ :Brown
  if (Ps3.data.button.triangle){ showMsgLed("triangle", 0xDE, 0xB8, 0x87); } // △ :BurlyWood
  if (Ps3.data.button.circle)  { showMsgLed("circle", 0x5F, 0x9E, 0xA0); }   // 〇 :CadetBlue

//  showAnalog();        // アナログスティックの表示
//  showAccelerometer(); // 加速度の表示

  delay(100);
}

// Serial文字列の送信とLEDの色を変更する関数
void showMsgLed(String s, uint8_t r, uint8_t g, uint8_t b) {
  M5.dis.setBrightness(LED_BRIGHTNESS); // LEDの光量(0-255)
  M5.dis.drawpix(0, getDispColor(r, g, b));
  Serial.println(s);
}

// FastLEDに設定する色を取得する関数
CRGB getDispColor(uint8_t r, uint8_t g, uint8_t b) {
  return (CRGB)((r << 16) | (g << 8) | b);
}

// アナログスティックの値を表示する関数
void showAnalog() {
  uint8_t lx, ly, rx, ry;

  lx = Ps3.data.analog.stick.lx;
  ly = Ps3.data.analog.stick.ly;
  rx = Ps3.data.analog.stick.rx;
  ry = Ps3.data.analog.stick.ry;

  // 値に変化があった場合に表示
  if ((b_lx != lx) || (b_ly != ly) || (b_rx != rx) || (b_ry != ry)) {
    Serial.print("LX=");
    Serial.print(lx);
    Serial.print(", LY=");
    Serial.print(ly);
    Serial.print(", RX=");
    Serial.print(rx);
    Serial.print(", RY=");
    Serial.println(ry);

    // 値を更新
    b_lx = lx;
    b_ly = ly;
    b_rx = rx;
    b_ry = ry;
  }
}


// 加速度を表示する関数
void showAccelerometer() {
  Serial.print("SX=");
  Serial.print(Ps3.data.sensor.accelerometer.x);
  Serial.print(", SY=");
  Serial.print(Ps3.data.sensor.accelerometer.y);
  Serial.print(", SZ=");
  Serial.println(Ps3.data.sensor.accelerometer.z); 
}

 上記スケッチをAtom Liteに書き込んだ後、Atom LiteのLEDが赤色に光ります。その状態でPS3の「PSボタン」を押してペアリングが成功するとAtom LiteのLEDが青色に変化します。

 その状態でPS3コントローラの各ボタンを押すとLEDの色が変化します(中には光の強度の関係で似たような色に見えるものもあります)。

 ちなみにArduino IDEのシリアルモニタを開いた状態で行うと、以下のようにどのボタンを押したかがわかるようにしています。

 またvoid loop( )関数でコメントアウトしているshowAnalog( )、showAccelerometer( )のコメントアウトを外すと、アナログスティックの値とコントローラの加速度の値もシリアルモニタに表示できます。


 以上がAtom LiteでPS3コントローラの値を取得する方法になります。

 参考資料にあるページ様のソースコードを参考にしつつやりましたが、割と簡単にPS3コントローラとのBluetooth通信ができることがわかってよかったです。

 今回はAtom LiteとPS3コントローラでやりましたが、ライブラリ的にはesp32となっているため、他のesp32系のマイコンボードでも同じように通信できると思います(未確認)。

 また検証していませんが、以下のようにPS4コントローラ用ライブラリを公開している方もいるので、PS4コントローラでも同じようなことができるようです。

 ゲーム機のコントローラをesp32で扱えるということは、簡単なラジコンみたいなものも作れそうなので楽しみが広がりますね。


・参考資料

【Blender】元動画ファイルのフレームレートを変更して出力する

 今回は久しぶりにBlenderの記事になります。

 タイトルにある通り、Blenderで元動画ファイルのフレームレートを変更して出力する方法になります。


 では、始めます。


・フレームレート変更方法
 今回は「30fpsの動画ファイルを24fpsに変換する」前提で進めます。

 最初にBlenderを起動させて適当な名前を付けてファイルを保存します。

 保存できたら画面上部にあるタブの「+」をクリックします。

 出てきた中から「Video Editing」→「Video Editing」を選択します。

 以下のようにビデオ編集画面になります。

「Add」→「Movie」を選択してフレームレートを変更したいファイルを読み込みます。

 読み込ませると以下のようになります。ちなみに青色が映像部分、緑色が音声部分になっています。

「Output Properties」のアイコンをクリックし、Frame Rateのプルダウンから変更したいfpsを選択します。今回は24fpsに変換したいので「24」を選択します。

 読み込んだ動画の音声部分が指定したフレームレートに合わせて自動的に短くなります。

 次に青色の映像部分をクリックした状態で右側にある「Time」→「Duration」のところに短くなった音声のフレーム数と同じ値を入力します。今回は192なのでその値を入力します。

 音声がない場合は設定する値は以下の計算式を入力します。

元の総フレーム数 / 元フレームレート * 変換後フレームレート

 なので今回の場合は以下の計算式をDurationに入力することになります。

241/30*24

 これで映像の方も同じフレーム数分再生するようになりました。

 ただしこの状態では映像は元のフレームレート数分のままなので、速度を調節する必要があります。

「Add」→「Effect Strip」→「Speed Control」を選択します。

 Speed Controlが追加されるのでそれを選択した状態で「Channel」に映像のChannel番号を入力します。

 映像にSpeed Controlが設定され、対応するフレーム数にちょうど合うようなスピードにしてくれます。

 これで映像自体のフレームレート変更が終わったので、後は動画として書き出すだけです。

「Output Properties」のFrame Rangeの「End」に出力する最後のフレーム数を設定します。

 動画として書き出すために「Output」の各項目を設定します。今回は音声を消したかったので「No Audio」を選択しています。

 準備ができたので、「Render」→「Render Animation」で動画として書き出します。

 これで元動画ファイルを24fpsに変換したmp4が出力されます。


 以上が元動画ファイルのフレームレートを変更して出力する方法になります。

 久しぶりにBlenderを触ってみようと思い立って最新版をダウンロードしたところ、3.1とかなりバージョンが進んでいて浦島太郎状態だったので、これからはサボらずに定期的にまたBlenderを触っていきたいですね…。


・参考資料

【Windows】Windows11の右クリックのメニューをWindows10の仕様に戻す

 自分は自作PCの方をWindows11にしているのですが、ファイルを右クリックをした時のメニューがWindows10の時と変わっていて使いにくいと感じていました。

 調べたところ元の右クリックメニューに戻す方法があることがわかったので今回はその備忘録になります。
 基本的には参考資料に挙げているページ様と同じになるので、詳しくはそちらを参照してください。

 では、始めます。


・右クリックのメニューを戻す
 方法としてはレジストリを編集することになります。

 検索を開いて「regedit」と入力して出てきた「レジストリエディター」をクリックして起動させます。

 起動させたらパスを書いてあるところに以下の内容をコピーして貼り付け、Enterキーを押下します。

コンピューター\HKEY_CURRENT_USER\Software\Classes\CLSID


「CLSID」が選択された状態になるので、「右クリック」→「新規」→「キー」を選択します。

 新たなキーが作成されるので、選択された状態で名前を以下のものに変更します。

{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}


 作成したものを選択した状態で「右クリック」→「新規」→「キー」を選択します。

 新たなキーを以下の名前で作成します。

InprocServer32


 作成したキーの「既定」をダブルクリックして表示された「値のデータ」に何も入力されていないことを確認して「OK」をクリックします。

 既定のデータ部分が空になっていることを確認します。

 最後にWindowsを再起動すれば、右クリックのメニューが以前のものに戻っています。

 また、Windows11のデフォルトの状態に戻したい場合は、作成した以下のキーを削除することでデフォルトに戻せます。

{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}


 以上がWindows11の右クリックのメニューをWindows10の仕様に戻す方法になります。

 右クリックのメニューに追加しているソフトとかもあるので、Windows11になるとそれが表示されなくなるのは困るのでもっと簡単に設定画面などから簡単に切り替えられるようになっていてほしいですね…。


・参考資料

【python/PySide6】printの内容をGUIに表示させる(PySide6でのバックグラウンド処理)

 以前PySide6を導入して簡単なGUIを作る記事とprintの内容をファイルに出力する方法の記事を書きました。


 今回はこれらの応用としてPySide6を使ってprintの内容をGUIに表示させる方法の備忘録になります。


 では、始めます。


1:PySide6でバックグラウンド処理を行う
 PySide6で何かの処理中に画面を随時更新する場合にはバックグラウンド処理を行う必要があるので、まずはその説明をします。

 基本的にはバックグラウンド処理したいクラスにSignalのスレッドを定義し、emit関数を使ってSignalの値を呼び出し側へ渡します。そして呼び出し元でそのSignalの値を取り出して処理していくという流れです。

 ちゃんと書くとそこそこ長くなるので、以下に要点だけを抽出した簡単なソースコードを記述しておきます。このソースコード単体では動かないので注意してください。

# メイン処理側
# スレッドクラスの準備
sub_thread = SubThread()
sub_thread.connect(get_signal)  # シグナルの接続
sub_thread.finished.connect(finish_thread) # スレッドが終了した際の処理の接続

def get_signal(signal_str):
    # シグナルを受け取った時の処理
    print(signal_str)

def finish_thread():
    # バックグラウンド処理が終わった時の処理
    print("finished")


# バックグラウンド処理側(Signalを送る側)
class SubThread(QThread):
    sub_signal = Signal(str) # Signalでstr型を送る

    def __init__(self, parent=None):
      QThread.__init__(self, parent)

    def send_signal(self):
      self.sub_signal.emit("test") # Signalで送信

 メイン側でバックグラウンド処理のクラスのオブジェクトを作成し、そこに「connect」関数でシグナルを受け取る処理の関数を設定します。「finished.connect」で処理が終了した後の処理も設定できます。
 そしてバックグラウンド処理のクラスではSignalをクラス変数として定義し、「emit」関数を設定することで値を渡すことができます。

 これでバックグラウンド処理がわかったので、この方法を使ってprintの内容をGUIに表示させるコードを書いていきます。


2:printの内容をGUIに表示させるサンプル
 今回はループでprintを行うpythonファイルをGUIのファイルから呼び出すという形でサンプルを作成しました。

 ループでprintを行うファイルは以下の通りです。

・print_log.py

#-*- coding:utf-8 -*-
import sys
from time import sleep

def count_up(num, count):
    print("--- start ----")

    for i in range(0, count):
        print(num + i)
        sleep(0.5)

    print("--- end ----")

if __name__ == '__main__':
    count_up(1, 10)


 GUIのファイルは以下の通りです。

gui_test.py

#-*- coding:utf-8 -*-
import sys, threading, re
from copy import deepcopy
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *

import print_log


class Form(QDialog):
    """ GUIクラス
    """

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        # Widgetsの設定(タイトル、固定横幅、固定縦幅)
        self.setWindowTitle("Title test")
        self.setFixedWidth(400)
        self.setFixedHeight(260)

        # num入力部分
        num_layout = QHBoxLayout()
        self.num_edit = QLineEdit("") # テキスト入力
        num_layout.addWidget(QLabel("num :"), 1)
        num_layout.addWidget(self.num_edit, 4)

        # count入力部分
        count_layout = QHBoxLayout()
        self.count_edit = QLineEdit("") # テキスト入力
        count_layout.addWidget(QLabel("count :"), 1)
        count_layout.addWidget(self.count_edit, 4)

        # ログ表示部分
        self.text_layout = QHBoxLayout()
        self.textbox = QListView()
        self.text_list = QStringListModel()
        self.textbox.setModel(self.text_list)
        self.text_layout.addWidget(self.textbox)

       # プログレスバー部分
        pb_layput = QHBoxLayout()
        self.pb = QProgressBar()
        self.pb.setFixedWidth(370)
        self.pb.setTextVisible(False)
        pb_layput.addWidget(self.pb)

        # ボタン部分
        run_layout = QHBoxLayout()
        self.run_button = QPushButton("start")
        self.run_button.clicked.connect(self.run_log)
        run_layout.addWidget(QLabel(""), 2)
        run_layout.addWidget(self.run_button, 1)
        run_layout.addWidget(QLabel(""), 2)

        # レイアウトを作成して各要素を配置
        layout = QVBoxLayout()
        layout.addLayout(num_layout)
        layout.addLayout(count_layout)
        layout.addLayout(self.text_layout)
        layout.addLayout(pb_layput)
        layout.addLayout(run_layout)

        # レイアウトを画面に設定
        self.setLayout(layout)

        # ログスレッドクラスの準備
        self.lp = LogThread()
        self.lp.log_thread.connect(self.show_log)  # シグナルスロットの接続
        self.lp.finished.connect(self.show_result) # スレッドが終了した際の処理の接続


    def run_log(self):
        # ログプロセスを実行する
        num = self.num_edit.text()
        count = self.count_edit.text()

        if self.is_number(num) and self.is_number(count):
            # GUIを非活性にする
            self.set_all_enabled(False)
            # プログレスバーの開始
            self.pb.setMinimum(0)
            self.pb.setMaximum(0)
            # 値を設定
            self.lp.set_count(int(num), int(count))
            # ログプロセスを実行する
            self.lp.start()
        else:
            # 入力値エラーとしてダイアログ表示
            QMessageBox.warning(self, "注意", "numとcountには正の半角整数を入力してください。")


    def is_number(self, number):
        # 正規表現で数字だった場合はTrue/そうでない場合はFalse
        if re.fullmatch(r"[0-9]+", number) is None:
            return False
        else:
            return True


    def show_log(self, log):
        # 書き込み中の進捗をGUIに表示する
        log_list = self.text_list.stringList()
        log_list.append(str(log))
        self.text_list.setStringList(log_list)
        self.textbox.scrollToBottom()


    def show_result(self):
        # 結果を表示する
        QMessageBox.information(self, "終了", "終了しました。")
        # プログレスバーの停止
        self.pb.setMinimum(0)
        self.pb.setMaximum(100)
        self.set_all_enabled(True) # GUIの表示を戻す


    def set_all_enabled(self, flg):
        # GUIの有効/無効を設定する
        self.num_edit.setEnabled(flg)
        self.count_edit.setEnabled(flg)
        self.run_button.setEnabled(flg)


class LogThread(QThread):
    """ ログファイルを読み取るクラス
    """
    log_thread = Signal(str)
    log_file_path = "./log.txt"
    read_flg = False
    num = 0
    count = 0


    def __init__(self, parent=None):
        """ コンストラクタ
        """
        QThread.__init__(self, parent)


    def __del__(self):
        # Threadオブジェクトが削除されたときにThreadを停止
        self.wait()


    def set_count(self, num, count):
        self.num = num
        self.count = count


    def run(self):
        # 標準出力の出力先をファイルにする
        sys.stdout = open(self.log_file_path, "w")

        # ログファイルを読み取るスレッドを開始
        self.read_flg = True
        read_thread = threading.Thread(target=self.read_log)
        read_thread.setDaemon(True)
        read_thread.start()

        try:
            # 別ファイルのprintする処理を開始
            print_log.count_up(self.num, self.count)
        except Exception as e:
            print(e)
        finally:
            # 標準出力を元に戻す
            sys.stdout.close()
            sys.stdout = sys.__stdout__
            self.read_flg = False


    def read_log(self):
        # 書き込み時のログファイルを読込み差分の行をSignalのemitに設定する関数
        old_lines = list()
        new_lines = list()

        while self.read_flg:
            with open(self.log_file_path, "r", encoding="utf-8") as f:
                sys.stdout.flush() # このflushの記述がないと処理途中でログ出力されない

                # listなので書き換えられないようdeepcopy
                new_lines = deepcopy(f.readlines())
                old_size = len(old_lines)
                new_size = len(new_lines)

                if  old_size < new_size:
                    # 差分の行をSignalで値を渡す
                    for i in range(old_size, new_size):
                        self.log_thread.emit(new_lines[i].replace("\n",""))
                    old_lines = deepcopy(new_lines)


if __name__ == '__main__':
    # Qtアプリケーションの作成
    app = QApplication(sys.argv)

    # フォームを作成して表示
    form = Form()
    form.show()

    # 画面表示のためのループ
    sys.exit(app.exec())

 上記2ファイルを以下のように同階層に配置します。

 gui_test.pyの方を実行すると以下のようにGUIが表示されるのでnumとcountのところに半角数字を入力します。

 startボタンを押下して実行すると画面が非活性になりリアルタイムでログの内容が表示され、処理が終わると終了ダイアログが表示されます。


 詳しくはソースコードを読んでいただきたいのですが、少し解説すると、ログの書き出しを行っている時にログファイルを開いてその行数を取得し、以前の行数と差分があれば差分の行を1行ずつSignalで送っているという処理を行っています(read_log関数)。

 重要なのが「sys.stdout.flush() 」という一文で、これをどこかに書いていないとprintの内容がリアルタイムでGUIに反映されず、処理が終わった後にまとめてGUIに反映されるという動きになります。
 この理由は、printのみではバッファに溜まり続けるだけで出力されないため、flushでバッファの内容を出力してあげる必要があるからです。


 以上がPySide6を使ってprintの内容をGUIに表示させる方法になります。

 Signalを使うのが初めてだったりflushの必要性を知らなかったりと割と手こずりましたが、GUIのバックグラウンド処理やリアルタイムに反映する記述方法がわかったので色々と楽しいことに使えそうです。


・参考資料

【python】printの表示内容をファイルに出力する

 今回はタイトルにあるようにprintでの表示内容をファイルに出力する方法の備忘録になります。

 少し特殊ですが「printの内容をファイルにログ的なものとして残しておきたいが、そのprintの内容がライブラリになっている」など簡単に処理を追加しにくい場合に今回の内容が必要になってくるかと思います。


 では、始めます。


1:printの内容をファイルに出力する
 やり方は簡単で以下の記述でできます。

import sys

# 表示内容の出力をファイルに変更
sys.stdout = open("test.log", "w")

# ファイルを閉じて標準出力を元に戻す
sys.stdout.close()
sys.stdout = sys.__stdout__

 sys.stdoutがprintを実行した際の出力先なので、openでファイルを指定します。その後は通常通りにprint関数を使うとコンソールには表示されず、指定したファイルに内容が書き込まれます。
 printの出力をコンソールに戻す場合は一度「sys.stdout.close()」した後に「sys.stdout = sys.__stdout__」することで元に戻すことができます。


2:サンプルコード
 特に難しいところはないと思いますが、実際にサンプルコードを書いて動かしてみます。

・print_log.py

#-*- coding:utf-8 -*-
import sys


def main():
    print("---- start ----")

    log_file_path = "./log.txt"
    sys.stdout = open(log_file_path, "w") # 表示内容の出力をファイルに変更

    loop_print(10) # ファイルに出力される

    # ファイルを閉じて標準出力を元に戻す
    sys.stdout.close()
    sys.stdout = sys.__stdout__

    loop_print(20) # コンソールに表示される

    print("---- end ----")


def loop_print(num):
    for i in range(0, 10):
        print(str(num + i))


if __name__ == '__main__':
    main()


 実行すると以下のように20~29の表示しかコンソールには表示されません。

 そして作成されたlog.txtファイルを開くと以下のように10~19が記述されています。

 このようにちゃんとファイルに出力する部分とコンソールに表示させる部分を切り替えられていることがわかります。


 以上がpythonでprintの表示内容をファイルに出力する方法になります。

 この方法を使うのはかなり特殊な場合かとは思いますが、この方法を使いたい時が自分に発生したので記事にした次第です。


・参考資料

【python】プラットフォームを判別する

 pythonを書いている中でマルチプラットフォームでの実行を考えた場合、プラットフォーム(システム)毎に処理を変更する必要が出てくることがあります。

 今回はそのプラットフォームを判別する方法になります。


 では、始めます。


1:プラットフォームを取得する
 方法は簡単で以下の記述で実行しているプラットフォームの文字列を取得することができます。

import sys

platform_str = sys.platform
print(platform_str)

 このsys.platformでプラットフォーム毎に取得される文字列は以下の通りです(参考ページより引用)。

システム platformの値
AIX 'aix'
Linux 'linux'
Windows 'win32'
Windows/Cygwin 'cygwin'
macOS 'darwin'

 pythonのバージョンによっては取得できる文字列などに変更がある場合があるので注意してください。


2:プラットフォームを判別する
 多くはWindowsMacOSLinuxぐらいの判別ができればよいかと思うので、以下がそのサンプルになります。

・judge_platform.py

#-*- coding:utf-8 -*-
import sys

if sys.platform.startswith('win'):
  print("Windows")
elif sys.platform.startswith('darwin'):
  print("MacOS")
elif sys.platform.startswith('linux'):
  print("Linux")

 pythonのバージョンによっては取得できる文字列が少し変更されてきた過去があるので、startswithを使って最初の文字列で判別するのが良いようです。


 以上がpythonでプラットフォームを判別する方法になります。

 判別自体は簡単ですが、取得できる文字列を残しておきたかったので記事にした次第です。
 特にGUIなどはプラットフォームによってはかなり違ってくるので、GUIのレイアウトや内容をプラットフォーム毎に切り替えたりする場合には必要になってくるのかなと思います。


・参考資料