PyGameのコントローラーにM5stack via Bluetooth

PyGameの迷路のコントローラーとしてM5stackを使ってみた。

M5stackとMacとの通信は以下の方法で。

https://isehara-3lv.sakura.ne.jp/blog/2022/05/05/m5stack-bluetoothでのmacとの送受信/

迷路ゲームについては、迷路作成は壁のばし法

https://yottagin.com/?p=1579

のクラスファイル流用して、ゲーム本体は、

https://news.mynavi.jp/techplus/article/zeropython-90/

から迷路作成ロジックだけ入れ替えですが、ほぼそのまま使えました。

M5stackのボタン処理は、ボタンが三つしか無いから、そのままではL/R/U/Dの処理ができないから、M5stackを傾けてボタンA/Cを操作したらU/Dになるように重力加速度の値で読み替えしてます。

ゲーム中にボタンBを押すとゲーム終了します。

#define M5STACK_MPU6886
#include "BluetoothSerial.h"
#include <M5Stack.h>

BluetoothSerial bts;
String btn;
float accX = 0.0F;  // Define variables for storing inertial sensor data
float accY = 0.0F;
float accZ = 0.0F;
float angle_th = 0.5F;  // r/l or u/l decision threashold

void read_imu() {
  M5.IMU.getAccelData(&accX,&accY,&accZ); //Stores the triaxial accelerometer. 
  M5.Lcd.setCursor(0, 90);
  M5.Lcd.printf(" accX");
  M5.Lcd.setCursor(0, 130);
  M5.Lcd.printf("%5.2f G", accX);
}

void setup() {
  M5.begin();
  M5.Power.begin(); //Init Power module.  
  M5.IMU.Init();    //Init IMU sensor.  
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0, 2);
  M5.Lcd.println("Maze game");
  bts.begin("ESP32test");
}

void loop() {
  btn = "";                           // button info. clear
  
  M5.update();
  if(M5.BtnA.wasPressed())
  {
    btn = "r";
  }
    if(M5.BtnB.wasPressed())
  {
    btn = "t";
  }
  if(M5.BtnC.wasPressed())
  {
    btn = "l";
  }

  read_imu();   // imu data read

  if(btn != "")
  {
    if (btn != "t")       // t(ButtonB) key terminate the Maze game
    {
      if (accX < -angle_th | accX > angle_th)       // if M5stack is tilted, r/l keys are changed to u/d
      {
        if (accX < -angle_th){ if (btn == "r") { btn = "u"; } else { btn = "d"; } } if (accX > angle_th)
        {
          if (btn == "r")
          {
            btn = "d";
          }
          else
          {
            btn = "u";
          }
        }
      }
    }

    bts.println(btn);      // send pressed key info.
    M5.Lcd.setCursor(0, 50, 2);
    M5.Lcd.println(btn);
  }

  delay(100);

}

Bluetoothシリアルがコードを弄ってると繋がらなくなることが頻繁に起きて、その都度リブートしてるのは面倒です。本質的な原因はなんなんだろう?

全体のコードは、

https://github.com/chateight/PlatformIO/tree/master/bte_serial/src

にあります。

pythonのmaze.pyがメイン処理、make_maze.pyが迷路作成コード、receive.pyはM5stackのキー入力確認用のスクリプトでゲームと直接は関係ありません。

 

admin

 

M5stack Bluetoothでのmacとの送受信

プロトコルの煩雑なbleではなく、シンプルなbluetoothで接続してみます。

<環境>

・M5stack gray

・Macbook Monterey

・Python3 : mac側の動作確認用スクリプト記述

<ライブラリ>

bluetoothをあたかもシリアルのように見せるライブラリ(M5stack :

BluetoothSerial.h)とpythonでシリアルインターフェースを扱うための pyserialがあるのでそれらを使います。

 

・pyserialをインストールする

pip3 install pyserial

 

BluetoothSerial.hのインストール

