榊原昇吾
Published

Grow up! stack chan -Void Aquarium()-

An interactive robot that raises stack-chan using M5Stack, Toio, and XMod, evolving when fed and sometimes flashing back past memories.

IntermediateShowcase (no instructions)2 days42
Grow up! stack chan -Void Aquarium()-

Things used in this project

Hardware components

M5Stack Core2 ESP32 IoT Development Kit
M5Stack Core2 ESP32 IoT Development Kit
×1
Color Sensor RGB TCS34725 Unit
M5Stack Color Sensor RGB TCS34725 Unit
×1
Sony toio
×1
M5Stack xMod
×1
power supply 9V
×1
wireless power module
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Plasma ball

Story

Read more

Code

M5stack core2 with Toio

C/C++
#include <Avatar.h>
#include <SD.h>
#include <SPI.h>
#include <M5Unified.h>
#include <Toio.h>
#include <Adafruit_TCS34725.h>
#include <Wire.h>

#include <faces/FaceTemplates.hpp>

#define HAPPY 1
#define ANGRY 2
#define SLURP 3

// Core2 の Port.A は SDA=32, SCL=33
static constexpr int SDA_PIN = 32;
static constexpr int SCL_PIN = 33;

using namespace m5avatar;

int randspeed = 0;
int randposX = 200;
int randtime = 3000;
int randdesire = 0;

int evolution = 0;
int flushbackcount = 0;

int naderu = 0;

int desirecount = 0;

int desire = 0;
int desirecolor[3] = {0,0,0};
int desirecolor1[3] = {72,45,55}; //赤色食べ物
int desirecolor2[3] = {121,220,200};  //緑色食べ物
int desirecolor3[3] = {31,115,190}; //青色食べ物
int desirecolor_other1[3] = {0,0,0}; //違う色食べ物1
int desirecolor_other2[3] = {0,0,0};  //違う色食べ物2
int fooddesire = 0;

int redpoint = 0;
int greenpoint = 0;
int bluepoint = 0;
int slurppoint = 0;

// Toio オブジェクト生成
Toio toio;

Avatar avatar;

// 積分時間・ゲインは環境に合わせて調整可能
Adafruit_TCS34725 tcs(
  TCS34725_INTEGRATIONTIME_50MS,   // 2.4/24/50/101/154/700ms など
  TCS34725_GAIN_4X                 // 1X/4X/16X/60X
);

// stackchanの設定
Face* faces[7];
const int num_faces = sizeof(faces) / sizeof(Face*);
int face_idx = 0;  // face index

const Expression expressions[] = {Expression::Angry, Expression::Sleepy,
                                  Expression::Happy, Expression::Sad,
                                  Expression::Doubt, Expression::Neutral};
const int num_expressions = sizeof(expressions) / sizeof(Expression);
int idx = 0;

ColorPalette* color_palettes[12];
const int num_palettes = sizeof(color_palettes) / sizeof(ColorPalette*);
int palette_idx = 0;

bool isShowingQR = false;
//////////////
// an example of customizing
class MyCustomFace : public Face {
   public:
    MyCustomFace()
        : Face(new RectMouth(50, 90, 4, 60), new BoundingRect(148, 123),
               // right eye, second eye arg is center position of eye in (y,x)
               new EllipseEye(16, 16, false), new BoundingRect(93, 50),
               //  left eye
               new EllipseEye(16, 16, true), new BoundingRect(96, 190),
               //  hide eye brows with setting these height zero
               new EllipseEyebrow(0, 0, false), new BoundingRect(67, 56),
               new EllipseEyebrow(0, 0, true), new BoundingRect(72, 190)) {}
};
class MyCustomFace2 : public Face {
   public:
    MyCustomFace2()
        : Face(new UShapeMouth(64, 64, 0, 16), new BoundingRect(194, 120),
               // right eye, second eye arg is center position of eye in (y,x)
               new EllipseEye(16, 16, false), new BoundingRect(93, 50),
               //  left eye
               new EllipseEye(16, 16, true), new BoundingRect(96, 190),
               //  hide eye brows with setting these height zero
               new EllipseEyebrow(0, 0, false), new BoundingRect(67, 56),
               new EllipseEyebrow(0, 0, true), new BoundingRect(72, 190)) {}
};
/**
 * @brief face template for "OωO" face
 *
 */
