OS8 Fett263 Prop Updates

I am currently working on adding the following features/updates to my prop for OS8. If there’s something you’d like me to look at (within reason) please post in the next week and I’ll consider if I can get it into the prop while OS8 Beta is still going on. Once beta is done anything else would have to be an OS9 consideration.

You can keep track of OS8 beta progress, and help out with testing here: ProffieOS 8.1 beta (call for testers!)

I’ll update this post as these features are merged/developed.

Merged (can be tested):

  • Edit Mode menu supports MOUNT_SD_SETTING
  • MENU_SPEC_TEMPLATE support
  • MENU_SPEC_TEMPLATE FETT263_MENU_SPEC (expansion of OS8 Default Settings Menu to support Gesture Editing)

In progress:

  • Manual Blade Array Switching and Manual Blade ID (from BC prop)
  • Color Hue rotation for ARG styles in place of ColorChange Color List selection
  • ColorChange fix (OS8 Color Change mode not compatible with existing features in my prop)
  • Button “Clicker” sound (from SaberSense prop)
  • “Simple” prop define (single define to disable “bells and whistles” features)

Requests (have to be looked into):

  • replace STDOUT with PVLOG
4 Likes

May I suggest that you change all (or most) of the STDOUT.println("your text here"); with PVLOG_NORMAL << "your text here\n"; (or PVLOG_DEBUG, PVLOG_STATUS, …). I saved 584 bytes of flash memory by changing them all to PVLOG_DEBUG in your prop.

Interesting, I’ll have a look. Thanks.

I can pm you my “version” since it is already done. If you want ?

Edit: I will remove my “additions” and pm you a “clean” version. Just white spaces removed and PVLOG... instead of STDOUT...

No need, I have a local with all the new additions already in progress so yours won’t align.

OK, no prob.
Edit: It’s done, in case you change your mind?

Is it possible to add OLED functionality when navigating the settings menu? Much like the sabertrio prop. That’s the only thing I liked about their prop. Their button controls are annoying.

I can take a look.

Yeah, I’ve been thinking about working on showing a BMP for each menu level (like edit blade length) for example.
If we really wanted to go crazy, we could have a meter associated with adjustment levels.

Yeah, was already discussing with @profezzorn and he already had several ideas as well. Will have to see how best to accomplish in both the new menu system and Edit Mode’s menu and if it’s feasible for OS8 or would need to wait for OS9.

For what it’s worth, here’s what they do:

#ifndef DISPLAY_SSD1306_H
#define DISPLAY_SSD1306_H

#ifndef PLI_OFF_TIME
#define PLI_OFF_TIME ((font_config.ProffieOSFontImageDuration == 0) ? 3500 : font_config.ProffieOSFontImageDuration)
#endif

#include "monoframe.h"

// Images/animations
IMAGE_FILESET(boot);
IMAGE_FILESET(font);
IMAGE_FILESET(blst);
IMAGE_FILESET(clsh);
IMAGE_FILESET(force);
IMAGE_FILESET(preon);
IMAGE_FILESET(out);
IMAGE_FILESET(in);
IMAGE_FILESET(pstoff);
IMAGE_FILESET(on);
IMAGE_FILESET(lock);
IMAGE_FILESET(idle);
/* To-Do, possibly differently
#ifdef OLED_USE_BLASTER_IMAGES
IMAGE_FILESET(reload);
IMAGE_FILESET(empty);
IMAGE_FILESET(jam);
IMAGE_FILESET(clipin);
IMAGE_FILESET(clipout);
IMAGE_FILESET(destruct);
#endif
*/
IMAGE_FILESET(lowbatt);

/* START of MODIFICATION by Sabertrio for OLED Functionalities. [1/2] */
/* Define additional images/animations for use in Sabertrio Proffie Preconfiguration. */
IMAGE_FILESET(no_blade);
IMAGE_FILESET(kyber);
IMAGE_FILESET(length);
IMAGE_FILESET(volumemenu);
IMAGE_FILESET(brightness);
IMAGE_FILESET(clashthreshold);
IMAGE_FILESET(controlsettings);
IMAGE_FILESET(settingsmenu);
IMAGE_FILESET(clashdetectlvl);
IMAGE_FILESET(forcepush);
IMAGE_FILESET(forcepushlength);
IMAGE_FILESET(lockupdelay);
IMAGE_FILESET(maxclash);
IMAGE_FILESET(powerlock);
IMAGE_FILESET(stabignite);
IMAGE_FILESET(swingignite);
IMAGE_FILESET(swingonspeed);
IMAGE_FILESET(thrustignite);
IMAGE_FILESET(twistignite);
IMAGE_FILESET(twistretract);
/* END of MODIFICATION by Sabertrio for OLED Functionalities. [1/2] */

enum Screen {
  SCREEN_UNSET,
  SCREEN_STARTUP,
  SCREEN_MESSAGE,
  SCREEN_ERROR_MESSAGE,
  SCREEN_PLI,
  SCREEN_IMAGE,  // also for animations
  SCREEN_OFF,
  SCREEN_DEFAULT
};

template<int Width, class col_t>
class Display : public MonoFrame<Width, col_t> {
public:
  virtual void Page() = 0;
  virtual void SB_Top() = 0;
  virtual Screen GetScreen() = 0;
  virtual void SB_IsOn(bool* on) = 0;
};

template<int Width, class col_t>
class DisplayControllerBase {
public:
  // Return zero if frame is not yet ready.
  // May be called before it's time to actually display next frame.
  virtual int FillFrameBuffer(bool advance) = 0;
  virtual void SetDisplay(Display<Width, col_t>* display) = 0;
  virtual Screen GetScreen() { return SCREEN_UNSET; }
  virtual void usb_connected() = 0;
};

template<int Width, class col_t>
class AbstractDisplayController : public DisplayControllerBase<Width, col_t> {
public:
  Display<Width, col_t>* display_;
  void SetDisplay(Display<Width, col_t>* display) override {
    display_ = display;
  }
  bool ImageScreen() { return display_ && display_->GetScreen() == SCREEN_IMAGE; }
};

template<int Width, class col_t>
class DisplayHelper1 {
public:
  DisplayControllerBase<Width, col_t> *a;
  int t_;
  int FillFrameBuffer() {
    if (t_ <= 0) {
      t_ += a->FillFrameBuffer(true);
    } else {
      a->FillFrameBuffer(false);
    }
    return t_;
  }
  void SetDisplay(Display<Width, col_t>* display)  {
    a->SetDisplay(display);
  }
  Screen GetScreen() override { return a->GetScreen(); }
  void usb_connected() override { a->usb_connected(); }
  void Advance(int t) { t_ -= t; }
};