BluetoothSerial.hはM5stackのライブラリから選択、インストールできます

 

<main.cpp>

#include "BluetoothSerial.h"
#include <M5Stack.h>

BluetoothSerial bts;

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0, 2);
  M5.Lcd.println("Bluetooth TEST");
  bts.begin("ESP32test");
}

void loop() {
  M5.Lcd.setCursor(0, 20, 2);
  M5.Lcd.printf("Start Bluetooth");
  bts.println("message from the M5stack");
  //Serial.println("loop");
  String str = bts.readString();
  M5.Lcd.setCursor(0, 60, 2);
  M5.Lcd.println(str);

  delay(1000);
}

 

・シリアルポートを調べる

M5stackにコード書き込んで、シリアルポートを調べます。

% ls -l /dev/tty.*

crw-rw-rw-  1 root  wheel  0x9000008  5  4 18:03 /dev/tty.ESP32test

 

<write.py>

該当のポート情報に上の情報を設定します。

import serial

mes = "messge from my Mac"
# initialize bt serial
m5data = serial.Serial('/dev/tty.ESP32test',115200, timeout=3)
# read from bt
line = m5data.readline()
print(line.decode()[:-2:])
# write to bt
m5data.write(mes.encode())
m5data.close()

・実行

% python write.py 

b”

‘’に何も表示されないということはタイムアウトしている。

・macをリブートして、USBシリアル経由でpyserialの機能を確認してみる。

% ls -l /dev/tty.*

crw-rw-rw-  1 root  wheel  0x9000008  5  5 09:39 /dev/tty.SLAB_USBtoUART

write.pyに以下のデバッグ行追加

Serial.println(“loop”);

% python write.py

b’loop\r\n’

デバッグ用にコンソールに出力させて、シリアルポート切り替えると受信できるからpyserialそのものは動いているんだろう。

じゃ、本来のbluetoothでどうなの?この時MacからESPtestのbluetoothに接続はしない。

% python write.py

b’message from the M5stack\r\n’

ちゃんと受信できるから、どこかクリアされない状態があった?

リブートで状態がクリアされているようで、結果としてmacの設定でbluetoothの接続をしてしまうとダメ。一度接続選択すると接続選択解除してもpythonスクリプトからは使えなくなるから接続の権利(と言ってもmacに変わりはないけど)を他でキープしているようだ。

P.S ここでは接続しないと、% ls -l /dev/tty.*で見えないからダメだけど、

ここでは接続してはいけない。

 

読み出し文字列中のb’’はバイトストリングと言うことらしいけど、実際には必要ないからdecode()して普通の文字列に変換してやります。また書き込みの時にはencode()が必要です。

M5stackのスクショってどうすれば取れるんだろう?

bluetooth経由でデータの送受信ができるとやれることが広がります。

 

admin

c++ threadインスタンスを無名にしてvectorに格納

マルチスレッドで、スレッドに名称をつけてスレッド数を管理するのは面倒です。

スレッド名を無名にして、vectorに格納しても同じ処理でのスレッド名に意味はないからその方が便利です。

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <cmath>

std::mutex mtx_;                // mutex for exclusive control
std::vector vec{};         // prime numbers array
int cal_int = 1;                // calc target integer
int max_int = 10000000;         // calc target max number
std::vector threads;       // array of threads
int num_thread = 12;                    // number of threads



void add_prime(int i)           // store the prime numberns in the vector
{
    std::lock_guard lock(mtx_);
    vec.push_back(i);
}

int get_int()                   // get the integer to be tested
{
    std::lock_guard lock(mtx_);
    ++cal_int;
    if (cal_int > 2 && cal_int%2 == 0)      // do not return an even number larger than four
    {
        ++cal_int;
    }
    return cal_int;
}

void ThreadA()
{
    while (true)
    {
        bool flag = false;
        int i = get_int();
        if (i > max_int)
        {
            break;
        }
        int sqt = sqrt(i);
        for (int j = 2; j <= sqt; ++j)
        {
            if (i%j == 0)
            {
                flag = true;
                break;
            }
        }
        if (flag != true)
        {
            add_prime(i);
        }
    }
}