class OmegaFace2 : public Face {
   public:
    OmegaFace2()
        : Face(new OmegaMouth(), new BoundingRect(225, 120),
               // right eye, second eye arg is center position of eye in (y,x)
               new EllipseEye(false), new BoundingRect(165, 44),
               //  left eye
               new EllipseEye(true), new BoundingRect(165, 44 + 154),
               //  hide eye brows with setting these height zero
               new EllipseEyebrow(0, 0, false), new BoundingRect(67, 56),
               new EllipseEyebrow(0, 0, true), new BoundingRect(72, 190)) {}
};

class GirlyFace3 : public Face {
   public:
    GirlyFace3()
        : Face(new UShapeMouth(44, 44, 0, 16), new BoundingRect(222, 120),
               // right eye, second eye arg is center position of eye
               new GirlyEye(64, 64, false), new BoundingRect(163, 54),
               //  left eye
               new GirlyEye(64, 64, true), new BoundingRect(163, 186),

               // right eyebrow
               new BowEyebrow(140, 160, false),
               new BoundingRect(163, 54),  // (y,x)
                                           //  left eyebrow
               new BowEyebrow(140, 160, true), new BoundingRect(163, 186)) {}
};

class DoggyFace2 : public Face {
   public:
    DoggyFace2()
        : Face(new DoggyMouth(50, 90, 4, 60), new BoundingRect(168, 123),
               // right eye, second eye arg is center position of eye
               new DoggyEye(false), new BoundingRect(103, 40),
               //  left eye
               new DoggyEye(true), new BoundingRect(106, 200),
               //  hide eye brows with setting these height zero
               new RectEyebrow(15, 2, false), new BoundingRect(67, 56),
               new RectEyebrow(15, 2, true), new BoundingRect(72, 190)) {}
};

class PinkDemonFace2 : public Face {
   public:
    PinkDemonFace2()
        : Face(new UShapeMouth(64, 64, 0, 16), new BoundingRect(214, 120),
               // right eye, second eye arg is center position of eye
               new PinkDemonEye(52, 134, false), new BoundingRect(134, 66),
               //  left eye
               new PinkDemonEye(52, 134, true), new BoundingRect(134, 178),

               //  hide eye brows with setting these height zero
               new EllipseEyebrow(15, 0, false), new BoundingRect(67, 56),
               new EllipseEyebrow(15, 0, true), new BoundingRect(72, 190)) {}
};

std::vector<ToioCore*> toiocore_list;
ToioCore* toiocore;

void changeemotion(int emotion){
  if(emotion == 1){ //喜ぶ
    avatar.setExpression(expressions[2]);
    avatar.setColorPalette(*color_palettes[7]);
  }else if(emotion == 2){ //怒る
    avatar.setExpression(expressions[0]);
    avatar.setColorPalette(*color_palettes[8]);
  }else if(emotion == 3){ //すねる
    avatar.setExpression(expressions[1]);
    avatar.setColorPalette(*color_palettes[9]);
  }else{
    avatar.setExpression(expressions[5]);
    avatar.setColorPalette(*color_palettes[2]);
  }
}