template<int Width, class col_t, class... OPS> class DisplayHelper2 {};
template<int Width, class col_t>
class DisplayHelper2<Width, col_t> {
 public:
  void set(DisplayHelper1<Width, col_t>* out) { }
};

template<int Width, class col_t, class OP, class... OPS>
class DisplayHelper2<Width, col_t, OP, OPS...> {
 public:
  void set(DisplayHelper1<Width, col_t>* out) {
    out->a = &op_;
    rest_.set(out + 1);
  }
 private:
  typename OP::template Controller<Width, col_t> op_;
  DisplayHelper2<Width, col_t, OPS...> rest_;
};

template<int Width, class col_t, class... OPERATIONS>
class DisplayHelper : public DisplayControllerBase<Width, col_t> {
public:
  int last_t_;
  DisplayHelper1<Width, col_t> operations_[sizeof...(OPERATIONS)];
  DisplayHelper2<Width, col_t, OPERATIONS ...> ops_;

  DisplayHelper() {
    ops_.set(operations_);
  }
  void SetDisplay(Display<Width, col_t>* display) override {
    for (size_t i = 0; i < sizeof...(OPERATIONS); i++) {
      operations_[i].SetDisplay(display);
    }
  }
  Screen GetScreen() override {
    Screen ret;
    for (size_t i = 0; i < sizeof...(OPERATIONS); i++) {
      ret = operations_[i].GetScreen();
      if (ret != SCREEN_UNSET) return ret;
    }
    return SCREEN_UNSET;
  }
  void usb_connected() override {
    for (size_t i = 0; i < sizeof...(OPERATIONS); i++) {
      operations_[i].usb_connected();
    }
  }

  int FillFrameBuffer(bool advance) override {
    for (size_t i = 0; i < sizeof...(OPERATIONS); i++) {
      operations_[i].Advance(last_t_);
    }
    last_t_ = 10000;
    for (size_t i = 0; last_t_ && i < sizeof...(OPERATIONS); i++) {
      last_t_ = std::min(last_t_, operations_[i].FillFrameBuffer());
    }
    return last_t_;
  }
};


// Operations

// Clear entire screen.
// Intended to be used as base layer.
struct ClearScreenOp {
  template<int Width, class col_t> struct Controller : public AbstractDisplayController<Width, col_t> {
    Screen GetScreen() override { return SCREEN_IMAGE; }
    int FillFrameBuffer(bool advance) override {
      this->display_->ClearRect(0, Width, 0, sizeof(col_t) * 8);
      return 1000; // Never changes.
    }
  };
};


// Op wrapper, skips the op if the base is not an image.
template<template<int, class> class T>
struct IfImageOp {
  template<int Width, class col_t> struct Controller : public T<Width, col_t> {
    int FillFrameBuffer(bool advance) override {
      if (!this->ImageScreen()) return 1000;
      return typename T<Width, col_t>::FillFrameBuffer(advance);
    }
  };
};


// Clear a rectangle.
template<int x1, int x2, int y1, int y2> struct ClearRectangleOp {
  template<int Width, class col_t> struct Controller : public AbstractDisplayController<Width, col_t> {
    int FillFrameBuffer(bool advance) override {
      this->display_->ClearRect(x1, x2, y1, y2);
      return 1000; // Never changes.
    }
  };
};

// TODO: Figure out how to replace this ifdef with std::enable_if
#ifdef PROP_HAS_BULLET_COUNT

// Draw bullet count on the screen.
// May need to be combined with a ClearRectangleOp
template<int x, int y, int digits> struct WriteBulletCountOp {
  template<int Width, class col_t> struct Controller : public AbstractDisplayController<Width, col_t> {
    int FillFrameBuffer(bool advance) override {
      char tmp[30];
      for (int i = 0; i < digits; i++) tmp[i] = '0';
      itoa(prop.GetBulletCount(), tmp+digits, 10);
      this->display_->DrawText(tmp + std::min<int>(digits, strlen(tmp+digits)), x, y, Starjedi10pt7bGlyphs);
      return 50; // new frame in 50 ms or less.
    }
  };
};

#endif



#if 0
class DisplayHelper {
  DisplayControllerBase *a;
  int t_;
  int FillFrameBuffer() {
    t_ += a->FillFrameBuffer(t_ <= 0);
  }
  void Advance(int t) { t_ -= t; }
};
class Combine {
  DisplayHelper a_, b_;
  int last_t_;

  int FilleFrameBuffer(bool advance) {
    if (advance) {
      a_.Advance(last_t_);
      b_.Advance(last_t_);
    }
    int last_t_ = a_.FillFrameBuffer();
    if (last_t_ > 0) last_t_ = std::min(last_t_, b_.FillFrameBuffer());
    return last_t_;
  }
};
#endif

template<int Width, class col_t>
class StandardDisplayController : public DisplayControllerBase<Width, col_t>, SaberBase, private AudioStreamWork
#ifdef ENABLE_DEVELOPER_COMMANDS
  , CommandParser