int main()
{
    std::chrono::system_clock::time_point  start, end;
    start = std::chrono::system_clock::now();

    std::cout << std::endl;
    std::cout << "Hardware concurrency = " << std::thread::hardware_concurrency() << std::endl;

    for(size_t i=0; i < num_thread; ++i){
        threads.emplace_back(std::thread(ThreadA));
    }

    for(auto& thread : threads){
        thread.join();
    }

    std::cout << std::endl;
    std::cout << "Number of threads = " << num_thread << std::endl;

    std::cout << std::endl;
    end = std::chrono::system_clock::now();
    double elapsed = std::chrono::duration_cast(end-start).count();
    std::cout << "elapsed time : " << elapsed << " ms" << std::endl;

    std::cout << std::endl;
    std::cout << "number of prime numbers : " << vec.size() << std::endl;

    std::cout << std::endl;
    std::cout << "last five prime numbers" << std::endl;
    std::sort(vec.begin(), vec.end() );
    
    for (auto itr = (vec.end() - 5); itr != vec.end(); ++itr)
    {
        std::cout << *itr << std::endl;
    }

}

こうすれば、変数を一箇所変えるだけでスレッド数が自由に変更できます。4以上の偶数は素数判定スレッドに渡さないで、処理時間短縮も図っています。

 

<実行結果>

Hardware concurrency = 12

Number of threads = 12

elapsed time : 1358 ms

number of prime numbers : 664579

last five prime numbers
9999937
9999943
9999971
9999973
9999991

https://isehara-3lv.sakura.ne.jp/blog/2022/05/01/c-マルチスレッドの効果/

で、およそ1.8秒でしたが1.3秒程度まで高速化。所詮、多数桁の素数計算には誤差とも言える範囲ですが。

 

admin

 

c++ マルチスレッドの効果

マルチスレッドは多くのアプリケーション、わかりやすいのはブラウザあたり、で普通に使われてますが、多重度を上げても必ずしも性能が向上するわけではありません。それはリソースの排他制御などでオーバーヘッドが発生するから。

で、素数計算プログラムでマルチスレッドの効果を検証してみた。

<ソースコード>

スレッド制御はstd::threadクラスを使います。

https://cpprefjp.github.io/reference/thread/thread.html

スレッドプログラミングで問題になる共通リソース、この場合には計算対象の整数(cal_int)と計算結果の格納(vec{})を排他制御(mutex)で、壊れないように制御します、理屈はセマフォーと同じです。

同じ処理の多重化ならば、特定の関数をスレッド化する訳なので、元のコードは一個で構いません。

スレッド数の可変は対象部分(std::thread th_x(ThreadA); とth_x.join();)をコメントアウトで対応。

算出結果のvectorはスレッドの出力タイミングでソートされた結果にはならないから、処理完了後にソートを実行。

スレッド処理の記述は、

https://qiita.com/nsnonsugar/items/be8a066c6627ab5b052a

を参考。

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <cmath>

std::mutex mtx_;                // mutex for exclusive control
std::vector vec{};         // prime numbers array
int cal_int = 1;                // calc target integer
int max_int = 10000000;          // calc target max number


void add_prime(int i)
{
    std::lock_guard lock(mtx_);
    vec.push_back(i);
}

int get_int()
{
    std::lock_guard lock(mtx_);
    ++cal_int;
    return cal_int;
}

void ThreadA()
{
    while (true)
    {
        bool flag = false;
        int i = get_int();
        if (i > max_int)
        {
            break;
        }
        int sqt = sqrt(i);
        for (int j = 2; j <= sqt; ++j){
            if (i%j == 0){
                flag = true;
                break;
            }
        }
        if (flag != true){
            add_prime(i);
        }
    }
}