void changeaction(int action){
  if(action == 1){  //  ご飯食べる
    toiocore->controlMotorWithTarget(0, 5, 0, 30, 0, 150, 270, 1, 0);
    delay(2000); // 押下のバウンス防止
    toiocore->controlMotorWithTarget(0, 5, 0, 50, 0, 150, 270, 360, 3);
    delay(2000); // 押下のバウンス防止
    toiocore->controlMotorWithTarget(0, 5, 0, 30, 0, 250, 270, 1, 0);
    delay(2000); // 押下のバウンス防止
    toiocore->controlMotorWithTarget(0, 5, 0, 50, 0, 250, 270, 360, 3);
    delay(2000); // 押下のバウンス防止
    toiocore->controlMotorWithTarget(0, 5, 0, 30, 0, 350, 270, 1, 0);
    delay(7000); // 押下のバウンス防止
  } else if(action == 2){   //遊ぶ
    ToioCoreIDData data = toiocore->getIDReaderData();
    toiocore->controlMotorWithTarget(0, 5, 0, 50, 0, randposX, 250, 360, 3);
    delay(1000); // 押下のバウンス防止
  }
}

void changedesire(){
  if(desire == 1){  //  おなかすいた
    avatar.setExpression(expressions[5]);
    if(fooddesire == 0){
      fooddesire = random(1,4);
    }
    for(int i = 0;i < 3;i++){
      if(fooddesire == 1){
        desirecolor[i] = desirecolor1[i];
        desirecolor_other1[i] = desirecolor2[i];
        desirecolor_other2[i] = desirecolor3[i];
        avatar.setColorPalette(*color_palettes[5]);
      }else if(fooddesire == 2){
        desirecolor[i] = desirecolor2[i];
        desirecolor_other1[i] = desirecolor1[i];
        desirecolor_other2[i] = desirecolor3[i];
        avatar.setColorPalette(*color_palettes[10]);
      }else if(fooddesire == 3){
        desirecolor[i] = desirecolor3[i];
        desirecolor_other1[i] = desirecolor1[i];
        desirecolor_other2[i] = desirecolor2[i];
        avatar.setColorPalette(*color_palettes[11]);
      }
    }
    desirecount++;
  } else if(desire == 2){   //遊びたい
    avatar.setExpression(expressions[5]);
    avatar.setColorPalette(*color_palettes[6]);
    desirecount++;
  } else if(desire == 0){
    avatar.setExpression(expressions[5]);
    avatar.setColorPalette(*color_palettes[2]);
  }
  if(desirecount == 500){
    changeemotion(SLURP);
    desire = 3;
    delay(1000);
    M5.Log.printf("naderu = %d \n",naderu);
    if(naderu >= 3){
      naderu = 0;
      desire = 0;
      desirecount = 0;
      slurppoint++;
      changeemotion(4);
    }
  }
}

void judgedesire(){
  if(desire!=0){
    M5.Log.printf("judgedesire");
    uint16_t r, g, b, c;
    tcs.getRawData(&r, &g, &b, &c);
    uint16_t colorTemp = tcs.calculateColorTemperature_dn40(r, g, b, c);
    uint16_t lux       = tcs.calculateLux(r, g, b);
  
  M5.Log.printf("r=%d,g=%d,b=%d\n correct r=%d,g=%d,b=%d \n",r,g,b,desirecolor[0],desirecolor[1],desirecolor[2]);
    if((r < desirecolor[0]+20)&&(r > desirecolor[0]-20)){
      if((g < desirecolor[1]+20)&&(g > desirecolor[1]-20)){
        if((b < desirecolor[2]+20)&&(b > desirecolor[2]-20)){
          changeemotion(1);
          changeaction(1);
          naderu = 0;
          desire = 0;
          desirecount = 0;
          if(fooddesire == 1){
            redpoint++;
          }else if(fooddesire == 2){
            greenpoint++;
          }else if(fooddesire == 3){
            bluepoint++;
          }
          fooddesire = 0;
          changeemotion(4);
          if(evolution >= 1){
            flushbackcount=flushbackcount+1;
          }
        }
      }
    }
    if((r < desirecolor_other1[0]+30)&&(r > desirecolor_other1[0]-30)){
      if((g < desirecolor_other1[1]+30)&&(g > desirecolor_other1[1]-30)){
        if((b < desirecolor_other1[2]+30)&&(b > desirecolor_other1[2]-30)){
          changeemotion(ANGRY);
          delay(3000);
          naderu = 0;
          desire = 0;
          desirecount = 0;
          fooddesire = 0;

          changeemotion(4);
        }
      }
    }
    if((r < desirecolor_other2[0]+30)&&(r > desirecolor_other2[0]-30)){
      if((g < desirecolor_other2[1]+30)&&(g > desirecolor_other2[1]-30)){
        if((b < desirecolor_other2[2]+30)&&(b > desirecolor_other2[2]-30)){
          changeemotion(ANGRY);
          delay(3000);
          naderu = 0;
          desire = 0;
          desirecount = 0;
          fooddesire = 0;

          changeemotion(4);
        }
      }
    }
  }
}

