mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-14 07:21:30 +10:00
Snake! (#10936)
* Snake! * Add spiLock to snake score saving * Check fixes * More careful locking * WIP: Big Display Node * Update src/graphics/HUB75Display.cpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add HUB75 Native * Add Tetris game module with core logic and UI integration - Implement TetrisGame class for game logic, including piece movement, rotation, and line clearing. - Create TetrisModule class to manage game state, input handling, and rendering on OLED display. - Introduce high score tracking with persistence and optional mesh broadcasting. - Define UI states for title, playing, paused, game over, and high scores. - Implement input handling for game controls and state transitions. - Add rendering functions for the game board, high scores, and title screen. * feat(snake): add snake graphics and update display logic in SnakeModule * Prompt for Initials for Tetris, too * refactor games module * Games refactor * hub75 native double buffer * Games tuning * Make joystick repeat events on held button * Add clouds and colors * Fix breakout and more color * difficulty tuning * trunk * refactor game announcements, etc * Scale chirpy gravity as game speed increases * Portduino, check for hub75 display before reading in hub75 options * Final(?) games tuning * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Properly ignore input when games screen not shown * Fail gracefully when HUB75 is selected but not supported. --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Ixitxachitl <kramerfm@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
Thomas Göttgens
coderabbitai[bot]
Ixitxachitl
parent
d3d02af817
commit
8d1fbbf55f
@@ -0,0 +1,156 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(USE_HUB75)
|
||||
|
||||
#include "HUB75Display.h"
|
||||
#include "TFTColorRegions.h"
|
||||
#include "TFTPalette.h"
|
||||
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
|
||||
#include <string.h>
|
||||
|
||||
HUB75Display::HUB75Display(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C)
|
||||
{
|
||||
// The BaseUI treats the panel as a generic raw framebuffer (not an SSD1306
|
||||
// page layout). Geometry is fixed by the wired panel size.
|
||||
#if defined(SCREEN_ROTATE)
|
||||
setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH);
|
||||
#else
|
||||
setGeometry(GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||
#endif
|
||||
LOG_DEBUG("HUB75Display %dx%d", (int)TFT_WIDTH, (int)TFT_HEIGHT);
|
||||
}
|
||||
|
||||
HUB75Display::~HUB75Display()
|
||||
{
|
||||
if (matrix) {
|
||||
delete matrix;
|
||||
matrix = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Bring up the matrix DMA driver from the wired HUB75 pins.
|
||||
bool HUB75Display::connect()
|
||||
{
|
||||
LOG_INFO("Do HUB75 init");
|
||||
|
||||
HUB75_I2S_CFG::i2s_pins pins = {HUB75_R1, HUB75_G1, HUB75_B1, HUB75_R2, HUB75_G2, HUB75_B2, HUB75_A,
|
||||
HUB75_B, HUB75_C, HUB75_D, HUB75_E, HUB75_LAT, HUB75_OE, HUB75_CLK};
|
||||
|
||||
HUB75_I2S_CFG cfg(TFT_WIDTH, TFT_HEIGHT, 1 /* chain length */, pins);
|
||||
|
||||
cfg.i2sspeed = HUB75_I2S_CFG::HZ_8M; // timing headroom (signal integrity)
|
||||
cfg.latch_blanking = 4; // clean row transitions
|
||||
cfg.clkphase = false; // inverted clock phase: fixes 1px skew of lower half
|
||||
cfg.double_buff = false; // single buffer: halves the internal-SRAM DMA
|
||||
// footprint. display() draws directly into the
|
||||
// live buffer with a dirty-diff
|
||||
|
||||
matrix = new MatrixPanel_I2S_DMA(cfg);
|
||||
bool ok = matrix->begin();
|
||||
if (!ok) {
|
||||
LOG_ERROR("HUB75 matrix->begin() failed (DMA buffer alloc?)");
|
||||
delete matrix;
|
||||
matrix = nullptr;
|
||||
return false;
|
||||
}
|
||||
matrix->setBrightness8(brightness);
|
||||
matrix->clearScreen();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HUB75Display::display()
|
||||
{
|
||||
if (!matrix)
|
||||
return;
|
||||
|
||||
const uint16_t onNative = graphics::TFTPalette::White;
|
||||
const uint16_t offNative = graphics::getThemeBodyBg();
|
||||
|
||||
const uint16_t onBe = (uint16_t)((onNative >> 8) | (onNative << 8));
|
||||
const uint16_t offBe = (uint16_t)((offNative >> 8) | (offNative << 8));
|
||||
|
||||
bool forceFull = firstFrame || !haveThemeDefaults || onBe != lastOnBe || offBe != lastOffBe;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const bool hasColorRegions = graphics::getTFTColorRegionCount() > 0;
|
||||
const uint32_t colorSig = graphics::getTFTColorFrameSignature();
|
||||
if (colorSig != lastColorSig)
|
||||
forceFull = true;
|
||||
#endif
|
||||
|
||||
for (uint16_t y = 0; y < displayHeight; y++) {
|
||||
const uint32_t yByteIndex = (y / 8) * displayWidth;
|
||||
const uint8_t yByteMask = (uint8_t)(1 << (y & 7));
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions)
|
||||
graphics::beginTFTColorRow((int16_t)y);
|
||||
#endif
|
||||
|
||||
for (uint16_t x = 0; x < displayWidth; x++) {
|
||||
const bool isset = (buffer[x + yByteIndex] & yByteMask) != 0;
|
||||
|
||||
// Skip pixels whose mono bit is unchanged (unless a full repaint is
|
||||
// forced). The panel is single-buffered, so untouched pixels persist.
|
||||
if (!forceFull && (((buffer_back[x + yByteIndex] & yByteMask) != 0) == isset))
|
||||
continue;
|
||||
|
||||
uint16_t be;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions)
|
||||
be = graphics::resolveTFTColorPixelRow((int16_t)x, isset, onBe, offBe);
|
||||
else
|
||||
be = isset ? onBe : offBe;
|
||||
#else
|
||||
be = isset ? onBe : offBe;
|
||||
#endif
|
||||
|
||||
const uint16_t c = (uint16_t)((be >> 8) | (be << 8)); // back to native RGB565
|
||||
const uint8_t r = (uint8_t)(((c >> 11) & 0x1F) << 3);
|
||||
const uint8_t g = (uint8_t)(((c >> 5) & 0x3F) << 2);
|
||||
const uint8_t b = (uint8_t)((c & 0x1F) << 3);
|
||||
matrix->drawPixelRGB888(x, y, r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
// Remember what the panel now shows so the next frame can diff against it.
|
||||
memcpy(buffer_back, buffer, displayBufferSize);
|
||||
|
||||
haveThemeDefaults = true;
|
||||
lastOnBe = onBe;
|
||||
lastOffBe = offBe;
|
||||
firstFrame = false;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
lastColorSig = colorSig;
|
||||
// Regions are re-registered every frame by the renderers; clear so they
|
||||
// don't accumulate across frames.
|
||||
graphics::clearTFTColorRegions();
|
||||
#endif
|
||||
}
|
||||
|
||||
void HUB75Display::sendCommand(uint8_t com)
|
||||
{
|
||||
if (!matrix)
|
||||
return;
|
||||
|
||||
switch (com) {
|
||||
case DISPLAYON:
|
||||
matrix->setBrightness8(brightness);
|
||||
break;
|
||||
case DISPLAYOFF:
|
||||
matrix->setBrightness8(0);
|
||||
break;
|
||||
default:
|
||||
// Drop all other SSD1306 init/config commands - not meaningful for the matrix.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HUB75Display::setDisplayBrightness(uint8_t _brightness)
|
||||
{
|
||||
brightness = _brightness;
|
||||
if (matrix)
|
||||
matrix->setBrightness8(brightness);
|
||||
}
|
||||
|
||||
#endif // USE_HUB75
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(USE_HUB75)
|
||||
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
#ifndef HUB75_BRIGHTNESS_DEFAULT
|
||||
#define HUB75_BRIGHTNESS_DEFAULT 180
|
||||
#endif
|
||||
|
||||
class MatrixPanel_I2S_DMA;
|
||||
|
||||
/**
|
||||
* A color display backend that drives a HUB75 RGB LED matrix panel from the
|
||||
* BaseUI color path.
|
||||
*/
|
||||
class HUB75Display : public OLEDDisplay
|
||||
{
|
||||
public:
|
||||
HUB75Display(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus);
|
||||
~HUB75Display();
|
||||
|
||||
// Write the buffer to the matrix (full-frame, region-aware color expansion).
|
||||
void display() override;
|
||||
|
||||
void setDisplayBrightness(uint8_t brightness);
|
||||
|
||||
protected:
|
||||
int getBufferOffset(void) override { return 0; }
|
||||
|
||||
void sendCommand(uint8_t com) override;
|
||||
|
||||
bool connect() override;
|
||||
|
||||
private:
|
||||
MatrixPanel_I2S_DMA *matrix = nullptr;
|
||||
uint8_t brightness = HUB75_BRIGHTNESS_DEFAULT;
|
||||
|
||||
bool firstFrame = true;
|
||||
bool haveThemeDefaults = false;
|
||||
uint16_t lastOnBe = 0;
|
||||
uint16_t lastOffBe = 0;
|
||||
uint32_t lastColorSig = 0;
|
||||
};
|
||||
|
||||
#endif // USE_HUB75
|
||||
+99
-1
@@ -29,6 +29,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#if HAS_SCREEN
|
||||
#include "EInkParallelDisplay.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#if defined(USE_HUB75)
|
||||
#include "graphics/HUB75Display.h" // ESP32 HUB75 (I2S-DMA)
|
||||
#endif
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
#include "HUB75Native.h" // native/Portduino HUB75 (rpi-rgb-led-matrix), from variants/native/portduino
|
||||
#endif
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
#include "TimeFormatters.h"
|
||||
@@ -67,6 +73,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#if BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#include "modules/WaypointModule.h"
|
||||
#include "sleep.h"
|
||||
#include "target_specific.h"
|
||||
@@ -99,7 +108,11 @@ namespace graphics
|
||||
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||
|
||||
// DEBUG
|
||||
#if BASEUI_HAS_GAMES
|
||||
#define NUM_EXTRA_FRAMES 4 // text message, debug frame, and the always-present games frame
|
||||
#else
|
||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||
#endif
|
||||
// if defined a pixel will blink to show redraws
|
||||
// #define SHOW_REDRAWS
|
||||
#define ASCII_BELL '\x07'
|
||||
@@ -369,6 +382,43 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
// Called to trigger an arcade-style initials picker (see showNumberPicker for the sibling flow).
|
||||
void Screen::showAlphanumericPicker(const char *message, const char *initialText, uint32_t durationMs, uint8_t length,
|
||||
std::function<void(const std::string &)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
if (length >= sizeof(NotificationRenderer::alphanumericValue))
|
||||
length = sizeof(NotificationRenderer::alphanumericValue) - 1;
|
||||
|
||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::textInputCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::alphanumeric_picker;
|
||||
NotificationRenderer::numDigits = length;
|
||||
|
||||
// Seed each position from initialText (uppercased & filtered to A-Z/0-9), defaulting to 'A'.
|
||||
const size_t seedLen = initialText ? strnlen(initialText, length) : 0;
|
||||
for (uint8_t i = 0; i < length; i++) {
|
||||
char c = (i < seedLen) ? initialText[i] : 'A';
|
||||
if (c >= 'a' && c <= 'z')
|
||||
c = static_cast<char>(c - 'a' + 'A');
|
||||
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')))
|
||||
c = 'A';
|
||||
NotificationRenderer::alphanumericValue[i] = c;
|
||||
}
|
||||
NotificationRenderer::alphanumericValue[length] = '\0';
|
||||
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, 2);
|
||||
ui->setTargetFPS(60);
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback)
|
||||
{
|
||||
@@ -412,6 +462,17 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
pi.drawFrame(display, state, x, y);
|
||||
}
|
||||
|
||||
#if BASEUI_HAS_GAMES
|
||||
// The games frame is a dedicated, always-present frame (unlike generic module frames it is placed
|
||||
// at a fixed position right after home), so it draws through its own trampoline rather than the
|
||||
// moduleFrames lockstep used by drawModuleFrame.
|
||||
static void drawGamesFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (gamesModule)
|
||||
gamesModule->drawFrame(display, state, x, y);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
||||
*
|
||||
@@ -499,6 +560,8 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
#else
|
||||
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||||
#endif
|
||||
#elif defined(USE_HUB75)
|
||||
dispdev = new HUB75Display(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_SSD1306)
|
||||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
@@ -517,7 +580,17 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
LOG_INFO("SSD1306 init success");
|
||||
}
|
||||
#elif ARCH_PORTDUINO
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
|
||||
// HUB75 RGB matrix (hzeller/rpi-rgb-led-matrix) is a BaseUI framebuffer panel, selected at
|
||||
// runtime via config.yaml Display: Panel: HUB75.
|
||||
if (portduino_config.displayPanel == hub75) {
|
||||
#if defined(HAS_HUB75_NATIVE)
|
||||
LOG_DEBUG("Make HUB75Native!");
|
||||
dispdev = new HUB75Native(address.address, -1, -1, GEOMETRY_RAWMODE, HW_I2C::I2C_ONE);
|
||||
#else
|
||||
LOG_ERROR("HUB75 panel requested but rpi-rgb-led-matrix not compiled in!");
|
||||
#endif
|
||||
} else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
if (portduino_config.displayPanel != no_screen) {
|
||||
LOG_DEBUG("Make TFTDisplay!");
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
@@ -1304,6 +1377,15 @@ void Screen::setFrames(FrameFocus focus)
|
||||
indicatorIcons.push_back(icon_home);
|
||||
}
|
||||
|
||||
#if BASEUI_HAS_GAMES
|
||||
// Games frame: always present (even with no game running), positioned directly after home.
|
||||
if (gamesModule) {
|
||||
fsi.positions.games = numframes;
|
||||
normalFrames[numframes++] = drawGamesFrame;
|
||||
indicatorIcons.push_back(joystick_small);
|
||||
}
|
||||
#endif
|
||||
|
||||
fsi.positions.textMessage = numframes;
|
||||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||||
indicatorIcons.push_back(icon_mail);
|
||||
@@ -2076,6 +2158,12 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
if (module && module->interceptingKeyboardInput())
|
||||
inputIntercepted = true;
|
||||
}
|
||||
#if BASEUI_HAS_GAMES
|
||||
// The games frame isn't a moduleFrame, so check it explicitly: while a game is running it
|
||||
// owns the D-pad (turns/pause) and we must not switch frames or open menus underneath it.
|
||||
if (gamesModule && gamesModule->interceptingKeyboardInput())
|
||||
inputIntercepted = true;
|
||||
#endif
|
||||
|
||||
// If no modules are using the input, move between frames
|
||||
if (!inputIntercepted) {
|
||||
@@ -2150,6 +2238,11 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||
menuHandler::homeBaseMenu();
|
||||
#if BASEUI_HAS_GAMES
|
||||
} else if (gamesModule && framesetInfo.positions.games != 255 &&
|
||||
this->ui->getUiState()->currentFrame == framesetInfo.positions.games) {
|
||||
gamesModule->launchGame(); // launch the game shown on the attract screen
|
||||
#endif
|
||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
|
||||
menuHandler::systemBaseMenu();
|
||||
#if HAS_GPS
|
||||
@@ -2217,6 +2310,11 @@ bool Screen::isOverlayBannerShowing()
|
||||
return NotificationRenderer::isOverlayBannerShowing();
|
||||
}
|
||||
|
||||
bool Screen::isGamesFrameShown()
|
||||
{
|
||||
return framesetInfo.positions.games != 255 && ui && ui->getUiState()->currentFrame == framesetInfo.positions.games;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#else
|
||||
|
||||
@@ -26,6 +26,9 @@ enum notificationTypeEnum {
|
||||
// LOCKED frame. Without this, a first-pair on a locked device cannot
|
||||
// complete because the PIN never renders.
|
||||
pairing_pin,
|
||||
// Arcade-style initials entry: like number_picker/hex_picker, but each position cycles
|
||||
// through A-Z and 0-9. The assembled string is returned via a text (std::string) callback.
|
||||
alphanumeric_picker,
|
||||
};
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
@@ -284,6 +287,10 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
bool isOverlayBannerShowing();
|
||||
|
||||
// True if the always-present games frame is the one currently on screen. Lets the games module
|
||||
// ignore D-pad input when the player has navigated to a different frame.
|
||||
bool isGamesFrameShown();
|
||||
|
||||
bool isScreenOn() { return screenOn; }
|
||||
|
||||
// Stores the last 4 of our hardware ID, to make finding the device for pairing easier
|
||||
@@ -346,6 +353,11 @@ class Screen : public concurrency::OSThread
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback);
|
||||
// Arcade-style initials entry. `length` positions each cycle A-Z/0-9 (UP/DOWN), LEFT/RIGHT
|
||||
// moves the cursor, SELECT advances; the assembled string is delivered to `bannerCallback`.
|
||||
// `initialText` pre-seeds the positions (uppercased & filtered), defaulting to 'A'.
|
||||
void showAlphanumericPicker(const char *message, const char *initialText, uint32_t durationMs, uint8_t length,
|
||||
std::function<void(const std::string &)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
@@ -735,6 +747,7 @@ class Screen : public concurrency::OSThread
|
||||
uint8_t system = 255;
|
||||
uint8_t gps = 255;
|
||||
uint8_t home = 255;
|
||||
uint8_t games = 255;
|
||||
uint8_t textMessage = 255;
|
||||
uint8_t nodelist_nodes = 255;
|
||||
uint8_t nodelist_location = 255;
|
||||
|
||||
@@ -16,8 +16,9 @@ struct TFTColorRegion {
|
||||
// Required by ST7789 driver: it scans until the first disabled entry.
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
#ifndef MAX_TFT_COLOR_REGIONS
|
||||
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
||||
#endif
|
||||
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||
|
||||
enum class TFTColorRole : uint8_t {
|
||||
@@ -39,7 +40,7 @@ enum class TFTColorRole : uint8_t {
|
||||
Count
|
||||
};
|
||||
|
||||
#if HAS_TFT || defined(HAS_SPI_TFT)
|
||||
#if HAS_TFT || defined(HAS_SPI_TFT) || defined(HAS_HUB75_NATIVE)
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 1
|
||||
#else
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
||||
|
||||
@@ -743,17 +743,18 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
int scale = 1;
|
||||
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
|
||||
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
|
||||
int textX_offset = 10;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
textX_offset = textX_offset * 4;
|
||||
const int scale = 2;
|
||||
scale = 2;
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
||||
const int bytesPerRow = (chirpy_width + 7) / 8;
|
||||
|
||||
for (int yy = 0; yy < chirpy_height; ++yy) {
|
||||
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
|
||||
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
|
||||
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < chirpy_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
@@ -767,6 +768,19 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
|
||||
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
|
||||
}
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Colour Chirpy on colour displays. The glyph is a filled head silhouette whose eyes are holes
|
||||
// (off pixels), so two stacked regions render the proper mascot without a second bitmap:
|
||||
// A) whole glyph -> green body / frame / legs
|
||||
// B) the eye band -> black face, with the eye holes turning white via the region's off-colour
|
||||
// Start the face band one column in from the head's left edge (col 6) so that edge stays green,
|
||||
// matching the green column already left on the right edge (col 31).
|
||||
graphics::registerTFTColorRegionDirect(iconX, iconY, chirpy_width * scale, chirpy_height * scale,
|
||||
graphics::TFTPalette::MeshtasticGreen, graphics::getThemeBodyBg());
|
||||
graphics::registerTFTColorRegionDirect(iconX + 7 * scale, iconY + 12 * scale, 24 * scale, 16 * scale,
|
||||
graphics::TFTPalette::Black, graphics::TFTPalette::White);
|
||||
#endif
|
||||
|
||||
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
|
||||
display->drawString(textX, getTextPositions(display)[line++], "Hello");
|
||||
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
|
||||
|
||||
@@ -54,6 +54,7 @@ bool NotificationRenderer::pauseBanner = false;
|
||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||
uint32_t NotificationRenderer::numDigits = 0;
|
||||
uint32_t NotificationRenderer::currentNumber = 0;
|
||||
char NotificationRenderer::alphanumericValue[16] = {0};
|
||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
@@ -277,6 +278,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
case notificationTypeEnum::hex_picker:
|
||||
drawHexPicker(display, state);
|
||||
break;
|
||||
case notificationTypeEnum::alphanumeric_picker:
|
||||
drawAlphanumericPicker(display, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +466,96 @@ void NotificationRenderer::drawHexPicker(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
// Arcade-style initials entry. Mirrors drawHexPicker's cursor/confirm flow, but each position
|
||||
// holds a character from ALPHANUMERIC_CHARS (cycled with UP/DOWN) instead of a packed digit, and
|
||||
// the assembled string is returned through textInputCallback.
|
||||
void NotificationRenderer::drawAlphanumericPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static const char ALPHANUMERIC_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
constexpr int ALPHANUMERIC_COUNT = sizeof(ALPHANUMERIC_CHARS) - 1; // exclude the NUL
|
||||
|
||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||
uint16_t lineCount = 0;
|
||||
|
||||
// Parse lines (identical to the number/hex pickers)
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||
if (lineStarts[lineCount + 1][0] == '\n')
|
||||
lineStarts[lineCount + 1] += 1;
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
auto alphaIndex = [&](char c) -> int {
|
||||
for (int i = 0; i < ALPHANUMERIC_COUNT; i++)
|
||||
if (ALPHANUMERIC_CHARS[i] == c)
|
||||
return i;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Handle input
|
||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
int idx = (alphaIndex(alphanumericValue[curSelected]) + 1) % ALPHANUMERIC_COUNT;
|
||||
alphanumericValue[curSelected] = ALPHANUMERIC_CHARS[idx];
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
||||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
||||
int idx = (alphaIndex(alphanumericValue[curSelected]) + ALPHANUMERIC_COUNT - 1) % ALPHANUMERIC_COUNT;
|
||||
alphanumericValue[curSelected] = ALPHANUMERIC_CHARS[idx];
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||
char k = inEvent.kbchar;
|
||||
if (k >= 'a' && k <= 'z')
|
||||
k = static_cast<char>(k - 'a' + 'A');
|
||||
if ((k >= 'A' && k <= 'Z') || (k >= '0' && k <= '9')) { // direct keyboard entry
|
||||
alphanumericValue[curSelected] = k;
|
||||
curSelected++;
|
||||
}
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||
curSelected++;
|
||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||
curSelected--;
|
||||
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
|
||||
alertBannerUntil != 0) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
if (curSelected < 0)
|
||||
curSelected = 0;
|
||||
if (curSelected == static_cast<int8_t>(numDigits)) {
|
||||
auto callback = textInputCallback; // capture before clearing to avoid re-entrancy surprises
|
||||
std::string result(alphanumericValue, numDigits);
|
||||
textInputCallback = nullptr;
|
||||
resetBanner();
|
||||
if (callback)
|
||||
callback(result);
|
||||
return;
|
||||
}
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
if (alertBannerMessage[0] == '\0')
|
||||
return;
|
||||
|
||||
uint16_t totalLines = lineCount + 2;
|
||||
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||
|
||||
for (uint16_t i = 0; i < lineCount; i++) {
|
||||
linePointers[i] = lineStarts[i];
|
||||
}
|
||||
std::string chars = " ";
|
||||
std::string arrowPointer = " ";
|
||||
for (uint16_t i = 0; i < numDigits; i++) {
|
||||
chars += std::string(1, alphanumericValue[i]) + " ";
|
||||
arrowPointer += (curSelected == static_cast<int8_t>(i)) ? "^ " : "_ ";
|
||||
}
|
||||
|
||||
linePointers[lineCount++] = chars.c_str();
|
||||
linePointers[lineCount++] = arrowPointer.c_str();
|
||||
|
||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
static uint32_t selectedNodenum = 0;
|
||||
|
||||
@@ -26,6 +26,7 @@ class NotificationRenderer
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static char alphanumericValue[16]; // working buffer for the alphanumeric_picker
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
@@ -43,6 +44,7 @@ class NotificationRenderer
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawHexPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlphanumericPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#if BASEUI_HAS_GAMES
|
||||
#include "modules/games/GamesModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -1817,6 +1820,13 @@ constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000;
|
||||
// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
#if BASEUI_HAS_GAMES
|
||||
// Hide the navigation bar while a game owns the screen (the attract screen doesn't intercept,
|
||||
// so the nav bar stays visible there).
|
||||
if (gamesModule && gamesModule->interceptingKeyboardInput())
|
||||
return;
|
||||
#endif
|
||||
|
||||
uint8_t frameToHighlight = state->currentFrame;
|
||||
if (state->frameState == IN_TRANSITION && state->transitionFrameTarget < screen->indicatorIcons.size()) {
|
||||
frameToHighlight = state->transitionFrameTarget;
|
||||
|
||||
@@ -319,6 +319,46 @@ const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
#define connection_icon_height 5
|
||||
const uint8_t connection_icon[] = {0x36, 0x41, 0x5D, 0x41, 0x36};
|
||||
|
||||
// Joystick icon (16x16): a round knob on a shaft over an elliptical base. Drawn on the Snake
|
||||
// attract screen as a "use the D-pad" controls hint.
|
||||
#define joystick_width 16
|
||||
#define joystick_height 16
|
||||
static const uint8_t joystick[] PROGMEM = {0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0,
|
||||
0x03, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0xF8, 0x1F,
|
||||
0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F};
|
||||
|
||||
// 8x8 joystick, for the navigation bar (which draws 8x8 glyphs, scaled up on hi-res displays).
|
||||
#define joystick_small_width 8
|
||||
#define joystick_small_height 8
|
||||
static const uint8_t joystick_small[] PROGMEM = {0x18, 0x3C, 0x3C, 0x18, 0x7E, 0x99, 0xC3, 0x7E};
|
||||
|
||||
#define snake_width 16
|
||||
#define snake_height 16
|
||||
static const uint8_t snake[] PROGMEM = {0x80, 0x1F, 0x40, 0x20, 0x20, 0x48, 0x20, 0x40, 0xA0, 0x44, 0x20,
|
||||
0x39, 0x40, 0xDE, 0x86, 0x44, 0x0A, 0x19, 0x32, 0x22, 0xC4, 0x47,
|
||||
0x1C, 0x4D, 0x6A, 0x4A, 0xFA, 0x47, 0x04, 0x20, 0xF8, 0x1F};
|
||||
|
||||
#define tetris_width 16
|
||||
#define tetris_height 16
|
||||
static const uint8_t tetris[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x80, 0xFD, 0xBF, 0xFD,
|
||||
0xBF, 0x81, 0x81, 0xBF, 0xFD, 0xA0, 0x05, 0xA0, 0x05, 0xA0, 0x05,
|
||||
0x20, 0x04, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
#define breakout_width 16
|
||||
#define breakout_height 16
|
||||
// Three staggered brick courses, a ball, and a paddle. XBM: 2 bytes/row, LSB = leftmost pixel.
|
||||
static const uint8_t breakout[] PROGMEM = {0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0xDD, 0xDD, 0xDD,
|
||||
0xDD, 0x00, 0x00, 0x77, 0x77, 0x77, 0x77, 0x00, 0x00, 0x80, 0x01,
|
||||
0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0xF0, 0x0F};
|
||||
|
||||
// Tiny Chirpy runner sprite: square head with two pill eyes + mouth, antenna, and long legs.
|
||||
// XBM, 2 bytes/row, LSB = leftmost pixel. Used by the Chirpy Runner game.
|
||||
#define chirpy_run_width 12
|
||||
#define chirpy_run_height 16
|
||||
static const uint8_t chirpy_run[] PROGMEM = {0x40, 0x00, 0x20, 0x00, 0x40, 0x00, 0xfc, 0x03, 0x04, 0x02, 0x94,
|
||||
0x02, 0x94, 0x02, 0x94, 0x02, 0x04, 0x02, 0xf4, 0x02, 0xfc, 0x03,
|
||||
0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x08, 0x01};
|
||||
|
||||
#ifdef OLED_TINY
|
||||
#include "img/icon_small.xbm"
|
||||
#else
|
||||
|
||||
Reference in New Issue
Block a user