feat(lockscreen): add customizable lockscreen shown on wake/power-on

This commit is contained in:
justin committed 2026-04-09 17:20:48 -07:00
1 parent 4c32a0b950
commit 26f9b6b20c
7 files changed
+921

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
#include "LockscreenActivity.h"
#include <GfxRenderer.h>
#include <Logging.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "activities/settings/LockscreenPlugin.h"
#include "components/UITheme.h"
#include "fontIds.h"
static constexpr int SCREEN_W = 480;
static constexpr int KEY_W = 130;
static constexpr int KEY_H = 80;
static constexpr int KEY_PAD = 8;
static constexpr int GRID_W = LockscreenActivity::COLS * (KEY_W + KEY_PAD) - KEY_PAD;
static constexpr int GRID_X = (SCREEN_W - GRID_W) / 2;
void LockscreenActivity::onEnter() {
Activity::onEnter();
success_ = false;
done_ = false;
renderPending_ = true;
pinLen_ = 0;
attempts_ = 0;
cursorRow_ = 0;
cursorCol_ = 0;
memset(pin_, 0, sizeof(pin_));
enterTime_ = millis();
requestUpdate();
}
void LockscreenActivity::onExit() {
Activity::onExit();
}
void LockscreenActivity::goToSleep() {
done_ = true;
success_ = false;
}
void LockscreenActivity::pressCurrentKey() {
const char* key = KEYS[cursorRow_][cursorCol_];
if (strcmp(key, "<") == 0) {
if (pinLen_ > 0) {
pinLen_--;
pin_[pinLen_] = '\0';
}
renderPending_ = true;
requestUpdate();
return;
}
if (strcmp(key, "OK") == 0) {
if (pinLen_ < PIN_LEN) return;
if (purpose_ == Purpose::CREATE) {
LockscreenPlugin::hashPin(pin_, SETTINGS.lockscreenPinHash);
SETTINGS.saveToFile();
success_ = true;
done_ = true;
finish();
} else {
if (LockscreenPlugin::checkPin(pin_, SETTINGS.lockscreenPinHash)) {
success_ = true;
done_ = true;
} else {
attempts_++;
pinLen_ = 0;
memset(pin_, 0, sizeof(pin_));
renderPending_ = true;
requestUpdate();
if (attempts_ >= MAX_ATTEMPTS) {
goToSleep();
}
}
}
return;
}
if (pinLen_ < PIN_LEN) {
pin_[pinLen_++] = key[0];
pin_[pinLen_] = '\0';
if (pinLen_ == PIN_LEN) {
cursorRow_ = 3;
cursorCol_ = 2;
}
renderPending_ = true;
requestUpdate();
}
}
void LockscreenActivity::renderPinDisplay(int startY, bool masked) const {
constexpr int BOX_W = 50;
constexpr int BOX_H = 60;
constexpr int BOX_PAD = 12;
constexpr int TOTAL_W = 4 * BOX_W + 3 * BOX_PAD;
int x = (SCREEN_W - TOTAL_W) / 2;
for (int i = 0; i < PIN_LEN; i++) {
int bx = x + i * (BOX_W + BOX_PAD);
renderer.drawRect(bx, startY, BOX_W, BOX_H);
if (i < pinLen_) {
char display[2] = {masked ? '*' : pin_[i], '\0'};
renderer.drawText(UI_12_FONT_ID, bx + BOX_W / 2 - 8, startY + BOX_H / 2 - 10, display);
}
}
}
void LockscreenActivity::renderKeypad(int startY) const {
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
int kx = GRID_X + c * (KEY_W + KEY_PAD);
int ky = startY + r * (KEY_H + KEY_PAD);
bool isCursor = (r == cursorRow_ && c == cursorCol_);
if (isCursor) {
renderer.fillRect(kx, ky, KEY_W, KEY_H);
renderer.drawText(UI_12_FONT_ID, kx + KEY_W / 2 - 8, ky + KEY_H / 2 - 10,
KEYS[r][c], false, EpdFontFamily::BOLD);
} else {
renderer.drawRect(kx, ky, KEY_W, KEY_H);
renderer.drawText(UI_12_FONT_ID, kx + KEY_W / 2 - 8, ky + KEY_H / 2 - 10,
KEYS[r][c], true, EpdFontFamily::REGULAR);
}
}
}
}
void LockscreenActivity::render(RenderLock&&) {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageH = renderer.getScreenHeight();
const bool isCreate = (purpose_ == Purpose::CREATE);
const bool masked = !isCreate;
renderer.clearScreen();
const char* title = isCreate ? "Set PIN" : "Unlock";
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, SCREEN_W, metrics.headerHeight}, title);
int y = metrics.topPadding + metrics.headerHeight + 80;
renderPinDisplay(y, masked);
y += 90;
renderKeypad(y);
if (isCreate) {
renderer.drawCenteredText(UI_10_FONT_ID, pageH - 40, "Navigate: Directional buttons | Select: Power");
} else if (attempts_ > 0) {
char msg[40];
snprintf(msg, sizeof(msg), "Incorrect PIN. %d attempt(s) remaining.", MAX_ATTEMPTS - attempts_);
renderer.drawCenteredText(UI_10_FONT_ID, pageH - 40, msg);
}
renderer.displayBuffer();
}
void LockscreenActivity::loop() {
if (purpose_ == Purpose::UNLOCK) {
if (millis() - enterTime_ > TIMEOUT_MS) {
goToSleep();
return;
}
}
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
cursorRow_ = (cursorRow_ - 1 + ROWS) % ROWS;
renderPending_ = true;
requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
cursorRow_ = (cursorRow_ + 1) % ROWS;
renderPending_ = true;
requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
cursorCol_ = (cursorCol_ - 1 + COLS) % COLS;
renderPending_ = true;
requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
cursorCol_ = (cursorCol_ + 1) % COLS;
renderPending_ = true;
requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Power) ||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
pressCurrentKey();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
if (pinLen_ > 0) {
pinLen_--;
pin_[pinLen_] = '\0';
renderPending_ = true;
requestUpdate();
}
}
}
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include "activities/settings/LockscreenPlugin.h"
#include "activities/Activity.h"
class LockscreenActivity final : public Activity {
public:
enum class Purpose { UNLOCK, CREATE };
static constexpr int COLS = 3;
static constexpr int ROWS = 4;
explicit LockscreenActivity(GfxRenderer& renderer,
MappedInputManager& mappedInput,
Purpose purpose)
: Activity("Lockscreen", renderer, mappedInput), purpose_(purpose) {}
void onEnter() override;
void onExit() override;
void loop() override;
bool skipLoopDelay() override { return true; }
void render(RenderLock&&) override;
bool wasSuccessful() const { return success_; }
bool isDone() const { return done_; }
bool needsRender() const { return renderPending_; }
void renderDirect() {
renderPending_ = false;
RenderLock rl(*this);
render(std::move(rl));
}
private:
static constexpr int PIN_LEN = 4;
static constexpr unsigned long TIMEOUT_MS = 120000;
static constexpr const char* KEYS[ROWS][COLS] = {
{"1", "2", "3"},
{"4", "5", "6"},
{"7", "8", "9"},
{"<", "0", "OK"},
};
Purpose purpose_;
bool success_ = false;
bool done_ = false;
bool renderPending_ = false;
int cursorRow_ = 0;
int cursorCol_ = 0;
char pin_[PIN_LEN + 1] = {};
int pinLen_ = 0;
int attempts_ = 0;
static constexpr int MAX_ATTEMPTS = 3;
unsigned long enterTime_ = 0;
void pressCurrentKey();
void renderPinDisplay(int startY, bool masked) const;
void renderKeypad(int startY) const;
void goToSleep();
};
+40
View File
@@ -0,0 +1,40 @@
#include "LockscreenPlugin.h"
namespace LockscreenPlugin {
const char* modeName(LockscreenMode mode) {
switch (mode) {
case LockscreenMode::ENABLED: return "Enabled";
default: return "Disabled";
}
}
bool shouldLock(LockscreenMode mode) {
return mode == LockscreenMode::ENABLED;
}
bool shouldLockOnWake(LockscreenMode mode) {
return shouldLock(mode);
}
bool shouldLockOnPower(LockscreenMode mode) {
return shouldLock(mode);
}
void hashPin(const char* pin, char outHash[9]) {
uint32_t hash = 5381;
for (const char* p = pin; *p; p++) {
hash = ((hash << 5) + hash) ^ static_cast<uint8_t>(*p);
}
hash ^= 0xA3F1C2B4u;
snprintf(outHash, 9, "%08X", hash);
}
bool checkPin(const char* pin, const char* storedHash) {
if (!pin || !storedHash || storedHash[0] == '\0') return false;
char computed[9];
hashPin(pin, computed);
return strncmp(computed, storedHash, 9) == 0;
}
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <Arduino.h>
enum class LockscreenMode : uint8_t {
MODE_OFF = 0,
ENABLED = 1,
};
namespace LockscreenPlugin {
const char* modeName(LockscreenMode mode);
bool shouldLock(LockscreenMode mode);
bool shouldLockOnWake(LockscreenMode mode);
bool shouldLockOnPower(LockscreenMode mode);
void hashPin(const char* pin, char outHash[9]);
bool checkPin(const char* pin, const char* storedHash);
}
@@ -0,0 +1,22 @@
#include "LockscreenSettingsPage.h"
#include "fontIds.h"
void LockscreenSettingsActivity::render(GfxRenderer& renderer, LockscreenMode currentMode) {
const int LABEL_X = 10;
const int VALUE_X = 160;
const int ROW_Y = 60;
renderer.drawText(UI_12_FONT_ID, LABEL_X, ROW_Y, "Lockscreen");
renderer.drawText(UI_12_FONT_ID, VALUE_X, ROW_Y, LockscreenPlugin::modeName(currentMode));
}
LockscreenMode LockscreenSettingsActivity::handleNext(LockscreenMode currentMode) {
uint8_t v = static_cast<uint8_t>(currentMode);
if (v < 1) v++;
return static_cast<LockscreenMode>(v);
}
LockscreenMode LockscreenSettingsActivity::handlePrev(LockscreenMode currentMode) {
uint8_t v = static_cast<uint8_t>(currentMode);
if (v > 0) v--;
return static_cast<LockscreenMode>(v);
}
@@ -0,0 +1,11 @@
#pragma once
#include <GfxRenderer.h>
#include "LockscreenPlugin.h"
class LockscreenSettingsActivity {
public:
static void render(GfxRenderer& renderer, LockscreenMode currentMode);
static LockscreenMode handleNext(LockscreenMode currentMode);
static LockscreenMode handlePrev(LockscreenMode currentMode);
};
+571
View File
@@ -0,0 +1,571 @@
#!/usr/bin/env python3
import os
import sys
import glob
import shutil
def read_file(path):
with open(path, "r", encoding="utf-8") as f:
return f.read()
def write_file(path, content):
with open(path, "w", encoding="utf-8") as f:
f.write(content)
def find_first(filename, repo_dir):
results = glob.glob(os.path.join(repo_dir, "**", filename), recursive=True)
if not results:
sys.exit(f"ERROR: Could not locate {filename} in {repo_dir}")
return results[0]
def copy_plugin_sources(plugin_dir, repo_dir):
settings_dest = os.path.join(repo_dir, "src", "activities", "settings")
activities_dest = os.path.join(repo_dir, "src", "activities")
os.makedirs(settings_dest, exist_ok=True)
settings_files = {
"LockscreenSettingsPage.h", "LockscreenSettingsPage.cpp",
"LockscreenPlugin.h", "LockscreenPlugin.cpp",
}
for fname in os.listdir(plugin_dir):
if not fname.endswith((".h", ".cpp", ".hpp")):
continue
src = os.path.join(plugin_dir, fname)
dst = os.path.join(settings_dest if fname in settings_files else activities_dest, fname)
shutil.copy2(src, dst)
print(f" ✓ {fname}")
def patch_cross_point_settings_h(repo_dir):
path = find_first("CrossPointSettings.h", repo_dir)
content = read_file(path)
if "lockscreenMode" in content:
print(" CrossPointSettings.h already patched, skipping.")
return
content = content.replace(
'#pragma once\n#include <HalStorage.h>',
'#pragma once\n#include <HalStorage.h>\n#include "activities/settings/LockscreenPlugin.h"'
)
content = content.replace(
' uint8_t imageRendering = IMAGES_DISPLAY;',
' uint8_t imageRendering = IMAGES_DISPLAY;\n'
' uint8_t lockscreenMode = 0;\n'
' char lockscreenPinHash[9] = {};'
)
write_file(path, content)
print(" CrossPointSettings.h patched.")
def patch_settings_list_h(repo_dir):
path = find_first("SettingsList.h", repo_dir)
content = read_file(path)
if "lockscreenMode" in content and "lockscreenPinHash" in content:
if '{StrId::STR_NONE_OPT, StrId::STR_NONE_OPT,\n StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}' in content:
content = content.replace(
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT,\n'
' StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n',
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n'
)
write_file(path, content)
print(" SettingsList.h patched (reduced lockscreenMode to 2 options).")
else:
print(" SettingsList.h already patched, skipping.")
return
if "lockscreenMode" in content and "lockscreenPinHash" not in content:
content = content.replace(
' "lockscreenMode", StrId::STR_NONE_OPT),\n',
' "lockscreenMode", StrId::STR_NONE_OPT),\n'
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.lockscreenPinHash,\n'
' sizeof(SETTINGS.lockscreenPinHash),\n'
' "lockscreenPinHash", StrId::STR_NONE_OPT),\n'
)
write_file(path, content)
print(" SettingsList.h patched (added lockscreenPinHash).")
return
entry = (
'\n'
' SettingInfo::Enum(StrId::STR_NONE_OPT, &CrossPointSettings::lockscreenMode,\n'
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n'
' "lockscreenMode", StrId::STR_NONE_OPT),\n'
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.lockscreenPinHash,\n'
' sizeof(SETTINGS.lockscreenPinHash),\n'
' "lockscreenPinHash", StrId::STR_NONE_OPT),\n'
)
content = content.replace(
' };\n return list;\n}\n',
entry + ' };\n return list;\n}\n'
)
write_file(path, content)
print(" SettingsList.h patched.")
def patch_settings_h(repo_dir):
path = find_first("SettingsActivity.h", repo_dir)
content = read_file(path)
already_has_plugins_tab = "categoryCount = 5" in content
already_has_plugins_vec = "pluginsSettings" in content
if already_has_plugins_tab and already_has_plugins_vec:
print(" SettingsActivity.h already patched, skipping.")
return
if not already_has_plugins_tab:
content = content.replace(
'static constexpr int categoryCount = 4;',
'static constexpr int categoryCount = 5;'
)
if not already_has_plugins_vec:
content = content.replace(
' std::vector<SettingInfo> systemSettings;',
' std::vector<SettingInfo> systemSettings;\n std::vector<SettingInfo> pluginsSettings;'
)
write_file(path, content)
print(" SettingsActivity.h patched.")
def patch_settings_cpp(repo_dir):
path = find_first("SettingsActivity.cpp", repo_dir)
content = read_file(path)
if '"LockscreenPlugin.h"' in content and "lockscreenPinHash" in content and '"lockscreenMode") return "Lockscreen"' in content:
print(" SettingsActivity.cpp already patched, skipping.")
return
changed = False
if '"LockscreenPlugin.h"' not in content:
content = content.replace(
'#include "SettingsList.h"',
'#include "SettingsList.h"\n'
'#include "activities/settings/LockscreenPlugin.h"\n'
'#include "activities/LockscreenActivity.h"'
)
changed = True
if 'STR_NONE_OPT};' not in content:
content = content.replace(
'StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};',
'StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM,\n'
' StrId::STR_NONE_OPT};'
)
changed = True
if 'pluginsSettings.clear()' not in content:
content = content.replace(
' systemSettings.clear();\n',
' systemSettings.clear();\n'
' pluginsSettings.clear();\n'
' for (const auto& setting : getSettingsList()) {\n'
' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n'
' pluginsSettings.push_back(setting);\n'
' }\n'
' }\n'
)
changed = True
elif 'pluginsSettings.push_back' not in content:
content = content.replace(
' pluginsSettings.clear();\n',
' pluginsSettings.clear();\n'
' for (const auto& setting : getSettingsList()) {\n'
' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n'
' pluginsSettings.push_back(setting);\n'
' }\n'
' }\n'
)
changed = True
elif '"lockscreenMode") {\n pluginsSettings.push_back' not in content:
content = content.replace(
' for (const auto& setting : getSettingsList()) {\n'
' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n'
' pluginsSettings.push_back(setting);\n'
' }\n'
' }\n'
'\n'
' for (const auto& setting : getSettingsList()) {',
' for (const auto& setting : getSettingsList()) {\n'
' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n'
' pluginsSettings.push_back(setting);\n'
' }\n'
' }\n'
'\n'
' for (const auto& setting : getSettingsList()) {'
)
# If pluginsSettings.push_back exists from another plugin but not lockscreen,
# append after the existing population loop
if '"lockscreenMode") {\n pluginsSettings.push_back' not in content:
import re
content = re.sub(
r'( pluginsSettings\.clear\(\);(\n for.*?push_back.*?\n \}\n)*)',
lambda m: m.group(0) +
' for (const auto& setting : getSettingsList()) {\n'
' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n'
' pluginsSettings.push_back(setting);\n'
' }\n'
' }\n',
content,
count=1,
flags=re.DOTALL
)
changed = True
if 'case 4:' not in content:
content = content.replace(
' case 3:\n'
' currentSettings = &systemSettings;\n'
' break;\n',
' case 3:\n'
' currentSettings = &systemSettings;\n'
' break;\n'
' case 4:\n'
' currentSettings = &pluginsSettings;\n'
' break;\n'
)
changed = True
if '"Plugins"' not in content:
content = content.replace(
' tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i});\n'
' }',
' const char* tabLabel = (i == 4) ? "Plugins" : I18N.get(categoryNames[i]);\n'
' tabs.push_back({tabLabel, selectedCategoryIndex == i});\n'
' }'
)
changed = True
if 'nextCatIdx' not in content:
content = content.replace(
' const auto confirmLabel = (selectedSettingIndex == 0)\n'
' ? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])\n'
' : tr(STR_TOGGLE);',
' const int nextCatIdx = (selectedCategoryIndex + 1) % categoryCount;\n'
' const char* nextCatLabel = (nextCatIdx == 4) ? "Plugins" : I18N.get(categoryNames[nextCatIdx]);\n'
' const auto confirmLabel = (selectedSettingIndex == 0) ? nextCatLabel : tr(STR_TOGGLE);'
)
changed = True
if 'selectedCategoryIndex == 4' not in content:
content = content.replace(
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },',
' [&settings, this](int index) -> std::string {\n'
' if (selectedCategoryIndex == 4) {\n'
' const auto& s = settings[index];\n'
' if (s.key && std::string(s.key) == "lockscreenMode") return "Lockscreen";\n'
' }\n'
' return std::string(I18N.get(settings[index].nameId));\n'
' },'
)
changed = True
elif '"lockscreenMode") return "Lockscreen"' not in content:
content = content.replace(
' if (selectedCategoryIndex == 4) {\n'
' const auto& s = settings[index];\n',
' if (selectedCategoryIndex == 4) {\n'
' const auto& s = settings[index];\n'
' if (s.key && std::string(s.key) == "lockscreenMode") return "Lockscreen";\n'
)
changed = True
if 'LockscreenPlugin::modeName' not in content:
content = content.replace(
' } else {\n'
' valueText = I18N.get(setting.enumValues[value]);\n'
' }',
' } else if (setting.key && std::string(setting.key) == "lockscreenMode") {\n'
' valueText = LockscreenPlugin::modeName(static_cast<LockscreenMode>(value));\n'
' } else {\n'
' valueText = I18N.get(setting.enumValues[value]);\n'
' }'
)
changed = True
if 'lockscreenPinHash' not in content:
content = content.replace(
' SETTINGS.saveToFile();\n'
'}\n'
'\n'
'void SettingsActivity::render',
' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n'
' if (SETTINGS.lockscreenMode == 0) {\n'
' memset(SETTINGS.lockscreenPinHash, 0, sizeof(SETTINGS.lockscreenPinHash));\n'
" } else if (SETTINGS.lockscreenPinHash[0] == '\\0') {\n"
' startActivityForResult(\n'
' std::make_unique<LockscreenActivity>(renderer, mappedInput, LockscreenActivity::Purpose::CREATE),\n'
' [](const ActivityResult&) { SETTINGS.saveToFile(); });\n'
' return;\n'
' }\n'
' }\n'
'\n'
' SETTINGS.saveToFile();\n'
'}\n'
'\n'
'void SettingsActivity::render'
)
changed = True
if changed:
write_file(path, content)
print(" SettingsActivity.cpp patched.")
else:
print(" SettingsActivity.cpp already patched, skipping.")
def patch_web_server(repo_dir):
path = find_first("CrossPointWebServer.cpp", repo_dir)
content = read_file(path)
if 'lockscreenMode") continue' in content:
print(" CrossPointWebServer.cpp already patched, skipping.")
return
target = (
' if (s.key && strcmp(s.key, "lockscreenMode") == 0) {\n'
' doc["name"] = "Lockscreen";\n'
' doc["category"] = "Plugins";\n'
' }\n'
)
if target in content:
content = content.replace(
target,
' if (s.key && (strcmp(s.key, "lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue;\n'
)
write_file(path, content)
print(" CrossPointWebServer.cpp patched (lockscreen hidden from WUI).")
return
name_override = (
'\n'
' if (s.key && (strcmp(s.key, "lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue;\n'
)
content = content.replace(
' doc["category"] = I18N.get(s.category);\n'
'\n'
' switch (s.type) {',
' doc["category"] = I18N.get(s.category);\n'
+ name_override
+ '\n'
' switch (s.type) {'
)
write_file(path, content)
print(" CrossPointWebServer.cpp patched (lockscreen hidden from WUI).")
def patch_main_cpp(repo_dir):
path = find_first("main.cpp", repo_dir)
content = read_file(path)
if "LockscreenPlugin" in content:
if "shouldLockOnWake" in content or "shouldLockOnPower" in content or "wasInReader" in content:
content = content.replace(
' const bool wasInReader = APP_STATE.lastSleepFromReader;\n'
' const bool shouldLock =\n'
' hasPinSet &&\n'
' ((lsMode == LockscreenMode::ENABLED) ||\n'
' (lsMode == LockscreenMode::ON_POWER && !wasInReader) ||\n'
' (lsMode == LockscreenMode::ON_WAKE && wasInReader));\n',
' const bool shouldLock = hasPinSet && LockscreenPlugin::shouldLock(lsMode);\n'
)
write_file(path, content)
print(" main.cpp patched (updated shouldLock call).")
else:
print(" main.cpp already patched, skipping.")
return
content = content.replace(
'#include "CrossPointSettings.h"',
'#include "CrossPointSettings.h"\n'
'#include "activities/settings/LockscreenPlugin.h"\n'
'#include "activities/LockscreenActivity.h"'
)
lockscreen_block = (
'\n'
' {\n'
' const auto lsMode = static_cast<LockscreenMode>(SETTINGS.lockscreenMode);\n'
' const bool hasPinSet = SETTINGS.lockscreenPinHash[0] != \'\\0\';\n'
' const bool shouldLock = hasPinSet && LockscreenPlugin::shouldLock(lsMode);\n'
'\n'
' if (shouldLock) {\n'
' LockscreenActivity lockAct(renderer, mappedInputManager, LockscreenActivity::Purpose::UNLOCK);\n'
' lockAct.onEnter();\n'
' lockAct.renderDirect();\n'
' while (!lockAct.isDone()) {\n'
' gpio.update();\n'
' lockAct.loop();\n'
' if (lockAct.needsRender()) {\n'
' lockAct.renderDirect();\n'
' }\n'
' delay(10);\n'
' }\n'
' lockAct.onExit();\n'
' if (!lockAct.wasSuccessful()) {\n'
' enterDeepSleep();\n'
' return;\n'
' }\n'
' }\n'
' }\n'
)
content = content.replace(
' RECENT_BOOKS.loadFromFile();\n'
'\n'
' // Boot to home screen if no book is open',
' RECENT_BOOKS.loadFromFile();\n'
+ lockscreen_block
+ '\n'
' // Boot to home screen if no book is open'
)
write_file(path, content)
print(" main.cpp patched.")
def patch_settings_html(repo_dir):
path = find_first("SettingsPage.html", repo_dir)
content = read_file(path)
if "lockscreenPin" in content:
print(" SettingsPage.html already patched, skipping.")
return
lines = [
"",
" <script>",
" (function() {",
" var _orig = loadSettings;",
" loadSettings = async function() {",
" await _orig.apply(this, arguments);",
" hideInternalRows();",
" injectLockscreenPin();",
" };",
"",
" function djb2Hash(str) {",
" var hash = 5381;",
" for (var i = 0; i < str.length; i++) {",
" hash = ((hash << 5) + hash) ^ str.charCodeAt(i);",
" hash = hash >>> 0;",
" }",
" hash = (hash ^ 0xA3F1C2B4) >>> 0;",
' return ("00000000" + hash.toString(16).toUpperCase()).slice(-8);',
" }",
"",
" function hideInternalRows() {",
' allSettings.forEach(function(s) {',
' if (s.hidden) {',
' var el = document.getElementById("setting-" + s.key);',
' if (el) { var row = el.closest(".setting-row"); if (row) row.style.display = "none"; }',
' }',
" });",
" }",
"",
" function injectLockscreenPin() {",
' var modeEl = document.getElementById("setting-lockscreenMode");',
" if (!modeEl) return;",
' var row = modeEl.closest(".setting-row");',
' if (!row || document.getElementById("lockscreen-pin-row")) return;',
' var pinRow = document.createElement("div");',
' pinRow.className = "setting-row";',
' pinRow.id = "lockscreen-pin-row";',
" pinRow.innerHTML =",
' \'<span class="setting-name">New PIN (4 digits)</span>\' +',
' \'<span class="setting-control">\' +',
' \'<input type="password" id="lockscreenPin" maxlength="4" \' +',
' \'pattern="[0-9]{4}" inputmode="numeric" \' +',
' \'placeholder="Leave blank to keep current" \' +',
' \'style="width:200px;" oninput="markChanged()">\' +',
" '</span>';",
" row.parentNode.insertBefore(pinRow, row.nextSibling);",
" updatePinRowVisibility();",
' modeEl.addEventListener("change", updatePinRowVisibility);',
" }",
"",
" function updatePinRowVisibility() {",
' var modeEl = document.getElementById("setting-lockscreenMode");',
' var pinRow = document.getElementById("lockscreen-pin-row");',
" if (!modeEl || !pinRow) return;",
' pinRow.style.display = parseInt(modeEl.value) === 1 ? "" : "none";',
" }",
"",
" var _origSave = saveSettings;",
" saveSettings = async function() {",
' var pinEl = document.getElementById("lockscreenPin");',
' var modeEl = document.getElementById("setting-lockscreenMode");',
" if (pinEl && modeEl) {",
" var pin = pinEl.value.trim();",
" if (parseInt(modeEl.value) === 0) {",
' var s = allSettings.find(function(x) { return x.key === "lockscreenPinHash"; });',
' if (s) { s.value = ""; originalValues["lockscreenPinHash"] = "CLEAR"; }',
" } else if (pin.length === 4 && /^[0-9]{4}$/.test(pin)) {",
" var hash = djb2Hash(pin);",
' var s = allSettings.find(function(x) { return x.key === "lockscreenPinHash"; });',
' if (s) { s.value = hash; originalValues["lockscreenPinHash"] = "CHANGED"; }',
' pinEl.value = "";',
" } else if (pin.length > 0) {",
' document.getElementById("message").textContent = "PIN must be exactly 4 digits.";',
' document.getElementById("message").className = "message error";',
' document.getElementById("message").style.display = "block";',
' setTimeout(function() { document.getElementById("message").style.display = "none"; }, 4000);',
" return;",
" }",
" }",
" await _origSave.apply(this, arguments);",
" };",
" })();",
" </script>",
"",
]
js = "\n".join(lines)
content = content.replace("</body>", js + "</body>")
write_file(path, content)
print(" SettingsPage.html patched.")
def patch(repo_dir: str):
plugin_dir = os.path.dirname(os.path.abspath(__file__))
print(" Copying plugin sources...")
copy_plugin_sources(plugin_dir, repo_dir)
print(" Patching CrossPointSettings.h...")
patch_cross_point_settings_h(repo_dir)
print(" Patching SettingsList.h...")
patch_settings_list_h(repo_dir)
print(" Patching SettingsActivity.h...")
patch_settings_h(repo_dir)
print(" Patching SettingsActivity.cpp...")
patch_settings_cpp(repo_dir)
print(" Patching CrossPointWebServer.cpp...")
patch_web_server(repo_dir)
print(" Patching main.cpp...")
patch_main_cpp(repo_dir)
print(" Patching SettingsPage.html (WUI PIN entry)...")
patch_settings_html(repo_dir)
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
patch(sys.argv[1])