/** * @file main * */ /********************* * INCLUDES *********************/ #define _DEFAULT_SOURCE /* needed for usleep() */ #include #include #define SDL_MAIN_HANDLED /*To fix SDL's "undefined reference to WinMain" issue*/ #include #include "lvgl/lvgl.h" //#include "lvgl/examples/lv_examples.h" //#include "lv_demos/lv_demo.h" #include "lv_drivers/display/monitor.h" #include "lv_drivers/indev/mouse.h" #include "lv_drivers/indev/keyboard.h" #include "lv_drivers/indev/mousewheel.h" // get PineTime header #include "displayapp/InfiniTimeTheme.h" #include #include #include "BootloaderVersion.h" #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/ble/NotificationManager.h" #include "components/brightness/BrightnessController.h" #include "components/motor/MotorController.h" #include "components/datetime/DateTimeController.h" #include "components/heartrate/HeartRateController.h" #include "components/fs/FS.h" #include "drivers/Spi.h" #include "drivers/SpiMaster.h" #include "drivers/SpiNorFlash.h" #include "drivers/St7789.h" #include "drivers/TwiMaster.h" #include "drivers/Cst816s.h" #include "drivers/PinMap.h" #include "systemtask/SystemTask.h" #include "drivers/PinMap.h" #include "touchhandler/TouchHandler.h" #include "buttonhandler/ButtonHandler.h" // get the simulator-headers #include "displayapp/DisplayApp.h" #include "displayapp/LittleVgl.h" #include #include #include // initialize NRF_WDT and NRF_POWER #include #include #include #include // std::pow // additional includes for 'saveScreenshot()' function #include // put_time #include #include #include // localtime #if defined(WITH_PNG) #include #endif #include #include "img/sim_background.h" // provides variable SIM_BACKGROUND /********************* * DEFINES *********************/ /********************** * TYPEDEFS **********************/ // copied from lv_drivers/display/monitor.c to get the SDL_Window for the InfiniTime screen extern "C" { typedef struct { SDL_Window * window; SDL_Renderer * renderer; SDL_Texture * texture; volatile bool sdl_refr_qry; #if MONITOR_DOUBLE_BUFFERED uint32_t * tft_fb_act; #else uint32_t tft_fb[LV_HOR_RES_MAX * LV_VER_RES_MAX]; #endif }monitor_t; extern monitor_t monitor; } void saveScreenshot() { auto now = std::chrono::system_clock::now(); auto in_time_t = std::chrono::system_clock::to_time_t(now); // timestamped png filename std::stringstream ss; ss << "InfiniSim_" << std::put_time(std::localtime(&in_time_t), "%F_%H%M%S"); std::string screenshot_filename_base = ss.str(); // TODO: use std::format once we have C++20 and new enough GCC 13 //std::string screenshot_filename_base = std::format("InfiniSim_%F_%H%M%S", std::chrono::floor(now)); //std::string screenshot_filename_base = "InfiniSim"; const int width = 240; const int height = 240; auto renderer = monitor.renderer; #if defined(WITH_PNG) std::string screenshot_filename = screenshot_filename_base + ".png"; FILE * fp2 = fopen(screenshot_filename.c_str(), "wb"); if (!fp2) { // dealing with error return; } // 1. Create png struct pointer png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png_ptr){ // dealing with error } png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_write_struct(&png_ptr, (png_infopp)NULL); // dealing with error } int bit_depth = 8; png_init_io(png_ptr, fp2); png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, \ PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, \ PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); // 3. Convert 1d array to 2d array to be suitable for png struct // I assumed the original array is 1d std::array row_pointers; //png_bytepp row_pointers = (png_bytepp)png_malloc(png_ptr, sizeof(png_bytep) * height); for (int i = 0; i < height; i++) { row_pointers[i] = (png_bytep)png_malloc(png_ptr, width*4); } constexpr size_t zoom = MONITOR_ZOOM; const Uint32 format = SDL_PIXELFORMAT_RGBA8888; SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, width*zoom, height*zoom, 32, format); SDL_RenderReadPixels(renderer, NULL, format, surface->pixels, surface->pitch); png_bytep pixels = (png_bytep)surface->pixels; for (int hi = 0; hi < height; hi++) { for (int wi = 0; wi < width; wi++) { int c = wi * 4; row_pointers.at(hi)[wi*4+0] = pixels[hi*surface->pitch*zoom + wi*4*zoom + 3]; // red row_pointers.at(hi)[wi*4+1] = pixels[hi*surface->pitch*zoom + wi*4*zoom + 2]; // greeen row_pointers.at(hi)[wi*4+2] = pixels[hi*surface->pitch*zoom + wi*4*zoom + 1]; // blue row_pointers.at(hi)[wi*4+3] = 255; // alpha } } // 4. Write png file png_write_info(png_ptr, info_ptr); png_write_image(png_ptr, row_pointers.data()); png_write_end(png_ptr, info_ptr); png_destroy_write_struct(&png_ptr, &info_ptr); fclose(fp2); SDL_FreeSurface(surface); #else std::string screenshot_filename = screenshot_filename_base + ".bmp"; const Uint32 format = SDL_PIXELFORMAT_RGB888; SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 24, format); SDL_RenderReadPixels(renderer, NULL, format, surface->pixels, surface->pitch); SDL_SaveBMP(surface, screenshot_filename.c_str()); SDL_FreeSurface(surface); #endif std::cout << "InfiniSim: Screenshot created: " << screenshot_filename << std::endl; } class GifManager { private: GifWriter writer = {}; std::chrono::system_clock::time_point last_frame; bool in_progress = false; static constexpr uint32_t delay_ds = 100/20; // in 1/100 s, so 1 ds = 10 ms static constexpr int sdl_width = 240; static constexpr int sdl_height = 240; public: GifManager() {} ~GifManager() { if (in_progress) { close(); } } bool is_in_progress() const { return in_progress; } void create_new() { assert(!in_progress); auto now = std::chrono::system_clock::now(); auto in_time_t = std::chrono::system_clock::to_time_t(now); // timestamped png filename std::stringstream ss; ss << "InfiniSim_" << std::put_time(std::localtime(&in_time_t), "%F_%H%M%S"); std::string screenshot_filename_base = ss.str(); // TODO: use std::format once we have C++20 and new enough GCC 13 //std::string screenshot_filename_base = std::format("InfiniSim_%F_%H%M%S", std::chrono::floor(now)); std::string screenshot_filename = screenshot_filename_base + ".gif"; std::cout << "InfiniSim: Screen-capture started: " << screenshot_filename << std::endl; GifBegin( &writer, screenshot_filename.c_str(), sdl_width, sdl_height, delay_ds, 8, true ); in_progress = true; write_frame(true); } void write_frame(bool force = false) { assert(in_progress); auto now = std::chrono::system_clock::now(); if (force || ((now - last_frame) > std::chrono::milliseconds(delay_ds*10)) ) { last_frame = std::chrono::system_clock::now(); auto renderer = monitor.renderer; constexpr size_t zoom = MONITOR_ZOOM; const Uint32 format = SDL_PIXELFORMAT_RGBA8888; SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, sdl_width*zoom, sdl_height*zoom, 32, format); SDL_RenderReadPixels(renderer, NULL, format, surface->pixels, surface->pitch); uint8_t *pixels = (uint8_t*) surface->pixels; std::array image; for (int hi = 0; hi < sdl_height; hi++) { for (int wi = 0; wi < sdl_width; wi++) { auto red = pixels[hi*surface->pitch*zoom + wi*4*zoom + 3]; // red auto green = pixels[hi*surface->pitch*zoom + wi*4*zoom + 2]; // green auto blue = pixels[hi*surface->pitch*zoom + wi*4*zoom + 1]; // blue image[(hi * sdl_width + wi)*4 + 0] = red; image[(hi * sdl_width + wi)*4 + 1] = green; image[(hi * sdl_width + wi)*4 + 2] = blue; image[(hi * sdl_width + wi)*4 + 3] = 255; // no alpha } } GifWriteFrame(&writer, image.data(), sdl_width, sdl_height, delay_ds, 8, false); } } void close() { assert(in_progress); in_progress = false; GifEnd(&writer); std::cout << "InfiniSim: Screen-capture finished" << std::endl; } }; /********************** * STATIC PROTOTYPES **********************/ static void hal_init(void); static int tick_thread(void *data); /********************** * STATIC VARIABLES **********************/ lv_indev_t *kb_indev; lv_indev_t *mouse_indev = nullptr; /********************** * MACROS **********************/ /********************** * GLOBAL FUNCTIONS **********************/ void nrfx_gpiote_evt_handler(nrfx_gpiote_pin_t pin, nrf_gpiote_polarity_t action) {} /********************* * DEFINES *********************/ /********************** * TYPEDEFS **********************/ /********************** * VARIABLES **********************/ /********************** * STATIC PROTOTYPES **********************/ /********************** * GLOBAL FUNCTIONS **********************/ constexpr NRF_TWIM_Type *NRF_TWIM1 = nullptr; static constexpr uint8_t touchPanelTwiAddress = 0x15; static constexpr uint8_t motionSensorTwiAddress = 0x18; static constexpr uint8_t heartRateSensorTwiAddress = 0x44; Pinetime::Drivers::SpiMaster spi {Pinetime::Drivers::SpiMaster::SpiModule::SPI0, {Pinetime::Drivers::SpiMaster::BitOrder::Msb_Lsb, Pinetime::Drivers::SpiMaster::Modes::Mode3, Pinetime::Drivers::SpiMaster::Frequencies::Freq8Mhz, Pinetime::PinMap::SpiSck, Pinetime::PinMap::SpiMosi, Pinetime::PinMap::SpiMiso}}; Pinetime::Drivers::Spi lcdSpi {spi, Pinetime::PinMap::SpiLcdCsn}; Pinetime::Drivers::St7789 lcd {lcdSpi, Pinetime::PinMap::LcdDataCommand, Pinetime::PinMap::LcdReset}; Pinetime::Drivers::Spi flashSpi {spi, Pinetime::PinMap::SpiFlashCsn}; Pinetime::Drivers::SpiNorFlash spiNorFlash {"spiNorFlash.raw"}; // The TWI device should work @ up to 400Khz but there is a HW bug which prevent it from // respecting correct timings. According to erratas heet, this magic value makes it run // at ~390Khz with correct timings. static constexpr uint32_t MaxTwiFrequencyWithoutHardwareBug {0x06200000}; Pinetime::Drivers::TwiMaster twiMaster {NRF_TWIM1, MaxTwiFrequencyWithoutHardwareBug, Pinetime::PinMap::TwiSda, Pinetime::PinMap::TwiScl}; Pinetime::Drivers::Cst816S touchPanel; // {twiMaster, touchPanelTwiAddress}; //#ifdef PINETIME_IS_RECOVERY // #include "displayapp/DummyLittleVgl.h" // #include "displayapp/DisplayAppRecovery.h" //#else // #include "displayapp/LittleVgl.h" // #include "displayapp/DisplayApp.h" //#endif Pinetime::Drivers::Bma421 motionSensor {twiMaster, motionSensorTwiAddress}; Pinetime::Drivers::Hrs3300 heartRateSensor {twiMaster, heartRateSensorTwiAddress}; TimerHandle_t debounceTimer; TimerHandle_t debounceChargeTimer; Pinetime::Controllers::Battery batteryController; Pinetime::Controllers::Ble bleController; Pinetime::Controllers::HeartRateController heartRateController; Pinetime::Applications::HeartRateTask heartRateApp(heartRateSensor, heartRateController); Pinetime::Controllers::FS fs {spiNorFlash}; Pinetime::Controllers::Settings settingsController {fs}; Pinetime::Controllers::MotorController motorController {}; Pinetime::Controllers::DateTime dateTimeController {settingsController}; Pinetime::Drivers::Watchdog watchdog; Pinetime::Controllers::NotificationManager notificationManager; Pinetime::Controllers::MotionController motionController; #if defined(INFINITIME_TIMERCONTROLLER) Pinetime::Controllers::TimerController timerController; #endif Pinetime::Controllers::AlarmController alarmController {dateTimeController}; Pinetime::Controllers::TouchHandler touchHandler; Pinetime::Controllers::ButtonHandler buttonHandler; Pinetime::Controllers::BrightnessController brightnessController {}; Pinetime::Applications::DisplayApp displayApp(lcd, touchPanel, batteryController, bleController, dateTimeController, watchdog, notificationManager, heartRateController, settingsController, motorController, motionController, #if defined(INFINITIME_TIMERCONTROLLER) timerController, #endif alarmController, brightnessController, touchHandler, fs); Pinetime::System::SystemTask systemTask(spi, spiNorFlash, twiMaster, touchPanel, batteryController, bleController, dateTimeController, #if defined(INFINITIME_TIMERCONTROLLER) timerController, #endif alarmController, watchdog, notificationManager, heartRateSensor, motionController, motionSensor, settingsController, heartRateController, displayApp, heartRateApp, fs, touchHandler, buttonHandler); // variable used in SystemTask.cpp Work loop std::chrono::time_point NoInit_BackUpTime; class Framework { public: // Contructor which initialize the parameters. Framework(bool visible_, int height_, int width_) : visible(visible_), height(height_), width(width_) { if (visible) { //SDL_Init(SDL_INIT_VIDEO); // Initializing SDL as Video SDL_CreateWindowAndRenderer(width, height, 0, &window, &renderer); SDL_SetWindowTitle(window, "LV Simulator Status"); { // move window a bit to the right, to not be over the PineTime Screen int x,y; SDL_GetWindowPosition(window, &x, &y); SDL_SetWindowPosition(window, x+LV_HOR_RES_MAX, y); } SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); // setting draw color SDL_RenderClear(renderer); // Clear the newly created window SDL_RenderPresent(renderer); // Reflects the changes done in the // window. } init_NRF_WDT(); init_NRF_POWER(); // Attempt to load background BMP from memory for the status display window const size_t SIM_BACKGROUND_size = sizeof(SIM_BACKGROUND); SDL_RWops *rw = SDL_RWFromMem((void*)SIM_BACKGROUND, SIM_BACKGROUND_size); SDL_Surface* simDisplayBgRaw = SDL_LoadBMP_RW(rw, 1); if (simDisplayBgRaw == NULL) { printf("Failed to load sim background image: %s\n", SDL_GetError()); } else { // convert the loaded image into a texture simDisplayTexture = SDL_CreateTextureFromSurface(renderer, simDisplayBgRaw); SDL_FreeSurface(simDisplayBgRaw); simDisplayBgRaw = NULL; } motorController.Init(); settingsController.Init(); printf("initial free_size = %u\n", xPortGetFreeHeapSize()); // update time to current system time once on startup dateTimeController.SetCurrentTime(std::chrono::system_clock::now()); systemTask.Start(); // initialize the first LVGL screen //const auto clockface = settingsController.GetClockFace(); //switch_to_screen(1+clockface); } // Destructor ~Framework(){ if (simDisplayTexture != NULL) { SDL_DestroyTexture(simDisplayTexture); } SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); } void draw_circle_red(int center_x, int center_y, int radius_){ SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); draw_circle_(center_x, center_y, radius_); } void draw_circle_green(int center_x, int center_y, int radius_){ SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); draw_circle_(center_x, center_y, radius_); } void draw_circle_blue(int center_x, int center_y, int radius_){ SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255); draw_circle_(center_x, center_y, radius_); } void draw_circle_yellow(int center_x, int center_y, int radius_){ SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255); draw_circle_(center_x, center_y, radius_); } void draw_circle_grey(int center_x, int center_y, int radius_){ SDL_SetRenderDrawColor(renderer, 128, 128, 128, 255); draw_circle_(center_x, center_y, radius_); } void draw_circle_white(int center_x, int center_y, int radius_){ SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); draw_circle_(center_x, center_y, radius_); } void draw_circle_(int center_x, int center_y, int radius_){ // Drawing circle for(int x=center_x-radius_; x<=center_x+radius_; x++){ for(int y=center_y-radius_; y<=center_y+radius_; y++){ if((std::pow(center_y-y,2)+std::pow(center_x-x,2)) <= std::pow(radius_,2)){ SDL_RenderDrawPoint(renderer, x, y); } } } } void refresh() { // left edge for all "bubbles" (circles) constexpr const int bubbleLeftEdge = 65; // always refresh the LVGL screen this->refresh_screen(); if (!visible) { return; } SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); SDL_RenderClear(renderer); // Render the background if it was able to be loaded if (simDisplayTexture != NULL) { SDL_RenderCopy(renderer, simDisplayTexture, NULL, NULL); } { // motorController.motor_is_running constexpr const int center_x = bubbleLeftEdge; constexpr const int center_y = 216; bool motor_is_running = nrf_gpio_pin_read(Pinetime::PinMap::Motor); if (motor_is_running) { draw_circle_red(center_x, center_y, 15); } else { draw_circle_grey(center_x, center_y, 15); } } { // ble.motor_is_running constexpr const int center_x = bubbleLeftEdge; constexpr const int center_y = 24; if (bleController.IsConnected()) { draw_circle_blue(center_x, center_y, 15); } else { draw_circle_grey(center_x, center_y, 15); } } // batteryController.percentRemaining { const int center_x = bubbleLeftEdge; const int center_y = 164; const int max_bar_length = 150; const int filled_bar_length = max_bar_length * (batteryController.percentRemaining/100.0); const int rect_height = 14; SDL_Rect rect { .x = center_x - rect_height/2, .y = center_y, .w = max_bar_length, .h = rect_height }; SDL_SetRenderDrawColor(renderer, 128, 128, 128, 255); SDL_RenderDrawRect(renderer, &rect); rect.w = filled_bar_length; rect.h++; rect.h-=2; SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255); SDL_RenderFillRect(renderer, &rect); //set color and new x pos, draw again } { // batteryController.isCharging constexpr const int center_x = bubbleLeftEdge; constexpr const int center_y = 120; if (batteryController.isCharging) { draw_circle_yellow(center_x, center_y, 15); } else { draw_circle_grey(center_x, center_y, 15); } } { // brightnessController.Level constexpr const int center_y = 72; const Pinetime::Controllers::BrightnessController::Levels level = brightnessController.Level(); uint8_t level_idx = 0; if (level == Pinetime::Controllers::BrightnessController::Levels::Low) { level_idx = 1; } else if (level == Pinetime::Controllers::BrightnessController::Levels::Medium) { level_idx = 2; } else if (level == Pinetime::Controllers::BrightnessController::Levels::High) { level_idx = 3; } for (uint8_t i=0; i<4; i++) { const int bubble_size = (i*2) + 5; const int center_x = bubbleLeftEdge + ((bubble_size+10) * i) - 5; if (i <= level_idx) { draw_circle_white(center_x, center_y, bubble_size); } else { draw_circle_grey(center_x, center_y, bubble_size); } } } // Show the change on the screen SDL_RenderPresent(renderer); } // prepared notficitions, one per message category const std::vector notification_messages { "0category:\nUnknown", "Lorem ipsum\ndolor sit amet,\nconsectetur adipiscing elit,\n", "1SimpleAlert", "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "2Email:", "Vitae aliquet nec ullamcorper sit amet.", "3News:", "Id\naliquet\nrisus\nfeugiat\nin\nante\nmetus\ndictum\nat.", "4IncomingCall:", "Ut porttitor leo a diam sollicitudin.", "5MissedCall:", "Ultrices tincidunt arcu non sodales neque sodales ut etiam sit.", "6Sms:", "Pellentesque dignissim enim sit amet.", "7VoiceMail:", "Urna nec tincidunt praesent semper feugiat nibh sed pulvinar proin.", "8Schedule:", "Tellus id interdum velit laoreet id donec ultrices tincidunt.", "9HighProriotyAlert:", "Viverra maecenas accumsan lacus vel facilisis volutpat est velit egestas.", "10InstantMessage:", "Volutpat consequat mauris nunc congue.", }; size_t notification_idx = 0; // which message to send next void send_notification() { Pinetime::Controllers::NotificationManager::Notification notif; const std::string &title = notification_messages.at(notification_idx*2); const std::string &message = notification_messages.at(notification_idx*2+1); std::copy(title.begin(), title.end(), notif.message.data()); notif.message[title.size()] = '\0'; // title and message is \0 separated std::copy(message.begin(), message.end(), notif.message.data()+title.size()+1); notif.message[title.size() + 1 + message.size()] = '\0'; // zero terminate the message notif.size = title.size() + 1 + message.size(); notif.category = static_cast(notification_idx % 11); notificationManager.Push(std::move(notif)); // send next message the next time notification_idx++; if (notification_idx >= notification_messages.size()/2) { notification_idx = 0; } if (settingsController.GetNotificationStatus() == Pinetime::Controllers::Settings::Notification::On) { if (screen_off_created) { // wake up! (deletes screen_off label) systemTask.PushMessage(Pinetime::System::Messages::GoToRunning); } displayApp.PushMessage(Pinetime::Applications::Display::Messages::NewNotification); } } // can't use SDL_PollEvent, as those are fed to lvgl // implement a non-descructive key-pressed handler (as not consuming SDL_Events) void handle_keys() { const Uint8 *state = SDL_GetKeyboardState(NULL); const bool key_shift = state[SDL_SCANCODE_LSHIFT] || state[SDL_SCANCODE_RSHIFT]; auto debounce = [&] (const char key_low, const char key_capital, const bool scancode, bool &key_handled) { if (scancode && !key_handled) { if (key_shift) { this->handle_key(key_capital); } else { this->handle_key(key_low); } key_handled = true; } else if (scancode && key_handled) { // ignore, already handled } else { key_handled = false; } }; debounce('r', 'R', state[SDL_SCANCODE_R], key_handled_r); debounce('n', 'N', state[SDL_SCANCODE_N], key_handled_n); debounce('m', 'M', state[SDL_SCANCODE_M], key_handled_m); debounce('b', 'B', state[SDL_SCANCODE_B], key_handled_b); debounce('v', 'V', state[SDL_SCANCODE_V], key_handled_v); debounce('c', 'C', state[SDL_SCANCODE_C], key_handled_c); debounce('l', 'L', state[SDL_SCANCODE_L], key_handled_l); debounce('p', 'P', state[SDL_SCANCODE_P], key_handled_p); debounce('s', 'S', state[SDL_SCANCODE_S], key_handled_s); debounce('h', 'H', state[SDL_SCANCODE_H], key_handled_h); debounce('i', 'I', state[SDL_SCANCODE_I], key_handled_i); debounce('w', 'W', state[SDL_SCANCODE_W], key_handled_w); // screen switcher buttons debounce('1', '!'+1, state[SDL_SCANCODE_1], key_handled_1); debounce('2', '!'+2, state[SDL_SCANCODE_2], key_handled_2); debounce('3', '!'+3, state[SDL_SCANCODE_3], key_handled_3); debounce('4', '!'+4, state[SDL_SCANCODE_4], key_handled_4); debounce('5', '!'+5, state[SDL_SCANCODE_5], key_handled_5); debounce('6', '!'+6, state[SDL_SCANCODE_6], key_handled_6); debounce('7', '!'+7, state[SDL_SCANCODE_7], key_handled_7); debounce('8', '!'+8, state[SDL_SCANCODE_8], key_handled_8); debounce('9', '!'+9, state[SDL_SCANCODE_9], key_handled_9); debounce('0', '!'+0, state[SDL_SCANCODE_0], key_handled_0); // direction keys debounce(':', ':', state[SDL_SCANCODE_UP], key_handled_up); debounce(';', ';', state[SDL_SCANCODE_DOWN], key_handled_down); debounce('<', '<', state[SDL_SCANCODE_LEFT], key_handled_left); debounce('>', '>', state[SDL_SCANCODE_RIGHT], key_handled_right); } // inject a swipe gesture to the touch handler and notify displayapp to notice it void send_gesture(Pinetime::Drivers::Cst816S::Gestures gesture) { Pinetime::Drivers::Cst816S::TouchInfos info; info.isValid = true; info.touching = true; info.gesture = gesture; touchHandler.ProcessTouchInfo(info); displayApp.PushMessage(Pinetime::Applications::Display::Messages::TouchEvent); info.touching = false; info.gesture = Pinetime::Drivers::Cst816S::Gestures::None; touchHandler.ProcessTouchInfo(info); } // modify the simulated controller depending on the pressed key void handle_key(SDL_Keycode key) { if (key == 'r') { motorController.StartRinging(); } else if (key == 'R') { motorController.StopRinging(); } else if (key == 'm') { motorController.RunForDuration(100); } else if (key == 'M') { motorController.RunForDuration(255); } else if (key == 'n') { send_notification(); } else if (key == 'N') { notificationManager.ClearNewNotificationFlag(); } else if (key == 'b') { bleController.Connect(); } else if (key == 'B') { bleController.Disconnect(); } else if (key == 'v') { if (batteryController.percentRemaining >= 90) { batteryController.percentRemaining = 100; } else { batteryController.percentRemaining += 10; } } else if (key == 'V') { if (batteryController.percentRemaining <= 10) { batteryController.percentRemaining = 0; } else { batteryController.percentRemaining -= 10; } } else if (key == 'c') { batteryController.isCharging = true; batteryController.isPowerPresent = true; } else if (key == 'C') { batteryController.isCharging = false; batteryController.isPowerPresent = false; } else if (key == 'l' && !screen_off_created) { brightnessController.Higher(); } else if (key == 'L' && !screen_off_created) { brightnessController.Lower(); } else if (key == 'p') { this->print_memory_usage = true; } else if (key == 'P') { this->print_memory_usage = false; } else if (key == 's') { motionSensor.steps += 500; } else if (key == 'S') { if (motionSensor.steps > 500) { motionSensor.steps -= 500; } else { motionSensor.steps = 0; } } else if (key == 'h') { if (heartRateController.State() == Pinetime::Controllers::HeartRateController::States::Stopped) { heartRateController.Start(); } else if (heartRateController.State() == Pinetime::Controllers::HeartRateController::States::NotEnoughData) { heartRateController.Update(Pinetime::Controllers::HeartRateController::States::Running, 10); } else { uint8_t heartrate = heartRateController.HeartRate(); heartRateController.Update(Pinetime::Controllers::HeartRateController::States::Running, heartrate + 10); } } else if (key == 'H') { heartRateController.Stop(); } else if (key == 'i') { saveScreenshot(); } else if (key == 'I') { if (!gif_manager.is_in_progress()) { gif_manager.create_new(); } else { gif_manager.close(); } } else if (key == 'w') { generate_weather_data(false); } else if (key == 'W') { generate_weather_data(true); } else if (key >= '0' && key <= '9') { this->switch_to_screen(key-'0'); } else if (key >= '!'+0 && key <= '!'+9) { this->switch_to_screen(key-'!'+10); } else if (key == ':') { // up send_gesture(Pinetime::Drivers::Cst816S::Gestures::SlideUp); } else if (key == ';') { // down send_gesture(Pinetime::Drivers::Cst816S::Gestures::SlideDown); } else if (key == '<') { // left send_gesture(Pinetime::Drivers::Cst816S::Gestures::SlideLeft); } else if (key == '>') { // up send_gesture(Pinetime::Drivers::Cst816S::Gestures::SlideRight); } batteryController.voltage = batteryController.percentRemaining * 50; } void generate_weather_data(bool clear) { if (clear) { systemTask.nimble().weather().SetCurrentWeather(0, 0, 0); std::array days; systemTask.nimble().weather().SetForecast(0, days); return; } auto timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); srand((int)timestamp); // Generate current weather data int16_t temperature = (rand() % 81 - 40) * 100; systemTask.nimble().weather().SetCurrentWeather((uint64_t)timestamp, temperature, rand() % 9); // Generate forecast data std::array days; for (int i = 0; i < Pinetime::Controllers::SimpleWeatherService::MaxNbForecastDays; i++) { days[i] = Pinetime::Controllers::SimpleWeatherService::Forecast::Day { (int16_t)(temperature - rand() % 10 * 100), (int16_t)(temperature + rand() % 10 * 100), Pinetime::Controllers::SimpleWeatherService::Icons(rand() % 9) }; } systemTask.nimble().weather().SetForecast((uint64_t)timestamp, days); } void handle_touch_and_button() { int x, y; uint32_t buttons = SDL_GetMouseState(&x, &y); const bool left_click = (buttons & SDL_BUTTON_LMASK) != 0; const bool right_click = (buttons & SDL_BUTTON_RMASK) != 0; if (left_click) { left_release_sent = false; systemTask.OnTouchEvent(); return; } else { if (!left_release_sent) { left_release_sent = true; systemTask.OnTouchEvent(); return; } } if (right_click != right_last_state) { right_last_state =right_click; systemTask.PushMessage(Pinetime::System::Messages::HandleButtonEvent); return; } } // helper function to switch between screens void switch_to_screen(uint8_t screen_idx) { if (screen_idx == 1) { settingsController.SetWatchFace(Pinetime::Applications::WatchFace::Analog); displayApp.StartApp(Pinetime::Applications::Apps::Clock, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 2) { settingsController.SetWatchFace(Pinetime::Applications::WatchFace::PineTimeStyle); displayApp.StartApp(Pinetime::Applications::Apps::Clock, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 4) { displayApp.StartApp(Pinetime::Applications::Apps::Paddle, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 5) { displayApp.StartApp(Pinetime::Applications::Apps::Twos, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 6) { displayApp.StartApp(Pinetime::Applications::Apps::Metronome, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 7) { displayApp.StartApp(Pinetime::Applications::Apps::FirmwareUpdate, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 8) { displayApp.StartApp(Pinetime::Applications::Apps::BatteryInfo, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 9) { displayApp.StartApp(Pinetime::Applications::Apps::FlashLight, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 0) { displayApp.StartApp(Pinetime::Applications::Apps::QuickSettings, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 11) { displayApp.StartApp(Pinetime::Applications::Apps::Music, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 12) { displayApp.StartApp(Pinetime::Applications::Apps::Paint, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 13) { displayApp.StartApp(Pinetime::Applications::Apps::SysInfo, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 14) { displayApp.StartApp(Pinetime::Applications::Apps::Steps, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 15) { displayApp.StartApp(Pinetime::Applications::Apps::Error, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else if (screen_idx == 17) { displayApp.StartApp(Pinetime::Applications::Apps::PassKey, Pinetime::Applications::DisplayApp::FullRefreshDirections::None); } else { std::cout << "unhandled screen_idx: " << int(screen_idx) << std::endl; } } static void screen_off_delete_cb(lv_obj_t *obj, lv_event_t event) { if (event == LV_EVENT_DELETE) { auto* fw = static_cast(obj->user_data); if (obj == fw->screen_off_bg) { // on delete make sure to not double free the screen_off objects fw->screen_off_created = false; } } } // render the current status of the simulated controller void refresh_screen() { const Pinetime::Controllers::BrightnessController::Levels level = brightnessController.Level(); if (level == Pinetime::Controllers::BrightnessController::Levels::Off) { if (!screen_off_created) { screen_off_created = true; screen_off_bg = lv_obj_create(lv_scr_act(), nullptr); lv_obj_set_size(screen_off_bg, 240, 240); lv_obj_set_pos(screen_off_bg, 0, 0); lv_obj_set_style_local_bg_color(screen_off_bg, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); screen_off_bg->user_data = this; // add callback to prevent double free through Alarm Screen lv_obj_set_event_cb(screen_off_bg, screen_off_delete_cb); screen_off_label = lv_label_create(lv_scr_act(), nullptr); lv_label_set_text_static(screen_off_label, "Screen is OFF"); lv_obj_align(screen_off_label, nullptr, LV_ALIGN_CENTER, 0, -20); } /* Periodically call the lv_task handler. * It could be done in a timer interrupt or an OS task too.*/ // only call the task handler if the screen is off, // when the screen is enabled the call is done in the SystemTask class lv_task_handler(); } else { if (screen_off_created) { screen_off_created = false; lv_obj_del(screen_off_bg); lv_obj_del(screen_off_label); } } if (print_memory_usage) { auto currentFreeHeap = xPortGetFreeHeapSize(); if (currentFreeHeap != lastFreeHeapSize) { auto minimumEverFreeHeap = xPortGetMinimumEverFreeHeapSize(); // 14KiB is the LVGL memory size used in InfiniTime constexpr uint32_t pinetime_heap_memory = configTOTAL_HEAP_SIZE; uint32_t mem_used = pinetime_heap_memory - currentFreeHeap; // The "budget" value shows how much free lvgl memory the PineTime // would have free and will go negative when more memory is used // in the simulator than is available on the real hardware. int32_t budget = configTOTAL_HEAP_SIZE - mem_used; printf("Mem: %5u used (change: %+5d, peak: %5u) %d budget left\n", mem_used, lastFreeHeapSize - currentFreeHeap, minimumEverFreeHeap, budget); lastFreeHeapSize = currentFreeHeap; } } if (gif_manager.is_in_progress()) { gif_manager.write_frame(); } } bool print_memory_usage = false; // variables to create and destroy an lvgl overlay to indicate a turned off screen bool screen_off_created = false; lv_obj_t *screen_off_bg; lv_obj_t *screen_off_label; private: bool key_handled_r = false; // r ... enable ringing, R ... disable ringing bool key_handled_m = false; // m ... let motor run, M ... stop motor bool key_handled_n = false; // n ... send notification, N ... clear new notification flag bool key_handled_b = false; // b ... connect Bluetooth, B ... disconnect Bluetooth bool key_handled_v = false; // battery Voltage and percentage, v ... increase, V ... decrease bool key_handled_c = false; // c ... charging, C ... not charging bool key_handled_l = false; // l ... increase brightness level, L ... lower brightness level bool key_handled_p = false; // p ... enable print memory usage, P ... disable print memory usage bool key_handled_s = false; // s ... increase step count, S ... decrease step count bool key_handled_h = false; // h ... set heartrate running, H ... stop heartrate bool key_handled_i = false; // i ... take screenshot, I ... start/stop Gif screen capture bool key_handled_w = false; // w ... generate weather data, W ... clear weather data // numbers from 0 to 9 to switch between screens bool key_handled_1 = false; bool key_handled_2 = false; bool key_handled_3 = false; bool key_handled_4 = false; bool key_handled_5 = false; bool key_handled_6 = false; bool key_handled_7 = false; bool key_handled_8 = false; bool key_handled_9 = false; bool key_handled_0 = false; // direction arrows bool key_handled_up = false; // inject swipe up bool key_handled_down = false; // inject swipe down bool key_handled_left = false; // inject swipe left bool key_handled_right = false; // inject swipe right bool visible; // show Simulator window int height; // Height of the window int width; // Width of the window SDL_Renderer *renderer = NULL; // Pointer for the renderer SDL_Window *window = NULL; // Pointer for the window SDL_Texture* simDisplayTexture = NULL; // Background for the sim status window bool left_release_sent = true; // make sure to send one mouse button release event bool right_last_state = false; // varable used to send message only on changing state size_t lastFreeHeapSize = configTOTAL_HEAP_SIZE; GifManager gif_manager; }; int mallocFailedCount = 0; int stackOverflowCount = 0; int main(int argc, char **argv) { // parse arguments bool fw_status_window_visible = true; bool arg_help = false; for (int i=1; i