int main()
{
    std::chrono::system_clock::time_point  start, end;
    start = std::chrono::system_clock::now();

    std::cout << std::endl;
    std::cout << "Hardware concurrency = " << std::thread::hardware_concurrency() << std::endl;

    std::thread th_a(ThreadA);
    std::thread th_b(ThreadA);
    std::thread th_c(ThreadA);
    std::thread th_d(ThreadA);

    th_a.join();
    th_b.join();
    th_c.join();
    th_d.join();

    std::cout << std::endl;
    end = std::chrono::system_clock::now();
    double elapsed = std::chrono::duration_cast(end-start).count();
    std::cout << "elapsed time : " << elapsed << " ms" << std::endl;

    std::sort(vec.begin(), vec.end() );
    for (auto itr = (vec.end() - 5); itr != vec.end(); ++itr){
    std::cout << *itr << std::endl;
    }

    return 0;
}

 

<実行結果>

対象が百万までではスレッド数が2以上による実行時間の差はほとんどない。スレッドの実行時間よりもオーバーヘッドの方が支配的だからだろう。

一方、千万までの計算ではスレッド処理の比重が増加して、スレッド数を増やせば実行時間は速くなるから、スレッド処理が支配的になるせいでしょう。

Hardware concurrency = 12

—100000まで計算—

1 thread

elapsed time : 223 ms
999953
999959
999961
999979
999983


2 threads 

elapsed time : 177 ms
999953
999959
999961
999979
999983


3 threads

elapsed time : 184 ms
999953
999959
999961
999979
999983


4 threads

elapsed time : 194 ms
999953
999959
999961
999979
999983


—10000000まで—

1 thread

elapsed time : 4519 ms
9999937
9999943
9999971
9999973
9999991


2 threads

elapsed time : 2646 ms
9999937
9999943
9999971
9999973
9999991


3 threads

elapsed time : 2103 ms
9999937
9999943
9999971
9999973
9999991


4 threads

elapsed time : 1829 ms
9999937
9999943
9999971
9999973
9999991



 

という結果が言わんとすることは、各スレッドの処理が重ければCPUのマルチコアが有効に働く、しかし処理が軽いとオーバーヘッドによりほとんど実行時間には影響しない。もちろんマルチスレッドの目的は処理の分散・並行処理による高速化以外に各スレッド処理のリアルタイム性の側面もあるのだから、一概には言えないわけだけれども、いずれにしろ適用アプリケーションを事前に考慮した上での実装が必要という当たり前の結論です。

 

admin

 

PyBind11 for c++

c++の高速性が必要でなおかつ、Pythonの書きやすさが必要な場合にはPythonからc++の処理の呼び出しが必要です。

いろいろな方法がありそうですが、PyBind11は2011年ごろに登場と比較的に新しく、それ故に機能も洗練されているだろうから動かしてみた。

ネットにも情報が多いから問題解決が難しくないだろうと思う。

<本家>

https://pybind11.readthedocs.io/en/stable/index.html#

“pybind11 is a lightweight header-only library that exposes C++ types in Python and vice versa,”

とあるようにpythonからc++も呼べるし、逆もまた可なりのヘッダーファイルライブラリにすぎないと。

<install>

$ pip3 install pybind11

—Installed path—

/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/pybind11/include/pybind11

<sample program & compile>

https://qiita.com/takuyakubo/items/b7503d7555bbc6c44aba

このリンク先にある、以下のclangコンパイルで作成できた。本家のドキュメントと全く同じですが。

% clang++ -O3 -Wall -shared -std=c++17 -fPIC `python -m pybind11 –includes` -undefined dynamic_lookup py_call.cpp -o example`python3-config –extension-suffix`

『コンパイラオプションは、最適化(-O3)、共有ライブラリとしてビルド(-shared -fPIC)、言語標準の指定(-std=c+17)、インクルードディレクトリの追加( `python3 -m pybind11 –includes`)、コンパイル対象(py_call.cpp)、出力(cpplcm`python3-config –extension-suffix`)となります』

