diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 1e0fbcca6..40f06eca2 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -258,6 +258,7 @@ set(CPPSRC ook_file.cpp ui_baseband_stats_view.cpp ui_navigation.cpp + ui_notifications.cpp ui_record_view.cpp ui_sd_card_status_view.cpp ui/ui_alphanum.cpp diff --git a/firmware/application/apps/ble_tx_app.cpp b/firmware/application/apps/ble_tx_app.cpp index 75e1cf879..f7a2cb9fc 100644 --- a/firmware/application/apps/ble_tx_app.cpp +++ b/firmware/application/apps/ble_tx_app.cpp @@ -225,7 +225,7 @@ void BLETxView::send_packet() { uint8_t min = 0x00; uint8_t max = 0x0F; - hexDigit = min + std::rand() % (max - min + 1); + hexDigit = min + rand() % (max - min + 1); } break; default: hexDigit = 0; diff --git a/firmware/application/apps/ui_debug.cpp b/firmware/application/apps/ui_debug.cpp index 8382a9226..a9e06bf91 100644 --- a/firmware/application/apps/ui_debug.cpp +++ b/firmware/application/apps/ui_debug.cpp @@ -2640,7 +2640,7 @@ void DebugPmemView::update() { DebugScreenTest::DebugScreenTest(NavigationView& nav) : nav_{nav} { set_focusable(true); - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); } bool DebugScreenTest::on_key(const KeyEvent key) { @@ -2650,10 +2650,10 @@ bool DebugScreenTest::on_key(const KeyEvent key) { nav_.pop(); break; case KeyEvent::Down: - painter.fill_rectangle({0, 0, screen_width, screen_height}, std::rand()); + painter.fill_rectangle({0, 0, screen_width, screen_height}, rand()); break; case KeyEvent::Left: - pen_color = std::rand(); + pen_color = rand(); break; default: break; @@ -2676,7 +2676,7 @@ bool DebugScreenTest::on_touch(const TouchEvent event) { void DebugScreenTest::paint(Painter& painter) { painter.fill_rectangle({0, 16, screen_width, screen_height - 16}, Theme::getInstance()->bg_darkest->foreground); painter.draw_string({10 * 8, screen_height / 2}, *Theme::getInstance()->bg_darkest, "Use Stylus"); - pen_color = std::rand(); + pen_color = rand(); } /* DebugLCRView *******************************************************/ diff --git a/firmware/application/event_m0.cpp b/firmware/application/event_m0.cpp index d32fa0d3a..58560fbfe 100644 --- a/firmware/application/event_m0.cpp +++ b/firmware/application/event_m0.cpp @@ -256,7 +256,9 @@ ui::Widget* EventDispatcher::touch_widget(ui::Widget* const w, ui::TouchEvent ev if (!w->hidden()) { // To achieve reverse depth ordering (last object drawn is // considered "top"), descend first. - for (const auto child : w->children()) { + auto& children = w->children(); + for (auto it = children.rbegin(); it != children.rend(); ++it) { // reverse, bc the lastly added will be "top" if overlaps + const auto& child = *it; const auto touched_widget = touch_widget(child, event); if (touched_widget) { return touched_widget; diff --git a/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.cpp b/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.cpp index 1187b33e5..5885970ab 100644 --- a/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.cpp +++ b/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.cpp @@ -339,7 +339,7 @@ void AdultToysView::randomizeMac() { } void AdultToysView::randomChn() { - channel_number = 37 + std::rand() % (39 - 37 + 1); + channel_number = 37 + rand() % (39 - 37 + 1); field_frequency.set_value(get_freq_by_channel_number(channel_number)); } diff --git a/firmware/application/external/blackjack/ui_blackjack.cpp b/firmware/application/external/blackjack/ui_blackjack.cpp index 6b42b19b4..77390061f 100644 --- a/firmware/application/external/blackjack/ui_blackjack.cpp +++ b/firmware/application/external/blackjack/ui_blackjack.cpp @@ -72,7 +72,7 @@ void BlackjackView::paint(Painter& painter) { if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); init_deck(); } } diff --git a/firmware/application/external/blespam/ui_blespam.cpp b/firmware/application/external/blespam/ui_blespam.cpp index d90a290c4..93dde5ab4 100644 --- a/firmware/application/external/blespam/ui_blespam.cpp +++ b/firmware/application/external/blespam/ui_blespam.cpp @@ -132,7 +132,7 @@ void BLESpamView::reset() { } void BLESpamView::randomChn() { - channel_number = 37 + std::rand() % (39 - 37 + 1); + channel_number = 37 + rand() % (39 - 37 + 1); field_frequency.set_value(get_freq_by_channel_number(channel_number)); } diff --git a/firmware/application/external/breakout/ui_breakout.cpp b/firmware/application/external/breakout/ui_breakout.cpp index d894e7673..af7c07015 100644 --- a/firmware/application/external/breakout/ui_breakout.cpp +++ b/firmware/application/external/breakout/ui_breakout.cpp @@ -655,7 +655,7 @@ void BreakoutView::paint(Painter& painter) { if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); init_game(); } } diff --git a/firmware/application/external/dinogame/ui_dinogame.cpp b/firmware/application/external/dinogame/ui_dinogame.cpp index 20a108e57..90012e471 100644 --- a/firmware/application/external/dinogame/ui_dinogame.cpp +++ b/firmware/application/external/dinogame/ui_dinogame.cpp @@ -125,7 +125,7 @@ void DinoGameView::paint(Painter& painter) { if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); init_game(); } } diff --git a/firmware/application/external/doom/ui_doom.cpp b/firmware/application/external/doom/ui_doom.cpp index 275dc4b50..3f391bf72 100644 --- a/firmware/application/external/doom/ui_doom.cpp +++ b/firmware/application/external/doom/ui_doom.cpp @@ -204,11 +204,11 @@ void spawn_entity(uint8_t type, uint8_t x, uint8_t y) { void spawn_random_entity(uint8_t type) { if (num_entities >= MAX_ENTITIES) return; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); uint8_t spawn_x, spawn_y; do { - spawn_x = std::rand() % LEVEL_WIDTH; - spawn_y = std::rand() % LEVEL_HEIGHT; + spawn_x = rand() % LEVEL_WIDTH; + spawn_y = rand() % LEVEL_HEIGHT; } while (get_block_at(spawn_x, spawn_y) == 0xF || (spawn_x == (uint8_t)player.pos.x && spawn_y == (uint8_t)player.pos.y)); @@ -227,9 +227,9 @@ void remove_entity(uint8_t index) { bool spawned = false; while (!spawned && attempts < 50) { - std::srand(LPC_RTC->CTIME0 + attempts); - uint8_t spawn_x = std::rand() % LEVEL_WIDTH; - uint8_t spawn_y = std::rand() % LEVEL_HEIGHT; + srand(LPC_RTC->CTIME0 + attempts); + uint8_t spawn_x = rand() % LEVEL_WIDTH; + uint8_t spawn_y = rand() % LEVEL_HEIGHT; int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x; int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y; @@ -270,9 +270,9 @@ void initialize_level() { uint8_t attempts = 0; while (initial_enemies < 2 && num_entities < MAX_ENTITIES && attempts < 50) { - std::srand(LPC_RTC->CTIME0 + attempts); - int8_t offset_x = (std::rand() % 11) - 5; - int8_t offset_y = (std::rand() % 11) - 5; + srand(LPC_RTC->CTIME0 + attempts); + int8_t offset_x = (rand() % 11) - 5; + int8_t offset_y = (rand() % 11) - 5; if (abs(offset_x) < 3 && abs(offset_y) < 3) { attempts++; @@ -307,10 +307,10 @@ void initialize_level() { attempts = 0; while (num_entities < MIN_ENTITIES && attempts < 100) { - std::srand(LPC_RTC->CTIME0 + attempts + 100); + srand(LPC_RTC->CTIME0 + attempts + 100); - uint8_t spawn_x = std::rand() % LEVEL_WIDTH; - uint8_t spawn_y = std::rand() % LEVEL_HEIGHT; + uint8_t spawn_x = rand() % LEVEL_WIDTH; + uint8_t spawn_y = rand() % LEVEL_HEIGHT; int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x; int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y; @@ -1045,7 +1045,7 @@ void DoomView::on_show() { void DoomView::paint(Painter& painter) { if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); scene = 0; up = down = left = right = fired = false; jogging = view_height = 0; diff --git a/firmware/application/external/game2048/ui_game2048.cpp b/firmware/application/external/game2048/ui_game2048.cpp index 694d951da..4e68a5b1b 100644 --- a/firmware/application/external/game2048/ui_game2048.cpp +++ b/firmware/application/external/game2048/ui_game2048.cpp @@ -33,7 +33,7 @@ Game2048View::Game2048View(NavigationView& nav) void Game2048View::on_show() { if (!initialized) { - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); reset_game(); initialized = true; } diff --git a/firmware/application/external/random_password/ui_random_password.cpp b/firmware/application/external/random_password/ui_random_password.cpp index 5c6372eac..1c87d58f6 100644 --- a/firmware/application/external/random_password/ui_random_password.cpp +++ b/firmware/application/external/random_password/ui_random_password.cpp @@ -239,10 +239,10 @@ void RandomPasswordView::on_freqchg(int64_t freq) { } void RandomPasswordView::set_random_freq() { - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); // this is only for seed to visit random freq, the radio is still real random - auto random_freq = 100000000 + (std::rand() % 900000000); // 100mhz to 1ghz + auto random_freq = 100000000 + (rand() % 900000000); // 100mhz to 1ghz receiver_model.set_target_frequency(random_freq); field_frequency.set_value(random_freq); } @@ -297,12 +297,12 @@ void RandomPasswordView::new_password() { /// roll worker for (int i = 0; i < password_length * 2; i += 2) { unsigned int seed = seeds_deque[i]; - std::srand(seed); + srand(seed); uint8_t rollnum = (uint8_t)(seeds_deque[i + 1] % 128); uint8_t nu = 0; - for (uint8_t o = 0; o < rollnum; ++o) nu = std::rand(); + for (uint8_t o = 0; o < rollnum; ++o) nu = rand(); nu++; - char c = charset[std::rand() % charset.length()]; + char c = charset[rand() % charset.length()]; initial_password += c; } diff --git a/firmware/application/external/snake/ui_snake.cpp b/firmware/application/external/snake/ui_snake.cpp index 2b62e9f77..25ba87b75 100644 --- a/firmware/application/external/snake/ui_snake.cpp +++ b/firmware/application/external/snake/ui_snake.cpp @@ -221,7 +221,7 @@ void SnakeView::paint(Painter& painter) { (void)painter; if (!initialized) { initialized = true; - std::srand(LPC_RTC->CTIME0); + srand(LPC_RTC->CTIME0); init_game(); } } diff --git a/firmware/application/external/tetris/ui_tetris.cpp b/firmware/application/external/tetris/ui_tetris.cpp index 3d8886955..1f0119f90 100644 --- a/firmware/application/external/tetris/ui_tetris.cpp +++ b/firmware/application/external/tetris/ui_tetris.cpp @@ -605,7 +605,7 @@ void pause_game() { } int main() { - std::srand(GenerateRandomSeed()); + srand(GenerateRandomSeed()); Init(); ShowLevelMenu(); joystick.attach(&ReadJoystickForLevel, 0.3); diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp index a178801ba..02b7531d8 100755 --- a/firmware/application/main.cpp +++ b/firmware/application/main.cpp @@ -145,6 +145,21 @@ Continuous (Fox-oring) rffc507x::RFFC507x first_if; ui::SystemView* system_view_ptr; +static uint32_t random_seed_state = 123456789; +extern "C" int rand(void) { + random_seed_state ^= random_seed_state << 13; + random_seed_state ^= random_seed_state >> 17; + random_seed_state ^= random_seed_state << 5; + return (int)(random_seed_state & 0x7FFFFFFF); +} +extern "C" void srand(unsigned int seed) { + if (seed == 0) { + random_seed_state = 123456789; + } else { + random_seed_state = seed; + } +} + static void event_loop() { static ui::Context context; static ui::SystemView system_view{ diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 3f740bd79..22489189f 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -976,6 +976,8 @@ SystemView::SystemView( navigation_view.push(); + add_child(¬ification_view); + if (pmem::config_splash()) { navigation_view.push(); } @@ -1061,20 +1063,10 @@ SplashScreenView::SplashScreenView(NavigationView& nav) }; } -uint32_t SplashScreenView::myrand(uint32_t* state) { - uint32_t x = *state; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - *state = x; - return x; -} - void SplashScreenView::get_random_splash_file(std::filesystem::path& path) { path = u""; - uint32_t rng_state = LPC_RTC->CTIME0; - if (rng_state == 0) rng_state = 0xABBACAFE; + srand(LPC_RTC->CTIME0); DIR dir; FILINFO fno; @@ -1096,7 +1088,7 @@ void SplashScreenView::get_random_splash_file(std::filesystem::path& path) { (ext[3] == 'P' || ext[3] == 'p')) { valid_count++; // Reservoir Sampling: - if (((rng_state = myrand(&rng_state)) % valid_count) == 0) { + if ((rand() % valid_count) == 0) { path = splash_dir / fno.fname; } } diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index 29c834cb5..650800e29 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -42,7 +42,7 @@ #include "ui_audio.hpp" #include "ui_sd_card_status_view.hpp" #include "ui_dfu_menu.hpp" - +#include "ui_notifications.hpp" #include "bitmap.hpp" #include "ui_bmpview.hpp" #include "ff.h" @@ -366,7 +366,6 @@ class SplashScreenView : public View { Button button_done{ {screen_width, 0, 1, 1}, ""}; - uint32_t myrand(uint32_t* state); void get_random_splash_file(std::filesystem::path& path); }; @@ -447,11 +446,12 @@ class SystemView : public View { private: uint8_t overlay_active{0}; + NavigationView navigation_view{}; SystemStatusView status_view{navigation_view}; InformationView info_view{navigation_view}; + NotificationView notification_view{navigation_view}; DfuMenu overlay{navigation_view}; DfuMenu2 overlay2{navigation_view}; - NavigationView navigation_view{}; Context& context_; }; diff --git a/firmware/application/ui_notifications.cpp b/firmware/application/ui_notifications.cpp new file mode 100644 index 000000000..d116f8524 --- /dev/null +++ b/firmware/application/ui_notifications.cpp @@ -0,0 +1,131 @@ +#include "ui_notifications.hpp" + +#include "ui_navigation.hpp" + +extern ui::SystemView* system_view_ptr; + +namespace ui { + +NotificationEntryView::NotificationEntryView(const NotificationEntry& entry, NotificationView* notifhandler) + : entry_(entry), notifhandler_(notifhandler) { + add_children({&background, &border, &title_text, &message_text, &close_button}); + border.set_outline(true); + if (entry_.icon != NOTIF_ICON_NONE) { + add_child(&icon_image); + icon_image.set_parent_rect({UI_POS_X(0) + 2, UI_POS_Y(1), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)}); + message_text.set_parent_rect({UI_POS_X(2) + 2, UI_POS_Y(1), UI_POS_WIDTH_REMAINING(6) - 2, UI_POS_HEIGHT(2)}); + if (entry_.icon == NOTIF_ICON_MESSAGE) { + icon_image.set_bitmap(&bitmap_icon_pocsag); + } + + } else { + message_text.set_parent_rect({UI_POS_X(1), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(5) - 1, UI_POS_HEIGHT(2)}); + } + title_text.set_style(Theme::getInstance()->bg_dark); + title_text.set(entry_.title); + message_text.set_style(Theme::getInstance()->bg_darkest); + message_text.set(entry_.message); + + close_button.on_select = [this](Button&) { + if (notifhandler_) { + notifhandler_->remove_notification(entry_.id); + } + }; + + message_text.on_select = [this](Text&) { + if (!entry_.source_app.empty() && notifhandler_) { + notifhandler_->open_notification(entry_.source_app); + // notifhandler_->remove_notification(entry_.id); + } + }; + title_text.on_select = message_text.on_select; +} + +NotificationView::NotificationView(NavigationView& nav) + : nav_(nav) { + signal_token_tick_second = rtc_time::signal_tick_second += [this]() { + this->on_tick_second(); + }; +} + +NotificationView::~NotificationView() { + rtc_time::signal_tick_second -= signal_token_tick_second; +} + +void NotificationView::on_tick_second() { + bool changed = false; + for (size_t i = 0; i < notification_views_.size();) { + if (notification_views_[i]->entry()->increase_time(1000)) { + notification_views_.erase(notification_views_.begin() + i); + changed = true; + } else { + i++; + } + } + + if (changed) { + rearrange_notifications(); + } +} + +void NotificationView::rearrange_notifications() { + size_t count = children().size(); + while (children().size() > 0) { + remove_child(children().back()); + } + + const int entry_height = UI_POS_HEIGHT(3) + 1; + int index = 0; + + for (auto& view_ptr : notification_views_) { + view_ptr->set_parent_rect({UI_POS_X(0), + (Coord)(UI_POS_Y(0) + (index * entry_height)), + UI_POS_MAXWIDTH, + entry_height}); + add_child(view_ptr.get()); + index++; + } + + set_parent_rect({UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, (Coord)(notification_views_.size() * entry_height)}); + set_dirty(); + if (count != children().size()) { + // refresh screen!!! + system_view_ptr->set_dirty(); + } +} + +void NotificationView::add_notification(NotificationEntry& entry) { + if (notification_views_.size() >= max_notifications) { + notification_views_.erase(notification_views_.begin()); + } + + entry.id = ++curr_not_id; + notification_views_.push_back(std::make_unique(entry, this)); + + rearrange_notifications(); +} + +void NotificationView::remove_notification(uint16_t id) { + bool found = false; + size_t found_index = 0; + + for (size_t i = 0; i < notification_views_.size(); i++) { + if (notification_views_[i]->id() == id) { + found = true; + found_index = i; + break; + } + } + + if (found) { + notification_views_.erase(notification_views_.begin() + found_index); + rearrange_notifications(); + } +} + +void NotificationView::open_notification(std::string app_name) { + if (app_name.empty()) return; + nav_.StartAppByName(app_name.c_str()); +} + +} // namespace ui \ No newline at end of file diff --git a/firmware/application/ui_notifications.hpp b/firmware/application/ui_notifications.hpp new file mode 100644 index 000000000..0c3d59ad1 --- /dev/null +++ b/firmware/application/ui_notifications.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include "ui.hpp" +#include "ui_widget.hpp" +#include "signal.hpp" +#include "rtc_time.hpp" +#include "event_m0.hpp" + +namespace ui { + +class NotificationView; +class NavigationView; + +enum notification_icon_t : uint8_t { + NOTIF_ICON_NONE = 0, + NOTIF_ICON_MESSAGE +}; + +class NotificationEntry { + public: + std::string source_app{}; + std::string title{}; + std::string message{}; + notification_icon_t icon = NOTIF_ICON_NONE; + uint16_t timeout = 10000; + uint16_t id = 0; + + NotificationEntry() = default; + NotificationEntry(const std::string& source_app, const std::string& title, const std::string& message, notification_icon_t icon = NOTIF_ICON_NONE, uint16_t timeout = 10000, uint16_t id = 0) + : source_app(source_app), title(title), message(message), icon(icon), timeout(timeout), id(id) {} + + static NotificationEntry build(const std::string& source_app, const std::string& title, const std::string& message, notification_icon_t icon = NOTIF_ICON_NONE, uint16_t timeout = 10000) { + return NotificationEntry{source_app, title, message, icon, timeout, 0}; + } + + bool increase_time(uint16_t delta) { + current_time += delta; + return (current_time >= timeout); + } + + private: + uint16_t current_time = 0; +}; + +class NotificationEntryView : public View { + public: + NotificationEntryView(const NotificationEntry& entry, NotificationView* notifhandler); + + NotificationEntryView(const NotificationEntryView&) = delete; + NotificationEntryView& operator=(const NotificationEntryView&) = delete; + + ui::Rectangle background{{1, 1, UI_POS_MAXWIDTH - 2, UI_POS_HEIGHT(3) - 2}, Theme::getInstance()->bg_darkest->background}; + ui::Rectangle border{{0, 0, UI_POS_MAXWIDTH, UI_POS_HEIGHT(3) + 1}, Theme::getInstance()->bg_light->background}; + ui::Text title_text{{UI_POS_X(0) + 1, UI_POS_Y(0) + 1, UI_POS_WIDTH_REMAINING(4) - 3, UI_POS_HEIGHT(1)}, ""}; + ui::Text message_text{{UI_POS_X(1), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(5) - 1, UI_POS_HEIGHT(2)}, ""}; + ui::Image icon_image{}; // 16*16 bitmap + ui::Button close_button{{UI_POS_X_RIGHT(4) - 2, UI_POS_Y(0) + 1, UI_POS_WIDTH(4), UI_POS_HEIGHT(2)}, "X"}; + + uint16_t id() const { return entry_.id; } + NotificationEntry* entry() { return &entry_; } + + private: + NotificationEntry entry_; + NotificationView* notifhandler_; +}; + +class NotificationView : public View { + public: + NotificationView(NavigationView& nav); + ~NotificationView(); + + void add_notification(NotificationEntry& entry); + void remove_notification(uint16_t id); + void open_notification(std::string app_name); + + private: + void on_tick_second(); + void rearrange_notifications(); + + NavigationView& nav_; + + // Changed to unique_ptr to handle non-copyable Views safely + std::vector> notification_views_{}; + + SignalToken signal_token_tick_second{}; + + uint16_t curr_not_id = 0; + constexpr static uint8_t max_notifications = 4; + + MessageHandlerRegistration message_handler_notifs{ + Message::ID::NotificationData, [this](Message* const p) { + const auto message = static_cast(p); + NotificationEntry entry = NotificationEntry::build(message->source_app, message->title, message->message, static_cast(message->icon), message->timeout); + this->add_notification(entry); + }}; +}; + +} // namespace ui \ No newline at end of file diff --git a/firmware/application/usb_serial_shell.cpp b/firmware/application/usb_serial_shell.cpp index e1e0bd4f4..2356acf32 100644 --- a/firmware/application/usb_serial_shell.cpp +++ b/firmware/application/usb_serial_shell.cpp @@ -1418,6 +1418,44 @@ static void cmd_getdevtype(BaseSequentialStream* chp, int argc, char* argv[]) { chprintf(chp, res.c_str()); } +static void cmd_notification(BaseSequentialStream* chp, int argc, char* argv[]) { + const char* usage = "usage: notif [appname]\r\n"; + if (argc < 1) { + chprintf(chp, usage); + return; + } + int iconindex = atoi(argv[0]); + std::string appname = (argc > 1) ? argv[1] : ""; + chprintf(chp, "Send title, and a \r\n"); + std::string title{}; + uint8_t msg[1]{0}; + do { + size_t bytes_read = chSequentialStreamRead(chp, &msg[0], 1); + if (bytes_read != 1) + break; + if (msg[0] == '\r') // end of title + break; + if (msg[0] == '\n') continue; + title += (char)msg[0]; + } while (title.size() < 48); + std::string message{}; + chprintf(chp, "Send message, and a \r\n"); + do { + size_t bytes_read = chSequentialStreamRead(chp, &msg[0], 1); + if (bytes_read != 1) + break; + if (msg[0] == '\r') // end of message + break; + if (msg[0] == '\n') continue; + message += (char)msg[0]; + } while (message.size() < 298); + + NotificationDataMessage msgn{appname.c_str(), title.c_str(), message.c_str(), (uint8_t)iconindex}; + EventDispatcher::send_message(msgn); + + chprintf(chp, "ok\r\n"); +} + static const ShellCommand commands[] = { {"reboot", cmd_reboot}, {"dfu", cmd_dfu}, @@ -1456,6 +1494,7 @@ static const ShellCommand commands[] = { {"getres", cmd_getres}, {"getflash", cmd_getflash}, {"getdevtype", cmd_getdevtype}, + {"notif", cmd_notification}, {NULL, NULL}}; static const ShellConfig shell_cfg1 = { diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 9b91af233..5476077be 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -154,6 +154,7 @@ class Message { MorseTXkey = 96, StreamTXConfiguration = 97, RTTYData = 98, + NotificationData = 99, MAX }; @@ -1793,4 +1794,33 @@ class RTTYDataMessage : public Message { static constexpr uint16_t max_len = 490; }; +class NotificationDataMessage : public Message { + public: + constexpr NotificationDataMessage(const char* source_app, const char* title, const char* message, uint8_t icon = 0, uint16_t timeout = 10000) + : Message{ID::NotificationData}, + icon(icon), + timeout(timeout) { + if (source_app) { + size_t len = std::min(strlen(source_app), (size_t)19); + memcpy(this->source_app, source_app, len); + this->source_app[len] = '\0'; + } + if (title) { + size_t len = std::min(strlen(title), (size_t)49); + memcpy(this->title, title, len); + this->title[len] = '\0'; + } + if (message) { + size_t len = std::min(strlen(message), (size_t)299); + memcpy(this->message, message, len); + this->message[len] = '\0'; + } + } + char source_app[20]{0}; // source application name, null-terminated, max 19 chars + null + char title[50]{0}; // title, null-terminated, max 49 chars + null + char message[300]{0}; // message, null-terminated, max 299 chars + null + uint8_t icon = 0; + uint16_t timeout = 10000; +}; + #endif /*__MESSAGE_H__*/ diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index ee9b52b41..ab97924b2 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -397,18 +397,35 @@ void Text::getWidgetName(std::string& result) { void Text::paint(Painter& painter) { const auto rect = screen_rect(); auto s = has_focus() ? style().invert() : style(); - auto max_len = (unsigned)rect.width() / s.font.char_width(); - auto text_view = std::string_view{text}; - painter.fill_rectangle(rect, s.background); + const int char_width = s.font.char_width(); + const int line_height = s.font.line_height(); + if (char_width == 0 || line_height == 0) return; + const size_t chars_per_line = rect.width() / char_width; + if (chars_per_line == 0) return; + size_t lines_capacity = rect.height() / line_height; + if (lines_capacity == 0) lines_capacity = 1; // at least one line, even if it overflows vertically + auto text_view = std::string_view{text}; + size_t current_offset = 0; + for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) { + if (current_offset >= text_view.length()) break; + size_t chunk_len = std::min(chars_per_line, text_view.length() - current_offset); + painter.draw_string( + rect.location() + Point(0, line_idx * line_height), + s, + text_view.substr(current_offset, chunk_len)); + current_offset += chunk_len; + } +} - if (text_view.length() > max_len) - text_view = text_view.substr(0, max_len); - - painter.draw_string( - rect.location(), - s, - text_view); +bool Text::on_touch(const TouchEvent event) { + if (event.type == TouchEvent::Type::Start) { + if (on_select) { + on_select(*this); + return true; + } + } + return false; } /* Labels ****************************************************************/ diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 8795d1f43..9ae9542b0 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -209,6 +209,7 @@ class Rectangle : public Widget { class Text : public Widget { public: + std::function on_select{}; Text() : text{""} { } @@ -223,6 +224,8 @@ class Text : public Widget { void getAccessibilityText(std::string& result) override; void getWidgetName(std::string& result) override; + bool on_touch(const TouchEvent event) override; + protected: // NB: Don't truncate this string. The UI will only render // as many characters as will fit in its rectange. diff --git a/firmware/standalone/common/ui/ui_widget.cpp b/firmware/standalone/common/ui/ui_widget.cpp index 96e5b10ef..34c183d83 100644 --- a/firmware/standalone/common/ui/ui_widget.cpp +++ b/firmware/standalone/common/ui/ui_widget.cpp @@ -386,7 +386,9 @@ void Text::set(std::string_view value) { text = std::string{value}; set_dirty(); } - +std::string Text::get() { + return text; +} void Text::getAccessibilityText(std::string& result) { result = text; } @@ -396,18 +398,35 @@ void Text::getWidgetName(std::string& result) { void Text::paint(Painter& painter) { const auto rect = screen_rect(); auto s = has_focus() ? style().invert() : style(); - auto max_len = (unsigned)rect.width() / s.font.char_width(); - auto text_view = std::string_view{text}; - painter.fill_rectangle(rect, s.background); + const int char_width = s.font.char_width(); + const int line_height = s.font.line_height(); + if (char_width == 0 || line_height == 0) return; + const size_t chars_per_line = rect.width() / char_width; + if (chars_per_line == 0) return; + size_t lines_capacity = rect.height() / line_height; + if (lines_capacity == 0) lines_capacity = 1; // at least one line, even if it overflows vertically + auto text_view = std::string_view{text}; + size_t current_offset = 0; + for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) { + if (current_offset >= text_view.length()) break; + size_t chunk_len = std::min(chars_per_line, text_view.length() - current_offset); + painter.draw_string( + rect.location() + Point(0, line_idx * line_height), + s, + text_view.substr(current_offset, chunk_len)); + current_offset += chunk_len; + } +} - if (text_view.length() > max_len) - text_view = text_view.substr(0, max_len); - - painter.draw_string( - rect.location(), - s, - text_view); +bool Text::on_touch(const TouchEvent event) { + if (event.type == TouchEvent::Type::Start) { + if (on_select) { + on_select(*this); + return true; + } + } + return false; } /* Labels ****************************************************************/ diff --git a/firmware/standalone/common/ui/ui_widget.hpp b/firmware/standalone/common/ui/ui_widget.hpp index 344c3e7cf..0fe89e9ba 100644 --- a/firmware/standalone/common/ui/ui_widget.hpp +++ b/firmware/standalone/common/ui/ui_widget.hpp @@ -210,6 +210,7 @@ class Rectangle : public Widget { class Text : public Widget { public: + std::function on_select{}; Text() : text{""} { } @@ -218,11 +219,14 @@ class Text : public Widget { Text(Rect parent_rect); void set(std::string_view value); + std::string get(); void paint(Painter& painter) override; void getAccessibilityText(std::string& result) override; void getWidgetName(std::string& result) override; + bool on_touch(const TouchEvent event) override; + protected: // NB: Don't truncate this string. The UI will only render // as many characters as will fit in its rectange. diff --git a/firmware/standalone/digitalrain/digitalrain.hpp b/firmware/standalone/digitalrain/digitalrain.hpp index 18100971e..9a22e5621 100644 --- a/firmware/standalone/digitalrain/digitalrain.hpp +++ b/firmware/standalone/digitalrain/digitalrain.hpp @@ -125,7 +125,7 @@ class DigitalRain { public: DigitalRain() { - std::srand(0); + srand(0); WIDTH = screen_width; HEIGHT = screen_height + 5; COLS = WIDTH / CHAR_WIDTH; diff --git a/firmware/test/include/doctest.h b/firmware/test/include/doctest.h index 6a134e16f..d02516978 100644 --- a/firmware/test/include/doctest.h +++ b/firmware/test/include/doctest.h @@ -6996,7 +6996,7 @@ int Context::run() { } else if (p->order_by.compare("name", true) == 0) { std::sort(testArray.begin(), testArray.end(), nameOrderComparator); } else if (p->order_by.compare("rand", true) == 0) { - std::srand(p->rand_seed); + srand(p->rand_seed); // random_shuffle implementation const auto first = &testArray[0];