void evolutionJudge(){
  if(evolution == 0){
    if(redpoint + greenpoint + bluepoint >= 3){
      //avatar.setFace(faces[6]);
    }
    if(redpoint >= 5 || greenpoint >= 5 || bluepoint >= 5 || slurppoint >= 3){
      avatar.setFace(faces[1]);
      delay(1000);
      avatar.setFace(faces[2]);
      delay(1000);
      avatar.setFace(faces[3]);
      delay(1000);
      avatar.setFace(faces[4]);
      delay(1000);
      avatar.setFace(faces[1]);
      delay(700);
      avatar.setFace(faces[2]);
      delay(700);
      avatar.setFace(faces[3]);
      delay(700);
      avatar.setFace(faces[4]);
      delay(700);
      avatar.setFace(faces[1]);
      delay(300);
      avatar.setFace(faces[2]);
      delay(300);
      avatar.setFace(faces[3]);
      delay(300);
      avatar.setFace(faces[4]);
      delay(300);
      avatar.setFace(faces[1]);
      delay(200);
      avatar.setFace(faces[2]);
      delay(200);
      avatar.setFace(faces[3]);
      delay(200);
      avatar.setFace(faces[4]);
      delay(200);
      
      if(slurppoint == 3){
        avatar.setFace(faces[5]);
        evolution = 0;
      }
      if((redpoint + greenpoint + bluepoint) >= 11)
      {
        avatar.setFace(faces[4]);
        evolution = 4;
      }
      if(redpoint >= 5)
      {
        avatar.setFace(faces[1]);
        evolution = 1;
      }
      if(greenpoint >= 5)
      {
        avatar.setFace(faces[2]);
        evolution = 2;
      }
      if(bluepoint >= 5)
      {
        avatar.setFace(faces[3]);
        evolution = 3;
      }
      redpoint = 0;
      greenpoint = 0;
      bluepoint = 0;
      slurppoint = 0;
    }
  }
}

void flushback(){
  if(flushbackcount == 3){
    avatar.setExpression(expressions[3]);
    avatar.setColorPalette(*color_palettes[12]);
    delay(2000);
    avatar.stop();
    delay(1000);
    M5.Lcd.fillScreen(BLACK);
    delay(500);
    if(evolution == 1){
      displayBMP("/robot.bmp", 0, 0);
    }else if(evolution == 2){
      displayBMP("/drop.bmp", 0, 0);
    }else if(evolution == 3){
      displayBMP("/light.bmp", 0, 0);
    }else if(evolution == 4){
      displayBMP("/sea.bmp", 0, 0);
    }
    delay(7000);
    M5.Lcd.fillScreen(BLACK);
    delay(500);

    avatar.init(8);

    avatar.setExpression(expressions[3]);
    delay(2000);
    avatar.setColorPalette(*color_palettes[2]);
    avatar.setExpression(expressions[5]);
    delay(2000);
    flushbackcount=0;
    evolution = 0;
  }
}