https://buildersbox.corp-sansan.com/entry/2019/12/09/110000

の説明から引用。

<sample code : py_call.cpp>

PYBIND11_MODULE以下がマクロで、pybind11で解釈されます。

m.def()中の”add”はPythonから呼び出される時の関数名で、&addはint add()関数がそれに相当すると定義しています。

#include <pybind11/pybind11.h>

int add(int i, int j) {

  return i + j;

}

PYBIND11_MODULE(example, m) {

  m.doc() = "pybind11 example plugin";                          // optional module docstring

  m.def("add", &add, "A function which adds two numbers");

}


<call from python>

コマンドラインで実行して、import exampleが実行できればモジュールが作成できています。

% python

Python 3.10.3 (v3.10.3:a342a49189, Mar 16 2022, 09:34:18) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin

Type "help", "copyright", "credits" or "license" for more information.

>>> import example

>>> example.__doc__

'pybind11 example plugin'

>>> example.add(24, 43)

67

作成されるのは、共有ライブラリファイルで、”example.cpython-310-darwin.so“がPythonから呼び出されるファイルになります。MacOSなのでこんな名前になります。Pythonも実装はcだから、インターフェースは出来て当たり前ということでしょう。

-rwxr-xr-x   1 hogehoge  staff  148016  4 28 10:07 example.cpython-310-darwin.so

このケースは一番単純な機能確認ですが、実際には配列などを引数にするときにはc++とPython間で何らかの変換が必要になるでしょう。

 

admin

‘wchar.h’ file not found #include_next

久々CMake使おうとしたら、buildでこのようなエラーが発生。

解決方法は、

https://qiita.com/m0n0/items/b13998de1da4c7c1964d

中にある、

% make SDKROOT=`xcrun --show-sdk-path` MACOSX_DEPLOYMENT_TARGET=
[ 33%] Building CXX object CMakeFiles/main.dir/main.cpp.o
[ 66%] Building CXX object CMakeFiles/main.dir/header.cpp.o
[100%] Linking CXX executable main
[100%] Built target main

を実行すると解決。Xcodeに関連するようで、おそらくMacOSのアップデートと関係するようです。

以前のcmake記事

https://isehara-3lv.sakura.ne.jp/blog/2022/03/03/cmakec-ビルドツール/

 

admin

VScodeでPyGame動作

コマンドラインからでなく、VScode環境で動かそうとすると、

import pygame

のリンクが見つからないとなる。

実行すると問題なく実行されるので、エディタ環境と実行環境でPythonのパスが違うようです。

 

“PYTHON : ENVIROMENTS” (Pythonマーククリック)

で、”Set as active workplace interpretor”でコンソールから実行する最新版数を設定したらimport pygameのリンク見つからないエラーは消えました。

 

admin

c++とPythonの実行速度差

ほんの一面に過ぎませんが、速度比較をしてみた。

やってみたことは整数100万までの素数を求める。ロジックはどちらも同じで平方根までの割り算でどこかで割り切れる(非素数)で判定、割り切れた時点で素数では無いから計算量削減のため打ち切り、しています。求めた素数はvector(c++)、list(python)に保存しています。

計算量(時間)は一桁増えるとほぼ30倍(10の2分の3乗)になるから、桁数の多い因数分解の計算量を現在の暗号原理とするのも理解できます。

<c++ code>

sqrtは本来は不動小数点ですが、clang +17では暗黙変換(出力も)されてます。コンパイラによってはエラーになるかも知れない。

#include <iostream>
#include <chrono>
#include <cmath>
#include <vector>

