Spirik / GEM

Good Enough Menu for Arduino
GNU Lesser General Public License v3.0
245 stars 36 forks source link

Context Loop only triggers on input #71

Closed TwystNeko closed 1 year ago

TwystNeko commented 1 year ago

So, I'm working on an incredibly silly mp3 player, and things are going fairly smooth.

I've set up a menuContextLoop which displays a "now playing screen". It's a static screen for now, so I didn't notice, but the loop is only triggering when a button is pressed, or the rotary encoder turns. I'm working off the "Party Hard" sketch, and it looks like I've set it up the same way, but it's just not running continuously. I have a progress bar, for example, that should update every second, but it never fires unless I turn the dial. The rest of the program works, but without the loop triggering as expected, I can't check if playback has stopped.

#include <Arduino.h>
#include <string.h>
#include <FastLED.h>
#include <U8g2lib.h>
#include <DYPlayerArduino.h>
#include <SD.h>
#include "bitmaps.h"
#include <GEM_u8g2.h>
#include "gemproxy.h"
#include "main.h"
#include <Encoder.h>
#include <Automaton.h>
#include "healzone.h"
#include "sg02.h"

#define HAS_HARDWARE_SERIAL

#define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0]))

#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif

#define NUM_LEDS 16
#define LED_DATA_PIN 4

#define BTN_A 20
#define BTN_B 19

#define BEEPER 23

#define ENC_A 5
#define ENC_B 6
#define ENC_BTN 22

#define DY_TX 0 // on the RX1 pin
#define DY_RX 1 
#define DY_BUSY 2 

#define SD_CS 10
#define SD_MOSI 11
#define SD_SCK 13
#define SD_MISO 12

#define LCD_CS 9
#define LCD_CLOCK 14
#define LCD_DATA 7

#define GEM_DISABLE_GLCD
#define GEM_DISABLE_ADAFRUIT_GFX

#define ENC_SLOWDOWN 2

using namespace DY;

U8G2_ST7920_128X64_F_SW_SPI lcd(U8G2_R0, LCD_CLOCK, LCD_DATA, LCD_CS);

Player mp3(&Serial1);

Encoder dial(5,6);

Atm_button dial_btn;
Atm_button a_btn;
Atm_button b_btn;

CRGBArray<NUM_LEDS> leds;
CRGBSet bottombar(leds(0,7));
CRGBSet topbar(leds(8,15));

void lcd_prepare(void) {
  lcd.setFont(u8g2_font_6x10_tf);
  lcd.setFontRefHeightExtendedText();
  lcd.setDrawColor(1);
  lcd.setFontPosTop();
  lcd.setFontDirection(0);
}

GEMProxy menu = GEMProxy(lcd, GEM_POINTER_DASH);

bool playback_state = false;
uint8_t next_song;
uint8_t current_song;
uint8_t ghue = 0;
long next_frame; 
long oldposition = -999;

void songContextEnter();
void songContextExit();
void songContextLoop();
void playSong(GEMCallbackData);

void bpm()
{
  if(playback_state) { 
  // colored stripes pulsing at a defined Beats-Per-Minute (BPM)
  uint8_t BeatsPerMinute = 64;
  CRGBPalette16 palette = RainbowColors_p;
  uint8_t beat = beatsin8( BeatsPerMinute, 16, 255);

  for( int i = 0; i <8; i++) { //9948
    topbar[i] = ColorFromPalette(palette, ghue+(i*20), beat-ghue+(i*30));
    bottombar[i] = ColorFromPalette(palette, ghue+(i*25), beat-ghue+(i*15));
  } 
  } else { 
    leds.fadeToBlackBy(64);
  }
}

song songlist[] = { 
  {"Turbo Killer", "Carpenter Brut", 209000, 144},
  {"Anarchy Road", "Carpenter Brut", 213000, 108},
  {"This FFFire", "Edgerunners OST", 224000, 144},
  {"I Really Wan..", "Edgerunners OST",  246000, 123},
  {"Clutch", "STRLGHT", 157000, 63},
  {"U Got That", "Halogen", 172000, 123},
  {"Full Bodied", "GHOST DATA",248000, 117},
  {"Projekt Melody", "GHOST DATA", 234000, 161},
  {"KNEEL", "Moxi Floxi", 192000, 129}
};

void check_playback() { 
  int busy = digitalRead(DY_BUSY);
  playback_state = !busy;
}

void progress_bar(uint8_t max, uint8_t current, uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t rounded = 0) { 
  // draws a progress bar 
  // bar is 2px inset of the W/H to look good - so 4 px smaller
  uint8_t max_length = w-4;
  uint8_t max_height = h-4;
  uint8_t mapped = map(current, 0, max, 0, max_length);
  if(rounded) { 
    lcd.drawRFrame(x,y,w,h,3);
    lcd.drawRBox(x+2,y+2,mapped, max_height, 1);
  } else { 
    lcd.drawFrame(x,y,w,h); 
    lcd.drawBox(x+2,y+2,mapped, max_height);
  }
}

GEMPageProxy menuPageMain("SONG PICKER");

void menuSetup() { 
  for(uint8_t i=0; i <ARRAY_SIZE(songlist); i++) { 
      menuPageMain.addMenuItem(new GEMItem(songlist[i].SongName, playSong, i+1));
  }
}

void clickHandler (int v, int btn) { 
  // handle the clicks, depending on context 1 = dial, 2 = A, 3 = B

  switch(btn) { 
    case 1:
    // dial
    if(!playback_state) { 
      if(v == 1) { 
        // longpress to stop
       // songContextExit();
         menu.context.exit();
      }
    }
    else { 
      if (v == 1) { 
        menu.registerKeyPress(GEM_KEY_OK);
      }
      if (v == 2) { 
        menu.registerKeyPress(GEM_KEY_CANCEL);
      }
    }
  }
}

