diff --git a/src/helpers/esp32/TouchPrefsSchema.h b/src/helpers/esp32/TouchPrefsSchema.h index 3a53cd1..8035b5e 100644 --- a/src/helpers/esp32/TouchPrefsSchema.h +++ b/src/helpers/esp32/TouchPrefsSchema.h @@ -9,7 +9,7 @@ namespace TouchPrefsSchema { static constexpr uint16_t MAGIC = 0x5743; // 'WC' (WadaCfg) -static constexpr uint8_t CURRENT_VERSION = 57; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Day/Night firmware theme, #296; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off) +static constexpr uint8_t CURRENT_VERSION = 58; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Day/Night firmware theme, #296; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off); v58: Attaky notification blink enable + room/DM color indexes (#423) static constexpr uint8_t BROKEN_MID_INSERT_VERSION = 44; // Persisted byte layout. New fields must be appended at the end: older blobs @@ -109,6 +109,9 @@ struct __attribute__((packed)) Config { // advert: a fresh random offset each time would let anyone averaging a night // of adverts recover the true centre, which is the opposite of the intent. uint16_t gps_fuzz_m; + uint8_t attaky_notify_enabled; // v58: blink both keyboard indicators for incoming messages + uint8_t attaky_notify_room_color; // v58: seven-color palette index for rooms/channels + uint8_t attaky_notify_dm_color; // v58: seven-color palette index for direct messages }; static constexpr size_t HEADER_SIZE = offsetof(Config, bright); @@ -121,10 +124,10 @@ static_assert(offsetof(Config, web_mirror) == offsetof(Config, rx_queue) + sizeo // rejection is silent: cfgFlush just returns false and every preference stops // persisting, with nothing in the log to say why. The struct grows most releases, // so guard the ceiling here rather than discover it as "settings do not save" on -// whichever board is using the file backend. 114 bytes today. +// whichever board is using the file backend. 117 bytes today. static_assert(sizeof(Config) <= 2048, "Config exceeds the SdNvsPrefs value cap; prefs would silently stop saving"); -static_assert(offsetof(Config, gps_fuzz_m) + sizeof(Config::gps_fuzz_m) == sizeof(Config), +static_assert(offsetof(Config, attaky_notify_dm_color) + sizeof(Config::attaky_notify_dm_color) == sizeof(Config), "new preference fields must remain trailing"); // lang_file must stay immediately before the tail, or a v48-era blob overlays // onto the wrong bytes. Checking both ends means the next person to append is @@ -150,6 +153,15 @@ static_assert(offsetof(Config, theme_mode) == static_assert(offsetof(Config, gps_fuzz_m) == offsetof(Config, theme_mode) + sizeof(Config::theme_mode), "gps_fuzz_m must follow theme_mode"); +static_assert(offsetof(Config, attaky_notify_enabled) == + offsetof(Config, gps_fuzz_m) + sizeof(Config::gps_fuzz_m), + "attaky_notify_enabled must follow gps_fuzz_m"); +static_assert(offsetof(Config, attaky_notify_room_color) == + offsetof(Config, attaky_notify_enabled) + sizeof(Config::attaky_notify_enabled), + "attaky_notify_room_color must follow attaky_notify_enabled"); +static_assert(offsetof(Config, attaky_notify_dm_color) == + offsetof(Config, attaky_notify_room_color) + sizeof(Config::attaky_notify_room_color), + "attaky_notify_dm_color must follow attaky_notify_room_color"); // Overlay a persisted blob on caller-provided defaults. Beta 57 wrote v44 with // retry_echo inserted before web_mirror, shifting every later value. That blob diff --git a/src/helpers/esp32/TouchPrefsStore.cpp b/src/helpers/esp32/TouchPrefsStore.cpp index b57a60d..b8e5470 100644 --- a/src/helpers/esp32/TouchPrefsStore.cpp +++ b/src/helpers/esp32/TouchPrefsStore.cpp @@ -144,6 +144,9 @@ static void cfgSetDefaults(TouchCfg& c) { c.loud_alerts = 0; // OFF: the standard chime pitch unless asked for c.theme_mode = 0; // Night: preserves the existing firmware appearance c.gps_fuzz_m = 0; // OFF: advertise the real position unless asked otherwise + c.attaky_notify_enabled = 0; // OFF: incoming messages do not blink the keyboard indicators + c.attaky_notify_room_color = 0; // red + c.attaky_notify_dm_color = 1; // green c.compact_chat = 0; // OFF: bubble chat layout (opt-in IRC-style dense rows) c.clock_floor = 0; // no persisted send-timestamp floor yet c.rx_queue = 1; // ON: buffered receive (test-channel default; opt-out toggle in Radio & Mesh) @@ -259,6 +262,11 @@ static void cfgLoadOrMigrate() { if (stored_version < 55) { s_cfg.loud_alerts = 0; } if (stored_version < 56) { s_cfg.theme_mode = 0; } // Night: unchanged appearance if (stored_version < 57) { s_cfg.gps_fuzz_m = 0; } // OFF: real position + if (stored_version < 58) { + s_cfg.attaky_notify_enabled = 0; + s_cfg.attaky_notify_room_color = 0; + s_cfg.attaky_notify_dm_color = 1; + } if (stored_version < 31) s_cfg.compact_chat = 0; // new trailing field: compact chat rows off by default if (stored_version < 32) s_cfg.clock_floor = 0; // new trailing field: no send-timestamp floor persisted yet (#89) if (stored_version < 33) s_cfg.rx_queue = 1; // buffered LoRa receive ON for the test channel (opt-out toggle in Radio & Mesh) @@ -1130,6 +1138,38 @@ bool touchPrefsSetMsgFlash(bool on) { return cfgFlush(); } +bool touchPrefsGetAttakyNotifyEnabled() { + if (!s_begun) touchPrefsBegin(); + return s_cfg.attaky_notify_enabled != 0; +} +bool touchPrefsSetAttakyNotifyEnabled(bool on) { + if (!s_begun) touchPrefsBegin(); + s_cfg.attaky_notify_enabled = on ? 1 : 0; + return cfgFlush(); +} +uint8_t touchPrefsGetAttakyNotifyRoomColor() { + if (!s_begun) touchPrefsBegin(); + return s_cfg.attaky_notify_room_color < TOUCH_ATTAKY_NOTIFY_COLOR_COUNT + ? s_cfg.attaky_notify_room_color : 0; +} +bool touchPrefsSetAttakyNotifyRoomColor(uint8_t color) { + if (color >= TOUCH_ATTAKY_NOTIFY_COLOR_COUNT) return false; + if (!s_begun) touchPrefsBegin(); + s_cfg.attaky_notify_room_color = color; + return cfgFlush(); +} +uint8_t touchPrefsGetAttakyNotifyDmColor() { + if (!s_begun) touchPrefsBegin(); + return s_cfg.attaky_notify_dm_color < TOUCH_ATTAKY_NOTIFY_COLOR_COUNT + ? s_cfg.attaky_notify_dm_color : 1; +} +bool touchPrefsSetAttakyNotifyDmColor(uint8_t color) { + if (color >= TOUCH_ATTAKY_NOTIFY_COLOR_COUNT) return false; + if (!s_begun) touchPrefsBegin(); + s_cfg.attaky_notify_dm_color = color; + return cfgFlush(); +} + // Console mode is a BOOT mode, so this is read before the UI is built. It fails // safe by construction: the only value that means console is exactly 1, so a // corrupt or unreadable pref boots the graphical UI, which is the mode everyone diff --git a/src/helpers/esp32/TouchPrefsStore.h b/src/helpers/esp32/TouchPrefsStore.h index 2418bd1..d55dfc7 100644 --- a/src/helpers/esp32/TouchPrefsStore.h +++ b/src/helpers/esp32/TouchPrefsStore.h @@ -196,6 +196,14 @@ bool touchPrefsSetFemLna(bool on); bool touchPrefsGetMsgFlash(); bool touchPrefsSetMsgFlash(bool on); +static constexpr uint8_t TOUCH_ATTAKY_NOTIFY_COLOR_COUNT = 7; +bool touchPrefsGetAttakyNotifyEnabled(); +bool touchPrefsSetAttakyNotifyEnabled(bool on); +uint8_t touchPrefsGetAttakyNotifyRoomColor(); +bool touchPrefsSetAttakyNotifyRoomColor(uint8_t color); +uint8_t touchPrefsGetAttakyNotifyDmColor(); +bool touchPrefsSetAttakyNotifyDmColor(uint8_t color); + /* Advertise on boot (#76): fire one flood self-advert ~6s after boot so peers with auto-add on * refresh our pubkey (useful after a reflash wiped storage). Opt-in, default off. All boards. */ bool touchPrefsGetConsoleMode(); // boot into the LVGL-free console (CONSOLE_MODE.md) diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 33a213b..1fd0950 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -5498,7 +5498,7 @@ enum { CAT_SENSORS, // expansion kit + Show-Sensors-tab toggle (V4-with-kit) CAT_DISPLAY, // screen timeout, UI size, bubbles, theme, orientation CAT_KEYBOARD, // secondary layouts + accent popups - CAT_SOUND, // notification sound + CAT_SOUND, // notification sound, or Attaky keyboard-indicator blink CAT_QUICKREPLIES, // quick-reply macros CAT_LOCK, // lock-screen wallpaper + text colour (T-Deck only) CAT_GENERAL, // reboot, run setup, storage/SD, misc device prefs (was "System") @@ -5534,7 +5534,11 @@ static const SettingsCatDef kSettingsCats[CAT_COUNT] = { { "Sensors", LV_SYMBOL_EYE_OPEN }, { "Display", LV_SYMBOL_IMAGE }, { "Keyboard", LV_SYMBOL_KEYBOARD }, +#if defined(ATTAKY_MESH_SERIES) + { "Notifications", LV_SYMBOL_AUDIO }, +#else { "Sound", LV_SYMBOL_AUDIO }, +#endif { "Quick replies", LV_SYMBOL_ENVELOPE }, { "Lock screen", LV_SYMBOL_EYE_CLOSE }, { "General", LV_SYMBOL_SETTINGS }, @@ -9162,6 +9166,96 @@ static void volumeStepCb(lv_event_t* e) { #endif #endif +#if defined(ATTAKY_MESH_SERIES) +struct AttakyNotifyColor { + uint32_t swatch_rgb; + uint8_t output_mask; +}; +static const AttakyNotifyColor kAttakyNotifyColors[] = { + { 0xFF0000, ATTAKY_NOTIFY_RED }, + { 0x00FF00, ATTAKY_NOTIFY_GREEN }, + { 0x0000FF, ATTAKY_NOTIFY_BLUE }, + { 0xFFFF00, ATTAKY_NOTIFY_RED | ATTAKY_NOTIFY_GREEN }, + { 0x00FFFF, ATTAKY_NOTIFY_GREEN | ATTAKY_NOTIFY_BLUE }, + { 0xFF00FF, ATTAKY_NOTIFY_RED | ATTAKY_NOTIFY_BLUE }, + { 0xFFFFFF, ATTAKY_NOTIFY_RED | ATTAKY_NOTIFY_GREEN | ATTAKY_NOTIFY_BLUE }, +}; +static_assert(sizeof(kAttakyNotifyColors) / sizeof(kAttakyNotifyColors[0]) == + TOUCH_ATTAKY_NOTIFY_COLOR_COUNT, + "Attaky notification palette and persisted indexes must match"); + +static lv_obj_t* s_attaky_notify_swatches[2][TOUCH_ATTAKY_NOTIFY_COLOR_COUNT] = {}; +static uint8_t s_attaky_notify_mask = ATTAKY_NOTIFY_OFF; +static uint8_t s_attaky_notify_transitions = 0; +static bool s_attaky_notify_lit = false; +static uint32_t s_attaky_notify_next_ms = 0; + +static void attakyNotificationStop() { + s_attaky_notify_mask = ATTAKY_NOTIFY_OFF; + s_attaky_notify_transitions = 0; + s_attaky_notify_lit = false; + s_attaky_notify_next_ms = 0; + if (!attakyKeyboardSetNotificationColor(ATTAKY_NOTIFY_OFF)) { + s_attaky_notify_transitions = 1; + s_attaky_notify_lit = true; + s_attaky_notify_next_ms = millis() + 50; + } +} + +static void attakyNotificationStart(uint8_t color_index) { + if (!touchPrefsGetAttakyNotifyEnabled()) return; + if (color_index >= TOUCH_ATTAKY_NOTIFY_COLOR_COUNT) color_index = 0; + s_attaky_notify_mask = kAttakyNotifyColors[color_index].output_mask; + s_attaky_notify_transitions = 6; + s_attaky_notify_lit = false; + s_attaky_notify_next_ms = millis(); +} + +static void attakyNotificationTick(uint32_t now) { + if (s_attaky_notify_transitions == 0 || + (int32_t)(now - s_attaky_notify_next_ms) < 0) return; + const bool next_lit = !s_attaky_notify_lit; + const uint8_t output = next_lit ? s_attaky_notify_mask : ATTAKY_NOTIFY_OFF; + if (!attakyKeyboardSetNotificationColor(output)) { + s_attaky_notify_next_ms = now + 50; + return; + } + s_attaky_notify_lit = next_lit; + --s_attaky_notify_transitions; + s_attaky_notify_next_ms = s_attaky_notify_transitions ? now + 220 : 0; +} + +static void attakyNotifySwatchesRefresh(int row) { + const uint8_t selected = row == 0 + ? touchPrefsGetAttakyNotifyRoomColor() + : touchPrefsGetAttakyNotifyDmColor(); + for (uint8_t i = 0; i < TOUCH_ATTAKY_NOTIFY_COLOR_COUNT; ++i) { + lv_obj_t* swatch = s_attaky_notify_swatches[row][i]; + if (!swatch || !lv_obj_is_valid(swatch)) continue; + lv_obj_set_style_border_width(swatch, i == selected ? 2 : 1, LV_PART_MAIN); + lv_obj_set_style_border_color(swatch, + lv_color_hex(i == selected ? 0xFFFFFF : COLOR_BORDER), LV_PART_MAIN); + } +} + +static void toggleAttakyNotificationsCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED); + touchPrefsSetAttakyNotifyEnabled(on); + if (!on) attakyNotificationStop(); +} + +static void attakyNotifyColorChosenCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + const uintptr_t choice = (uintptr_t)lv_event_get_user_data(e); + const int row = (choice & 0x100u) ? 1 : 0; + const uint8_t color = (uint8_t)(choice & 0xFFu); + if (row == 0) touchPrefsSetAttakyNotifyRoomColor(color); + else touchPrefsSetAttakyNotifyDmColor(color); + attakyNotifySwatchesRefresh(row); +} +#endif + static void settingsFieldFocusCb(lv_event_t* e) { const lv_event_code_t code = lv_event_get_code(e); // Drop LV_EVENT_PRESSED: it fires on touch-down before LVGL can tell @@ -9774,13 +9868,14 @@ static lv_obj_t* createSettingsModal(const char* title, SettingsModalKind kind) lv_obj_set_height(content, LV_SIZE_CONTENT); lv_obj_set_pos(content, 0, 0); lv_obj_set_style_pad_all(content, 0, LV_PART_MAIN); + lv_obj_set_style_pad_right(content, 6, LV_PART_MAIN); lv_obj_set_style_border_width(content, 0, LV_PART_MAIN); lv_obj_clear_flag(content, LV_OBJ_FLAG_SCROLLABLE); resetSettingsModalState(); g_set_modal.root = root; g_set_modal.kind = kind; - s_settings_content_w = (sw - 8) - 12; // modal body width minus its 6px padding each side + s_settings_content_w = (sw - 8) - 12 - 6; // body content width minus the right control gutter #if CAP_KEYBOARD && CAP_KEYPAD_NAV // Physical keyboard: once the caller has finished adding this modal's fields // (deferred to the next frame), focus its first text field so the user can @@ -13437,7 +13532,49 @@ static void buildDeviceSettings(int sec) { // The "Show Sensors tab" toggle is appended here too (moved from Display). } - if (sec == DSEC_SOUND) { // --- Sound --- + if (sec == DSEC_SOUND) { // --- Sound / Attaky notifications --- +#if defined(ATTAKY_MESH_SERIES) + { + int rh = settingsRowLabel(body, y, 6, TR("Incoming message blink"), COLOR_SUB, nullptr, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); + if (touchPrefsGetAttakyNotifyEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED); + lv_obj_add_event_cb(sw, toggleAttakyNotificationsCb, LV_EVENT_VALUE_CHANGED, nullptr); + y += LV_MAX(34, rh + 12); + } + auto addNotifyColorRow = [&](const char* label, int row) { + y += settingsRowLabel(body, y, 0, TR(label), COLOR_SUB, &g_font_12, 0) + 4; + const lv_coord_t gap = 3; + const lv_coord_t row_w = s_settings_content_w - 2; + const lv_coord_t swatch_w = (row_w - gap * (TOUCH_ATTAKY_NOTIFY_COLOR_COUNT - 1)) / + TOUCH_ATTAKY_NOTIFY_COLOR_COUNT; + const lv_coord_t swatch_h = SC(26); + for (uint8_t i = 0; i < TOUCH_ATTAKY_NOTIFY_COLOR_COUNT; ++i) { + lv_obj_t* swatch = lv_btn_create(body); + s_attaky_notify_swatches[row][i] = swatch; + lv_obj_set_size(swatch, swatch_w, swatch_h); + lv_obj_set_pos(swatch, 2 + i * (swatch_w + gap), y); + lv_obj_set_style_radius(swatch, 4, LV_PART_MAIN); + for (lv_style_selector_t state : { (lv_style_selector_t)LV_PART_MAIN, + (lv_style_selector_t)(LV_PART_MAIN | LV_STATE_FOCUSED), + (lv_style_selector_t)(LV_PART_MAIN | LV_STATE_FOCUS_KEY), + (lv_style_selector_t)(LV_PART_MAIN | LV_STATE_PRESSED) }) { + lv_obj_set_style_bg_color(swatch, lv_color_hex(kAttakyNotifyColors[i].swatch_rgb), state); + lv_obj_set_style_bg_opa(swatch, LV_OPA_COVER, state); + } + lv_obj_set_style_outline_color(swatch, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN | LV_STATE_FOCUS_KEY); + lv_obj_set_style_outline_width(swatch, 2, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + lv_obj_set_style_outline_opa(swatch, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + lv_obj_set_style_outline_pad(swatch, 2, LV_PART_MAIN | LV_STATE_FOCUS_KEY); + const uintptr_t choice = (row ? 0x100u : 0u) | i; + lv_obj_add_event_cb(swatch, attakyNotifyColorChosenCb, LV_EVENT_CLICKED, (void*)choice); + } + attakyNotifySwatchesRefresh(row); + y += swatch_h + 10; + }; + addNotifyColorRow("Room / channel color", 0); + addNotifyColorRow("Direct message color", 1); +#endif // Sound toggle — T-Deck I2S speaker, Heltec V4 expansion-kit piezo buzzer, or // the Tanmatsu's ES8156 codec / speaker amp. #if defined(HAS_UI_SOUND) || defined(HAS_TANMATSU) @@ -56123,6 +56260,15 @@ void UITask::newMsgImpl(uint8_t path_len, const char* from_name, const char* tex } #endif +#if defined(ATTAKY_MESH_SERIES) + if (g_last_event == UIEventType::contactMessage) { + attakyNotificationStart(touchPrefsGetAttakyNotifyDmColor()); + } else if (g_last_event == UIEventType::channelMessage || + g_last_event == UIEventType::roomMessage) { + attakyNotificationStart(touchPrefsGetAttakyNotifyRoomColor()); + } +#endif + #if CAP_LUA_SDK_EXT // Hand a running Lua app the incoming message (app.on_message), AFTER the block // and spam filters so an app sees exactly the messages the user does. Permission @@ -57771,6 +57917,7 @@ void UITask::loop() { } #endif #if defined(HAS_ATTAKY_MESH_KEYBOARD) + attakyNotificationTick(now); { lv_obj_t* akb_ta = g_lv.keyboard ? lv_keyboard_get_textarea(g_lv.keyboard) : nullptr; attakyKeyboardPoll(akb_ta != nullptr); @@ -57789,6 +57936,10 @@ void UITask::loop() { break; // keyboard rebound above; akb_ta is stale from here } else if (s_kb_panel) lv_textarea_add_char(akb_ta, '\n'); // enter-sends off: compose multi-line + else if (s_term_input_ta && s_kb_bind_ta == s_term_input_ta) { + terminalSubmit(); // terminal: submit once, clear, and keep the field ready + break; + } else lv_event_send(g_lv.keyboard, LV_EVENT_READY, nullptr); // settings field: confirm } else if (key == 0x08 || key == 0x7F) lv_textarea_del_char(akb_ta); diff --git a/test/test_touch_prefs_schema.cpp b/test/test_touch_prefs_schema.cpp index c8a2ffc..065eb7b 100644 --- a/test/test_touch_prefs_schema.cpp +++ b/test/test_touch_prefs_schema.cpp @@ -27,6 +27,9 @@ static Config safeDefaults() { c.boot_wifi_open = 0; c.theme_mode = 0; c.gps_fuzz_m = 0; + c.attaky_notify_enabled = 0; + c.attaky_notify_room_color = 0; + c.attaky_notify_dm_color = 1; return c; } @@ -85,6 +88,9 @@ int main() { assert(migrated.loud_alerts == 0); assert(migrated.theme_mode == 0); assert(migrated.gps_fuzz_m == 0); + assert(migrated.attaky_notify_enabled == 0); + assert(migrated.attaky_notify_room_color == 0); + assert(migrated.attaky_notify_dm_color == 1); // Reproduce beta 57 exactly: retry was inserted before the old suffix, then // the v43 bytes were copied and the full shifted v44 structure was written. @@ -116,7 +122,9 @@ int main() { constexpr size_t v53_size = offsetof(Config, boot_wifi_time); static_assert(v53_size + sizeof(Config::boot_wifi_time) + sizeof(Config::boot_wifi_open) + sizeof(Config::loud_alerts) + sizeof(Config::theme_mode) - + sizeof(Config::gps_fuzz_m) == sizeof(Config), + + sizeof(Config::gps_fuzz_m) + sizeof(Config::attaky_notify_enabled) + + sizeof(Config::attaky_notify_room_color) + + sizeof(Config::attaky_notify_dm_color) == sizeof(Config), "v53 is the current layout minus every byte appended since"); Config v53 = safeDefaults(); @@ -155,6 +163,9 @@ int main() { current.loud_alerts = 1; current.theme_mode = 1; current.gps_fuzz_m = 150; + current.attaky_notify_enabled = 1; + current.attaky_notify_room_color = 6; + current.attaky_notify_dm_color = 4; migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, ¤t, sizeof(current), &stored_version)); assert(stored_version == TouchPrefsSchema::CURRENT_VERSION); @@ -177,15 +188,16 @@ int main() { assert(migrated.app_hide == safeDefaults().app_hide); assert(migrated.theme_mode == 0); - // v56 appends only theme_mode. It was written as v54 on the contributing + // v56 appended only theme_mode. It was written as v54 on the contributing // branch, but v54 and v55 were taken by boot_wifi_* and loud_alerts before it - // merged, so the field sits behind those and the blob one version short of - // current is the one that must leave it at the Night default. + // merged, so the field sits behind those and a historical v55 blob must leave + // it at the Night default. constexpr size_t v55_size = offsetof(Config, theme_mode); - static_assert(offsetof(Config, gps_fuzz_m) + sizeof(Config::gps_fuzz_m) == sizeof(Config), - "gps_fuzz_m must remain last"); - static_assert(v55_size + sizeof(Config::theme_mode) + sizeof(Config::gps_fuzz_m) == sizeof(Config), - "v55 is the current layout minus theme_mode and gps_fuzz_m"); + static_assert(v55_size + sizeof(Config::theme_mode) + sizeof(Config::gps_fuzz_m) + + sizeof(Config::attaky_notify_enabled) + + sizeof(Config::attaky_notify_room_color) + + sizeof(Config::attaky_notify_dm_color) == sizeof(Config), + "v55 is the current layout minus every byte appended since"); Config v55 = safeDefaults(); v55.ver = 55; v55.kb_force_legacy = 1; @@ -198,6 +210,28 @@ int main() { assert(migrated.loud_alerts == 1); assert(migrated.theme_mode == 0); assert(migrated.gps_fuzz_m == 0); + assert(migrated.attaky_notify_enabled == 0); + assert(migrated.attaky_notify_room_color == 0); + assert(migrated.attaky_notify_dm_color == 1); + + constexpr size_t v57_size = offsetof(Config, attaky_notify_enabled); + static_assert(v57_size + sizeof(Config::attaky_notify_enabled) + + sizeof(Config::attaky_notify_room_color) + + sizeof(Config::attaky_notify_dm_color) == sizeof(Config), + "v57 is the current layout minus the Attaky notification fields"); + Config v57 = safeDefaults(); + v57.ver = 57; + v57.gps_fuzz_m = 250; + v57.attaky_notify_enabled = 1; // outside the stored v57 extent + v57.attaky_notify_room_color = 6; + v57.attaky_notify_dm_color = 5; + migrated = safeDefaults(); + assert(TouchPrefsSchema::overlayStored(migrated, &v57, v57_size, &stored_version)); + assert(stored_version == 57); + assert(migrated.gps_fuzz_m == 250); + assert(migrated.attaky_notify_enabled == 0); + assert(migrated.attaky_notify_room_color == 0); + assert(migrated.attaky_notify_dm_color == 1); Config invalid = safeDefaults(); uint8_t garbage[sizeof(Config)] = {}; diff --git a/variants/attaky_mesh_series/AttakyMeshSeriesKeyboard.cpp b/variants/attaky_mesh_series/AttakyMeshSeriesKeyboard.cpp index 0a0a830..4977d0f 100644 --- a/variants/attaky_mesh_series/AttakyMeshSeriesKeyboard.cpp +++ b/variants/attaky_mesh_series/AttakyMeshSeriesKeyboard.cpp @@ -46,6 +46,7 @@ static const uint8_t SHIFT_HALF = 0, SHIFT_ROW = 3, SHIFT_COL = 0; static bool s_inited = false; static bool s_present[2] = { false, false }; +static uint8_t s_notify_color = ATTAKY_NOTIFY_OFF; static uint8_t s_prev[2][5] = { { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } }; @@ -72,6 +73,11 @@ static uint8_t awRead(uint8_t addr, uint8_t reg) { return (uint8_t)Wire.read(); } +static uint8_t p1Value(uint8_t row_bits) { + const uint8_t led_bits = (uint8_t)((~s_notify_color & 0x07u) << 5); + return (uint8_t)((row_bits & 0x1Fu) | led_bits); +} + static bool initHalf(uint8_t addr) { bool ok = true; ok &= awWrite(addr, AW_REG_SWRST, 0x00); @@ -81,7 +87,7 @@ static bool initHalf(uint8_t addr) { ok &= awWrite(addr, AW_REG_CFG_P0, 0xFF); ok &= awWrite(addr, AW_REG_CFG_P1, 0x00); ok &= awWrite(addr, AW_REG_OUT_P0, 0x00); - ok &= awWrite(addr, AW_REG_OUT_P1, P1_IDLE); + ok &= awWrite(addr, AW_REG_OUT_P1, p1Value(P1_IDLE)); return ok; } @@ -97,11 +103,11 @@ static void ensureInit() { static void scanHalf(uint8_t addr, uint8_t cols[5]) { for (uint8_t row = 0; row < 5; row++) { - awWrite(addr, AW_REG_OUT_P1, (uint8_t)(ROW_DRIVE[row] | 0xE0)); + awWrite(addr, AW_REG_OUT_P1, p1Value(ROW_DRIVE[row])); delayMicroseconds(50); cols[row] = (uint8_t)(awRead(addr, AW_REG_IN_P0) & 0x1F); } - awWrite(addr, AW_REG_OUT_P1, P1_IDLE); + awWrite(addr, AW_REG_OUT_P1, p1Value(P1_IDLE)); } void attakyKeyboardPoll(bool active) { @@ -155,4 +161,20 @@ int attakyKeyboardReadKey() { bool attakyKeyboardPresent() { return s_present[0] || s_present[1]; } +bool attakyKeyboardSetNotificationColor(uint8_t color_mask) { + s_notify_color = color_mask & 0x07u; + ensureInit(); + if (!attakyI2cLock(20)) return false; + + bool wrote = false; + bool ok = true; + for (int h = 0; h < 2; ++h) { + if (!s_present[h]) continue; + wrote = true; + if (!awWrite(KB_ADDR[h], AW_REG_OUT_P1, p1Value(P1_IDLE))) ok = false; + } + attakyI2cUnlock(); + return wrote && ok; +} + #endif diff --git a/variants/attaky_mesh_series/AttakyMeshSeriesKeyboard.h b/variants/attaky_mesh_series/AttakyMeshSeriesKeyboard.h index b764219..09f7baa 100644 --- a/variants/attaky_mesh_series/AttakyMeshSeriesKeyboard.h +++ b/variants/attaky_mesh_series/AttakyMeshSeriesKeyboard.h @@ -5,8 +5,16 @@ #include +enum : uint8_t { + ATTAKY_NOTIFY_OFF = 0, + ATTAKY_NOTIFY_BLUE = 1u << 0, + ATTAKY_NOTIFY_GREEN = 1u << 1, + ATTAKY_NOTIFY_RED = 1u << 2, +}; + void attakyKeyboardPoll(bool active); int attakyKeyboardReadKey(); bool attakyKeyboardPresent(); +bool attakyKeyboardSetNotificationColor(uint8_t color_mask); #endif