void setup() {
  auto cfg = M5.config();
  M5.begin();
//  M5.Power.begin();
  M5.Lcd.clear();
  M5.Lcd.setRotation(0); // 画面の向きを設定
//  M5.Lcd.fillScreen(BLACK);

// I2C 初期化(Core2 の Port.A)
  Wire.begin(SDA_PIN, SCL_PIN);
// センサ初期化
if (!tcs.begin(TCS34725_ADDRESS, &Wire)) {
  M5.Display.setTextColor(TFT_RED, TFT_BLACK);
  M5.Display.println("TCS3472 not found (addr 0x29).");
  while (1) { delay(100); }
}
  // LED(オンボード照明)を少し弱めに
  tcs.setInterrupt(false);         // 変換動作開始(必要に応じて)
  tcs.setIntegrationTime(TCS34725_INTEGRATIONTIME_50MS);
  tcs.setGain(TCS34725_GAIN_4X);

  M5.Display.setTextColor(TFT_GREEN, TFT_BLACK);
  M5.Display.println("Sensor OK.");
  if (!SD.begin(GPIO_NUM_4, SPI, 40000000)) {
//      M5.Lcd.println("SD Card Mount Failed");
      return;
  }

    faces[0] = avatar.getFace();  // native face
    faces[1] = new DoggyFace2();
    faces[2] = new OmegaFace2();
    faces[3] = new GirlyFace3();
    faces[4] = new PinkDemonFace2();
    faces[5] = new MyCustomFace();
    faces[6] = new MyCustomFace2();

    color_palettes[0] = new ColorPalette();
    color_palettes[1] = new ColorPalette();
    color_palettes[2] = new ColorPalette();
    color_palettes[3] = new ColorPalette();
    color_palettes[4] = new ColorPalette();
    color_palettes[5] = new ColorPalette();
    color_palettes[6] = new ColorPalette();
    color_palettes[7] = new ColorPalette();
    color_palettes[8] = new ColorPalette();
    color_palettes[9] = new ColorPalette();
    color_palettes[10] = new ColorPalette();
    color_palettes[11] = new ColorPalette();
    color_palettes[12] = new ColorPalette();
    color_palettes[1]->set(COLOR_PRIMARY,
                           M5.Lcd.color24to16(0x383838));  // eye
    color_palettes[1]->set(COLOR_BACKGROUND,
                           M5.Lcd.color24to16(0xfac2a8));  // skin
    color_palettes[1]->set(COLOR_SECONDARY,
                           TFT_PINK);  // cheek
    color_palettes[2]->set(COLOR_PRIMARY, TFT_YELLOW);
    color_palettes[2]->set(COLOR_BACKGROUND, TFT_DARKCYAN);
    color_palettes[3]->set(COLOR_PRIMARY, TFT_DARKGREY);
    color_palettes[3]->set(COLOR_BACKGROUND, TFT_WHITE);
    color_palettes[4]->set(COLOR_PRIMARY, TFT_RED);
    color_palettes[4]->set(COLOR_BACKGROUND, TFT_PINK);
    color_palettes[5]->set(COLOR_PRIMARY, TFT_RED);  //おなかすいた 赤
    color_palettes[5]->set(COLOR_BACKGROUND, TFT_YELLOW);
    color_palettes[6]->set(COLOR_PRIMARY, TFT_BLACK); //遊びたい
    color_palettes[6]->set(COLOR_BACKGROUND, TFT_GREEN);
    color_palettes[7]->set(COLOR_PRIMARY, TFT_BLACK); //喜び
    color_palettes[7]->set(COLOR_BACKGROUND, TFT_BLUE);
    color_palettes[8]->set(COLOR_PRIMARY, TFT_BLACK); //怒り
    color_palettes[8]->set(COLOR_BACKGROUND, TFT_RED);
    color_palettes[9]->set(COLOR_PRIMARY, TFT_BLACK); //すねる
    color_palettes[9]->set(COLOR_BACKGROUND, TFT_WHITE);
    color_palettes[10]->set(COLOR_PRIMARY, TFT_GREEN);  //おなかすいた 緑
    color_palettes[10]->set(COLOR_BACKGROUND, TFT_YELLOW);
    color_palettes[11]->set(COLOR_PRIMARY, TFT_BLUE);  //おなかすいた 青
    color_palettes[11]->set(COLOR_BACKGROUND, TFT_YELLOW);
    color_palettes[12]->set(COLOR_PRIMARY, TFT_LIGHTGREY);  //GREY
    color_palettes[12]->set(COLOR_BACKGROUND, TFT_WHITE);

    avatar.setFace(faces[5]);
    avatar.init(8);  // start drawing w/ 8bit color mode
    avatar.setColorPalette(*color_palettes[2]);

  M5.Log.println("- toio コア キューブをスキャンします。");
  toiocore_list = toio.scan(3);
  size_t n = toiocore_list.size();
  if (n == 0) {
    M5.Log.println("- toio コア キューブが見つかりませんでした。");
    return;
  }
  M5.Log.printf("- %d 個の toio コア キューブが見つかりました。\n", n);

  M5.Log.println("- toio コア キューブに BLE 接続します。");
  toiocore = toiocore_list.at(0);
  bool connected = toiocore->connect();
  if (!connected) {
    M5.Log.println("- BLE 接続に失敗しました。");
    return;
  }

  // ボタン押下状態イベントのコールバックをセット
  toiocore->onButton([](bool state) {
    M5.Log.printf("naderu = %d \n",naderu);
    naderu++;
  });

    M5.Log.println("- 準備完了");
}

