m3stackいじり始めて間がないが、今回は高速描画ツールであるLovyanGFXを使ってみた。
なぜこのアプリを作りたいと思ったかは、以下の理由がある。
➀ ロボットの顔にしたい
➁ 当初参考➀のLGFXFacesというアプリを動かしてそこから使いたい表情を抽出しようとしたが今のウワンには複雑なので抽出できなかった
➂ 参考➁のLovyanGFXを入れて、スケッチ1_simple_useのコードを眺めているとできそうな気がした
ということで、簡単な顔表情を自作することにしました。
【参考】
➀robo8080/LGFX_Faces
➁lovyan03/LovyanGFX
その結果、簡単に以下のようなFaceが作成できたのでまとめておこうと思う。
以下の動画のようにm5stackだとほぼ同様なコードで同じようなものが作成できる。
仕事のお伴に🎵#m5stack 兄弟 pic.twitter.com/Gj9lV2UB9c
— ウワン (@MuAuan) July 14, 2021
最終的なコードは以下のようになりました。
コード
# define LGFX_AUTODETECT // 自動認識 (D-duino-32 XS, PyBadge はパネルID読取りが出来ないため自動認識の対象から外れています)
//#include <M5Core2.h> //core2
// define must ahead #include <M5Stack.h> //Stack
# define M5STACK_MPU6886
# include <M5Stack.h>
//#include <M5StickC.h>
# define LGFX_USE_V1
# include <LovyanGFX.hpp>
# include <LGFX_AUTODETECT.hpp> // クラス"LGFX"を準備します
static LGFX lcd; // LGFXのインスタンスを作成。
static LGFX_Sprite sprite(&lcd); // スプライトを使う場合はLGFX_Spriteのインスタンスを作成。
float pitch = 0.0F;
float roll = 0.0F;
float yaw = 0.0F;
float pitch0 = 0.0F;
float roll0 = 0.0F;
float yaw0 = 0.0F;
int sk=0;
void setup(void)
{
init_IMU();
lcd.init();
// 回転方向を 0~3 の4方向から設定します。(4~7を使用すると上下反転になります。)
lcd.setRotation(1);
// バックライトの輝度を 0~255 の範囲で設定します。
lcd.setBrightness(128);
lcd.setColorDepth(24); // RGB888の24ビットに設定
funny_MPU();
kiss_MPU();
delay(100);
lcd.startWrite(); // ここでstartWrite()することで、SPIバスを占有したままにする。
}
void init_IMU(){
float pitch_, roll_,yaw_;
M5.begin();
M5.IMU.Init(); //Core2, Core
for (uint32_t i=0;i<100; ++i){
M5.IMU.getAhrsData(&pitch_,&roll_,&yaw_); //Core2, Core
pitch0 += pitch_;
roll0 += roll_;
yaw0 += yaw_;
}
pitch0 = pitch0/100;
roll0 = roll0/100;
yaw0 = yaw0/100;
}
void get_IMU(){
M5.IMU.getAhrsData(&pitch,&roll,&yaw); //Core2, Core
lcd.setCursor(0, 20);
lcd.printf(" %5.2f %5.2f %5.2f ", pitch-pitch0, roll-roll0, yaw-yaw0);
delay(1);
}
void funny_MPU(){
sprite.createSprite(80,60);
if(sk%100<10&&(abs(roll-roll0))<5){
sprite.fillEllipse(30, 30, 30, 30-3*(sk%100), 0xFFFF);
sprite.fillCircle(30-pitch+pitch0, 30-roll+roll0, 15, 0x0000);
}else{
sprite.fillCircle(30, 30, 30, 0xFFFF);
sprite.fillCircle(30-pitch+pitch0, 30-roll+roll0, 15, 0x0000);
}
sprite.pushSprite(80, 60); // lcdの座標64,0にスプライトを描画
//Right eye
sprite.createSprite(80,60);
sprite.fillCircle(30, 30, 30, 0xFFFF);
sprite.fillCircle(30-pitch+pitch0, 30-roll+roll0, 15, 0x0000);
sprite.pushSprite(80+100, 60); // lcdの座標64,0にスプライトを描画
delay(1);
sprite.deleteSprite();
}
void kiss_MPU(){
sprite.createSprite(120,80);
if(sk%100<10&&(abs(roll-roll0))<5){
sprite.fillEllipse(40, 40, 30-22, 16, 0xF800);
sprite.fillEllipse(40, 40, 22-18, 11-9, 0x0000);
}else{
sprite.fillEllipse(40, 40, 30-int(pitch-pitch0)%8*2, 16-int(roll-roll0)%8, 0xF800);
sprite.fillEllipse(40, 40, 22-int(pitch-pitch0)%8*2, 11-int(roll-roll0)%8, 0x0000);
}
sprite.pushSprite(80+40, 60+60);
delay(1);
sprite.deleteSprite();
}
void loop(void)
{
M5.update();
if (M5.BtnA.wasReleased() || M5.BtnA.pressedFor(1000, 200)) {
init_IMU();
}
get_IMU();
sk += 1;
funny_MPU();
kiss_MPU();
}
・LovyanGFXの特徴
今回は役不足なので、上記参考➁の解説を見てください。
一応、完全引用すると以下のような特徴があるそうです。
ここで使いたいのは、高速化はもちろんですが、ちらつきなどに効果的なオフスクリーンバッファ(sprite)です。
---上記参考➁より引用
ESP32とSPI, I2C, 8ビットパラレル接続のLCD / ATSAMD51とSPI接続のLCDの組み合わせで動作するグラフィックライブラリです。
AdafruitGFX や TFT_eSPI と互換性をある程度持ちつつ、より高機能・高速動作を目標としています。
既存のライブラリに対して、以下のアドバンテージがあります。
・ArduinoESP32 / ESP-IDF 対応
・16bit / 24bitカラーモード両対応(実際の色数はLCDの仕様によります)
・DMA転送を用いた通信動作中の別処理実行
・オフスクリーンバッファ(スプライト)の高速な回転/拡縮描画
・複数LCDの同時利用
そして、以下のような描画関数が使えます。
// 基本的な図形の描画関数は以下の通りです。
fillScreen ( color); // 画面全体の塗り潰し
drawPixel ( x, y , color); // 点
drawFastVLine ( x, y , h , color); // 垂直線
drawFastHLine ( x, y, w , color); // 水平線
drawRect ( x, y, w, h , color); // 矩形の外周
fillRect ( x, y, w, h , color); // 矩形の塗り
drawRoundRect ( x, y, w, h, r, color); // 角丸の矩形の外周
fillRoundRect ( x, y, w, h, r, color); // 角丸の矩形の塗り
drawCircle ( x, y , r, color); // 円の外周
fillCircle ( x, y , r, color); // 円の塗り
drawEllipse ( x, y, rx, ry , color); // 楕円の外周
fillEllipse ( x, y, rx, ry , color); // 楕円の塗り
drawLine ( x0, y0, x1, y1 , color); // 2点間の直線
drawTriangle ( x0, y0, x1, y1, x2, y2, color); // 3点間の三角形の外周
fillTriangle ( x0, y0, x1, y1, x2, y2, color); // 3点間の三角形の塗り
drawBezier ( x0, y0, x1, y1, x2, y2, color); // 3点間のベジエ曲線
drawBezier ( x0, y0, x1, y1, x2, y2, x3, y3, color); // 4点間のベジエ曲線
drawArc ( x, y, r0, r1, angle0, angle1, color); // 円弧の外周
fillArc ( x, y, r0, r1, angle0, angle1, color); // 円弧の塗り
そして、ウワンが読んだこの1_simple_useにはいろいろな使い方が適切なコメントと共に示されています。
・M5Stack CORE, CORE2のIMU
ここでは、IMUを利用して目玉を動かそうとしています。
そこで、IMUの時時刻刻な測定値を目玉の位置に反映します。
結論としては、IMUはこれまでやってきたatomやm5stickCと同様、MPU6886が内蔵されています。
しかも、精度のいいgetAhrsData(&pitch_,&roll_,&yaw_);
が使えます。
一応、サンプルスケッチは以下のとおりです。
ここで、注意すべきはMPU6886の使い方が異なり、define行が異なります。
なお、以下のコードではボタンを押すとinit_IMU()
で初期化するコードを追加しています。
あとは、表示のコードが並んでいます。
IMUコード
//#include <M5Core2.h> //core2
// define must ahead #include <M5Stack.h> //Stack
# define M5STACK_MPU6886
# include <M5Stack.h>
//#include <M5StickC.h>
float accX = 0.0F;
float accY = 0.0F;
float accZ = 0.0F;
float gyroX = 0.0F;
float gyroY = 0.0F;
float gyroZ = 0.0F;
float pitch = 0.0F;
float roll = 0.0F;
float yaw = 0.0F;
float pitch0 = 0.0F;
float roll0 = 0.0F;
float yaw0 = 0.0F;
float temp = 0.0F;
// the setup routine runs once when M5Stack starts up
void setup(){
// Initialize the M5Stack object
//M5.begin();
/*
Power chip connected to gpio21, gpio22, I2C device
Set battery charging voltage and current
If used battery, please call this function in your project
*/
//M5.Power.begin();
//M5.IMU.Init();
init_IMU();
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(GREEN , BLACK);
M5.Lcd.setTextSize(2);
}
void init_IMU(){
float pitch_, roll_,yaw_;
M5.begin();
M5.IMU.Init(); //Core2, Core
// 最初に初期化関数を呼び出します。
for (uint32_t i=0;i<100; ++i){
M5.IMU.getAhrsData(&pitch_,&roll_,&yaw_); //Core2, Core
pitch0 += pitch_;
roll0 += roll_;
yaw0 += yaw_;
}
pitch0 = pitch0/100;
roll0 = roll0/100;
yaw0 = yaw0/100;
}
// the loop routine runs over and over again forever
void loop() {
M5.update();
if (M5.BtnA.wasReleased() || M5.BtnA.pressedFor(1000, 200)) {
init_IMU();
M5.Lcd.print('A');
}
// put your main code here, to run repeatedly:
M5.IMU.getGyroData(&gyroX,&gyroY,&gyroZ);
M5.IMU.getAccelData(&accX,&accY,&accZ);
M5.IMU.getAhrsData(&pitch,&roll,&yaw);
M5.IMU.getTempData(&temp);
M5.Lcd.setCursor(0, 20);
M5.Lcd.printf("%6.2f %6.2f %6.2f ", gyroX, gyroY, gyroZ);
M5.Lcd.setCursor(220, 42);
M5.Lcd.print(" o/s");
M5.Lcd.setCursor(0, 65);
M5.Lcd.printf(" %5.2f %5.2f %5.2f ", accX, accY, accZ);
M5.Lcd.setCursor(220, 87);
M5.Lcd.print(" G");
M5.Lcd.setCursor(0, 110);
M5.Lcd.printf(" %5.2f %5.2f %5.2f ", pitch-pitch0, roll-roll0, yaw-yaw0);
M5.Lcd.setCursor(220, 132);
M5.Lcd.print(" degree");
M5.Lcd.setCursor(0, 155);
M5.Lcd.printf("Temperature : %.2f C", temp);
delay(1);
}
・顔表情のコード解説
Lib
この自動認識がすごいなと思います。
#define LGFX_USE_V1
をコメントアウトすると、V0を使うことになるようです。
# define LGFX_AUTODETECT // 自動認識 (D-duino-32 XS, PyBadge はパネルID読取りが出来ないため自動認識の対象から外れています)
//#include <M5Core2.h> //core2
// define must ahead #include <M5Stack.h> //Stack
# define M5STACK_MPU6886
# include <M5Stack.h>
//#include <M5StickC.h>
# define LGFX_USE_V1
# include <LovyanGFX.hpp>
# include <LGFX_AUTODETECT.hpp> // クラス"LGFX"を準備します
変数定義
static LGFX lcd; // LGFXのインスタンスを作成。
static LGFX_Sprite sprite(&lcd); // スプライトを使う場合はLGFX_Spriteのインスタンスを作成。
float pitch = 0.0F;
float roll = 0.0F;
float yaw = 0.0F;
float pitch0 = 0.0F;
float roll0 = 0.0F;
float yaw0 = 0.0F;
int sk=0;
setup
ここで、初期顔を描画します。
IMUとLovynGFXの初期化をしています。
void setup(void)
{
init_IMU();
lcd.init();
// 回転方向を 0~3 の4方向から設定します。(4~7を使用すると上下反転になります。)
lcd.setRotation(1);
// バックライトの輝度を 0~255 の範囲で設定します。
lcd.setBrightness(128);
lcd.setColorDepth(24); // RGB888の24ビットに設定
funny_MPU();
kiss_MPU();
delay(100);
lcd.startWrite(); // ここでstartWrite()することで、SPIバスを占有したままにする。
}
IMU値取得
init_IMU()はIMU値の初期値を取得する関数です。
そして、get_IMU()で時時刻刻変化するIMU値を取得します。
void init_IMU(){
float pitch_, roll_,yaw_;
M5.begin();
M5.IMU.Init(); //Core2, Core
for (uint32_t i=0;i<100; ++i){
M5.IMU.getAhrsData(&pitch_,&roll_,&yaw_); //Core2, Core
pitch0 += pitch_;
roll0 += roll_;
yaw0 += yaw_;
}
pitch0 = pitch0/100;
roll0 = roll0/100;
yaw0 = yaw0/100;
}
void get_IMU(){
M5.IMU.getAhrsData(&pitch,&roll,&yaw); //Core2, Core
lcd.setCursor(0, 20);
lcd.printf(" %5.2f %5.2f %5.2f ", pitch-pitch0, roll-roll0, yaw-yaw0);
delay(1);
}
顔表情描画
funny_MPU()で目を描画します。口はここではkiss_MPU()で描画します。
MPUをつけたのは、それぞれpitchとrollで計算される表情としているためです。
眼は、右目だけ瞬きさせています。Winkですね。
口描画は唇をどの程度の厚さにするかで苦労していますが、あまり趣味(デザインセンス)はよくないようです。
void funny_MPU(){
sprite.createSprite(80,60);
if(sk%100<10&&(abs(roll-roll0))<5){
sprite.fillEllipse(30, 30, 30, 30-3*(sk%100), 0xFFFF);
sprite.fillCircle(30-pitch+pitch0, 30-roll+roll0, 15, 0x0000);
}else{
sprite.fillCircle(30, 30, 30, 0xFFFF);
sprite.fillCircle(30-pitch+pitch0, 30-roll+roll0, 15, 0x0000);
}
sprite.pushSprite(80, 60); // lcdの座標64,0にスプライトを描画
//Right eye
sprite.createSprite(80,60);
sprite.fillCircle(30, 30, 30, 0xFFFF);
sprite.fillCircle(30-pitch+pitch0, 30-roll+roll0, 15, 0x0000);
sprite.pushSprite(80+100, 60); // lcdの座標64,0にスプライトを描画
delay(1);
sprite.deleteSprite();
}
void kiss_MPU(){
sprite.createSprite(120,80);
if(sk%100<10&&(abs(roll-roll0))<5){
sprite.fillEllipse(40, 40, 30-22, 16, 0xF800);
sprite.fillEllipse(40, 40, 22-18, 11-9, 0x0000);
}else{
sprite.fillEllipse(40, 40, 30-int(pitch-pitch0)%8*2, 16-int(roll-roll0)%8, 0xF800);
sprite.fillEllipse(40, 40, 22-int(pitch-pitch0)%8*2, 11-int(roll-roll0)%8, 0x0000);
}
sprite.pushSprite(80+40, 60+60);
delay(1);
sprite.deleteSprite();
}
loop関数
ここでは、BtnAを押すと初期化(目玉が真ん中に来る)するようにしました。
実際にロボットで使うときは、描画モード変更して笑顔や悲しみ、怒りなどを適当に変更できるようにしたいと思っています。
void loop(void)
{
M5.update();
if (M5.BtnA.wasReleased() || M5.BtnA.pressedFor(1000, 200)) {
init_IMU();
}
get_IMU();
sk += 1;
funny_MPU();
kiss_MPU();
}
まとめ
・顔表情をm5stackで開発してみた
・LovyanGFXを利用してちらつきなく描画できた
・IMU(傾き情報)測定結果に応じた表情を表現できた
・簡単だが、いろいろなシチュエーションで利用できるので、いろいろ遊びたい