#endif
{
public:
  static const int WIDTH = Width;
  static const int HEIGHT = sizeof(col_t) * 8;

  enum ScreenLayout {
    LAYOUT_NATIVE,
    LAYOUT_LANDSCAPE,
    LAYOUT_PORTRAIT,
  };

  enum class Tristate : uint8_t {
    Unknown,
    True,
    False
  };

  Display<Width, col_t>* display_;
  virtual void SetDisplay(Display<Width, col_t>* display) override {
    display_ = display;
  }

  // Clear and go to native mode.
  void Clear() {
    display_->Clear();
    layout_ = LAYOUT_NATIVE;
    xor_ = 0;
    invert_y_ = false;
  }

  // Idempotent
  void ConvertToNative() {
    bool invert_y = invert_y_;
    switch (layout_) {
    case LAYOUT_NATIVE:
      break;
    case LAYOUT_LANDSCAPE:
      display_->ConvertLandscape(invert_y);
      invert_y = false;
      break;
    case LAYOUT_PORTRAIT:
      display_->ConvertPortrait();
    }
    if (invert_y) {
      display_->FlipY();
    }
    if (xor_) {
      display_->Invert();
    }
  }

  virtual Screen GetScreen() { return screen_; }

  uint32_t last_delay_ = 0;
  uint32_t t_ = 0;  // time since we switched screen

  // Fill frame buffer and return how long to display it.
  int FillFrameBuffer(bool advance) override {
    if (advance) {
      if (next_screen_ != SCREEN_UNSET) {
        screen_ = next_screen_;
        next_screen_ = SCREEN_UNSET;
        t_ = 0;
      } else {
        t_ += last_delay_;
      }
    }
    return last_delay_ = FillFrameBuffer2(advance);
  }

#ifdef USB_CLASS_MSC
  bool EscapeIdleIfNeeded() {
    return looped_idle_ == Tristate::True && USBD_Configured();
  }
#else
  bool EscapeIdleIfNeeded() { return false; }
#endif

  void ShowDefault(bool ignore_lockup = false) {
    screen_ = SCREEN_PLI;
    t_ = 0;
    if (SaberBase::IsOn()) {
      if (SaberBase::Lockup() && IMG_lock && !ignore_lockup) {
        SetFile(&IMG_lock, 3600000.0);
      } else if (looped_on_ != Tristate::False) {
        SetFile(&IMG_on, font_config.ProffieOSOnImageDuration);
      }
    } else {
      // Off
      if (looped_idle_ != Tristate::False) {
        if (EscapeIdleIfNeeded()) {
          SetMessage("    usb\nconnected");
        } else {
          SetFile(&IMG_idle, 3600000.0);
        }
      }
    }
  }

  int FillFrameBuffer2(bool advance) {
    switch (screen_) {
      default:
      case SCREEN_UNSET:
      case SCREEN_OFF:
        Clear();
        return 3600000; // Long time!

      case SCREEN_STARTUP:
        Clear();
        // DrawText("==SabeR===", 0,15, Starjedi10pt7bGlyphs);
        // DrawText("++Teensy++",-4,31, Starjedi10pt7bGlyphs);
        if (WIDTH < 128) {
          display_->DrawText("p-os", 0,15, Starjedi10pt7bGlyphs);
        } else {
          display_->DrawText("proffieos", 0,15, Starjedi10pt7bGlyphs);
        }
        display_->DrawText(version,0,31, Starjedi10pt7bGlyphs);
        if (HEIGHT > 32) {
          display_->DrawText("installed: ",0,47, Starjedi10pt7bGlyphs);
          display_->DrawText(install_time,0,63, Starjedi10pt7bGlyphs);
        }
        next_screen_ = SCREEN_PLI;
        if (font_config.ProffieOSTextMessageDuration != -1) {
          return font_config.ProffieOSTextMessageDuration;
        } else if (font_config.ProffieOSFontImageDuration > 0) {
          return font_config.ProffieOSFontImageDuration;
        } else {
          return 3500;
        }

      case SCREEN_PLI:
        if (!SaberBase::IsOn() && t_ >= PLI_OFF_TIME) {
          STDERR << "Time = " << t_ << ". PLI_OFF_TIME expired.\n";
          screen_ = SCREEN_OFF;
          return FillFrameBuffer2(advance);
        }
        Clear();
        display_->DrawBatteryBar(BatteryBar16, battery_monitor.battery_percent());
        if (HEIGHT > 32) {
          char tmp[32];
          strcpy(tmp, "volts x.xx");
          float v = battery_monitor.battery();
          tmp[6] = '0' + (int)floorf(v);
          tmp[8] = '0' + ((int)floorf(v * 10)) % 10;
          tmp[9] = '0' + ((int)floorf(v * 100)) % 10;
          display_->DrawText(tmp,0,55, Starjedi10pt7bGlyphs);
        }
        return 200;  // redraw once every 200 ms

      case SCREEN_ERROR_MESSAGE:
      case SCREEN_MESSAGE: {
	uint32_t t;
        if (font_config.ProffieOSTextMessageDuration != -1) {
	  t = font_config.ProffieOSTextMessageDuration;
        } else if (font_config.ProffieOSFontImageDuration > 0) {
	  t = font_config.ProffieOSFontImageDuration;
        } else {
	  t = 3500;
        }
	if (t_ >= t) {
	  screen_ = SCREEN_DEFAULT;
	  ShowDefault();
	  return FillFrameBuffer2(advance);
	}
        Clear();
        // Aurebesh Font option.
#ifdef USE_AUREBESH_FONT
        const Glyph* font = Aurebesh10pt7bGlyphs;
#else
        const Glyph* font = Starjedi10pt7bGlyphs;
#endif
        if (strchr(message_, '\n')) {
          display_->DrawText(message_, 0, 15, font);
        } else {
        // centered
          display_->DrawText(message_, 0, HEIGHT / 2 + 7, font);
        }
        return 200;  // redraw once every 200 ms
      }

      case SCREEN_IMAGE:
        if (EscapeIdleIfNeeded() && current_effect_ == &IMG_idle) {
          // We are idle-looping, and usb is connected. Time to stop.
          SetMessage("    usb\nconnected");
          return FillFrameBuffer2(advance);
        }
        MountSDCard();
        {
          int count = 0;
          while (!frame_available_) {
            if (count++ > 3) return 0;
            if (eof_) advance = false;
            advance_ = advance;
            lock_fb_ = false;
            scheduleFillBuffer();
          }
        }
        lock_fb_ = true;
        if (eof_) {
          // STDERR << "FRAME COUNT @ EOF= " << frame_count_ << " left=" << (effect_display_duration_ - t_) <<  "\n";
          if (frame_count_ == 1 && t_ < effect_display_duration_) {
            ConvertToNative();
            frame_available_ = false;
            return effect_display_duration_ - t_;
          }
        } else {
          if (frame_available_ && advance) frame_count_++;
          if (looped_frames_ == 1 || t_ < effect_display_duration_) {
            ConvertToNative();
            frame_available_ = false;
            if (font_config.ProffieOSAnimationFrameRate > 0.0) {
              return 1000 / font_config.ProffieOSAnimationFrameRate;
            }
            if (looped_frames_ > 1) {
              return 1000 / looped_frames_;
            } else {
              // STDERR << "-> 41\n";
              return 41;   // ~24 fps
            }
          }
        }

        // This image/animation is done, time to choose the next thing to display.
      case SCREEN_DEFAULT:
        // STDERR << "MOVING ON...\n";
        ShowDefault();
        return FillFrameBuffer2(advance);
    }
  }

  void SB_On() override {
    if (!ShowFile(&IMG_on, font_config.ProffieOSOnImageDuration)) {
      ShowDefault();
      last_delay_ = t_ = 0;
      display_->Page();
    }
  }

  void SB_On2() override {
    if (IMG_out) {
      ShowFileWithSoundLength(&IMG_out, font_config.ProffieOSOutImageDuration);
    }
  }

  void usb_connected() override {
    if (EscapeIdleIfNeeded() && current_effect_ == &IMG_idle) {
      // We are idle-looping, and usb is connected. Time to stop.
      SetMessage("    usb\nconnected");
      SetScreenNow(SCREEN_MESSAGE);
    }
  }

  void ShowFileWithSoundLength(Effect* e, float duration) {
    if (duration == 0.0) {
      duration = SaberBase::sound_length * 1000;
    }
    ShowFile(e, round(duration));
  }

 void SB_Effect2(EffectType effect, float location) override {
   switch (effect) {
     case EFFECT_NEWFONT:
       looped_on_ = Tristate::Unknown;
       looped_idle_ = Tristate::Unknown;
       if (IMG_font) {
	 ShowFileWithSoundLength(&IMG_font, font_config.ProffieOSFontImageDuration);
       } else if (prop.current_preset_name()) {
	 SetMessage(prop.current_preset_name());
	 SetScreenNow(SCREEN_MESSAGE);
       } else if (IMG_idle) {
	 ShowFile(&IMG_idle, 3600000.0);
       }
       break;
     case EFFECT_LOCKUP_BEGIN:
       ShowDefault();
       break;
     case EFFECT_LOCKUP_END:
       ShowDefault(true);
       break;
     case EFFECT_BATTERY_LEVEL:
       // Show On-Demand battery meter
       SetScreenNow(SCREEN_PLI);
       break;
     case EFFECT_SD_CARD_NOT_FOUND:
       SetErrorMessage("sd card\nnot found");
       break;
     case EFFECT_ERROR_IN_FONT_DIRECTORY:
       SetErrorMessage("err font\ndirectory");
       break;
     case EFFECT_ERROR_IN_BLADE_ARRAY:
       SetErrorMessage("err blade\narray");
       break;
     case EFFECT_FONT_DIRECTORY_NOT_FOUND:
       SetErrorMessage("font dir\nnot found");
       break;
     case EFFECT_LOW_BATTERY:
       // Maybe we should make this blink or something?
       if (IMG_lowbatt) {
	 ShowFile(&IMG_lowbatt, 5000);
       } else {
	 SetErrorMessage("low\nbattery");
       }
       break;
     case EFFECT_BOOT:
       if (IMG_boot) {
	 ShowFileWithSoundLength(&IMG_boot,
				 font_config.ProffieOSBootImageDuration != -1.0 ?
				 font_config.ProffieOSBootImageDuration :
				 font_config.ProffieOSFontImageDuration);
       } else {
	 SetScreenNow(SCREEN_STARTUP);
       }
       break;
     case EFFECT_BLAST:
       ShowFileWithSoundLength(&IMG_blst, font_config.ProffieOSBlastImageDuration);
       break;
     case EFFECT_CLASH:
       ShowFileWithSoundLength(&IMG_clsh, font_config.ProffieOSClashImageDuration);
       break;
     case EFFECT_FORCE:
       ShowFileWithSoundLength(&IMG_force, font_config.ProffieOSForceImageDuration);
       break;
     case EFFECT_PREON:
       ShowFile(&IMG_preon, round(SaberBase::sound_length * 1000));
       break;
     case EFFECT_POSTOFF:
       ShowFileWithSoundLength(&IMG_pstoff, font_config.ProffieOSPstoffImageDuration);
       break;

   /* START of MODIFICATION by Sabertrio for OLED Functionalities. [2/2] */
   /* New cases to display additional images/animations for Sabertrio Proffie Preconfiguration. */
  case EFFECT_NOBLADE:
    looped_idle_ = Tristate::Unknown;
    if (IMG_no_blade) {
      ShowFile(&IMG_no_blade, font_config.ProffieOSFontImageDuration);
    } else if (prop.current_preset_name()) {
      SetMessage(prop.current_preset_name());
      SetScreenNow(SCREEN_MESSAGE);
    } else if (IMG_idle) {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_KYBER:
    looped_idle_ = Tristate::Unknown;
    if (IMG_kyber) {
      ShowFile(&IMG_kyber, font_config.ProffieOSFontImageDuration);
    } else if (prop.current_preset_name()) {
      SetMessage(prop.current_preset_name());
      SetScreenNow(SCREEN_MESSAGE);
    } else if (IMG_idle) {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_VOLUMEMENU:
    looped_idle_ = Tristate::Unknown;
    if (IMG_volumemenu) {
      ShowFile(&IMG_volumemenu, font_config.ProffieOSFontImageDuration);
    } else if (prop.current_preset_name()) {
      SetMessage(prop.current_preset_name());
      SetScreenNow(SCREEN_MESSAGE);
    } else if (IMG_idle) {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_LENGTH:
    looped_idle_ = Tristate::Unknown;
    if (IMG_length) {
      ShowFile(&IMG_length, font_config.ProffieOSFontImageDuration);
    } else if (prop.current_preset_name()) {
      SetMessage(prop.current_preset_name());
      SetScreenNow(SCREEN_MESSAGE);
    } else if (IMG_idle) {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_CLEARSCREEN:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_on, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_BRIGHTNESS:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_brightness, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_CLASHTHRESHOLD:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_clashthreshold, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_CONTROLSETTINGS:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_controlsettings, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_SETTINGSMENU:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_settingsmenu, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_CLASHDETECTLVL:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_clashdetectlvl, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_FORCEPUSH:
    looped_idle_ = Tristate::Unknown;
    if(IMG_on) {
      ShowFile(&IMG_forcepush, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

  case EFFECT_FORCEPUSHLENGTH:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_forcepushlength, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

  case EFFECT_LOCKUPDELAY:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_lockupdelay, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

  case EFFECT_MAXCLASH:
    looped_idle_ = Tristate::Unknown;
    if(IMG_on) {
      ShowFile(&IMG_maxclash, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

  case EFFECT_POWERLOCK:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_powerlock, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

  case EFFECT_STABIGNITE:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_stabignite, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

   case EFFECT_SWINGIGNITE:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_swingignite, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_SWINGONSPEED:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_swingonspeed, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_THRUSTIGNITE:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_thrustignite, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_TWISTIGNITE:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_twistignite, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;

    case EFFECT_TWISTRETRACT:
    looped_idle_ = Tristate::Unknown;
    if (IMG_on) {
      ShowFile(&IMG_twistretract, font_config.ProffieOSOnImageDuration);
    } else {
      ShowFile(&IMG_idle, 3600000.0);
    }
    return;
	/* END of MODIFICATION by Sabertrio for OLED Functionalities. [2/2] */

/* To-Do, possibly differently
   #ifdef OLED_USE_BLASTER_IMAGES
   case EFFECT_RELOAD:
   ShowFileWithSoundLength(&IMG_reload, font_config.ProffieOSReloadImageDuration);
   break;
   case EFFECT_EMPTY:
   ShowFileWithSoundLength(&IMG_empty, font_config.ProffieOSEmptyImageDuration);
   break;
   case EFFECT_JAM:
   ShowFileWithSoundLength(&IMG_jam, font_config.ProffieOSJamImageDuration);
   break;
   case EFFECT_CLIP_IN:
   ShowFileWithSoundLength(&IMG_clipin, font_config.ProffieOSClipinImageDuration);
   break;
   case EFFECT_CLIP_OUT:
   ShowFileWithSoundLength(&IMG_clipout, font_config.ProffieOSClipoutImageDuration);
   break;
   case EFFECT_DESTRUCT:
   ShowFileWithSoundLength(&IMG_destruct, font_config.ProffieOSDestructImageDuration);
   break;
   #endif
*/

       break;
     default: break;
   }
 }

  // If called from SB_Effect2, you must call SetScreenNow after.
  void SetMessage(const char* text) {
    if (screen_ == SCREEN_ERROR_MESSAGE) return;
    strncpy(message_, text, sizeof(message_));
    message_[sizeof(message_)-1] = 0;
    screen_ = SCREEN_MESSAGE;
  }

  // Calls SetScreenNow already.
  void SetErrorMessage(const char* text) {
    strncpy(message_, text, sizeof(message_));
    message_[sizeof(message_)-1] = 0;
    SetScreenNow(SCREEN_ERROR_MESSAGE);
  }

  void SB_Top(uint64_t total_cycles) override {
    display_->SB_Top();
  }

  void SB_Off2(OffType offtype) override {
    if (offtype == OFF_IDLE) {
      SetScreenNow(SCREEN_OFF);
    } else if (IMG_in) {
      ShowFileWithSoundLength(&IMG_in, font_config.ProffieOSInImageDuration);
    } else if (IMG_idle) {
      ShowFile(&IMG_idle, 3600000.0);
    } else {
      SetScreenNow(SCREEN_PLI);
    }
  }

  // TODO: Don't update the display when we don't need to
  // and return false here so that we can go into lower power modes.
  void SB_IsOn(bool* on) override {
     display_->SB_IsOn(on);
  }

  void SetScreenNow(Screen screen) {
    if (screen_ == SCREEN_ERROR_MESSAGE) return;
    // No need to wake the sleeping bear just to tell it to go to bed again.
    if (screen == SCREEN_OFF && screen_ == SCREEN_OFF) return;
    last_delay_ = t_ = 0;
    next_screen_ = screen_ = screen;
    display_->Page();
  }

  bool SetFile(Effect* effect, float duration) {
    if (screen_ == SCREEN_ERROR_MESSAGE) return false;
    if (!*effect) return false;
    MountSDCard();
    eof_ = true;
    file_.Play(effect);
    frame_available_ = false;
    frame_count_ = 0;
    screen_ = SCREEN_IMAGE;
    eof_ = false;
    current_effect_ = effect;
    effect_display_duration_ = duration;
    return true;
  }

  bool ShowFile(Effect* effect, float duration) {
    if (SetFile(effect, duration)) {
      SetScreenNow(SCREEN_IMAGE);
      return true;
    }
    return false;
  }

  void ShowFile(const char* file) {
    if (screen_ == SCREEN_ERROR_MESSAGE) return;
    MountSDCard();
    eof_ = true;
    file_.Play(file);
    frame_available_ = false;
    frame_count_ = 0;
    SetScreenNow(SCREEN_IMAGE);
    eof_ = false;
  }


  // AudioStreamWork implementation
  size_t space_available() const override {
    if (lock_fb_) return 0;
    if (eof_ && advance_) return 0;
    if (frame_available_) return 0;

    // Always low priority
    return 1;
  }

#define TAG2(X, Y) (((X) << 8) | (Y))
  int ReadColorAsGray(FileReader* f) {
    uint8_t rgba[4];
    f->Read(rgba, 4);
    return rgba[0] + rgba[1] + rgba[2];
  }

  bool ReadImage(FileReader* f) {
    uint32_t file_end = 0;
    // STDERR << "ReadImage " << f->Tell() << " size = " << f->FileSize() << "\n";
    if (ypos_ >= looped_frames_ || ypos_ == 0) {
      if (looped_frames_ > 1) f->Seek(0);
      ypos_ = 0;
      uint32_t file_start = f->Tell();
      int a = f->Read();
      int b = f->Read();
      int width, height;
      switch (TAG2(a, b)) {
        default:
          STDERR << "Unknown image format. a=" << a << " b=" << b << " pos=" << f->Tell() << "\n";
          return false;

        case TAG2('P', '4'):
          // PBM
          f->skipwhite();
          width = f->readIntValue();
          f->skipwhite();
          height = f->readIntValue();
          f->Read();
          xor_ = 255;
          invert_y_ = false;
          break;

        case TAG2('B', 'M'):
        case TAG2('B', 'A'):
        case TAG2('C', 'I'):
        case TAG2('C', 'P'):
        case TAG2('I', 'C'):
        case TAG2('P', 'T'):
          // STDERR << "BMP detected!\n";
          xor_ = 255;
          invert_y_ = true; // bmp data is bottom to top
          // BMP
          file_end = file_start + f->ReadType<uint32_t>();
          f->Skip(4);
          uint32_t offset = f->ReadType<uint32_t>();

#if 0
          uint32_t ctable = f->Tell() + f->ReadType<uint32_t>();
          // STDERR << "OFFSET = " << offset << " CTABLE=" << ctable << "\n";
          width = f->ReadType<uint32_t>();
          height = f->ReadType<uint32_t>();
          // STDERR << "Width=" << width << " Height=" << height << "\n";
          f->Seek(ctable);
          int c0 = ReadColorAsGray(f);
          int c1 = ReadColorAsGray(f);
          xor_ = c0 > c1 ? 255 : 0;
#else
          f->Skip(4);
          width = f->ReadType<uint32_t>();
          height = f->ReadType<uint32_t>();
          // STDERR << "Width=" << width << " Height=" << height << "\n";
#endif
          // First frame is near the end, seek to it.
          f->Seek(file_start + offset + width * height / 8 - sizeof(MonoFrame<WIDTH, col_t>::frame_buffer_));
      }
      if (width != WIDTH && width != HEIGHT) {
        STDERR << "Wrong size image: " << width << "x" << height << "\n";
        return false;
      }
      if (width == WIDTH) {
        layout_ = LAYOUT_LANDSCAPE;
        looped_frames_ = height / HEIGHT;
      } else {
        looped_frames_ = height / WIDTH;
      }
      if (current_effect_ == &IMG_on) {
        looped_on_ = looped_frames_ > 1 ? Tristate::True : Tristate::False;
      }
      if (current_effect_ == &IMG_idle) {
        looped_idle_ = looped_frames_ > 1 ? Tristate::True : Tristate::False;
      }
    }
    // STDERR << "ypos=" << ypos_ << " avail=" << f->Available() << "\n";
    if (f->Available() < sizeof(MonoFrame<WIDTH, col_t>::frame_buffer_)) return false;
    f->Read((uint8_t*)display_->frame_buffer_, sizeof(MonoFrame<WIDTH, col_t>::frame_buffer_));
    ypos_++;
    if (looped_frames_ > 1) {
      if (ypos_ >= looped_frames_) {
        f->Seek(f->FileSize());
      } else {
        if (invert_y_) {
          // Seek two frames back, because BMP are backwards.
          f->Seek(f->Tell() - 2 * sizeof(MonoFrame<WIDTH, col_t>::frame_buffer_));
        }
      }
    } else {
      if (file_end) f->Seek(file_end);
    }
    return true;
  }

  struct ReadState {
    int ypos = 0;
    uint32_t file_pos = 0;
  };

  bool FillBuffer() override {
    if (eof_ && advance_) {
      // STDERR << "e&a\n";
      return true;
    }
    if (file_.OpenFile()) {
      if (!file_.IsOpen()) {
        eof_ = true;
  // STDERR << "not open\n";
        return false;
      }
      ypos_ = looped_frames_;
      return true;  // return to ProcessAudioStream which will return here when no audiostreams are in need of data.
    }
    if (lock_fb_) {
      // STDERR << "locked\n";
      return true;
    }
    if (!frame_available_) {
      // STDERR << "ADVANCE=" << advance_ << " last_file_pos_= " << last_state_.file_pos << " ypos=" << last_state_.ypos << "\n";
      if (!advance_) {
        file_.Seek(last_state_.file_pos);
        ypos_ = last_state_.ypos;
      } else {
        advance_ = false;
      }
      ReadState state;
      state.ypos = ypos_;
      state.file_pos = file_.Tell();
      if (ReadImage(&file_)) {
        frame_available_ = true;
        last_state_ = state;
      } else {
        STDERR << "read image fail\n";
        eof_ = true;
      }
    }
    return true;
  }

  bool IsActive() override {
    return screen_ == SCREEN_IMAGE;
  }

  void CloseFiles() override {
    file_.Close();
  }

#ifdef ENABLE_DEVELOPER_COMMANDS
  bool Parse(const char* cmd, const char* e) override {
    if (!strcmp(cmd, "setmessage") && e) {
      STDOUT << "Setting message: " << e << "\n";
      SetMessage(e);
      SetScreenNow(SCREEN_MESSAGE);
      return true;
    }
    return false;
  }
#endif

private:
  // Variables related to frame buffer layout.
  uint8_t xor_ = 0;
  bool invert_y_ = 0;
  volatile ScreenLayout layout_;

  // Screen state
  Screen screen_ = SCREEN_STARTUP;
  Screen next_screen_ = SCREEN_UNSET;
  char message_[32];

  // File reading
  EffectFileReader file_;
  volatile bool frame_available_ = true;
  volatile bool eof_ = true;
  int32_t frame_count_ = 0;
  volatile int32_t looped_frames_ = 0;
  int32_t ypos_ = 0;
  bool lock_fb_ = false;
  ReadState last_state_;
  volatile bool advance_ = true;

  // True if IMG_on is looped.
  volatile Tristate looped_on_ = Tristate::Unknown;
  // True if IMG_idle is looped.
  volatile Tristate looped_idle_ = Tristate::Unknown;
  volatile float effect_display_duration_;
  volatile Effect* current_effect_;
};

template<template<int, class> class T>
struct BaseLayerOp {
  template<int Width, class col_t> struct Controller : public T<Width, col_t> {};
};

template<int WIDTH, class col_t, class POWER_PIN = PowerPINS<> >
class SSD1306Template : public Display<WIDTH, col_t>, I2CDevice, Looper, StateMachine {
public:
  static const int HEIGHT = sizeof(col_t) * 8;
  const char* name() override { return "SSD1306"; }

  enum Commands {
    SETCONTRAST = 0x81,
    DISPLAYALLON_RESUME = 0xA4,
    DISPLAYALLON = 0xA5,
    NORMALDISPLAY = 0xA6,
    INVERTDISPLAY = 0xA7,
    DISPLAYOFF = 0xAE,
    DISPLAYON = 0xAF,

    SETDISPLAYOFFSET = 0xD3,
    SETCOMPINS = 0xDA,

    SETVCOMDETECT = 0xDB,

    SETDISPLAYCLOCKDIV = 0xD5,
    SETPRECHARGE = 0xD9,

    SETMULTIPLEX = 0xA8,

    SETLOWCOLUMN = 0x00,
    SETHIGHCOLUMN = 0x10,

    SETSTARTLINE = 0x40,

    MEMORYMODE = 0x20,
    COLUMNADDR = 0x21,
    PAGEADDR   = 0x22,

    COMSCANINC = 0xC0,
    COMSCANDEC = 0xC8,

    SEGREMAP = 0xA0,

    CHARGEPUMP = 0x8D,

    EXTERNALVCC = 0x1,
    SWITCHCAPVCC = 0x2,

    // Scrolling commands
    ACTIVATE_SCROLL = 0x2F,
    DEACTIVATE_SCROLL = 0x2E,
    SET_VERTICAL_SCROLL_AREA = 0xA3,
    RIGHT_HORIZONTAL_SCROLL = 0x26,
    LEFT_HORIZONTAL_SCROLL = 0x27,
    VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL = 0x29,
    VERTICAL_AND_LEFT_HORIZONTAL_SCROLL = 0x2A,
  };

  void SetController(DisplayControllerBase<WIDTH, col_t>* controller) {
    controller_ = controller;
    controller->SetDisplay(this);
  }

  explicit SSD1306Template(DisplayControllerBase<WIDTH, col_t>* controller) : I2CDevice(0x3C) {
    SetController(controller);
  }
  explicit SSD1306Template(DisplayControllerBase<WIDTH, col_t>* controller, int id) : I2CDevice(id) {
    SetController(controller);
  }
  void Send(int c) { writeByte(0, c); }

  static const size_t chunk_size = WIDTH * HEIGHT / 8 / 16;
  static const size_t num_chunks = WIDTH * HEIGHT / 8 / chunk_size;

  uint8_t chunk[chunk_size + 1];
  void GetChunk() {
    chunk[0] = 0x40;
    memcpy(chunk + 1, i + (unsigned char *)Display<WIDTH, col_t>::frame_buffer_, chunk_size);
  }

  int FillFrameBuffer() {
    return controller_->FillFrameBuffer(true);
  }

  void Page() override {
    millis_to_display_ = 0;
    next_millis_to_display_ = 0;
  }
  void SB_Top() override {
    STDOUT.print("display fps: ");
    loop_counter_.Print();
    STDOUT.println("");
  }

  Screen GetScreen() override {
    return controller_->GetScreen();
  }

  // TODO: Don't update the display when we don't need to
  // and return false here so that we can go into lower power modes.
  void SB_IsOn(bool* on) override {
    *on = on_;
  }

  void Loop() override {
    if (millis() > oled_delay){

#ifdef USB_CLASS_MSC
    static bool last_connected = false;
    bool connected = USBD_Configured();
    if (connected && !last_connected) controller_->usb_connected();
    last_connected = connected;
#endif

    STATE_MACHINE_BEGIN();
    while(true) {
      on_ = true;
      power_.Init();
      power_.Power(true);

      while (!i2cbus.inited()) YIELD();
      while (!I2CLock()) YIELD();

      // Init sequence
      Send(DISPLAYOFF);                    // 0xAE
      Send(SETDISPLAYCLOCKDIV);            // 0xD5
      Send(0x80);                          // the suggested ratio 0x80

      Send(SETMULTIPLEX);                  // 0xA8
      Send(HEIGHT - 1);

      Send(SETDISPLAYOFFSET);              // 0xD3
      Send(0x0);                                   // no offset

      Send(SETSTARTLINE | 0x0);            // 0x40 line #0

      Send(CHARGEPUMP);                    // 0x8D
      Send(0x14);

      Send(MEMORYMODE);                    // 0x20
      Send(0x01);                          // vertical address mode

#if defined (OLED_FLIP_180)
#if defined (OLED_MIRRORED)
      // Flip 180 and mirrored OLED operation
      Send(SEGREMAP | 0x1);        // 0xa0 | 1
#else
      // Flip 180
      Send(SEGREMAP);        // 0xa0 | 1
#endif
      Send(COMSCANINC);
#elif defined (OLED_MIRRORED)
      // mirrored OLED operation
      Send(SEGREMAP);        // 0xa0 | 1
      Send(COMSCANDEC);
#else
      // normal OLED operation
      Send(SEGREMAP | 0x1);        // 0xa0 | 1
      Send(COMSCANDEC);
#endif


      Send(SETCOMPINS);                    // 0xDA
      if (HEIGHT == 64 || WIDTH==64) {
        Send(0x12);
      } else {
        Send(0x02);  // may need to be 0x12 for some displays
      }
      Send(SETCONTRAST);                   // 0x81
      Send(0x8F);
      Send(SETPRECHARGE);                  // 0xd9
      Send(0xF1);
      Send(SETVCOMDETECT);                 // 0xDB
      Send(0x40);
      Send(DISPLAYALLON_RESUME);           // 0xA4
      Send(NORMALDISPLAY);                 // 0xA6

      Send(DEACTIVATE_SCROLL);

      Send(DISPLAYON);                     //--turn on oled panel

      I2CUnlock();

      STDOUT.println("Display initialized.");

      while (true) {
        millis_to_display_ = next_millis_to_display_;
        next_millis_to_display_ = 0;
        while (millis_to_display_ == 0) {
          YIELD();
          millis_to_display_ = FillFrameBuffer();
          // STDERR << "millis_to_display_ = " << millis_to_display_ << "\n";
        }
        frame_start_time_ = millis();
        lock_fb_ = true;

        // STDOUT << "SCREEN = " << (int)GetScreen() << " m= " << millis_to_display_ << "  clear=" << Display<WIDTH, col_t>::isClear() << "\n";
        if (GetScreen() == SCREEN_OFF && Display<WIDTH, col_t>::isClear()) break;

        // I2C
        loop_counter_.Update();
#ifdef PROFFIEBOARD
        i = -(int)NELEM(transactions);
        while (!I2CLockAndRun()) YIELD();
        while (lock_fb_) YIELD();
#else
        do { YIELD(); } while (!I2CLock());
        Send(COLUMNADDR);
        Send((128 - WIDTH)/2);   // Column start address (0 = reset)
        Send(WIDTH-1 + (128 - WIDTH)/2); // Column end address (127 = reset)

        Send(PAGEADDR);
        Send(0); // Page start address (0 = reset)
        Send(sizeof(col_t) - 1);

        //STDOUT.println(TWSR & 0x3, DEC);

        for (i=0; i < WIDTH * HEIGHT / 8; ) {
          // send a bunch of data in one xmission
          Wire.beginTransmission(address_);
          GetChunk();
          for (size_t x=0; x <= chunk_size; x++) {
            Wire.write(chunk[x]);
          }
          Wire.endTransmission();
          I2CUnlock(); do { YIELD(); } while (!I2CLock());
        }
        lock_fb_ = false;
        I2CUnlock();
#endif
        while (millis() - frame_start_time_ < millis_to_display_) {
          if (next_millis_to_display_ == 0) {
            next_millis_to_display_ = FillFrameBuffer();
            // STDERR << "next_millis_to_display_ = " << next_millis_to_display_ << "\n";

          }
          YIELD();
        }
      }

      STDERR << "DISPLAY SLEEP\n";

      // Time to shut down... for now.
      while (!I2CLock()) YIELD();
      Send(DISPLAYOFF);                    // 0xAE
      I2CUnlock();

      power_.Power(false);
      power_.DeInit();
      on_ = false;
      while (GetScreen() == SCREEN_OFF) YIELD();
      STDERR << "DISPLAY WAKEUP\n";
    }

    STATE_MACHINE_END();
  }
}
#ifdef PROFFIEBOARD
  static constexpr uint8_t transactions[] = {
    COLUMNADDR,
    (128 - WIDTH)/2,   // Column start address (0 = reset)
    WIDTH-1 + (128 - WIDTH)/2, // Column end address (127 = reset)
    PAGEADDR,
    0,  // Page start address (0 = reset)
    sizeof(col_t) - 1
  };
  void RunLocked() override {
    size_t size;
    if (i < 0) {
      chunk[0] = 0;
      chunk[1] = transactions[NELEM(transactions)+i];
      i++;
      size = 2;
    } else {
      GetChunk();
      size = chunk_size + 1;
      i += chunk_size;
    }
    if (!stm32l4_i2c_notify(Wire._i2c, &SSD1306Template::DataSent, this, (I2C_EVENT_ADDRESS_NACK | I2C_EVENT_DATA_NACK | I2C_EVENT_ARBITRATION_LOST | I2C_EVENT_BUS_ERROR | I2C_EVENT_OVERRUN | I2C_EVENT_RECEIVE_DONE | I2C_EVENT_TRANSMIT_DONE | I2C_EVENT_TRANSFER_DONE))) {
      goto fail;
    }
    if (!stm32l4_i2c_transmit(Wire._i2c, address_, chunk, size, 0)) {
      goto fail;
    }
    return;
  fail:
    lock_fb_ = false;
    I2CUnlock();
    return;
  }
  static void DataSent(void *x, unsigned long event) { ((SSD1306Template*)x)->DataSent(); }
  void DataSent() {
    stm32l4_i2c_notify(Wire._i2c, nullptr, 0, 0);
    I2CUnlock();
    if (i < WIDTH * HEIGHT / 8) {
      I2CLockAndRun();
    } else {
      lock_fb_ = false;
    }
  }
#endif

private:
  int i;
  uint32_t millis_to_display_;
  uint32_t next_millis_to_display_;
  uint32_t frame_start_time_;
  volatile bool lock_fb_ = false;
  DisplayControllerBase<WIDTH, col_t>* controller_;
  LoopCounter loop_counter_;
  POWER_PIN power_;
  bool on_ = false;

  uint32_t oled_delay = 5000;

};


#ifdef PROFFIEBOARD
template<int WIDTH, class col_t, class POWER_PIN>
constexpr uint8_t SSD1306Template<WIDTH, col_t, POWER_PIN>::transactions[];
#endif

using SSD1306 = SSD1306Template<128, uint32_t>;

#endif

1 Like

It’s a lot easier if you assume that there is only one setup though.
Once you make the menu system extensible, having one image for each menu becomes pretty difficult, unless you have some kind of system that generates images for the menus you have…

Or maybe we need something like the sound library, but for images.

How about some much smaller font (I am thinking 5 lines per screen) text based, instead of 2 lines like we have now ? Something like this:
VerySmallFont.h

Why not text? Could you do text? Each menu level could have a name like a soundfont.

Because… ?
What problem does that solve?

Text turns out to have it’s own set of problems:

  1. If you don’t know the size of the display, you don’t know how long to make your text.
  2. Fonts are big, color fonts are even bigger. (And also, I have no idea where you find a color font.) Currently, there is no support for printing text to color screens, everything is an image or an animation.
  3. It doesn’t allow for much customization, except replacing the font.

Because of this I have been trying to steer things away from just printing text to screens for a while. Although, I’m open to making that work, if we can solve some of the problems above.

I’m thinking this may be the way to go, if the image exists display it, if not then the default shows. So if the font supports menu images they display, if not the hum or idle image remains.

If possible maybe just append the image support to the sound library, so the existing “Say” functions across menus don’t need modification, if feasible?

How would you not know the size of your display ? Isn’t the smallest OLED 64x32 and the standard one (used on lightsabers) 128x32, if we accommodate for the smallest, wouldn’t that “cover” for everything ?

Then add user #define SCREEN_WIDTH (value 32, 64, 128,…) for those of us who want to maximize the text display potential.

We make the font as small as possible, both in terms of character size (5 pixels tall) and we use as little possible amount of characters A to Z in all caps only, 0 to 9, minimum punctuation or special characters and a 1 pixel tall blank line the width of the screen to separate each line of text. I had, back when I was in engineering school, a HP48S with a grand total of 32k of memory. I could store and display a lot of text using that font, because the font was as tiny (in terms of bytes) as it could be to begin with.

I know it is only for black and white but for reading text, isn’t that the most optimal ?

Does it have to be customizable ? It’s for reading a menu, should it not be optimized rather than customized ?

I won’t post many, but here’s a few.
This is what CFX does:
Screenshot 2025-03-29 at 12.33.28 PM

Here’s what S3 uses:
Screenshot 2025-03-29 at 12.23.34 PM

I think they’re “perfect” as is and there’s actually no “legal” reason we can’t use them.
Of course custom ones could be used.

What I think would be a cool version is if once rotating the hilt to adjust blade length or volume level for example, we use something like DrawBatteryBar to indicate a segmented meter corresponding to the current value from min to max.

1 Like

That’s a good question.
I usually divide things into “debugging and configuration” and “using the hilt”.
The first category is lean and mean, while the other should be impressive and configurable.
I think at some point I made an assumption that menus belong on the second category, but since I don’t really use these menus myself, my opinion might not be the one that matters.

I guess the counter-question would be:
If it should be small and optimized, and we already have voice prompts, why do we need to put it on the screen at all?

PQF files could do this fairly easily.
You could have one image/animation with text, and a separate overlay which displays a bar of some sort, and that bar would be controlled by a variable that comes from the current menu.

I’ve been thinking that it would be nice to have PQF support for OLEDs as well…

It gets a little harder when you’re selecting a color… or a font… or something else that doesn’t fit this model. Although, I suppose having a “bar” that just tells you how far into the menu you are might still be handy.

I would say the answer to that is “because we can”.
Secondly, taking in info with 2 senses is a more engaging experience than just one?