int main(){

int sqt;
bool flag;
std::vector vec{};

std::chrono::system_clock::time_point  start, end;
start = std::chrono::system_clock::now();

for (int i = 2; i <= 1000000; ++i){         // check prime numbers
    flag = false;
    sqt = sqrt(i);
    for (int j = 2; j <= sqt; ++j){
        if (i%j == 0){
            flag = true;
            break;
        }
    } 
    if (flag != true){
        vec.push_back(i);
    }
}

end = std::chrono::system_clock::now();
double elapsed = std::chrono::duration_cast(end-start).count();
std::cout << "elapsed time : " << elapsed << " ms" << std::endl;

std::cout << std::endl;
std::cout << "number of prime integers(up to 1M) : " << vec.size() << std::endl;
std::cout << std::endl;
std::cout << "last five prime integers " << std::endl;

for (auto itr = (vec.end() - 5); itr != vec.end(); ++itr){
    std::cout << *itr << std::endl;
}
}


<Python code>

c++とpython(for in range)ではループの上限値の捉え方が違うのでループ回数に+1しないと正しい結果が出ない。

それは、内部ループのforで素数の二乗(例えば9, 25, 49)が素数判定されてしまうから。

import time
import math

start = time.time()
set = list()

flag = False
sqt = 0;
for i in range(2, 1000001):        # check prime numbers
    flag = False;
    sqt = math.sqrt(i)
    for j in range(2, int(sqt) + 1):
        if i%j == 0:
            flag = True
            break
    if flag != True:
        set.append(i)


end = time.time()
print("elapsed time : ",'{:.0f}'.format((end-start)*1000)+ " ms")
print("number of prime integers(up to 1M) : ",len(set))
print("")
print("last five prime integers")
for i in range(5):
    print(set[-5 + i])

以下に最後部分の素数と実行時間を記載しています。

c++

elapsed time : 268 ms

number of prime integers(up to 1M) : 78498

last five prime integers 
999953
999959
999961
999979
999983


Python

elapsed time :  9433 ms
number of prime integers(up to 1M) :  78498

last five prime integers
999953
999959
999961
999979
999983

その差はおよそ40倍近くc++の方が高速で、実際のアプリでの体感速度ももちろん種類によるけど、概ね一桁は違うんだろうと思う。

もちろんPythonも全てインタプリタ処理されるわけではなく、例えばループ処理はキャッシュされているだろうけど、やはり実行効率は比較にならない。

 

admin

PyGame(pythonのゲームライブラリ)

Pythonでゲームを作るというのは、他の言語を使うよりは相対的な敷居は低そうです。Python自体にすでに豊富なライブラリもあるから。PyGameはゲームエンジンというよりはゲームライブラリで、ゲームに必要なGUIとかボタン処理(event listener)、あるいは計算量の多い処理をライブラリ化することで、Pythonでのゲーム作成を簡単にできるようにしてくれます。

・本家

https://www.pygame.org/news

取っ掛かりのページ、

IMG_0425.PNG

・PyGameをインストール

% pip3 install pygame    

python起動して、

>>> import pygame

が問題なければ正常にインストールされてます。

・サンプルプログラム

https://www.pygame.org/download.shtml

からダウンロードして、tar.gzを解凍するとsamplesディレクトリがあるのでそれを持ってくる。

Numpyとpyopenglが必要なサンプルあったのでインストール。

% pip3 install numpy

% pip3 install PyOpenGL

Pythonとゲームの構造の両方を習得できるから良いツールでは無いかと思う。

 

admin

Unit V2カメラ Mac OSでの接続

今までMac OSだと、ドライバー必要と思っていたけど、以下のやり方でドライバーは入れなくても使えます。

Linuxのように完全に手続き不要ではなく、ifconfigで接続先を切り替える方法です。

ifconfig(部分)で以下のようなメッセージが出てますが、

en6: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	options=6467<RXCSUM,TXCSUM,VLAN_MTU,TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZE
 % sudo ifconfig en6 down
 % sudo ifconfig en6 up  

のように入力するとUnit V2に接続されるので、Chromeからhttp://10.254.239.1/

を入力すれば管理画面が出てきます。

参考は、https://qiita.com/kowloon/items/99073be408f6432711aad

です。

 

admin