diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 0c9a2b46f..a140e9124 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -191,6 +191,10 @@ set(EXTCPPSRC #doom external/doom/main.cpp external/doom/ui_doom.cpp + + #screening + # external/screening/main.cpp + # external/screening/ui_screening.cpp ) set(EXTAPPLIST @@ -240,4 +244,5 @@ set(EXTAPPLIST stopwatch breakout doom + # screening ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 400c1f60f..f2975f8fa 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -69,6 +69,8 @@ MEMORY ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k + /*ram_external_app_screening (rwx) : org = 0xADDF0000, len = 32k*/ + } SECTIONS @@ -348,5 +350,14 @@ SECTIONS *(*ui*external_app*doom*); } > ram_external_app_doom +/* + .external_app_screening : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_screening.application_information)); + *(*ui*external_app*screening*); + } > ram_external_app_screening + +*/ + } diff --git a/firmware/application/external/stopwatch/ui_stopwatch.cpp b/firmware/application/external/stopwatch/ui_stopwatch.cpp index 68e70c375..112dc8740 100644 --- a/firmware/application/external/stopwatch/ui_stopwatch.cpp +++ b/firmware/application/external/stopwatch/ui_stopwatch.cpp @@ -1,5 +1,6 @@ /* * Copyright 2025 Mark Thompson + * copyleft Mr. Robot of F.Society * * This file is part of PortaPack. * @@ -27,14 +28,46 @@ using namespace portapack; namespace ui::external_app::stopwatch { -StopwatchView::StopwatchView(NavigationView& nav) { +// clang-format off +// clang-format doesn't allow use as this way but it's not worth to have const var for this thing. too expensive + +// minute +#define TOTAL_M1_POS {0 * 8 * 4, 3 * 16} +#define TOTAL_M2_POS {1 * 8 * 4, 3 * 16} + +// sec +#define TOTAL_S1_POS {2 * 8 * 4 + 24, 3 * 16} +#define TOTAL_S2_POS {3 * 8 * 4 + 24, 3 * 16} + +// ms +#define TOTAL_MS1_POS {4 * 8 * 4 + 7 * 8, 3 * 16 + 24} +#define TOTAL_MS2_POS {5 * 8 * 4 + 5 * 8, 3 * 16 + 24} +#define TOTAL_MS3_POS {6 * 8 * 4 + 3 * 8, 3 * 16 + 24} + +#define LAP_Y 9 * 16 + +//lap min +#define LAP_M1_POS {0 * 8 * 4, LAP_Y} +#define LAP_M2_POS {1 * 8 * 4, LAP_Y} + +//lap sec +#define LAP_S1_POS {2 * 8 * 4 + 24, LAP_Y} +#define LAP_S2_POS {3 * 8 * 4 + 24, LAP_Y} + +//lap ms +#define LAP_MS1_POS {4 * 8 * 4 + 7 * 8, LAP_Y + 24} +#define LAP_MS2_POS {5 * 8 * 4 + 5 * 8, LAP_Y + 24} +#define LAP_MS3_POS {6 * 8 * 4 + 3 * 8, LAP_Y + 24} +// clang-format on + +StopwatchView::StopwatchView(NavigationView& nav) + : painter{} { add_children({ &labels, &button_run_stop, &button_reset_lap, &button_done, - &big_display, - &lap_display, + &options_ms_display_level, }); button_run_stop.on_select = [this](Button&) { @@ -54,6 +87,14 @@ StopwatchView::StopwatchView(NavigationView& nav) { button_done.on_select = [&nav](Button&) { nav.pop(); }; + + options_ms_display_level.on_change = [&](size_t, ui::OptionsField::value_t) { + clean_ms_display(options_ms_display_level.selected_index()); + }; + + options_ms_display_level.set_selected_index(0); + + refresh_painting(); } void StopwatchView::focus() { @@ -61,6 +102,10 @@ void StopwatchView::focus() { } void StopwatchView::run() { + options_ms_display_level.hidden(true); + // ^ this won't take efferts if don't set_dirty, but needed to let user can't change during ticking + if (!paused) refresh_painting(); + paused = false; running = true; start_time = chTimeNow() - previously_elapsed; button_run_stop.set_text("STOP"); @@ -68,7 +113,9 @@ void StopwatchView::run() { } void StopwatchView::stop() { + options_ms_display_level.hidden(false); running = false; + paused = true; end_time = chTimeNow(); previously_elapsed = end_time - start_time; button_run_stop.set_text("START"); @@ -77,25 +124,186 @@ void StopwatchView::stop() { void StopwatchView::reset() { lap_time = end_time = start_time = previously_elapsed = 0; - big_display.set(0); - lap_display.set(0); + for (uint8_t i = 0; i < 7; i++) { + refresh_painting(); + } + refresh_painting(); + set_dirty(); } void StopwatchView::lap() { lap_time = chTimeNow(); - lap_display.set((lap_time - start_time) * 1000); // convert elapsed time in ms to MHz for BigFrequency widget } -void StopwatchView::paint(Painter& painter) { - (void)painter; - if (running) { - end_time = chTimeNow(); - big_display.set((end_time - start_time) * 1000); // convert elapsed time in ms to MHz for BigFrequency widget +void StopwatchView::paint(Painter&) { +} + +void StopwatchView::refresh_painting() { + // minute + painter.draw_char(TOTAL_M1_POS, *Theme::getInstance()->fg_light, ' ', 4); + painter.draw_char(TOTAL_M2_POS, *Theme::getInstance()->fg_light, ' ', 4); + + // sec + painter.draw_char(TOTAL_S1_POS, *Theme::getInstance()->fg_light, ' ', 4); + painter.draw_char(TOTAL_S2_POS, *Theme::getInstance()->fg_light, ' ', 4); + + // ms + painter.draw_char(TOTAL_MS1_POS, *Theme::getInstance()->fg_light, ' ', 2); + painter.draw_char(TOTAL_MS2_POS, *Theme::getInstance()->fg_light, ' ', 2); + painter.draw_char(TOTAL_MS3_POS, *Theme::getInstance()->fg_light, ' ', 2); + + // lap min + painter.draw_char(LAP_M1_POS, *Theme::getInstance()->fg_light, ' ', 4); + painter.draw_char(LAP_M2_POS, *Theme::getInstance()->fg_light, ' ', 4); + + // lap sec + painter.draw_char(LAP_S1_POS, *Theme::getInstance()->fg_light, ' ', 4); + painter.draw_char(LAP_S2_POS, *Theme::getInstance()->fg_light, ' ', 4); + + // lap ms + painter.draw_char(LAP_MS1_POS, *Theme::getInstance()->fg_light, ' ', 2); + painter.draw_char(LAP_MS2_POS, *Theme::getInstance()->fg_light, ' ', 2); + painter.draw_char(LAP_MS3_POS, *Theme::getInstance()->fg_light, ' ', 2); +} + +/*when user seted it to display more, then display less later, this prevent the remain value exist on screen*/ +void StopwatchView::clean_ms_display(uint8_t level) { + level++; + switch (level) { + case 0: + painter.draw_char(TOTAL_MS1_POS, *Theme::getInstance()->fg_light, ' ', 2); + painter.draw_char(TOTAL_MS2_POS, *Theme::getInstance()->fg_light, ' ', 2); + painter.draw_char(TOTAL_MS3_POS, *Theme::getInstance()->fg_light, ' ', 2); + break; + case 1: + painter.draw_char(TOTAL_MS2_POS, *Theme::getInstance()->fg_light, ' ', 2); + painter.draw_char(TOTAL_MS3_POS, *Theme::getInstance()->fg_light, ' ', 2); + break; + case 2: + painter.draw_char(TOTAL_MS3_POS, *Theme::getInstance()->fg_light, ' ', 2); + break; } } +void StopwatchView::resume_last() { + // minute + painter.draw_char(TOTAL_M1_POS, *Theme::getInstance()->fg_light, last_displayed[0], 4); + painter.draw_char(TOTAL_M2_POS, *Theme::getInstance()->fg_light, last_displayed[1], 4); + + // sec + painter.draw_char(TOTAL_S1_POS, *Theme::getInstance()->fg_light, last_displayed[2], 4); + painter.draw_char(TOTAL_S2_POS, *Theme::getInstance()->fg_light, last_displayed[3], 4); + + // ms + painter.draw_char(TOTAL_MS1_POS, *Theme::getInstance()->fg_light, last_displayed[4], 2); + painter.draw_char(TOTAL_MS2_POS, *Theme::getInstance()->fg_light, last_displayed[5], 2); + painter.draw_char(TOTAL_MS3_POS, *Theme::getInstance()->fg_light, last_displayed[6], 2); +} + +/* NB: + * Due to the flaw of dirty management, it's using work around, to reduce screen flickering: + * + * for example when xx:15:xxx turn to xx:16:xxx, it actually only paint 6, the 1 is old, + * but not dirty, so we can see without flikering. + * + * So with these work around, it won't show false info, but bare in mind that it could be, if you add more things. + */ void StopwatchView::frame_sync() { - set_dirty(); + uint32_t elapsed_ticks = 0; + + if (running) { + end_time = chTimeNow(); + elapsed_ticks = end_time - start_time; + } else if (previously_elapsed > 0) { + elapsed_ticks = previously_elapsed; + } + + constexpr uint32_t TICKS_PER_SECOND = CH_FREQUENCY; + constexpr uint32_t TICKS_PER_MINUTE = TICKS_PER_SECOND * 60; + + uint32_t minutes = elapsed_ticks / TICKS_PER_MINUTE; + uint32_t seconds = (elapsed_ticks % TICKS_PER_MINUTE) / TICKS_PER_SECOND; + uint32_t milliseconds = ((elapsed_ticks % TICKS_PER_SECOND) * 1000) / TICKS_PER_SECOND; + + // minute + if (last_displayed[0] != (minutes / 10) % 10) + painter.draw_char(TOTAL_M1_POS, *Theme::getInstance()->fg_red, '0' + (minutes / 10) % 10, 4); + if (last_displayed[1] != minutes % 10) + painter.draw_char(TOTAL_M2_POS, *Theme::getInstance()->fg_red, '0' + minutes % 10, 4); + + // sec + if (last_displayed[2] != (seconds / 10) % 10) + painter.draw_char(TOTAL_S1_POS, *Theme::getInstance()->fg_green, '0' + (seconds / 10) % 10, 4); + if (last_displayed[3] != seconds % 10) + painter.draw_char(TOTAL_S2_POS, *Theme::getInstance()->fg_green, '0' + seconds % 10, 4); + + // ms + /* v place holder to aligh logic*/ + if ((true) && last_displayed[4] != (milliseconds / 100) % 10) + painter.draw_char(TOTAL_MS1_POS, *Theme::getInstance()->fg_yellow, '0' + (milliseconds / 100) % 10, 2); + if ((options_ms_display_level.selected_index() >= 1) && last_displayed[5] != (milliseconds / 10) % 10) + painter.draw_char(TOTAL_MS2_POS, *Theme::getInstance()->fg_yellow, '0' + (milliseconds / 10) % 10, 2); + if ((options_ms_display_level.selected_index() >= 2) && last_displayed[6] != milliseconds % 10) + painter.draw_char(TOTAL_MS3_POS, *Theme::getInstance()->fg_yellow, '0' + milliseconds % 10, 2); + + // min + last_displayed[0] = (minutes / 10) % 10; + last_displayed[1] = minutes % 10; + // sec + last_displayed[2] = (seconds / 10) % 10; + last_displayed[3] = seconds % 10; + // ms + last_displayed[4] = (milliseconds / 100) % 10; + last_displayed[5] = (milliseconds / 10) % 10; + last_displayed[6] = milliseconds % 10; + + if (lap_time > 0) { + uint32_t lap_elapsed = lap_time - start_time; + + uint32_t lap_minutes = lap_elapsed / TICKS_PER_MINUTE; + uint32_t lap_seconds = (lap_elapsed % TICKS_PER_MINUTE) / TICKS_PER_SECOND; + uint32_t lap_milliseconds = ((lap_elapsed % TICKS_PER_SECOND) * 1000) / TICKS_PER_SECOND; + + // lap min + if (lap_last_displayed[0] == (lap_minutes / 10) % 10) { + painter.draw_char(LAP_M1_POS, *Theme::getInstance()->fg_light, '0' + (lap_minutes / 10) % 10, 4); + } + if (lap_last_displayed[1] == lap_minutes % 10) { + painter.draw_char(LAP_M2_POS, *Theme::getInstance()->fg_light, '0' + lap_minutes % 10, 4); + } + + // lap sec + if (lap_last_displayed[2] == (lap_seconds / 10) % 10) { + painter.draw_char(LAP_S1_POS, *Theme::getInstance()->fg_light, '0' + (lap_seconds / 10) % 10, 4); + } + if (lap_last_displayed[3] == lap_seconds % 10) { + painter.draw_char(LAP_S2_POS, *Theme::getInstance()->fg_light, '0' + lap_seconds % 10, 4); + } + + // lap ms + if (lap_last_displayed[4] == (lap_milliseconds / 100) % 10) { + painter.draw_char(LAP_MS1_POS, *Theme::getInstance()->fg_light, '0' + (lap_milliseconds / 100) % 10, 2); + } + if (lap_last_displayed[5] == (lap_milliseconds / 10) % 10) { + painter.draw_char(LAP_MS2_POS, *Theme::getInstance()->fg_light, '0' + (lap_milliseconds / 10) % 10, 2); + } + if (lap_last_displayed[6] == lap_milliseconds % 10) { + painter.draw_char(LAP_MS3_POS, *Theme::getInstance()->fg_light, '0' + lap_milliseconds % 10, 2); + } + + // lp m + lap_last_displayed[0] = (lap_minutes / 10) % 10; + lap_last_displayed[1] = lap_minutes % 10; + + // lp s + lap_last_displayed[2] = (lap_seconds / 10) % 10; + lap_last_displayed[3] = lap_seconds % 10; + + // lp mss + lap_last_displayed[4] = (lap_milliseconds / 100) % 10; + lap_last_displayed[5] = (lap_milliseconds / 10) % 10; + lap_last_displayed[6] = lap_milliseconds % 10; + } } } // namespace ui::external_app::stopwatch \ No newline at end of file diff --git a/firmware/application/external/stopwatch/ui_stopwatch.hpp b/firmware/application/external/stopwatch/ui_stopwatch.hpp index 2cc7c9f78..c30856afc 100644 --- a/firmware/application/external/stopwatch/ui_stopwatch.hpp +++ b/firmware/application/external/stopwatch/ui_stopwatch.hpp @@ -1,5 +1,6 @@ /* * Copyright 2025 Mark Thompson + * copyleft Mr. Robot of F.Society * * This file is part of PortaPack. * @@ -39,25 +40,35 @@ class StopwatchView : public View { void stop(); void reset(); void lap(); + void cover_display_area_with_0(); + void refresh_painting(); + void resume_last(); + void clean_ms_display(uint8_t level = 0); bool running{false}; + bool paused{false}; long long start_time{0}; long long end_time{0}; long long lap_time{0}; long long previously_elapsed{0}; + uint8_t last_displayed[7] = {0, 0, 0, 0, 0, 0, 0}; + /* m m s s ms ms ms*/ + uint8_t lap_last_displayed[7] = {0, 0, 0, 0, 0, 0, 0}; + /* m m s s ms ms ms*/ Labels labels{ {{0 * 8, 1 * 16}, "TOTAL:", Theme::getInstance()->fg_light->foreground}, {{0 * 8, 7 * 16}, "LAP:", Theme::getInstance()->fg_light->foreground}, }; - BigFrequency big_display{ - {4, 2 * 16 + 4, 28 * 8, 52}, - 0}; + OptionsField options_ms_display_level{ + {4 * 8 * 4 + 7 * 8 + 4, 2 * 16}, + 5, + {{"& - -", 0}, + {"& & -", 1}, + {"& & &", 2}}}; - BigFrequency lap_display{ - {4, 8 * 16 + 4, 28 * 8, 52}, - 0}; + Painter painter; Button button_run_stop{ {72, 210, 96, 24}, diff --git a/firmware/common/ui_painter.cpp b/firmware/common/ui_painter.cpp index b5711453b..4728ff041 100644 --- a/firmware/common/ui_painter.cpp +++ b/firmware/common/ui_painter.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * copyleft Mr. Robot * * This file is part of PortaPack. * @@ -35,10 +36,34 @@ Style Style::invert() const { .foreground = background}; } -int Painter::draw_char(Point p, const Style& style, char c) { +int Painter::draw_char(Point p, const Style& style, char c, uint8_t zoom_factor) { const auto glyph = style.font.glyph(c); - display.draw_glyph(p, glyph, style.foreground, style.background); - return glyph.advance().x(); + + if (zoom_factor <= 1) { + display.draw_glyph(p, glyph, style.foreground, style.background); + } else { + // dot to square + const uint8_t* pixels = glyph.pixels(); + for (int y = 0; y < glyph.h(); y++) { // each line + for (int x = 0; x < glyph.w(); x++) { // each clum + // pos + int byte_index = (y * glyph.w() + x) / 8; + int bit_pos = (y * glyph.w() + x) % 8; + + // fill + Color pixel_color = ((pixels[byte_index] & (1 << bit_pos)) != 0) ? style.foreground : style.background; + /* ^ true: fg, false: bg + ^ the byte_index-th bit AKA current bit + ^ current px in current byte */ + display.fill_rectangle( + {p.x() + x * zoom_factor, p.y() + y * zoom_factor, + zoom_factor, zoom_factor}, + pixel_color); + } + } + } + + return glyph.advance().x() * zoom_factor; } int Painter::draw_string(Point p, const Style& style, std::string_view text) { diff --git a/firmware/common/ui_painter.hpp b/firmware/common/ui_painter.hpp index 5343826fe..11d47aae5 100644 --- a/firmware/common/ui_painter.hpp +++ b/firmware/common/ui_painter.hpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * copyleft Mr. Robot * * This file is part of PortaPack. * @@ -69,7 +70,7 @@ class Painter { Painter(const Painter&) = delete; Painter(Painter&&) = delete; - int draw_char(Point p, const Style& style, char c); + int draw_char(Point p, const Style& style, char c, uint8_t zoom_factor = 1); int draw_string(Point p, const Style& style, std::string_view text); int draw_string(Point p, const Font& font, Color foreground, Color background, std::string_view text);