void displayBMP(const char *filename, int16_t x, int16_t y) {
    File bmpFile = SD.open(filename);
    if (!bmpFile) {
        M5.Lcd.println("File not found");
        return;
    }
    
    uint8_t header[54];
    bmpFile.read(header, 54);
    
    int bmpWidth  = *(int*)&header[18];
    int bmpHeight = *(int*)&header[22];
    uint16_t bitDepth = *(uint16_t*)&header[28];
    
    if (bitDepth != 24) {
        M5.Lcd.println("Only 24-bit BMP supported");
        bmpFile.close();
        return;
    }
    
    int rowSize = (bmpWidth * 3 + 3) & ~3;
    uint8_t rowBuffer[rowSize];
    
    for (int row = 0; row < bmpHeight; row++) {
        bmpFile.seek(54 + (bmpHeight - 1 - row) * rowSize);
        bmpFile.read(rowBuffer, rowSize);
        for (int col = 0; col < bmpWidth; col++) {
            int index = col * 3;
            // BGR -> RGB に変換
            uint16_t color = M5.Lcd.color565(rowBuffer[index + 2], rowBuffer[index + 1], rowBuffer[index]);
            M5.Lcd.drawPixel(x + col, y + row, color);
        }
    }
    bmpFile.close();
}

void loop() {
  M5.update();

  // If you want to handle event callbacks, you shoud call loop() method  of Toio Object here.
  // イベントを扱う場合は、必ずここで Toio オブジェクトの
  // loop() メソッドを呼び出すこと
  toio.loop();
    //M5.Log.printf("press/release A btn\n");
    if(desire == 0){  //欲求がないとき
      randspeed = random(30,50);
      randposX = random(150,300);
      randtime = random(1000,5000);
      toiocore->controlMotorWithTarget(0, 5, 0, randspeed, 0, randposX, 270, 1, 0);
      delay(randtime); // 押下のバウンス防止
      toiocore->controlMotorWithTarget(0, 5, 0, 0, 0, randposX, 270, 1, 0);
      delay(500); // 押下のバウンス防止
      //確率で何かしらの欲求が発生
      randdesire = random(0,100);
      M5.Log.printf("randdesire = %d\n",randdesire);
      M5.Log.printf("desire = %d\n",desire);
      if(randdesire < 20){  //おなかすいた
        desire = 1;
      }else if(randdesire > 100){  //あそびたい
        desire = 2;
      }
    }
    //stackchan 状態変更
    changedesire();
    judgedesire();
    evolutionJudge();
    flushback();

    delay(100);
}

Credits

榊原昇吾
1 project • 0 followers

Comments