add proper firmware flashing screen
This commit is contained in:
parent
7349fbb208
commit
a04388fd6c
@ -205,6 +205,55 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
|
||||
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawImageRotated(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
|
||||
const ImageRotation rotation, const bool invert) const {
|
||||
// Calculate output dimensions based on rotation
|
||||
const int outWidth = (rotation == ROTATE_90 || rotation == ROTATE_270) ? height : width;
|
||||
const int outHeight = (rotation == ROTATE_90 || rotation == ROTATE_270) ? width : height;
|
||||
|
||||
// Draw each pixel from the source bitmap with rotation
|
||||
for (int srcY = 0; srcY < height; srcY++) {
|
||||
for (int srcX = 0; srcX < width; srcX++) {
|
||||
// Read source pixel (1-bit packed, MSB first)
|
||||
const int byteIndex = srcY * ((width + 7) / 8) + (srcX / 8);
|
||||
const int bitIndex = 7 - (srcX % 8);
|
||||
const bool srcPixel = (bitmap[byteIndex] >> bitIndex) & 1;
|
||||
|
||||
// Apply inversion if requested
|
||||
const bool pixelState = invert ? !srcPixel : srcPixel;
|
||||
|
||||
// Skip black pixels (transparent on black background)
|
||||
if (!pixelState) continue;
|
||||
|
||||
// Calculate destination coordinates based on rotation (clockwise)
|
||||
int dstX, dstY;
|
||||
switch (rotation) {
|
||||
case ROTATE_0:
|
||||
dstX = srcX;
|
||||
dstY = srcY;
|
||||
break;
|
||||
case ROTATE_90: // 90° clockwise
|
||||
dstX = height - 1 - srcY;
|
||||
dstY = srcX;
|
||||
break;
|
||||
case ROTATE_180:
|
||||
dstX = width - 1 - srcX;
|
||||
dstY = height - 1 - srcY;
|
||||
break;
|
||||
case ROTATE_270: // 270° clockwise (= 90° counter-clockwise)
|
||||
dstX = srcY;
|
||||
dstY = width - 1 - srcX;
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw the pixel at the rotated position
|
||||
// Note: pixelState=true means white pixel, but drawPixel(state=true) draws black
|
||||
// So we invert: draw with state=false for white pixels
|
||||
drawPixel(x + dstX, y + dstY, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
||||
const float cropX, const float cropY, const bool invert) const {
|
||||
// For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit)
|
||||
|
||||
@ -11,6 +11,9 @@ class GfxRenderer {
|
||||
public:
|
||||
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
||||
|
||||
// Image rotation (clockwise)
|
||||
enum ImageRotation { ROTATE_0 = 0, ROTATE_90 = 90, ROTATE_180 = 180, ROTATE_270 = 270 };
|
||||
|
||||
// Logical screen orientation from the perspective of callers
|
||||
enum Orientation {
|
||||
Portrait, // 480x800 logical coordinates (current default)
|
||||
@ -69,6 +72,8 @@ class GfxRenderer {
|
||||
// Handles current render mode (BW, GRAYSCALE_MSB, GRAYSCALE_LSB)
|
||||
void fillRectGray(int x, int y, int width, int height, uint8_t grayLevel) const;
|
||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||
void drawImageRotated(const uint8_t bitmap[], int x, int y, int width, int height,
|
||||
ImageRotation rotation, bool invert = false) const;
|
||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
||||
float cropY = 0, bool invert = false) const;
|
||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, bool invert = false) const;
|
||||
|
||||
@ -44,6 +44,8 @@ board_build.partitions = partitions.csv
|
||||
|
||||
extra_scripts =
|
||||
pre:scripts/build_html.py
|
||||
pre:scripts/version_hash.py
|
||||
pre:scripts/pre_flash.py
|
||||
|
||||
; Libraries
|
||||
lib_deps =
|
||||
|
||||
47
scripts/pre_flash.py
Normal file
47
scripts/pre_flash.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""
|
||||
Pre-upload script that sends FLASH command to device before firmware upload.
|
||||
|
||||
This allows the firmware to display "Flashing firmware..." on the e-ink display
|
||||
before the actual flash begins. The e-ink retains this message throughout the
|
||||
flash process since it doesn't require power to maintain the display.
|
||||
|
||||
Protocol: Sends "FLASH:version\n" where version is read from platformio.ini
|
||||
"""
|
||||
|
||||
Import("env")
|
||||
import serial
|
||||
import time
|
||||
from version_utils import get_version
|
||||
|
||||
|
||||
def before_upload(source, target, env):
|
||||
"""Send FLASH command with version to device before upload begins."""
|
||||
port = env.GetProjectOption("upload_port", None)
|
||||
|
||||
if not port:
|
||||
import serial.tools.list_ports
|
||||
|
||||
# Look for common ESP32-C3 USB serial port patterns
|
||||
ports = [
|
||||
p.device
|
||||
for p in serial.tools.list_ports.comports()
|
||||
if "usbmodem" in p.device.lower() or "ttyacm" in p.device.lower()
|
||||
]
|
||||
port = ports[0] if ports else None
|
||||
|
||||
if port:
|
||||
try:
|
||||
version = get_version(env)
|
||||
ser = serial.Serial(port, 115200, timeout=1)
|
||||
ser.write(f"FLASH:{version}\n".encode())
|
||||
ser.flush()
|
||||
ser.close()
|
||||
time.sleep(0.8) # Wait for e-ink fast refresh (~500ms) plus margin
|
||||
print(f"[pre_flash] Flash notification sent to {port} (version {version})")
|
||||
except Exception as e:
|
||||
print(f"[pre_flash] Notification skipped: {e}")
|
||||
else:
|
||||
print("[pre_flash] No serial port found, skipping notification")
|
||||
|
||||
|
||||
env.AddPreAction("upload", before_upload)
|
||||
26
scripts/version_hash.py
Normal file
26
scripts/version_hash.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""
|
||||
Build script that adds git hash to CROSSPOINT_VERSION for dev builds.
|
||||
|
||||
This script runs during the build process and modifies the CROSSPOINT_VERSION
|
||||
define to include the short git commit hash, e.g., "0.15.1-dev@7349fbb"
|
||||
"""
|
||||
|
||||
Import("env")
|
||||
from version_utils import get_git_short_hash
|
||||
|
||||
|
||||
# Only modify version for non-release builds
|
||||
pio_env = env.get("PIOENV", "default")
|
||||
if pio_env != "gh_release":
|
||||
git_hash = get_git_short_hash()
|
||||
if git_hash:
|
||||
# Get current build flags and find/replace CROSSPOINT_VERSION
|
||||
build_flags = env.get("BUILD_FLAGS", [])
|
||||
new_flags = []
|
||||
for flag in build_flags:
|
||||
if "CROSSPOINT_VERSION" in flag and "-dev" in flag:
|
||||
# Replace -dev with -dev@hash
|
||||
flag = flag.replace("-dev", f"-dev@{git_hash}")
|
||||
new_flags.append(flag)
|
||||
env.Replace(BUILD_FLAGS=new_flags)
|
||||
print(f"[version_hash] Added git hash to version: @{git_hash}")
|
||||
47
scripts/version_utils.py
Normal file
47
scripts/version_utils.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""
|
||||
Shared version utilities for PlatformIO build scripts.
|
||||
|
||||
Provides functions to get version info including git hash for dev builds.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import subprocess
|
||||
|
||||
|
||||
def get_git_short_hash():
|
||||
"""Get the short hash of the current HEAD commit."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--short", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_version(env):
|
||||
"""
|
||||
Get version from platformio.ini, adding -dev@hash suffix for dev builds.
|
||||
|
||||
Args:
|
||||
env: PlatformIO environment object
|
||||
|
||||
Returns:
|
||||
Version string, e.g., "0.15.1" or "0.15.1-dev@7349fbb"
|
||||
"""
|
||||
config = configparser.ConfigParser()
|
||||
config.read("platformio.ini")
|
||||
version = config.get("crosspoint", "version", fallback="unknown")
|
||||
|
||||
# Add -dev@hash suffix for non-release environments (matches platformio.ini build_flags)
|
||||
pio_env = env.get("PIOENV", "default")
|
||||
if pio_env != "gh_release":
|
||||
version += "-dev"
|
||||
git_hash = get_git_short_hash()
|
||||
if git_hash:
|
||||
version += f"@{git_hash}"
|
||||
|
||||
return version
|
||||
65
src/images/LockIcon.h
Normal file
65
src/images/LockIcon.h
Normal file
@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Lock icon dimensions (original orientation - shackle on top)
|
||||
static constexpr int LOCK_ICON_WIDTH = 32;
|
||||
static constexpr int LOCK_ICON_HEIGHT = 40;
|
||||
|
||||
// 32x40 pixel padlock icon (1-bit bitmap, MSB first)
|
||||
// 0 = black pixel, 1 = white pixel
|
||||
// Original orientation: shackle on top, body below
|
||||
// Use drawImageRotated() to rotate as needed for different screen orientations
|
||||
static const uint8_t LockIcon[] = {
|
||||
// Row 0-1: Empty space above shackle
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
// Row 2-3: Shackle top curve
|
||||
0x00, 0x0F, 0xF0, 0x00, // ....####....
|
||||
0x00, 0x3F, 0xFC, 0x00, // ..########..
|
||||
// Row 4-5: Shackle upper sides
|
||||
0x00, 0x78, 0x1E, 0x00, // .####..####.
|
||||
0x00, 0xE0, 0x07, 0x00, // ###......###
|
||||
// Row 6-9: Extended shackle legs (longer for better visual)
|
||||
0x00, 0xC0, 0x03, 0x00, // ##........##
|
||||
0x01, 0xC0, 0x03, 0x80, // ###......###
|
||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
||||
// Row 10-13: Shackle legs continue into body
|
||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
||||
0x01, 0x80, 0x01, 0x80, // ##........##
|
||||
// Row 14-15: Body top
|
||||
0x0F, 0xFF, 0xFF, 0xF0, // ############
|
||||
0x1F, 0xFF, 0xFF, 0xF8, // ##############
|
||||
// Row 16-17: Body top edge
|
||||
0x3F, 0xFF, 0xFF, 0xFC, // ################
|
||||
0x3F, 0xFF, 0xFF, 0xFC, // ################
|
||||
// Row 18-29: Solid body (no keyhole)
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
// Row 30-33: Body lower section
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
// Row 34-35: Body bottom edge
|
||||
0x3F, 0xFF, 0xFF, 0xFC,
|
||||
0x1F, 0xFF, 0xFF, 0xF8,
|
||||
// Row 36-37: Body bottom
|
||||
0x0F, 0xFF, 0xFF, 0xF0,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
// Row 38-39: Empty space below
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
116
src/main.cpp
116
src/main.cpp
@ -26,6 +26,7 @@
|
||||
#include "activities/settings/SettingsActivity.h"
|
||||
#include "activities/util/FullScreenMessageActivity.h"
|
||||
#include "fontIds.h"
|
||||
#include "images/LockIcon.h"
|
||||
|
||||
#define SPI_FQ 40000000
|
||||
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
||||
@ -128,6 +129,118 @@ void logMemoryState(const char* tag, const char* context) {
|
||||
#define logMemoryState(tag, context) ((void)0)
|
||||
#endif
|
||||
|
||||
// Flash command detection - receives "FLASH\n" from pre_flash.py script
|
||||
static String flashCmdBuffer;
|
||||
|
||||
void checkForFlashCommand() {
|
||||
while (Serial.available()) {
|
||||
char c = Serial.read();
|
||||
if (c == '\n') {
|
||||
if (flashCmdBuffer.startsWith("FLASH")) {
|
||||
// Extract version if provided (format: "FLASH:x.y.z")
|
||||
String newVersion = "";
|
||||
if (flashCmdBuffer.length() > 6 && flashCmdBuffer.charAt(5) == ':') {
|
||||
newVersion = flashCmdBuffer.substring(6);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [FLS] Flash command received (version: %s), displaying message\n", millis(),
|
||||
newVersion.length() > 0 ? newVersion.c_str() : "unknown");
|
||||
|
||||
// Display flash message with inverted colors (black background, white text)
|
||||
renderer.clearScreen(0x00); // Black background
|
||||
renderer.drawCenteredText(NOTOSANS_14_FONT_ID, 200, "Flashing firmware...", false, EpdFontFamily::BOLD);
|
||||
|
||||
// Show new version under title if provided
|
||||
if (newVersion.length() > 0) {
|
||||
String versionText = "v" + newVersion;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 260, versionText.c_str(), false);
|
||||
}
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Do not disconnect USB", false);
|
||||
|
||||
// Get screen dimensions and orientation for positioning
|
||||
const int screenW = renderer.getScreenWidth();
|
||||
const int screenH = renderer.getScreenHeight();
|
||||
|
||||
// Show current version in bottom-left corner (orientation-aware)
|
||||
// "Bottom-left" is relative to the current orientation
|
||||
constexpr int versionMargin = 10;
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION);
|
||||
int versionX, versionY;
|
||||
switch (renderer.getOrientation()) {
|
||||
case GfxRenderer::Portrait: // Bottom-left is actual bottom-left
|
||||
versionX = versionMargin;
|
||||
versionY = screenH - 30;
|
||||
break;
|
||||
case GfxRenderer::PortraitInverted: // Bottom-left is actual top-right
|
||||
versionX = screenW - textWidth - versionMargin;
|
||||
versionY = 20;
|
||||
break;
|
||||
case GfxRenderer::LandscapeClockwise: // Bottom-left is actual bottom-right
|
||||
versionX = screenW - textWidth - versionMargin;
|
||||
versionY = screenH - 30;
|
||||
break;
|
||||
case GfxRenderer::LandscapeCounterClockwise: // Bottom-left is actual bottom-left
|
||||
versionX = versionMargin;
|
||||
versionY = screenH - 30;
|
||||
break;
|
||||
}
|
||||
renderer.drawText(SMALL_FONT_ID, versionX, versionY, CROSSPOINT_VERSION, false);
|
||||
|
||||
// Position and rotate lock icon based on current orientation (USB port location)
|
||||
// USB port locations: Portrait=bottom-left, PortraitInverted=top-right,
|
||||
// LandscapeCW=top-left, LandscapeCCW=bottom-right
|
||||
// Position offsets: edge margin + half-width offset to center on USB port
|
||||
constexpr int edgeMargin = 28; // Distance from screen edge
|
||||
constexpr int halfWidth = LOCK_ICON_WIDTH / 2; // 16px offset for centering
|
||||
int iconX, iconY;
|
||||
GfxRenderer::ImageRotation rotation;
|
||||
int outW, outH; // Note: 90/270 rotation swaps output dimensions
|
||||
switch (renderer.getOrientation()) {
|
||||
case GfxRenderer::Portrait: // USB at bottom-left, shackle points right
|
||||
rotation = GfxRenderer::ROTATE_90;
|
||||
outW = LOCK_ICON_HEIGHT;
|
||||
outH = LOCK_ICON_WIDTH;
|
||||
iconX = edgeMargin;
|
||||
iconY = screenH - outH - edgeMargin - halfWidth;
|
||||
break;
|
||||
case GfxRenderer::PortraitInverted: // USB at top-right, shackle points left
|
||||
rotation = GfxRenderer::ROTATE_270;
|
||||
outW = LOCK_ICON_HEIGHT;
|
||||
outH = LOCK_ICON_WIDTH;
|
||||
iconX = screenW - outW - edgeMargin;
|
||||
iconY = edgeMargin + halfWidth;
|
||||
break;
|
||||
case GfxRenderer::LandscapeClockwise: // USB at top-left, shackle points down
|
||||
rotation = GfxRenderer::ROTATE_180;
|
||||
outW = LOCK_ICON_WIDTH;
|
||||
outH = LOCK_ICON_HEIGHT;
|
||||
iconX = edgeMargin + halfWidth;
|
||||
iconY = edgeMargin;
|
||||
break;
|
||||
case GfxRenderer::LandscapeCounterClockwise: // USB at bottom-right, shackle points up
|
||||
rotation = GfxRenderer::ROTATE_0;
|
||||
outW = LOCK_ICON_WIDTH;
|
||||
outH = LOCK_ICON_HEIGHT;
|
||||
iconX = screenW - outW - edgeMargin - halfWidth;
|
||||
iconY = screenH - outH - edgeMargin;
|
||||
break;
|
||||
}
|
||||
renderer.drawImageRotated(LockIcon, iconX, iconY, LOCK_ICON_WIDTH, LOCK_ICON_HEIGHT, rotation);
|
||||
|
||||
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
|
||||
}
|
||||
flashCmdBuffer = "";
|
||||
} else if (c != '\r') {
|
||||
flashCmdBuffer += c;
|
||||
// Prevent buffer overflow from random serial data (increased for version info)
|
||||
if (flashCmdBuffer.length() > 30) {
|
||||
flashCmdBuffer = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void exitActivity() {
|
||||
if (currentActivity) {
|
||||
logMemoryState("MAIN", "Before onExit");
|
||||
@ -391,6 +504,9 @@ void setup() {
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Check for flash command from pre_flash.py script (must be first to catch before upload)
|
||||
checkForFlashCommand();
|
||||
|
||||
static unsigned long maxLoopDuration = 0;
|
||||
const unsigned long loopStartTime = millis();
|
||||
static unsigned long lastMemPrint = 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user