mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-29 07:37:48 +01:00
Compare commits
9 Commits
65ee33a141
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bb033a6ca | |||
| 4d0eb3d187 | |||
| 5532055cdc | |||
| 61f05b4e58 | |||
| 961da2ba33 | |||
| 96f5b1f0ee | |||
| f5a780c1c8 | |||
| 5c3cdaaae4 | |||
| f814c45532 |
4
Firmware/.vscode/settings.json
vendored
4
Firmware/.vscode/settings.json
vendored
@@ -80,6 +80,8 @@
|
|||||||
"thread": "cpp",
|
"thread": "cpp",
|
||||||
"cinttypes": "cpp",
|
"cinttypes": "cpp",
|
||||||
"typeinfo": "cpp",
|
"typeinfo": "cpp",
|
||||||
"variant": "cpp"
|
"variant": "cpp",
|
||||||
|
"ranges": "cpp",
|
||||||
|
"shared_mutex": "cpp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public:
|
|||||||
float get_voltage() const;
|
float get_voltage() const;
|
||||||
float get_charge() const;
|
float get_charge() const;
|
||||||
float get_current() const;
|
float get_current() const;
|
||||||
|
float get_percentage() const;
|
||||||
|
|
||||||
void pooler(); // FIXME:
|
void pooler(); // FIXME:
|
||||||
private:
|
private:
|
||||||
@@ -33,6 +34,7 @@ private:
|
|||||||
volatile float _voltage;
|
volatile float _voltage;
|
||||||
volatile float _current;
|
volatile float _current;
|
||||||
volatile float _charge;
|
volatile float _charge;
|
||||||
|
volatile float _percentage;
|
||||||
|
|
||||||
TaskHandle_t _pooler_task;
|
TaskHandle_t _pooler_task;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ static constexpr uint16_t DesignCapMah = 180; // 100mOhm
|
|||||||
|
|
||||||
constexpr float mahToCap(float mah) { return mah * (1000.0 / 5.0) * RSense; }
|
constexpr float mahToCap(float mah) { return mah * (1000.0 / 5.0) * RSense; }
|
||||||
constexpr float capToMah(uint16_t cap) { return cap * (5.0 / 1000.0) / RSense; }
|
constexpr float capToMah(uint16_t cap) { return cap * (5.0 / 1000.0) / RSense; }
|
||||||
|
// lsb is 1/256%
|
||||||
|
constexpr float regToPercent(uint16_t reg) { return static_cast<float>(reg) / 256.0f; }
|
||||||
constexpr float regToCurrent(uint16_t reg) {
|
constexpr float regToCurrent(uint16_t reg) {
|
||||||
return static_cast<float>(static_cast<int16_t>(reg)) * 0.0015625f / RSense; // Convert to mA
|
return static_cast<float>(static_cast<int16_t>(reg)) * 0.0015625f / RSense; // Convert to mA
|
||||||
}
|
}
|
||||||
@@ -103,6 +105,7 @@ void BatMon::pooler() {
|
|||||||
_charge = capToMah(ReadRegister(0x05));
|
_charge = capToMah(ReadRegister(0x05));
|
||||||
_current = regToCurrent(ReadRegister(0x0B));
|
_current = regToCurrent(ReadRegister(0x0B));
|
||||||
_voltage = regToVoltage(ReadRegister(0x09));
|
_voltage = regToVoltage(ReadRegister(0x09));
|
||||||
|
_percentage = regToPercent(ReadRegister(0x06));
|
||||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||||
if (_voltage < 3.0f) {
|
if (_voltage < 3.0f) {
|
||||||
Shutdowner::get().shutdown();
|
Shutdowner::get().shutdown();
|
||||||
@@ -113,3 +116,4 @@ void BatMon::pooler() {
|
|||||||
float BatMon::get_voltage() const { return _voltage; }
|
float BatMon::get_voltage() const { return _voltage; }
|
||||||
float BatMon::get_charge() const { return _charge; }
|
float BatMon::get_charge() const { return _charge; }
|
||||||
float BatMon::get_current() const { return _current; }
|
float BatMon::get_current() const { return _current; }
|
||||||
|
float BatMon::get_percentage() const { return _percentage; }
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ public:
|
|||||||
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
|
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
|
||||||
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
|
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
|
||||||
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
|
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
|
||||||
|
[[nodiscard]] float percentage() const override { return BatMon::get().get_percentage(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
class EspRuntime::StorageService final : public cardboy::sdk::IStorage {
|
class EspRuntime::StorageService final : public cardboy::sdk::IStorage {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -31,12 +32,12 @@ using Framebuffer = typename AppContext::Framebuffer;
|
|||||||
using Clock = typename AppContext::Clock;
|
using Clock = typename AppContext::Clock;
|
||||||
|
|
||||||
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowUpGlyph{
|
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowUpGlyph{
|
||||||
0b00011000, 0b00111100, 0b01111110, 0b11111111, 0b00011000, 0b00011000, 0b00011000, 0b00011000,
|
0b00010000, 0b00111000, 0b01111100, 0b11111110, 0b00010000, 0b00010000, 0b00010000, 0b00010000,
|
||||||
0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00000000, 0b00000000};
|
0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00000000, 0b00000000};
|
||||||
|
|
||||||
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowDownGlyph{
|
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowDownGlyph{
|
||||||
0b00000000, 0b00000000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000,
|
0b00000000, 0b00000000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000,
|
||||||
0b00011000, 0b00011000, 0b00011000, 0b11111111, 0b01111110, 0b00111100, 0b00011000, 0b00000000};
|
0b00010000, 0b00010000, 0b00010000, 0b11111110, 0b01111100, 0b00111000, 0b00010000, 0b00000000};
|
||||||
|
|
||||||
struct TimeSnapshot {
|
struct TimeSnapshot {
|
||||||
bool hasWallTime = false;
|
bool hasWallTime = false;
|
||||||
@@ -130,12 +131,14 @@ private:
|
|||||||
if (!notifications.empty() && (upPressed || downPressed)) {
|
if (!notifications.empty() && (upPressed || downPressed)) {
|
||||||
const std::size_t count = notifications.size();
|
const std::size_t count = notifications.size();
|
||||||
lastNotificationInteractionMs = clock.millis();
|
lastNotificationInteractionMs = clock.millis();
|
||||||
navPressed = true;
|
|
||||||
if (count > 1) {
|
if (count > 1) {
|
||||||
if (upPressed)
|
if (upPressed && selectedNotification > 0) {
|
||||||
selectedNotification = (selectedNotification + count - 1) % count;
|
selectedNotification--;
|
||||||
else if (downPressed)
|
navPressed = true;
|
||||||
selectedNotification = (selectedNotification + 1) % count;
|
} else if (downPressed && selectedNotification < count - 1) {
|
||||||
|
selectedNotification++;
|
||||||
|
navPressed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,6 +447,25 @@ private:
|
|||||||
|
|
||||||
framebuffer.frameReady();
|
framebuffer.frameReady();
|
||||||
|
|
||||||
|
const std::uint32_t nowMs = clock.millis();
|
||||||
|
const bool hasNotifications = !notifications.empty();
|
||||||
|
const bool showNotificationDetails =
|
||||||
|
hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs);
|
||||||
|
|
||||||
|
if (!showNotificationDetails) {
|
||||||
|
if (auto* battery = context.battery(); battery && battery->hasData()) {
|
||||||
|
const float percentage = battery->percentage();
|
||||||
|
if (std::isfinite(percentage) && percentage >= 0.0f) {
|
||||||
|
char pct[8];
|
||||||
|
std::snprintf(pct, sizeof(pct), "%.0f%%", static_cast<double>(percentage));
|
||||||
|
const int pctWidth = font16x8::measureText(pct, 1, 1);
|
||||||
|
const int pctX = framebuffer.width() - pctWidth - 4;
|
||||||
|
const int pctY = 4;
|
||||||
|
font16x8::drawText(framebuffer, pctX, pctY, pct, 1, true, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const int scaleTime = 4;
|
const int scaleTime = 4;
|
||||||
const int scaleSeconds = 2;
|
const int scaleSeconds = 2;
|
||||||
const int scaleSmall = 1;
|
const int scaleSmall = 1;
|
||||||
@@ -456,10 +478,6 @@ private:
|
|||||||
int cardHeight = 0;
|
int cardHeight = 0;
|
||||||
const int cardWidth = framebuffer.width() - cardMarginSide * 2;
|
const int cardWidth = framebuffer.width() - cardMarginSide * 2;
|
||||||
|
|
||||||
const std::uint32_t nowMs = clock.millis();
|
|
||||||
const bool hasNotifications = !notifications.empty();
|
|
||||||
const bool showNotificationDetails =
|
|
||||||
hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs);
|
|
||||||
|
|
||||||
if (hasNotifications) {
|
if (hasNotifications) {
|
||||||
const auto& note = notifications[selectedNotification];
|
const auto& note = notifications[selectedNotification];
|
||||||
@@ -500,8 +518,10 @@ private:
|
|||||||
const int arrowsTotalWide = arrowWidth * 2 + arrowSpacing;
|
const int arrowsTotalWide = arrowWidth * 2 + arrowSpacing;
|
||||||
const int arrowX = counterX + (counterWidth - arrowsTotalWide) / 2;
|
const int arrowX = counterX + (counterWidth - arrowsTotalWide) / 2;
|
||||||
const int arrowY = cardMarginTop + cardPadding + textLineHeight + 1;
|
const int arrowY = cardMarginTop + cardPadding + textLineHeight + 1;
|
||||||
drawArrow(framebuffer, arrowX, arrowY, true, scaleSmall);
|
if (selectedNotification > 0)
|
||||||
drawArrow(framebuffer, arrowX + arrowWidth + arrowSpacing, arrowY, false, scaleSmall);
|
drawArrow(framebuffer, arrowX, arrowY, true, scaleSmall);
|
||||||
|
if (selectedNotification < notifications.size() - 1)
|
||||||
|
drawArrow(framebuffer, arrowX + arrowWidth + arrowSpacing, arrowY, false, scaleSmall);
|
||||||
const int arrowHeight = font16x8::kGlyphHeight * scaleSmall;
|
const int arrowHeight = font16x8::kGlyphHeight * scaleSmall;
|
||||||
cardHeight = std::max(cardHeight, arrowY + arrowHeight - cardMarginTop);
|
cardHeight = std::max(cardHeight, arrowY + arrowHeight - cardMarginTop);
|
||||||
}
|
}
|
||||||
@@ -524,28 +544,13 @@ private:
|
|||||||
const int summaryX = (framebuffer.width() - summaryWidth) / 2;
|
const int summaryX = (framebuffer.width() - summaryWidth) / 2;
|
||||||
const int summaryY = cardMarginTop;
|
const int summaryY = cardMarginTop;
|
||||||
font16x8::drawText(framebuffer, summaryX, summaryY, summary, scaleSmall, true, 1);
|
font16x8::drawText(framebuffer, summaryX, summaryY, summary, scaleSmall, true, 1);
|
||||||
|
|
||||||
if (notifications.size() > 1) {
|
|
||||||
const int arrowWidth = font16x8::kGlyphWidth * scaleSmall;
|
|
||||||
const int arrowSpacing = std::max(1, scaleSmall);
|
|
||||||
const int arrowsTotalWide = arrowWidth * 2 + arrowSpacing;
|
|
||||||
const int arrowX = (framebuffer.width() - arrowsTotalWide) / 2;
|
|
||||||
const int arrowY = summaryY + textLineHeight + 1;
|
|
||||||
drawArrow(framebuffer, arrowX, arrowY, true, scaleSmall);
|
|
||||||
drawArrow(framebuffer, arrowX + arrowWidth + arrowSpacing, arrowY, false, scaleSmall);
|
|
||||||
const int arrowHeight = font16x8::kGlyphHeight * scaleSmall;
|
|
||||||
cardHeight = std::max(cardHeight, arrowY + arrowHeight - cardMarginTop);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const int defaultTimeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8;
|
const int defaultTimeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8;
|
||||||
int timeY = defaultTimeY;
|
const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16;
|
||||||
if (cardHeight > 0)
|
const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48);
|
||||||
timeY = cardMarginTop + cardHeight + 16;
|
const int timeY = std::clamp(defaultTimeY, minTimeY, maxTimeY);
|
||||||
const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16;
|
|
||||||
const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48);
|
|
||||||
timeY = std::clamp(timeY, minTimeY, maxTimeY);
|
|
||||||
|
|
||||||
char hoursMinutes[6];
|
char hoursMinutes[6];
|
||||||
std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute);
|
std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute);
|
||||||
@@ -562,24 +567,18 @@ private:
|
|||||||
const std::string dateLine = formatDate(snap);
|
const std::string dateLine = formatDate(snap);
|
||||||
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 16, dateLine, scaleSmall, 1);
|
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 16, dateLine, scaleSmall, 1);
|
||||||
|
|
||||||
const std::string instruction = holdActive ? "KEEP HOLDING A+SELECT" : "HOLD A+SELECT";
|
const std::string instruction = "HOLD A+SELECT TO UNLOCK";
|
||||||
const int instructionWidth = font16x8::measureText(instruction, scaleSmall, 1);
|
const int instructionWidth = font16x8::measureText(instruction, scaleSmall, 1);
|
||||||
const int barHeight = 14;
|
const int barHeight = 18;
|
||||||
const int barY = framebuffer.height() - 24;
|
const int barY = framebuffer.height() - 30;
|
||||||
const int textY = barY + (barHeight - textLineHeight) / 2;
|
const int textY = barY + (barHeight - textLineHeight) / 2 + 1;
|
||||||
const int textX = 8;
|
drawCenteredText(framebuffer, textY, instruction, scaleSmall, 1);
|
||||||
font16x8::drawText(framebuffer, textX, textY, instruction, scaleSmall, true, 1);
|
|
||||||
|
|
||||||
int barX = textX + instructionWidth + 12;
|
const int barWidth = framebuffer.width() - 64;
|
||||||
int barWidth = framebuffer.width() - barX - 8;
|
const int barX = 32;
|
||||||
if (barWidth < 40) {
|
|
||||||
barWidth = 40;
|
|
||||||
barX = std::min(barX, framebuffer.width() - barWidth - 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawRectOutline(framebuffer, barX, barY, barWidth, barHeight);
|
|
||||||
|
|
||||||
if (holdActive || holdProgressMs > 0) {
|
if (holdActive || holdProgressMs > 0) {
|
||||||
|
drawRectOutline(framebuffer, barX, barY, barWidth, barHeight);
|
||||||
const int innerWidth = barWidth - 2;
|
const int innerWidth = barWidth - 2;
|
||||||
const int innerHeight = barHeight - 2;
|
const int innerHeight = barHeight - 2;
|
||||||
const float ratio = std::clamp(holdProgressMs / static_cast<float>(kUnlockHoldMs), 0.0f, 1.0f);
|
const float ratio = std::clamp(holdProgressMs / static_cast<float>(kUnlockHoldMs), 0.0f, 1.0f);
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ namespace apps {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using cardboy::sdk::AppButtonEvent;
|
using cardboy::sdk::AppButtonEvent;
|
||||||
using cardboy::sdk::AppTimeoutEvent;
|
|
||||||
using cardboy::sdk::AppContext;
|
using cardboy::sdk::AppContext;
|
||||||
using cardboy::sdk::AppEvent;
|
using cardboy::sdk::AppEvent;
|
||||||
|
using cardboy::sdk::AppTimeoutEvent;
|
||||||
using cardboy::sdk::AppTimerEvent;
|
using cardboy::sdk::AppTimerEvent;
|
||||||
|
|
||||||
using Framebuffer = typename AppContext::Framebuffer;
|
using Framebuffer = typename AppContext::Framebuffer;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ using cardboy::sdk::InputState;
|
|||||||
constexpr char kSnakeAppName[] = "Snake";
|
constexpr char kSnakeAppName[] = "Snake";
|
||||||
|
|
||||||
constexpr int kBoardWidth = 32;
|
constexpr int kBoardWidth = 32;
|
||||||
constexpr int kBoardHeight = 20;
|
constexpr int kBoardHeight = 18;
|
||||||
constexpr int kCellSize = 10;
|
constexpr int kCellSize = 10;
|
||||||
constexpr int kInitialSnakeLength = 5;
|
constexpr int kInitialSnakeLength = 5;
|
||||||
constexpr int kScorePerFood = 10;
|
constexpr int kScorePerFood = 10;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public:
|
|||||||
[[nodiscard]] virtual float voltage() const { return 0.0f; }
|
[[nodiscard]] virtual float voltage() const { return 0.0f; }
|
||||||
[[nodiscard]] virtual float charge() const { return 0.0f; }
|
[[nodiscard]] virtual float charge() const { return 0.0f; }
|
||||||
[[nodiscard]] virtual float current() const { return 0.0f; }
|
[[nodiscard]] virtual float current() const { return 0.0f; }
|
||||||
|
[[nodiscard]] virtual float percentage() const { return 0.0f; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class IStorage {
|
class IStorage {
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
|
|||||||
public:
|
public:
|
||||||
[[nodiscard]] bool hasData() const override { return false; }
|
[[nodiscard]] bool hasData() const override { return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class DesktopStorage final : public cardboy::sdk::IStorage {
|
class DesktopStorage final : public cardboy::sdk::IStorage {
|
||||||
public:
|
public:
|
||||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
|
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
|
||||||
@@ -265,7 +264,7 @@ struct Backend {
|
|||||||
using Clock = DesktopClock;
|
using Clock = DesktopClock;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace cardboy::backend::desktop
|
}; // namespace cardboy::backend::desktop
|
||||||
|
|
||||||
namespace cardboy::backend {
|
namespace cardboy::backend {
|
||||||
using DesktopBackend = desktop::Backend;
|
using DesktopBackend = desktop::Backend;
|
||||||
|
|||||||
@@ -1,25 +1,67 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
FetchContent_Declare(uni_algo
|
||||||
|
URL https://github.com/uni-algo/uni-algo/archive/refs/tags/v1.2.0.tar.gz
|
||||||
|
URL_HASH SHA256=f2a1539cd8635bc6088d05144a73ecfe7b4d74ee0361fabed6f87f9f19e74ca9
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchContent_MakeAvailable(uni_algo)
|
||||||
|
|
||||||
add_library(cardboy_sdk STATIC
|
add_library(cardboy_sdk STATIC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/status_bar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/status_bar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/framebuffer_hooks.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/framebuffer_hooks.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/persistent_settings.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/persistent_settings.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/font_repository.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/psf_font.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/builtin_fonts.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(cardboy_sdk PROPERTIES
|
set_target_properties(cardboy_sdk PROPERTIES
|
||||||
EXPORT_NAME sdk
|
EXPORT_NAME sdk
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(cardboy_sdk
|
|
||||||
PUBLIC
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
||||||
|
|
||||||
target_link_libraries(cardboy_sdk
|
target_link_libraries(cardboy_sdk
|
||||||
PUBLIC
|
PUBLIC
|
||||||
cardboy_backend_interface
|
cardboy_backend_interface
|
||||||
${CARDBOY_SDK_BACKEND_LIBRARY}
|
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||||
|
uni-algo::uni-algo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_include_directories(cardboy_sdk
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PSF_FONT_FILES
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/spleen8x16.psf
|
||||||
|
)
|
||||||
|
|
||||||
|
find_program(XXD_EXECUTABLE xxd)
|
||||||
|
if (NOT XXD_EXECUTABLE)
|
||||||
|
message(FATAL_ERROR "xxd not found; required to embed PSF fonts")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(_cardboy_font_generated_sources "")
|
||||||
|
foreach(psf_file IN LISTS PSF_FONT_FILES)
|
||||||
|
get_filename_component(psf_name "${psf_file}" NAME)
|
||||||
|
get_filename_component(psf_stem "${psf_file}" NAME_WE)
|
||||||
|
set(psf_symbol "${psf_stem}_psf")
|
||||||
|
set(out_cpp "${CMAKE_CURRENT_BINARY_DIR}/${psf_name}.cpp")
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${out_cpp}"
|
||||||
|
COMMAND ${XXD_EXECUTABLE} -i -n ${psf_symbol} "${psf_file}" "${out_cpp}"
|
||||||
|
DEPENDS "${psf_file}"
|
||||||
|
COMMENT "Embedding PSF font ${psf_name}"
|
||||||
|
)
|
||||||
|
set_source_files_properties("${out_cpp}" PROPERTIES GENERATED TRUE)
|
||||||
|
list(APPEND _cardboy_font_generated_sources "${out_cpp}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if (_cardboy_font_generated_sources)
|
||||||
|
target_sources(cardboy_sdk PRIVATE ${_cardboy_font_generated_sources})
|
||||||
|
endif()
|
||||||
|
|||||||
BIN
Firmware/sdk/core/assets/fonts/spleen8x16.psf
Normal file
BIN
Firmware/sdk/core/assets/fonts/spleen8x16.psf
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
8
Firmware/sdk/core/include/cardboy/gfx/builtin_fonts.hpp
Normal file
8
Firmware/sdk/core/include/cardboy/gfx/builtin_fonts.hpp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace cardboy::gfx::builtin {
|
||||||
|
|
||||||
|
void ensureRegistered();
|
||||||
|
|
||||||
|
} // namespace cardboy::gfx::builtin
|
||||||
|
|
||||||
@@ -1,30 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "cardboy/gfx/Fonts.hpp"
|
#include "cardboy/gfx/builtin_fonts.hpp"
|
||||||
|
#include "cardboy/gfx/font_repository.hpp"
|
||||||
|
|
||||||
#include <array>
|
#include <cstddef>
|
||||||
#include <cctype>
|
#include <cstdint>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <uni_algo/ranges.h>
|
||||||
|
|
||||||
|
#include "uni_algo/ranges_conv.h"
|
||||||
|
|
||||||
namespace font16x8 {
|
namespace font16x8 {
|
||||||
|
|
||||||
constexpr int kGlyphWidth = 8;
|
constexpr int kGlyphWidth = 8;
|
||||||
constexpr int kGlyphHeight = 16;
|
constexpr int kGlyphHeight = 16;
|
||||||
constexpr unsigned char kFallbackChar = '?';
|
constexpr std::uint32_t kFallbackCodepoint = static_cast<std::uint32_t>('?');
|
||||||
|
|
||||||
inline unsigned char normalizeChar(char ch) {
|
|
||||||
unsigned char uc = static_cast<unsigned char>(ch);
|
|
||||||
if (uc >= 'a' && uc <= 'z')
|
|
||||||
uc = static_cast<unsigned char>(std::toupper(static_cast<unsigned char>(uc)));
|
|
||||||
if (!std::isprint(static_cast<unsigned char>(uc)))
|
|
||||||
return kFallbackChar;
|
|
||||||
return uc;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const std::array<uint8_t, kGlyphHeight>& glyphBitmap(char ch) {
|
|
||||||
unsigned char uc = normalizeChar(ch);
|
|
||||||
return fonts_Terminess_Powerline[uc];
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Rotation {
|
enum class Rotation {
|
||||||
None,
|
None,
|
||||||
@@ -37,22 +28,86 @@ struct TextBounds {
|
|||||||
int height = 0;
|
int height = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
inline const cardboy::gfx::FontRecord* defaultFontRecord() {
|
||||||
|
using cardboy::gfx::FontRepository;
|
||||||
|
using cardboy::gfx::builtin::ensureRegistered;
|
||||||
|
static const cardboy::gfx::FontRecord* record = []() -> const cardboy::gfx::FontRecord* {
|
||||||
|
ensureRegistered();
|
||||||
|
auto& repo = FontRepository::instance();
|
||||||
|
if (const auto* spleen = repo.getFont("Spleen 8x16"))
|
||||||
|
return spleen;
|
||||||
|
return repo.getFont("Terminess 8x16");
|
||||||
|
}();
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const cardboy::gfx::ParsedPsfFont* defaultFont() {
|
||||||
|
const auto* record = defaultFontRecord();
|
||||||
|
if (!record)
|
||||||
|
return nullptr;
|
||||||
|
if (!record->font.valid)
|
||||||
|
return nullptr;
|
||||||
|
return &record->font;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::uint32_t bytesPerRow(const cardboy::gfx::ParsedPsfFont& font) { return (font.view.width + 7u) / 8u; }
|
||||||
|
|
||||||
|
inline std::size_t countCodepoints(std::string_view text) {
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (auto cp: una::ranges::utf8_view{text}) {
|
||||||
|
(void) cp;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int fontGlyphWidth() {
|
||||||
|
const auto* font = defaultFont();
|
||||||
|
if (!font || font->view.width == 0)
|
||||||
|
return kGlyphWidth;
|
||||||
|
return static_cast<int>(font->view.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int fontGlyphHeight() {
|
||||||
|
const auto* font = defaultFont();
|
||||||
|
if (!font || font->view.height == 0)
|
||||||
|
return kGlyphHeight;
|
||||||
|
return static_cast<int>(font->view.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
template<typename Framebuffer>
|
template<typename Framebuffer>
|
||||||
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true,
|
inline void drawGlyphCodepoint(Framebuffer& fb, int x, int y, std::uint32_t codepoint, int scale = 1, bool on = true,
|
||||||
Rotation rotation = Rotation::None) {
|
Rotation rotation = Rotation::None) {
|
||||||
const auto& rows = glyphBitmap(ch);
|
const auto* font = detail::defaultFont();
|
||||||
if (rotation == Rotation::None && scale == 1 && on && ((x % 8) == 0)) {
|
if (!font)
|
||||||
for (int row = 0; row < kGlyphHeight; ++row) {
|
return;
|
||||||
const uint8_t rowBits = rows[row];
|
const auto glyphIndex = cardboy::gfx::glyphIndexForCodepoint(*font, codepoint);
|
||||||
|
const auto* glyph = font->view.glyphPointer(glyphIndex);
|
||||||
|
if (!glyph)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int width = detail::fontGlyphWidth();
|
||||||
|
const int height = detail::fontGlyphHeight();
|
||||||
|
const std::uint32_t rowStride = detail::bytesPerRow(*font);
|
||||||
|
|
||||||
|
if (rotation == Rotation::None && scale == 1 && on && rowStride == 1 && ((x % 8) == 0)) {
|
||||||
|
for (int row = 0; row < height; ++row) {
|
||||||
|
const std::uint8_t rowBits = glyph[row];
|
||||||
fb.drawBits8(x, y + row, rowBits);
|
fb.drawBits8(x, y + row, rowBits);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int row = 0; row < kGlyphHeight; ++row) {
|
|
||||||
const uint8_t rowBits = rows[row];
|
for (int row = 0; row < height; ++row) {
|
||||||
for (int col = 0; col < kGlyphWidth; ++col) {
|
const std::uint8_t* rowData = glyph + row * rowStride;
|
||||||
const uint8_t mask = static_cast<uint8_t>(1u << (kGlyphWidth - 1 - col));
|
for (int col = 0; col < width; ++col) {
|
||||||
if ((rowBits & mask) == 0)
|
const std::uint8_t byte = rowData[col / 8];
|
||||||
|
const std::uint8_t mask = static_cast<std::uint8_t>(0x80u >> (col % 8));
|
||||||
|
if ((byte & mask) == 0)
|
||||||
continue;
|
continue;
|
||||||
for (int sx = 0; sx < scale; ++sx) {
|
for (int sx = 0; sx < scale; ++sx) {
|
||||||
for (int sy = 0; sy < scale; ++sy) {
|
for (int sy = 0; sy < scale; ++sy) {
|
||||||
@@ -65,10 +120,10 @@ inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, boo
|
|||||||
break;
|
break;
|
||||||
case Rotation::Clockwise90:
|
case Rotation::Clockwise90:
|
||||||
dstX += row * scale + sx;
|
dstX += row * scale + sx;
|
||||||
dstY += (kGlyphWidth - 1 - col) * scale + sy;
|
dstY += (width - 1 - col) * scale + sy;
|
||||||
break;
|
break;
|
||||||
case Rotation::CounterClockwise90:
|
case Rotation::CounterClockwise90:
|
||||||
dstX += (kGlyphHeight - 1 - row) * scale + sx;
|
dstX += (height - 1 - row) * scale + sx;
|
||||||
dstY += col * scale + sy;
|
dstY += col * scale + sy;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -79,13 +134,25 @@ inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename Framebuffer>
|
||||||
|
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true,
|
||||||
|
Rotation rotation = Rotation::None) {
|
||||||
|
const auto codepoint = static_cast<std::uint32_t>(static_cast<unsigned char>(ch));
|
||||||
|
drawGlyphCodepoint(fb, x, y, codepoint, scale, on, rotation);
|
||||||
|
}
|
||||||
|
|
||||||
inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int letterSpacing = 1,
|
inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int letterSpacing = 1,
|
||||||
Rotation rotation = Rotation::None) {
|
Rotation rotation = Rotation::None) {
|
||||||
if (text.empty())
|
if (text.empty())
|
||||||
return {};
|
return {};
|
||||||
const int advance = (kGlyphWidth + letterSpacing) * scale;
|
const std::size_t glyphCount = detail::countCodepoints(text);
|
||||||
const int extent = static_cast<int>(text.size()) * advance - letterSpacing * scale;
|
if (glyphCount == 0)
|
||||||
const int height = kGlyphHeight * scale;
|
return {};
|
||||||
|
const int glyphWidth = detail::fontGlyphWidth();
|
||||||
|
const int glyphHeight = detail::fontGlyphHeight();
|
||||||
|
const int advance = (glyphWidth + letterSpacing) * scale;
|
||||||
|
const int extent = static_cast<int>(glyphCount) * advance - letterSpacing * scale;
|
||||||
|
const int height = glyphHeight * scale;
|
||||||
switch (rotation) {
|
switch (rotation) {
|
||||||
case Rotation::None:
|
case Rotation::None:
|
||||||
return {extent, height};
|
return {extent, height};
|
||||||
@@ -99,8 +166,12 @@ inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int le
|
|||||||
inline int measureText(std::string_view text, int scale = 1, int letterSpacing = 1) {
|
inline int measureText(std::string_view text, int scale = 1, int letterSpacing = 1) {
|
||||||
if (text.empty())
|
if (text.empty())
|
||||||
return 0;
|
return 0;
|
||||||
const int advance = (kGlyphWidth + letterSpacing) * scale;
|
const std::size_t glyphCount = detail::countCodepoints(text);
|
||||||
return static_cast<int>(text.size()) * advance - letterSpacing * scale;
|
if (glyphCount == 0)
|
||||||
|
return 0;
|
||||||
|
const int glyphWidth = detail::fontGlyphWidth();
|
||||||
|
const int advance = (glyphWidth + letterSpacing) * scale;
|
||||||
|
return static_cast<int>(glyphCount) * advance - letterSpacing * scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename Framebuffer>
|
template<typename Framebuffer>
|
||||||
@@ -108,17 +179,21 @@ inline void drawText(Framebuffer& fb, int x, int y, std::string_view text, int s
|
|||||||
int letterSpacing = 1, Rotation rotation = Rotation::None) {
|
int letterSpacing = 1, Rotation rotation = Rotation::None) {
|
||||||
if (text.empty())
|
if (text.empty())
|
||||||
return;
|
return;
|
||||||
const int advance = (kGlyphWidth + letterSpacing) * scale;
|
const int glyphWidth = detail::fontGlyphWidth();
|
||||||
|
const int advance = (glyphWidth + letterSpacing) * scale;
|
||||||
|
|
||||||
if (rotation == Rotation::None) {
|
if (rotation == Rotation::None) {
|
||||||
int cursor = x;
|
int cursor = x;
|
||||||
for (char ch: text) {
|
for (auto cp: una::ranges::utf8_view{text}) {
|
||||||
drawGlyph(fb, cursor, y, ch, scale, on, rotation);
|
const auto codepoint = static_cast<std::uint32_t>(cp);
|
||||||
|
drawGlyphCodepoint(fb, cursor, y, codepoint, scale, on, rotation);
|
||||||
cursor += advance;
|
cursor += advance;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int cursor = y;
|
int cursor = y;
|
||||||
for (char ch: text) {
|
for (auto cp: una::ranges::utf8_view{text}) {
|
||||||
drawGlyph(fb, x, cursor, ch, scale, on, rotation);
|
const auto codepoint = static_cast<std::uint32_t>(cp);
|
||||||
|
drawGlyphCodepoint(fb, x, cursor, codepoint, scale, on, rotation);
|
||||||
cursor += advance;
|
cursor += advance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
Firmware/sdk/core/include/cardboy/gfx/font_repository.hpp
Normal file
50
Firmware/sdk/core/include/cardboy/gfx/font_repository.hpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cardboy/gfx/psf_font.hpp"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace cardboy::gfx {
|
||||||
|
|
||||||
|
struct FontInfo {
|
||||||
|
std::string_view name;
|
||||||
|
const std::uint8_t* data;
|
||||||
|
std::size_t size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FontRecord {
|
||||||
|
std::string name;
|
||||||
|
const std::uint8_t* data = nullptr;
|
||||||
|
std::size_t size = 0;
|
||||||
|
ParsedPsfFont font;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FontRepository {
|
||||||
|
public:
|
||||||
|
static FontRepository& instance();
|
||||||
|
|
||||||
|
const FontRecord* addFont(std::string_view name, const std::uint8_t* data, std::size_t size);
|
||||||
|
const FontRecord* getFont(std::string_view name) const;
|
||||||
|
std::vector<FontInfo> listFonts() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FontRepository() = default;
|
||||||
|
~FontRepository() = default;
|
||||||
|
|
||||||
|
FontRepository(const FontRepository&) = delete;
|
||||||
|
FontRepository& operator=(const FontRepository&) = delete;
|
||||||
|
|
||||||
|
const FontRecord* addOrUpdateLocked(std::string_view name, const std::uint8_t* data, std::size_t size);
|
||||||
|
|
||||||
|
mutable std::mutex mutex_;
|
||||||
|
std::vector<std::unique_ptr<FontRecord>> fonts_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace cardboy::gfx
|
||||||
48
Firmware/sdk/core/include/cardboy/gfx/psf_font.hpp
Normal file
48
Firmware/sdk/core/include/cardboy/gfx/psf_font.hpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace cardboy::gfx {
|
||||||
|
|
||||||
|
enum class PsfFormat { Unknown, Psf1, Psf2 };
|
||||||
|
|
||||||
|
struct PsfFontView {
|
||||||
|
PsfFormat format = PsfFormat::Unknown;
|
||||||
|
std::span<const std::uint8_t> blob = {};
|
||||||
|
std::span<const std::uint8_t> glyphs = {};
|
||||||
|
std::span<const std::uint8_t> unicode = {};
|
||||||
|
std::uint32_t glyphCount = 0;
|
||||||
|
std::uint32_t bytesPerGlyph = 0;
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
bool hasUnicode = false;
|
||||||
|
|
||||||
|
const std::uint8_t* glyphPointer(std::uint32_t index) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PsfUnicodeEntry {
|
||||||
|
std::uint32_t codepoint = 0;
|
||||||
|
std::uint32_t glyphIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedPsfFont {
|
||||||
|
PsfFontView view;
|
||||||
|
std::vector<PsfUnicodeEntry> unicodeEntries;
|
||||||
|
std::unordered_map<std::uint32_t, std::uint32_t> unicodeToGlyph;
|
||||||
|
std::uint32_t defaultGlyphIndex = 0;
|
||||||
|
bool valid = false;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
ParsedPsfFont parsePsfFont(std::span<const std::uint8_t> blob);
|
||||||
|
|
||||||
|
std::uint32_t glyphIndexForCodepoint(const ParsedPsfFont& font, std::uint32_t codepoint);
|
||||||
|
|
||||||
|
} // namespace cardboy::gfx
|
||||||
|
|
||||||
@@ -56,9 +56,6 @@ private:
|
|||||||
fb.drawPixel(x, y, true);
|
fb.drawPixel(x, y, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int x = 0; x < width; ++x)
|
|
||||||
fb.drawPixel(x, 0, false);
|
|
||||||
|
|
||||||
const int textY = 1;
|
const int textY = 1;
|
||||||
const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1;
|
const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1;
|
||||||
if (bottomSeparatorY < fillHeight) {
|
if (bottomSeparatorY < fillHeight) {
|
||||||
|
|||||||
31
Firmware/sdk/core/src/builtin_fonts.cpp
Normal file
31
Firmware/sdk/core/src/builtin_fonts.cpp
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#include "cardboy/gfx/builtin_fonts.hpp"
|
||||||
|
|
||||||
|
#include "cardboy/gfx/font_repository.hpp"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
extern unsigned char spleen8x16_psf[];
|
||||||
|
extern unsigned int spleen8x16_psf_len;
|
||||||
|
|
||||||
|
namespace cardboy::gfx::builtin {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void registerFonts() {
|
||||||
|
static bool registered = false;
|
||||||
|
if (registered)
|
||||||
|
return;
|
||||||
|
registered = true;
|
||||||
|
const auto* spleenData = reinterpret_cast<const std::uint8_t*>(spleen8x16_psf);
|
||||||
|
const std::size_t spleenSize = static_cast<std::size_t>(spleen8x16_psf_len);
|
||||||
|
FontRepository::instance().addFont("Spleen 8x16", spleenData, spleenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ensureRegistered() {
|
||||||
|
registerFonts();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cardboy::gfx::builtin
|
||||||
66
Firmware/sdk/core/src/font_repository.cpp
Normal file
66
Firmware/sdk/core/src/font_repository.cpp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#include "cardboy/gfx/font_repository.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace cardboy::gfx {
|
||||||
|
|
||||||
|
FontRepository& FontRepository::instance() {
|
||||||
|
static FontRepository repository;
|
||||||
|
return repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FontRecord* FontRepository::addFont(std::string_view name, const std::uint8_t* data, std::size_t size) {
|
||||||
|
if (name.empty() || data == nullptr || size == 0)
|
||||||
|
return nullptr;
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
return addOrUpdateLocked(name, data, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FontRecord* FontRepository::getFont(std::string_view name) const {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
const auto it = std::find_if(fonts_.begin(), fonts_.end(),
|
||||||
|
[&](const std::unique_ptr<FontRecord>& record) { return record->name == name; });
|
||||||
|
if (it == fonts_.end())
|
||||||
|
return nullptr;
|
||||||
|
return it->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FontInfo> FontRepository::listFonts() const {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
std::vector<FontInfo> result;
|
||||||
|
result.reserve(fonts_.size());
|
||||||
|
for (const auto& record: fonts_) {
|
||||||
|
result.push_back(FontInfo{
|
||||||
|
.name = record->name,
|
||||||
|
.data = record->data,
|
||||||
|
.size = record->size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FontRecord* FontRepository::addOrUpdateLocked(std::string_view name,
|
||||||
|
const std::uint8_t* data,
|
||||||
|
std::size_t size) {
|
||||||
|
auto it = std::find_if(fonts_.begin(), fonts_.end(),
|
||||||
|
[&](const std::unique_ptr<FontRecord>& record) { return record->name == name; });
|
||||||
|
if (it == fonts_.end()) {
|
||||||
|
auto record = std::make_unique<FontRecord>();
|
||||||
|
record->name = std::string{name};
|
||||||
|
record->data = data;
|
||||||
|
record->size = size;
|
||||||
|
record->font = parsePsfFont(std::span<const std::uint8_t>(data, size));
|
||||||
|
fonts_.push_back(std::move(record));
|
||||||
|
it = std::prev(fonts_.end());
|
||||||
|
} else {
|
||||||
|
(*it)->data = data;
|
||||||
|
(*it)->size = size;
|
||||||
|
(*it)->font = parsePsfFont(std::span<const std::uint8_t>(data, size));
|
||||||
|
}
|
||||||
|
if (!(*it)->font.valid) {
|
||||||
|
(*it)->font.error = "Failed to parse PSF font";
|
||||||
|
}
|
||||||
|
return it->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cardboy::gfx
|
||||||
211
Firmware/sdk/core/src/psf_font.cpp
Normal file
211
Firmware/sdk/core/src/psf_font.cpp
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
#include "cardboy/gfx/psf_font.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
|
namespace cardboy::gfx {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::uint8_t kPsf1Magic0 = 0x36;
|
||||||
|
constexpr std::uint8_t kPsf1Magic1 = 0x04;
|
||||||
|
|
||||||
|
constexpr std::uint8_t kPsf1Mode512 = 0x01;
|
||||||
|
constexpr std::uint8_t kPsf1ModeHasTab = 0x02;
|
||||||
|
|
||||||
|
constexpr std::uint16_t kPsf1Separator = 0xFFFF;
|
||||||
|
constexpr std::uint16_t kPsf1StartSeq = 0xFFFE;
|
||||||
|
|
||||||
|
constexpr std::uint32_t kPsf2Magic = 0x864ab572;
|
||||||
|
constexpr std::uint8_t kPsf2Separator = 0xFF;
|
||||||
|
constexpr std::uint8_t kPsf2StartSeq = 0xFE;
|
||||||
|
|
||||||
|
struct Psf1Header {
|
||||||
|
std::uint8_t magic[2];
|
||||||
|
std::uint8_t mode;
|
||||||
|
std::uint8_t charsize;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Psf2Header {
|
||||||
|
std::uint32_t magic;
|
||||||
|
std::uint32_t version;
|
||||||
|
std::uint32_t headerSize;
|
||||||
|
std::uint32_t flags;
|
||||||
|
std::uint32_t length;
|
||||||
|
std::uint32_t charSize;
|
||||||
|
std::uint32_t height;
|
||||||
|
std::uint32_t width;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename It>
|
||||||
|
std::uint32_t decodeUtf8(It& it, const It end) {
|
||||||
|
if (it == end)
|
||||||
|
return 0xFFFFFFFFu;
|
||||||
|
const std::uint8_t first = *it++;
|
||||||
|
if ((first & 0x80u) == 0)
|
||||||
|
return first;
|
||||||
|
if ((first & 0xE0u) == 0xC0u) {
|
||||||
|
if (it == end)
|
||||||
|
return 0xFFFFFFFFu;
|
||||||
|
const std::uint8_t b1 = *it++;
|
||||||
|
return ((first & 0x1Fu) << 6u) | (b1 & 0x3Fu);
|
||||||
|
}
|
||||||
|
if ((first & 0xF0u) == 0xE0u) {
|
||||||
|
if (std::distance(it, end) < 2)
|
||||||
|
return 0xFFFFFFFFu;
|
||||||
|
const std::uint8_t b1 = *it++;
|
||||||
|
const std::uint8_t b2 = *it++;
|
||||||
|
return ((first & 0x0Fu) << 12u) | ((b1 & 0x3Fu) << 6u) | (b2 & 0x3Fu);
|
||||||
|
}
|
||||||
|
if ((first & 0xF8u) == 0xF0u) {
|
||||||
|
if (std::distance(it, end) < 3)
|
||||||
|
return 0xFFFFFFFFu;
|
||||||
|
const std::uint8_t b1 = *it++;
|
||||||
|
const std::uint8_t b2 = *it++;
|
||||||
|
const std::uint8_t b3 = *it++;
|
||||||
|
return ((first & 0x07u) << 18u) | ((b1 & 0x3Fu) << 12u) | ((b2 & 0x3Fu) << 6u) | (b3 & 0x3Fu);
|
||||||
|
}
|
||||||
|
return 0xFFFFFFFFu;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const std::uint8_t* PsfFontView::glyphPointer(std::uint32_t index) const {
|
||||||
|
if (glyphs.empty() || index >= glyphCount)
|
||||||
|
return nullptr;
|
||||||
|
const std::size_t offset = static_cast<std::size_t>(index) * bytesPerGlyph;
|
||||||
|
if (offset + bytesPerGlyph > glyphs.size())
|
||||||
|
return nullptr;
|
||||||
|
return glyphs.data() + offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsedPsfFont parsePsfFont(std::span<const std::uint8_t> blob) {
|
||||||
|
ParsedPsfFont parsed;
|
||||||
|
parsed.view.blob = blob;
|
||||||
|
if (blob.size() < sizeof(Psf1Header))
|
||||||
|
return parsed;
|
||||||
|
|
||||||
|
const auto* psf1 = reinterpret_cast<const Psf1Header*>(blob.data());
|
||||||
|
if (psf1->magic[0] == kPsf1Magic0 && psf1->magic[1] == kPsf1Magic1) {
|
||||||
|
parsed.view.format = PsfFormat::Psf1;
|
||||||
|
const std::uint32_t count = (psf1->mode & kPsf1Mode512) ? 512u : 256u;
|
||||||
|
const std::uint32_t glyphBytes = static_cast<std::uint32_t>(psf1->charsize);
|
||||||
|
if (blob.size() < sizeof(Psf1Header) + static_cast<std::size_t>(count) * glyphBytes)
|
||||||
|
return parsed;
|
||||||
|
parsed.view.glyphCount = count;
|
||||||
|
parsed.view.bytesPerGlyph = glyphBytes;
|
||||||
|
parsed.view.width = 8;
|
||||||
|
parsed.view.height = glyphBytes;
|
||||||
|
parsed.view.glyphs = blob.subspan(sizeof(Psf1Header), static_cast<std::size_t>(count) * glyphBytes);
|
||||||
|
parsed.view.hasUnicode = (psf1->mode & kPsf1ModeHasTab) != 0;
|
||||||
|
if (parsed.view.hasUnicode && parsed.view.glyphs.size() < blob.size())
|
||||||
|
parsed.view.unicode = blob.subspan(sizeof(Psf1Header) + parsed.view.glyphs.size());
|
||||||
|
} else if (blob.size() >= sizeof(Psf2Header)) {
|
||||||
|
const auto* psf2 = reinterpret_cast<const Psf2Header*>(blob.data());
|
||||||
|
if (psf2->magic != kPsf2Magic)
|
||||||
|
return parsed;
|
||||||
|
parsed.view.format = PsfFormat::Psf2;
|
||||||
|
parsed.view.glyphCount = psf2->length;
|
||||||
|
parsed.view.bytesPerGlyph = psf2->charSize;
|
||||||
|
parsed.view.width = psf2->width;
|
||||||
|
parsed.view.height = psf2->height;
|
||||||
|
const std::size_t glyphOffset = psf2->headerSize;
|
||||||
|
const std::size_t glyphBytes = static_cast<std::size_t>(psf2->length) * psf2->charSize;
|
||||||
|
if (blob.size() < glyphOffset + glyphBytes)
|
||||||
|
return parsed;
|
||||||
|
parsed.view.glyphs = blob.subspan(glyphOffset, glyphBytes);
|
||||||
|
parsed.view.hasUnicode = (psf2->flags & 0x01u) != 0;
|
||||||
|
if (parsed.view.hasUnicode && glyphOffset + glyphBytes < blob.size())
|
||||||
|
parsed.view.unicode = blob.subspan(glyphOffset + glyphBytes);
|
||||||
|
} else {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.valid = true;
|
||||||
|
|
||||||
|
// Build Unicode mapping if available.
|
||||||
|
if (parsed.view.hasUnicode && !parsed.view.unicode.empty()) {
|
||||||
|
parsed.unicodeEntries.reserve(parsed.view.glyphCount);
|
||||||
|
parsed.unicodeToGlyph.reserve(parsed.view.glyphCount);
|
||||||
|
|
||||||
|
if (parsed.view.format == PsfFormat::Psf1) {
|
||||||
|
const auto* data = parsed.view.unicode.data();
|
||||||
|
std::size_t remaining = parsed.view.unicode.size();
|
||||||
|
for (std::uint32_t glyph = 0; glyph < parsed.view.glyphCount && remaining >= sizeof(std::uint16_t); ++glyph) {
|
||||||
|
bool inSequence = false;
|
||||||
|
while (remaining >= sizeof(std::uint16_t)) {
|
||||||
|
const std::uint16_t low = static_cast<std::uint16_t>(data[0]);
|
||||||
|
const std::uint16_t high = static_cast<std::uint16_t>(data[1]) << 8u;
|
||||||
|
const std::uint16_t value = static_cast<std::uint16_t>(low | high);
|
||||||
|
data += sizeof(std::uint16_t);
|
||||||
|
remaining -= sizeof(std::uint16_t);
|
||||||
|
if (value == kPsf1Separator) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (value == kPsf1StartSeq) {
|
||||||
|
inSequence = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inSequence) {
|
||||||
|
parsed.unicodeEntries.push_back({value, glyph});
|
||||||
|
parsed.unicodeToGlyph.emplace(value, glyph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (parsed.view.format == PsfFormat::Psf2) {
|
||||||
|
auto begin = parsed.view.unicode.begin();
|
||||||
|
auto end = parsed.view.unicode.end();
|
||||||
|
for (std::uint32_t glyph = 0; glyph < parsed.view.glyphCount && begin != end; ++glyph) {
|
||||||
|
bool inSequence = false;
|
||||||
|
while (begin != end) {
|
||||||
|
const std::uint8_t byte = *begin;
|
||||||
|
if (byte == kPsf2Separator) {
|
||||||
|
++begin;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (byte == kPsf2StartSeq) {
|
||||||
|
inSequence = true;
|
||||||
|
++begin;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto it = begin;
|
||||||
|
const std::uint32_t codepoint = decodeUtf8(it, end);
|
||||||
|
if (codepoint == 0xFFFFFFFFu) {
|
||||||
|
begin = end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
begin = it;
|
||||||
|
if (!inSequence) {
|
||||||
|
parsed.unicodeEntries.push_back({codepoint, glyph});
|
||||||
|
parsed.unicodeToGlyph.emplace(codepoint, glyph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.unicodeToGlyph.empty()) {
|
||||||
|
for (std::uint32_t glyph = 0; glyph < parsed.view.glyphCount; ++glyph) {
|
||||||
|
parsed.unicodeToGlyph.emplace(glyph, glyph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose '?' as fallback if present.
|
||||||
|
const auto it = parsed.unicodeToGlyph.find(static_cast<std::uint32_t>('?'));
|
||||||
|
if (it != parsed.unicodeToGlyph.end())
|
||||||
|
parsed.defaultGlyphIndex = it->second;
|
||||||
|
else
|
||||||
|
parsed.defaultGlyphIndex = 0;
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t glyphIndexForCodepoint(const ParsedPsfFont& font, std::uint32_t codepoint) {
|
||||||
|
const auto it = font.unicodeToGlyph.find(codepoint);
|
||||||
|
if (it != font.unicodeToGlyph.end())
|
||||||
|
return it->second;
|
||||||
|
return font.defaultGlyphIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cardboy::gfx
|
||||||
@@ -50,16 +50,12 @@ std::string StatusBar::prepareRightText() const {
|
|||||||
|
|
||||||
std::string right;
|
std::string right;
|
||||||
if (_services->battery && _services->battery->hasData()) {
|
if (_services->battery && _services->battery->hasData()) {
|
||||||
const float current = _services->battery->current();
|
const float current = _services->battery->current();
|
||||||
const float chargeMah = _services->battery->charge();
|
const float chargeMah = _services->battery->charge();
|
||||||
const float fallbackV = _services->battery->voltage();
|
const float percentage = _services->battery->percentage();
|
||||||
char buf[64];
|
char buf[64];
|
||||||
if (std::isfinite(current) && std::isfinite(chargeMah)) {
|
std::snprintf(buf, sizeof(buf), "%.2fmA %.2fmAh %.0f%%", static_cast<double>(current),
|
||||||
std::snprintf(buf, sizeof(buf), "cur %.2fmA chr %.2fmAh", static_cast<double>(current),
|
static_cast<double>(chargeMah), static_cast<double>(percentage));
|
||||||
static_cast<double>(chargeMah));
|
|
||||||
} else {
|
|
||||||
std::snprintf(buf, sizeof(buf), "vol %.2fV", static_cast<double>(fallbackV));
|
|
||||||
}
|
|
||||||
right.assign(buf);
|
right.assign(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user