void songContextEnter() { 
  lcd.clear();
  lcd.setFont(healzone);
  // just a demo for now!
  lcd.drawStr(-5,0,"! NOW *&*");
  lcd.drawStr(2,20,"PLAYING");
  lcd.setFont(sg02);
  lcd.drawStr(10,42, songlist[current_song].SongName);
  lcd.drawStr(10,51, songlist[current_song].Artist);
}

void songContextLoop() { 

  //Serial.println("song loop");
  if(playback_state) { 
    Serial.println("playing");
    lcd.drawDisc(120,8,8,U8G2_DRAW_ALL);
  }
  if(!playback_state) { 
    Serial.println("not playing");
    lcd.drawBox(116,4,124,12);
    //menu.context.exit();
  }

  lcd.sendBuffer();
}

void songContextExit() { 
  mp3.stop();
  menu.reInit();
  menu.drawMenu();
  menu.clearContext();
}

void playSong(GEMCallbackData data) { 
  char fname[16];
  sprintf(fname, "/%05d.MP3", data.valInt);
  current_song = (data.valInt -1);
  mp3.playSpecifiedDevicePath(Device::Sd, fname);
  // start playback animation here, with 
  menu.context.loop = songContextLoop;
  menu.context.enter = songContextEnter;
  menu.context.exit = songContextExit;
  menu.context.allowExit = false;
  menu.context.enter();
}

void setup() {
  Serial.begin(9600);
  Serial.println("coming online....");
  FastLED.addLeds<NEOPIXEL, LED_DATA_PIN>(leds, NUM_LEDS);
  FastLED.setBrightness(16);
  lcd.begin();
  lcd_prepare();
  lcd.clearBuffer();
  mp3.begin();
  delay(100);
  mp3.setVolume(20); // leave it at 20
  menuSetup();

  oldposition = dial.read();
  dial_btn.begin(ENC_BTN)
    .longPress(2,500)
    .onPress( [] (int idx, int v, int up) {
      switch(v) { 
        case 1:
          clickHandler(1, 1);
          return;
        case 2:
          songContextExit();
          return;
      }
    });

  a_btn.begin(BTN_A)
    .longPress(2,500)
    .onPress( [] (int idx, int v, int up) {
      clickHandler(v, 2);
    });

  a_btn.begin(BTN_B)
    .longPress(2,500)
    .onPress( [] (int idx, int v, int up) {
      clickHandler(v,3);
    });

  menu.setSplash(onosendai_width,onosendai_height,onosendai_bits)
      .hideVersion()
      .setMenuPageCurrent(menuPageMain)
      .init();
      menu.drawMenu();

}

void loop() {

  // do input checks! Separate function?
  long newposition = dial.read();
  automaton.run();

  if(newposition != oldposition) { 
    if(abs(newposition - oldposition) > ENC_SLOWDOWN) { 
      if(newposition > oldposition) { 
        menu.registerKeyPress(GEM_KEY_DOWN);
      } else if(newposition < oldposition) { 
        menu.registerKeyPress(GEM_KEY_UP);
      }
    }
    oldposition = newposition;
    playback_state = !digitalRead(2);
  }

  lcd.sendBuffer();

}
Spirik commented 1 year ago

Hi TwystNeko!

From the top of my head, try adding menu.registerKeyPress(GEM_KEY_NONE); to your loop() in the case when no useful interaction was performed during the iteration of the loop.

The thing is, context.loop() function is called from within GEM, specifically from within ::dispatchKeyPress() function which in turn is called from within ::registerKeyPress(). So you need to call registerKeyPress() in order to invoke user-defined loop. And when you press the button you are doing exactly that - calling ::registerKeyPress() method. So even if are not going to interact with your sketch via pressing the button, you should explicitly call ::registerKeyPress() method anyway, supplying it with dummy code GEM_KEY_NONE.

So your loop may look something like this:

void loop() {

  // do input checks! Separate function?
  long newposition = dial.read();
  automaton.run();

  if(newposition != oldposition) { 
    if(abs(newposition - oldposition) > ENC_SLOWDOWN) { 
      if(newposition > oldposition) { 
        menu.registerKeyPress(GEM_KEY_DOWN);
      } else if(newposition < oldposition) { 
        menu.registerKeyPress(GEM_KEY_UP);
      }
    }
    oldposition = newposition;
    playback_state = !digitalRead(2);
  } else {
    menu.registerKeyPress(GEM_KEY_NONE);
  }

  lcd.sendBuffer();

}

Alternatively, this probably can work as well:

void loop() {

  // do input checks! Separate function?
  long newposition = dial.read();
  automaton.run();

  if(newposition != oldposition) { 
    if((abs(newposition - oldposition) > ENC_SLOWDOWN) && menu.readyForKey()) { 
      if(newposition > oldposition) { 
        menu.registerKeyPress(GEM_KEY_DOWN);
      } else if(newposition < oldposition) { 
        menu.registerKeyPress(GEM_KEY_UP);
      }
    }
    oldposition = newposition;
    playback_state = !digitalRead(2);
  }

  lcd.sendBuffer();

}

Note call to menu.readyForKey() . That method checks if GEM is ready to accept an interaction and internally calls registerKeyPress(GEM_KEY_NONE); if some conditions are met (yours seems to).

TwystNeko commented 1 year ago

That did it! Thanks!