diff --git a/generated/ui_320x240/images.c b/generated/ui_320x240/images.c index 925c229..070387e 100644 --- a/generated/ui_320x240/images.c +++ b/generated/ui_320x240/images.c @@ -1,6 +1,6 @@ #include "images.h" -const ext_img_desc_t images[68] = { +const ext_img_desc_t images[69] = { { "meshtastic_boot_logo_image", &img_meshtastic_boot_logo_image }, { "settings_button_image", &img_settings_button_image }, { "map_button_image", &img_map_button_image }, @@ -69,4 +69,5 @@ const ext_img_desc_t images[68] = { { "home_fair_signal_image", &img_home_fair_signal_image }, { "home_strong_signal_image", &img_home_strong_signal_image }, { "home_good_signal_image", &img_home_good_signal_image }, + { "lock_slash_image", &img_lock_slash_image }, }; diff --git a/generated/ui_320x240/images.h b/generated/ui_320x240/images.h index 31aeda2..e0ede36 100644 --- a/generated/ui_320x240/images.h +++ b/generated/ui_320x240/images.h @@ -75,6 +75,7 @@ extern const lv_img_dsc_t img_home_weak_signal_image; extern const lv_img_dsc_t img_home_fair_signal_image; extern const lv_img_dsc_t img_home_strong_signal_image; extern const lv_img_dsc_t img_home_good_signal_image; +extern const lv_img_dsc_t img_lock_slash_image; #ifndef EXT_IMG_DESC_T #define EXT_IMG_DESC_T @@ -84,7 +85,7 @@ typedef struct _ext_img_desc_t { } ext_img_desc_t; #endif -extern const ext_img_desc_t images[68]; +extern const ext_img_desc_t images[69]; #ifdef __cplusplus diff --git a/generated/ui_320x240/ui_image_lock_slash_image.c b/generated/ui_320x240/ui_image_lock_slash_image.c new file mode 100644 index 0000000..c89148f --- /dev/null +++ b/generated/ui_320x240/ui_image_lock_slash_image.c @@ -0,0 +1,79 @@ +#ifdef __has_include + #if __has_include("lvgl.h") + #ifndef LV_LVGL_H_INCLUDE_SIMPLE + #define LV_LVGL_H_INCLUDE_SIMPLE + #endif + #endif +#endif + +#if defined(LV_LVGL_H_INCLUDE_SIMPLE) +#include "lvgl.h" +#elif defined(LV_BUILD_TEST) +#include "../lvgl.h" +#else +#include "lvgl/lvgl.h" +#endif + + +#ifndef LV_ATTRIBUTE_MEM_ALIGN +#define LV_ATTRIBUTE_MEM_ALIGN +#endif + +#ifndef LV_ATTRIBUTE_IMG_LOCK_SLASH_IMAGE +#define LV_ATTRIBUTE_IMG_LOCK_SLASH_IMAGE +#endif + +static const +LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMG_LOCK_SLASH_IMAGE +uint8_t img_lock_slash_image_map[] = { + + 0xdb,0xde,0x3c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xdb,0xde,0x00,0x00,0x00,0x00,0xdb,0xde,0xba,0xd6,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xba,0xd6,0xdb,0xde,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x3c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x3c,0xe7,0xdb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xdb,0xde,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x3c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x9e,0xf7,0xdb,0xde,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x3c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xdb,0xde,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xdb,0xde,0x3c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xba,0xd6,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xba,0xd6,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0xfb,0xde,0x3c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x5d,0xef,0xdb,0xde,0xdb,0xde,0x5d,0xef,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0xfb,0xde,0x3c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x3c,0xe7,0xdb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xba,0xd6,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0xdb,0xde,0xba,0xd6,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x3c,0xe7,0xfb,0xde,0x18,0xc6,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xba,0xd6,0xdb,0xde,0x00,0x00,0x00,0x00, + 0x00,0x00,0xba,0xd6,0xba,0xd6,0x3c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x3c,0xe7,0xba,0xd6,0xba,0xd6,0x00,0x00, + 0x00,0x00,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0x00,0x00, + 0x00,0x00,0xfb,0xde,0xdb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x5d,0xef,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xdb,0xde,0xfb,0xde,0x00,0x00, + 0x00,0x00,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x00,0x00, + 0x00,0x00,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x79,0xce,0xfb,0xde,0x3c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x00,0x00, + 0x00,0x00,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x00,0x00,0x00,0x00,0xfb,0xde,0x3c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x00,0x00, + 0x00,0x00,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x00,0x00,0x00,0x00,0x00,0x00,0xfb,0xde,0x3c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x5d,0xef,0xfb,0xde,0xfb,0xde,0xdb,0xde,0xfb,0xde,0x00,0x00, + 0x00,0x00,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xfb,0xde,0x3c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x00,0x00, + 0x00,0x00,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x59,0xce,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xfb,0xde,0x3c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x3c,0xe7,0xdb,0xde,0x00,0x00, + 0x00,0x00,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x3c,0xe7,0xdb,0xde, + 0x00,0x00,0xfb,0xde,0xdb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7, + 0x00,0x00,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde, + 0x00,0x00,0xba,0xd6,0xdb,0xde,0x3c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0xfb,0xde, + 0x00,0x00,0x00,0x00,0xfb,0xde,0xdb,0xde,0x1c,0xe7,0xdb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xdb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0x3c,0xe7, + 0x00,0x00,0x00,0x00,0x00,0x00,0xba,0xd6,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0xfb,0xde,0x1c,0xe7,0xfb,0xde,0xfb,0xde,0x3c,0xe7,0xdb,0xde, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0e,0x0e,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x1b,0x1b,0x0a,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x0e,0xac,0xb8,0x27,0x00,0x00,0x00,0x15,0x72,0xbf,0xde,0xde,0xbf,0x73,0x17,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0e,0xb8,0xff,0xc3,0x26,0x00,0x00,0x85,0xff,0xfe,0xe8,0xe8,0xfe,0xff,0xc0,0x2d,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x27,0xc3,0xff,0xc3,0x26,0x00,0x50,0xa6,0x59,0x25,0x25,0x58,0xc8,0xff,0xc0,0x17,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x26,0xc3,0xff,0xc3,0x26,0x00,0x02,0x00,0x00,0x00,0x00,0x22,0xc8,0xff,0x73,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x26,0xc2,0xff,0xc3,0x26,0x00,0x00,0x00,0x00,0x00,0x00,0x58,0xfe,0xbf,0x0a,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x34,0xea,0xff,0xc3,0x26,0x00,0x00,0x00,0x00,0x00,0x24,0xe8,0xdd,0x1b,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x1f,0xe1,0xff,0xff,0xc3,0x26,0x00,0x00,0x00,0x00,0x33,0xe6,0xe4,0x21,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0xac,0xfa,0xff,0xff,0xff,0xc3,0x26,0x00,0x00,0x41,0xdb,0xfe,0xfa,0xac,0x37,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x37,0xdd,0xff,0xee,0xe2,0xe0,0xea,0xff,0xc3,0x26,0x00,0x42,0xd7,0xe3,0xee,0xff,0xdd,0x37,0x00,0x00,0x00,0x00,0x00,0x04,0xa6,0xff,0xa7,0x32,0x20,0x1d,0x35,0xc2,0xff,0xc3,0x26,0x00,0x18,0x20,0x32,0xa7,0xff,0xa6,0x04,0x00,0x00, + 0x00,0x00,0x15,0xd6,0xf0,0x32,0x00,0x00,0x00,0x00,0x26,0xc3,0xff,0xc3,0x26,0x00,0x00,0x00,0x32,0xf0,0xd6,0x15,0x00,0x00,0x00,0x00,0x1d,0xe1,0xe4,0x20,0x00,0x00,0x00,0x00,0x00,0x26,0xc3,0xff,0xc3,0x26,0x00,0x00,0x20,0xe5,0xe1,0x1e,0x00,0x00, + 0x00,0x00,0x1f,0xe2,0xe2,0x1f,0x00,0x00,0x00,0x00,0x00,0x00,0x26,0xc3,0xff,0xc3,0x26,0x00,0x18,0xd7,0xd7,0x18,0x00,0x00,0x00,0x00,0x1f,0xe2,0xe2,0x1f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x26,0xc3,0xff,0xc3,0x26,0x00,0x42,0x41,0x02,0x00,0x00, + 0x00,0x00,0x1f,0xe2,0xe2,0x1f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x26,0xc3,0xff,0xc3,0x26,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1d,0xe1,0xe4,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x26,0xc3,0xff,0xc3,0x26,0x00,0x00,0x00,0x00, + 0x00,0x00,0x15,0xd6,0xf0,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x24,0xc0,0xff,0xc3,0x26,0x00,0x00,0x00,0x00,0x00,0x04,0xa6,0xff,0xa7,0x32,0x20,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1d,0x5a,0xee,0xff,0xc3,0x27,0x00,0x00, + 0x00,0x00,0x00,0x37,0xdd,0xff,0xf0,0xe4,0xe2,0xe2,0xe2,0xe2,0xe2,0xe2,0xe2,0xe2,0xe4,0xef,0xff,0xf9,0xff,0xb8,0x0e,0x00,0x00,0x00,0x00,0x00,0x37,0xa6,0xd6,0xe1,0xe2,0xe2,0xe2,0xe2,0xe2,0xe2,0xe2,0xe2,0xe1,0xd6,0xa4,0x56,0xb7,0xac,0x0e,0x00, + 0x00,0x00,0x00,0x00,0x00,0x04,0x15,0x1d,0x1e,0x1f,0x1f,0x1f,0x1f,0x1f,0x1f,0x1e,0x1d,0x15,0x04,0x00,0x0e,0x0e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + +}; + +const lv_image_dsc_t img_lock_slash_image = { + .header.magic = LV_IMAGE_HEADER_MAGIC, + .header.cf = LV_COLOR_FORMAT_RGB565A8, + .header.flags = 0, + .header.w = 24, + .header.h = 24, + .header.stride = 48, + .data_size = sizeof(img_lock_slash_image_map), + .data = img_lock_slash_image_map, +}; + diff --git a/include/ILogEntry.h b/include/ILogEntry.h new file mode 100644 index 0000000..7507200 --- /dev/null +++ b/include/ILogEntry.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +/** + * Generic interface base class for any log entries (stored via class LogRotate) + */ +class ILogEntry +{ + public: + virtual size_t size(void) const = 0; + virtual size_t serialize(std::function write) const = 0; + virtual size_t deserialize(std::function read) = 0; + virtual ~ILogEntry() = default; + + protected: + ILogEntry(void) = default; + + private: + ILogEntry(const ILogEntry &) = delete; + ILogEntry &operator=(const ILogEntry &) = delete; +}; diff --git a/include/LGFXDriver.h b/include/LGFXDriver.h index fc3ea39..9c30c92 100644 --- a/include/LGFXDriver.h +++ b/include/LGFXDriver.h @@ -308,7 +308,7 @@ template void LGFXDriver::init_lgfx(void) ILOG_INFO("Calibrating touch..."); #ifdef T_DECK // FIXME: read calibration data from persistent storage using lfs_file_read - uint16_t parameters[8] = {11, 19, 6, 314, 218, 15, 229, 313}; + uint16_t parameters[8] = {0, 2, 0, 314, 223, 5, 224, 314}; #elif defined(WT32_SC01) uint16_t parameters[8] = {0, 2, 0, 479, 319, 0, 319, 479}; #elif defined(T_HMI) @@ -324,7 +324,7 @@ template void LGFXDriver::init_lgfx(void) #elif defined(SENSECAP_INDICATOR) uint16_t parameters[8] = {23, 3, 0, 479, 476, 2, 475, 479}; #else - uint16_t parameters[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + uint16_t parameters[8] = {0, 0, 0, 319, 239, 0, 239, 319}; ILOG_WARN("Touch screen has no calibration data!!!"); #endif diff --git a/include/LogMessage.h b/include/LogMessage.h new file mode 100644 index 0000000..e2d5994 --- /dev/null +++ b/include/LogMessage.h @@ -0,0 +1,66 @@ +#pragma once + +#include "ILogEntry.h" +#include +#include +#include + +constexpr uint32_t messagePayloadSize = 233; + +/** + * @brief Header for storing message logs containing the actual size of the payload + * Note: this struct does have vtable pointers, i.e. sizeof(LogMessageHeader)-8 is the real data size + */ +struct LogMessageHeader : public ILogEntry { + uint16_t _size; + time_t time; + uint32_t from; + uint32_t to; + uint8_t ch; + enum MsgStatus : uint8_t { eNone, eDefault, eHeard, eNoResponse, eAcked, eFailed, eDeleted, eUnread } status; + bool trashFlag; + uint32_t reserved; +}; + +/** + * @brief Structure for storing message logs containing the actual payload + */ +struct LogMessage : public LogMessageHeader { + uint8_t bytes[messagePayloadSize]; +}; + +/** + * Log message envelope that implements the actual interface for ILogEntry + * (size, serialize and deserialize) + */ +class LogMessageEnv : public LogMessage +{ + public: + LogMessageEnv(void) = default; + LogMessageEnv(uint32_t _from, uint32_t _to, uint16_t _ch, time_t _time, MsgStatus _status, bool _trashFlag, uint32_t _len, + const uint8_t *msg) + { + assert(_len < messagePayloadSize); + _size = (uint16_t)_len; + time = _time; + from = _from; + to = _to; + ch = _ch; + status = _status; + trashFlag = _trashFlag; + reserved = 0; + memcpy(bytes, msg, _len); + } + + size_t size(void) const override { return sizeof(LogMessageHeader) - 8 + _size; } + + virtual size_t serialize(std::function write) const override + { + return write((uint8_t *)&_size, sizeof(LogMessageHeader) - 8) + write(bytes, _size); + } + + virtual size_t deserialize(std::function read) override + { + return read((uint8_t *)&_size, sizeof(LogMessageHeader) - 8) + read(bytes, _size); + } +}; \ No newline at end of file diff --git a/include/LogRotate.h b/include/LogRotate.h new file mode 100644 index 0000000..f97adaf --- /dev/null +++ b/include/LogRotate.h @@ -0,0 +1,69 @@ +#pragma once + +#include "FS.h" +#include "ILogEntry.h" +#include + +/** + * Generic LogRotate class that writes log-rotation like files into (arduino) FS storage file system + * @param fs arduino file system FS/LittleFS or derived classes + * @param logDir directory to store the logs (absolute path) + * @param maxLen the maximum length of the variable log entry length + * The maximum storage is limited by: + * @param maxSize the total storage in bytes (default is 200kB) + * @param maxFiles number of log files (default is 50) + * @param maxFileSize per log file (default size is 4000 bytes to fit into a physical block + * including fs descriptor data) + * + * If the maximum storage is exceeded then old files are deleted to fit the new log entry. + * Note: for performance reasons the logs are not renumbered + */ +class LogRotate +{ + public: + LogRotate(fs::FS &fs, const char *logDir, uint32_t maxLen, + uint32_t maxSize = 102400, uint32_t maxFiles = 25, uint32_t maxFileSize = 4000); + //uint32_t maxSize = 4096, uint32_t maxFiles = 10, uint32_t maxFileSize = 400); + + // initialize the log directory + void init(void); + // write a log entry to fs + bool write(const ILogEntry &entry); + // read the next log entry from fs + bool readNext(ILogEntry &entry); + // remove all logs from fs + bool clear(void); + // request log count + uint32_t count(void); + // request current log number + uint32_t current(void); + + private: + LogRotate(const LogRotate &) = delete; + LogRotate &operator=(const LogRotate &) = delete; + + // create filename from number + String logFileName(uint32_t num); + // remove oldest log and return freed size + size_t removeLog(void); + // scan all files in logdir to get min/max log + void scanLogDir(uint32_t &num, uint32_t &minLog, uint32_t &maxLog, uint32_t &size, uint32_t &total); + + const uint32_t c_maxLen; // maximum size a single log entry could be + const uint32_t c_maxSize; // max storage size in bytes (default is 100kB) + const uint32_t c_maxFiles; // max log files number (default is 50) + const uint32_t c_maxFileSize; // max file size per log file + + fs::FS &_fs; + File rootDir; // directory (for reading logs) + File currentFile; // current file (when reading) + String rootDirName; // path of log directory + String currentLogName; // current log file name (when writing) + uint32_t numFiles; // number of log files + uint32_t minLogNum; // logfile with smallest number + uint32_t maxLogNum; // logfile with largest number after init() + uint32_t currentLogRead; // current log number (when reading) + uint32_t currentLogWrite; // current log number (when writing) + uint32_t currentSize; // size of current written log file + uint32_t totalSize; // size of all logs +}; \ No newline at end of file diff --git a/include/MeshtasticView.h b/include/MeshtasticView.h index 6e06352..d4256de 100644 --- a/include/MeshtasticView.h +++ b/include/MeshtasticView.h @@ -2,6 +2,7 @@ #include "DeviceGUI.h" #include "DisplayDriverConfig.h" +#include "LogMessage.h" #include "ResponseHandler.h" #include "lvgl.h" #include "mesh-pb-constants.h" @@ -47,7 +48,7 @@ class MeshtasticView : public DeviceGUI }; // methods to update view - virtual void setupUIConfig(const meshtastic_DeviceUIConfig& uiconfig) {} + virtual void setupUIConfig(const meshtastic_DeviceUIConfig &uiconfig) {} virtual void setMyInfo(uint32_t nodeNum); virtual void setDeviceMetaData(int hw_model, const char *version, bool has_bluetooth, bool has_wifi, bool has_eth, bool can_shutdown); @@ -57,7 +58,7 @@ class MeshtasticView : public DeviceGUI virtual void addNode(uint32_t nodeNum, uint8_t channel, const char *userShort, const char *userLong, uint32_t lastHeard, eRole role, bool hasKey, bool viaMqtt); virtual void updateNode(uint32_t nodeNum, uint8_t channel, const char *userShort, const char *userLong, uint32_t lastHeard, - eRole role, bool hasKey, bool viaMqtt); + eRole role, bool hasKey, bool viaMqtt); virtual void updatePosition(uint32_t nodeNum, int32_t lat, int32_t lon, int32_t alt, uint32_t sats, uint32_t precision); virtual void updateMetrics(uint32_t nodeNum, uint32_t bat_level, float voltage, float chUtil, float airUtil); virtual void updateEnvironmentMetrics(uint32_t nodeNum, const meshtastic_EnvironmentMetrics &metrics) {} @@ -102,7 +103,10 @@ class MeshtasticView : public DeviceGUI virtual void handleResponse(uint32_t from, uint32_t id, const meshtastic_RouteDiscovery &route) {} virtual void handlePositionResponse(uint32_t from, uint32_t request_id, int32_t rx_rssi, float rx_snr, bool isNeighbor) {} virtual void packetReceived(const meshtastic_MeshPacket &p); - virtual void newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg); + virtual void newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg, uint32_t &msgtime, bool restore = false) {} + virtual void restoreMessage(const LogMessage &msg) {} + virtual void notifyRestoreMessages(int32_t percentage) {} + virtual void notifyMessagesRestored(void) {} virtual void notifyResync(bool show); virtual void notifyReboot(bool show); virtual void notifyShutdown(void); diff --git a/include/ResponseHandler.h b/include/ResponseHandler.h index 542c7cf..b4a2c9b 100644 --- a/include/ResponseHandler.h +++ b/include/ResponseHandler.h @@ -1,36 +1,55 @@ #pragma once +#include #include #include /** - * @brief Generic class that stores an id together with a timestamp - * and returns a requestId(cookie). + * @brief Generic class that stores an id together with a timestamp, cookie and callback function. + * Returns a unique requestId for later reference. + * Callbacks can be used to decouple the reply handler from the actual caller. */ class ResponseHandler { public: - enum RequestType { noRequest, TextMessageRequest, TraceRouteRequest, PositionRequest, RemoteConfigRequest }; + struct Request; + + enum RequestType { noRequest, TextMessageRequest, TraceRouteRequest, PositionRequest, RemoteConfigRequest, anyRequest }; + enum EventType { found, removed, timeout, user1, user2, user3 }; + + using Callback = std::function; struct Request { uint32_t id; - void *cookie; - enum RequestType type; unsigned long timestamp; + enum RequestType type; + void *cookie; + Callback cb; }; ResponseHandler(uint32_t timeout); - uint32_t addRequest(uint32_t id, RequestType type, void *cookie = nullptr); - Request findRequest(uint32_t requestId); - Request removeRequest(uint32_t requestId); + // add new request, pass optional anytype cookie and optional callback function + virtual uint32_t addRequest(uint32_t id, RequestType type, void *cookie = nullptr, Callback cb = nullptr); + // findRequest, call cb on found if pass != -1 and if request type matches + virtual Request findRequest(uint32_t requestId, RequestType match = anyRequest, int32_t pass = -1); + // removeRequest, call cb on found if pass != -1) and if event type matches + virtual Request removeRequest(uint32_t requestId, RequestType match = anyRequest, int32_t pass = -1); + // custom request for implementing user function in derived class + virtual Request customRequest(uint32_t requestId, RequestType match = anyRequest, int32_t pass = -1) { return Request{}; } + // task handler, must be called periodically + virtual void task_handler(void); - void task_handler(void); + ~ResponseHandler() = default; protected: - uint32_t generatePacketId(void); + virtual uint32_t generatePacketId(void); static uint32_t rollingPacketId; uint32_t requestIdCounter; uint32_t maxTime; std::unordered_map pendingRequest; + + private: + ResponseHandler(const ResponseHandler &) = delete; + ResponseHandler &operator=(const ResponseHandler &) = delete; }; \ No newline at end of file diff --git a/include/TFTView_320x240.h b/include/TFTView_320x240.h index 2668b6e..2be25ae 100644 --- a/include/TFTView_320x240.h +++ b/include/TFTView_320x240.h @@ -71,13 +71,16 @@ class TFTView_320x240 : public MeshtasticView void handleResponse(uint32_t from, uint32_t id, const meshtastic_Routing &routing, const meshtastic_MeshPacket &p) override; void handleResponse(uint32_t from, uint32_t id, const meshtastic_RouteDiscovery &route) override; void handlePositionResponse(uint32_t from, uint32_t request_id, int32_t rx_rssi, float rx_snr, bool isNeighbor) override; + void notifyRestoreMessages(int32_t percentage) override; + void notifyMessagesRestored(void) override; void notifyResync(bool show) override; void notifyReboot(bool show) override; void notifyShutdown(void) override; void blankScreen(bool enable) override; void screenSaving(bool enabled) override; bool isScreenLocked(void) override; - void newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg) override; + void newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg, uint32_t &msgtime, bool restore = true) override; + void restoreMessage(const LogMessage &msg) override; void removeNode(uint32_t nodeNum) override; enum BasicSettings { @@ -146,7 +149,7 @@ class TFTView_320x240 : public MeshtasticView // own chat message virtual void handleAddMessage(char *msg); // add own message to current chat - virtual void addMessage(uint32_t requestId, char *msg); + virtual void addMessage(lv_obj_t *container, uint32_t msgTime, uint32_t requestId, char *msg, LogMessage::MsgStatus status); // add new message to container virtual void newMessage(uint32_t nodeNum, lv_obj_t *container, uint8_t channel, const char *msg); // create empty message container for node or group channel @@ -222,6 +225,12 @@ class TFTView_320x240 : public MeshtasticView void showLoRaFrequency(const meshtastic_Config_LoRaConfig &cfg); void setBellText(bool banner, bool sound); void setChannelName(const meshtastic_Channel &ch); + uint32_t timestamp(char* buf, uint32_t time, bool update); + + // response callbacks + void onTextMessageCallback(const ResponseHandler::Request &, ResponseHandler::EventType, int32_t); + void onPositionCallback(const ResponseHandler::Request &, ResponseHandler::EventType, int32_t); + void onTracerouteCallback(const ResponseHandler::Request &, ResponseHandler::EventType, int32_t); // lvgl event callbacks // static void ui_event_HomeButton(lv_event_t * e); diff --git a/include/ViewController.h b/include/ViewController.h index 33ae335..83d70b7 100644 --- a/include/ViewController.h +++ b/include/ViewController.h @@ -1,6 +1,7 @@ #pragma once #include "IClientBase.h" +#include "LogRotate.h" #include class MeshtasticView; @@ -60,7 +61,8 @@ class ViewController virtual bool sendConfig(meshtastic_ModuleConfig_PaxcounterConfig &&paxCounter, uint32_t nodeId = 0); virtual bool sendConfig(const char ringtone[231], uint32_t nodeId = 0); - virtual void sendTextMessage(uint32_t to, uint8_t ch, uint8_t hopLimit, uint32_t requestId, bool usePkc, const char *textmsg); + virtual void sendTextMessage(uint32_t to, uint8_t ch, uint8_t hopLimit, uint32_t msgTime, uint32_t requestId, bool usePkc, const char *textmsg); + virtual void removeTextMessages(uint32_t from, uint32_t to, uint8_t ch); virtual bool requestPosition(uint32_t to, uint8_t ch, uint32_t requestId); virtual void traceRoute(uint32_t to, uint8_t ch, uint8_t hopLimit, uint32_t requestId); @@ -87,17 +89,25 @@ class ViewController virtual void requestAdditionalConfig(void); // request specific config virtual uint32_t requestConfig(meshtastic_AdminMessage_ConfigType type, uint32_t nodeId = 0); + // begin loading persistent messages + virtual void beginRestoreTextMessages(void); + // incrementally load persistent messages + virtual void restoreTextMessages(void); // handle received packet and update view bool handleFromRadio(const meshtastic_FromRadio &from); // handle meshPacket bool packetReceived(const meshtastic_MeshPacket &p); MeshtasticView *view; + LogRotate log; IClientBase *client; uint32_t sendId; uint32_t myNodeNum; time_t lastrun10; time_t lastSetup; - bool setupDone; - bool requestConfigRequired; + time_t restoreTimer; + bool setupDone; // true if ui config has been loaded and screens are setup in the view + bool configCompleted; // true if all data from node has been received + bool messagesRestored; // true if log messages have been restored + bool requestConfigRequired; // true if config needs to be reloaded from the node }; diff --git a/source/LogRotate.cpp b/source/LogRotate.cpp new file mode 100644 index 0000000..a76b44d --- /dev/null +++ b/source/LogRotate.cpp @@ -0,0 +1,255 @@ +#include "LogRotate.h" +#include "ILog.h" +#include + +#define FILE_PREFIX "log_" + +LogRotate::LogRotate(fs::FS &fs, const char *logDir, uint32_t maxLen, uint32_t maxSize, uint32_t maxFiles, uint32_t maxFileSize) + : c_maxLen(maxLen), c_maxSize(maxSize), c_maxFiles(maxFiles), c_maxFileSize(maxFileSize), _fs(fs), rootDirName(logDir), + numFiles(0), minLogNum(0), maxLogNum(0), currentLogRead(0), currentLogWrite(0), currentSize(0), totalSize(0) + +{ +} + +void LogRotate::init(void) +{ + if (!_fs.exists(rootDirName)) { + _fs.mkdir(rootDirName); + ILOG_INFO("LogRotate: no log files found."); + } else { + scanLogDir(numFiles, minLogNum, maxLogNum, currentSize, totalSize); + currentLogRead = minLogNum; + currentLogWrite = maxLogNum; + ILOG_INFO("LogRotate: found %d log files using %d bytes (%d%%).", numFiles, totalSize, (totalSize * 100) / c_maxSize); + if (currentSize > c_maxFileSize - c_maxLen) { + ILOG_DEBUG("currentSize(%d) > c_maxFileSize(%d) - c_maxLen(%d)", currentSize, c_maxFileSize, c_maxLen); + ILOG_DEBUG("numFiles(%d) > c_maxFiles(%d) || totalSize(%d) >= c_maxSize(%d)", numFiles, c_maxFiles, totalSize, + c_maxSize); + while ((numFiles > c_maxFiles || totalSize >= c_maxSize) && removeLog()) + ; + numFiles++; + currentLogWrite++; + currentSize = 0; + } + } + + if (minLogNum == 0) { + numFiles = 1; + minLogNum = 1; + currentLogRead = 0; + currentLogWrite = 1; + } + currentLogName = logFileName(currentLogWrite); + ILOG_INFO("Logging to %s", currentLogName.c_str()); +} + +/** + * retrieve next entry from list of logFiles + */ + +bool LogRotate::readNext(ILogEntry &entry) +{ + if (!rootDir) { + rootDir = _fs.open(rootDirName); + if (!rootDir) + return false; + } + if (!currentFile) { + if (currentLogRead == 0 || currentLogRead > maxLogNum) + return false; + do { + currentFile = _fs.open(logFileName(currentLogRead), FILE_READ); + if (currentFile && currentFile.available()) { + break; + } + currentFile.close(); + currentLogRead++; + } while (currentLogRead <= maxLogNum); + if (!currentFile || !currentFile.available() || currentLogRead > maxLogNum) { + rootDir.close(); + return false; + } + ILOG_DEBUG("-> reading %s (%d bytes)", currentFile.name(), currentFile.size()); + } + + // elegant way to let the logentry do its work it knows best and pass just a temporary function for reading + if (!entry.deserialize([this](uint8_t *buf, size_t size) { return this->currentFile.read(buf, size); })) { + currentFile.close(); + currentLogRead++; + return readNext(entry); + } else + return true; +} + +bool LogRotate::write(const ILogEntry &entry) +{ + time_t start = millis(); + if (currentSize + entry.size() >= c_maxFileSize || totalSize + entry.size() >= c_maxSize) { + // log rotation + ILOG_DEBUG("LogRotation: %d >= %d || %d >= %d", currentSize + entry.size(), c_maxFileSize, totalSize + entry.size(), + c_maxSize); + numFiles++; + currentSize = 0; + currentLogWrite++; + currentLogName = logFileName(currentLogWrite); + while ((numFiles >= c_maxFiles || totalSize + entry.size() > c_maxSize) && removeLog()) + ; + } + + // elegant way to let the logentry do its work it knows best and pass just a temporary function for writing + File file = _fs.open(currentLogName, FILE_APPEND); + entry.serialize([&file](const uint8_t *buf, size_t size) { return file.write(buf, size); }); + file.close(); + + currentSize += entry.size(); + totalSize += entry.size(); + + ILOG_DEBUG("LogRotate: %d bytes written in %d ms to %s (%d/%d bytes, total: %d)", entry.size(), millis() - start, + currentLogName.c_str(), currentSize, c_maxFileSize, totalSize); + return true; +} + +/** + * remove all log files + */ +bool LogRotate::clear(void) +{ + time_t start = millis(); + File root = _fs.open(rootDirName); + + int count = 0; + bool error = false; + File file = root.openNextFile(); + while (file) { + String name = rootDirName + '/' + file.name(); + if (!file.isDirectory()) { + file.close(); + if (_fs.remove(name)) { + count++; + ILOG_DEBUG("removed %s", name.c_str()); + } else { + ILOG_ERROR("failed to remove %s!", name.c_str()); + error = true; + } + } + else { + file.close(); + } + file = root.openNextFile(); + } + ILOG_DEBUG("removed %d logs in %d ms", count, millis() - start); + + numFiles = 1; + minLogNum = 1; + currentLogWrite = 1; + currentSize = 0; + totalSize = 0; + currentLogName = logFileName(currentLogWrite); + return error; +} + +/** + * Return number of log files + */ +uint32_t LogRotate::count(void) +{ + return numFiles; +} + +/** + * Return current log number that is processed for reading + */ +uint32_t LogRotate::current(void) +{ + return currentLogRead; +} + +/** + * Generate a log file name based on num + */ +String LogRotate::logFileName(uint32_t num) +{ + char filename[40]; + sprintf(filename, "%s/" FILE_PREFIX "%06d.log", rootDirName.c_str(), num); + return filename; +} + +/** + * remove the oldest log + */ +size_t LogRotate::removeLog(void) +{ + ILOG_DEBUG("removeLog minLogNum=%d, numFiles=%d, totalSize=%d", minLogNum, numFiles, totalSize); + size_t size = 0; + if (minLogNum > 0) { + String log = logFileName(minLogNum); + File file = _fs.open(log, FILE_READ); + size = file.size(); + file.close(); + if (_fs.remove(log)) { + ILOG_DEBUG("removed %s, freeing %d bytes", log.c_str(), size); + totalSize -= size; + numFiles--; + } else { + ILOG_ERROR("failed to remove %s", log.c_str()); + size = 0; + } + minLogNum++; + } + return size; +} + +/** + * Scan the log directory (rootDirName) for files and return the following + * @param num number of logs + * @param minLog log starting number + * @param maxLog log ending number + * @param logSize size of last log + * @param total total size of all logs + */ +void LogRotate::scanLogDir(uint32_t &num, uint32_t &minLog, uint32_t &maxLog, uint32_t &logSize, uint32_t &total) +{ + num = maxLog = logSize = 0; + minLog = UINT32_MAX; + + ILOG_DEBUG("scanning log folder %s", rootDirName.c_str()); + if (!rootDir) + rootDir = _fs.open(rootDirName); + File file = rootDir.openNextFile(); + while (file) { + if (!file.isDirectory()) { + num++; + size_t size = file.size(); + total += size; + ILOG_DEBUG(" %s(%d bytes)", file.name(), size); + + String format; + if (file.name()[0] == '/') { + format = rootDirName + '/' + String(FILE_PREFIX) + String("%u.log"); + } + else { + format = String(FILE_PREFIX) + String("%u.log"); + } + + uint32_t logNum = 0; + if (sscanf(file.name(), format.c_str(), &logNum) > 0 && logNum > 0) { + if (logNum < minLog) { + minLog = logNum; + } + if (logNum > maxLog) { + maxLog = logNum; + logSize = size; + } + } + else { + ILOG_ERROR("sscanf() failed for %s", file.name()); + } + } + file.close(); + file = rootDir.openNextFile(); + } + rootDir.close(); + + if (minLog == UINT32_MAX) + minLog = 0; +} diff --git a/source/MeshtasticView.cpp b/source/MeshtasticView.cpp index 9c643b0..09e2c05 100644 --- a/source/MeshtasticView.cpp +++ b/source/MeshtasticView.cpp @@ -116,8 +116,6 @@ void MeshtasticView::packetReceived(const meshtastic_MeshPacket &p) } } -void MeshtasticView::newMessage(uint32_t from, uint32_t to, uint8_t channel, const char *msg) {} - void MeshtasticView::removeNode(uint32_t nodeNum) {} // -------- helpers -------- diff --git a/source/ResponseHandler.cpp b/source/ResponseHandler.cpp index 566dba2..2bfae30 100644 --- a/source/ResponseHandler.cpp +++ b/source/ResponseHandler.cpp @@ -2,7 +2,6 @@ #include "Arduino.h" #include "ILog.h" - uint32_t ResponseHandler::rollingPacketId = 0; /** @@ -10,34 +9,40 @@ uint32_t ResponseHandler::rollingPacketId = 0; * * @param timeout */ -ResponseHandler::ResponseHandler(uint32_t timeout) : requestIdCounter(0), maxTime(timeout) { +ResponseHandler::ResponseHandler(uint32_t timeout) : requestIdCounter(0), maxTime(timeout) +{ rollingPacketId = random(UINT32_MAX & 0x7fffffff); } -uint32_t ResponseHandler::addRequest(uint32_t id, RequestType type, void *cookie) +uint32_t ResponseHandler::addRequest(uint32_t id, RequestType type, void *cookie, Callback cb) { requestIdCounter++; uint32_t requestId = generatePacketId(); - pendingRequest[requestId] = Request{.id = id, .cookie = cookie, .type = type, .timestamp = millis()}; + pendingRequest[requestId] = Request{.id = id, .timestamp = millis(), .type = type, .cookie = cookie, .cb = cb}; return requestId; } -ResponseHandler::Request ResponseHandler::findRequest(uint32_t requestId) +ResponseHandler::Request ResponseHandler::findRequest(uint32_t requestId, RequestType match, int32_t pass) { const auto it = pendingRequest.find(requestId); if (it != pendingRequest.end()) { - return it->second; + Request &req = it->second; + if (req.cb && pass != -1 && (match == anyRequest || match == req.type)) + req.cb(req, found, pass); + return req; } return Request{}; } -ResponseHandler::Request ResponseHandler::removeRequest(uint32_t requestId) +ResponseHandler::Request ResponseHandler::removeRequest(uint32_t requestId, RequestType match, int32_t pass) { Request req{}; const auto it = pendingRequest.find(requestId); if (it != pendingRequest.end()) { req = it->second; ILOG_DEBUG("removing request %08x", it->first); + if (req.cb && pass != -1 && (match == anyRequest || match == req.type)) + req.cb(req, removed, pass); pendingRequest.erase(it); } return req; @@ -45,7 +50,7 @@ ResponseHandler::Request ResponseHandler::removeRequest(uint32_t requestId) /** * @brief: Generate a unique packet id - * + * */ uint32_t ResponseHandler::generatePacketId(void) { @@ -65,8 +70,11 @@ void ResponseHandler::task_handler(void) ILOG_DEBUG("ResponseHandler has %d pending request(s)", pendingRequest.size()); auto it = pendingRequest.begin(); while (it != pendingRequest.end()) { - if (it->second.timestamp + maxTime < millis()) { + Request &req = it->second; + if (req.timestamp + maxTime < millis()) { ILOG_DEBUG("removing timed out request %08x", it->first); + if (req.cb) + req.cb(req, timeout, 0); it = pendingRequest.erase(it); } else { it++; diff --git a/source/TFTView_320x240.cpp b/source/TFTView_320x240.cpp index 1ca5b2a..d6b9cba 100644 --- a/source/TFTView_320x240.cpp +++ b/source/TFTView_320x240.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include #include @@ -45,6 +45,8 @@ .blue = (C >> 0) & 0xff, .green = (C >> 8) & 0xff, .red = (C >> 16) & 0xff \ } +#define VALID_TIME(T) (T > 1000000 && T < UINT32_MAX) + constexpr lv_color_t colorRed = LV_COLOR_HEX(0xff5555); constexpr lv_color_t colorDarkRed = LV_COLOR_HEX(0xa70a0a); constexpr lv_color_t colorOrange = LV_COLOR_HEX(0xff8c04); @@ -219,10 +221,12 @@ void TFTView_320x240::setupUIConfig(const meshtastic_DeviceUIConfig &uiconfig) // touch screen calibration data uint16_t *parameters = (uint16_t *)db.uiConfig.calibration_data.bytes; if (db.uiConfig.calibration_data.size == 16 && (parameters[0] || parameters[7])) { +#ifndef IGNORE_CALIBRATION_DATA bool done = displaydriver->calibrate(parameters); char buf[32]; lv_snprintf(buf, sizeof(buf), _("Screen Calibration: %s"), done ? _("done") : _("default")); lv_label_set_text(objects.basic_settings_calibration_label, buf); +#endif } lv_disp_trig_activity(NULL); @@ -243,6 +247,7 @@ void TFTView_320x240::updateBootMessage(void) */ void TFTView_320x240::init_screens(void) { + ILOG_DEBUG("init screens..."); ui_init(); apply_hotfix(); @@ -523,6 +528,8 @@ void TFTView_320x240::ui_events_init(void) lv_obj_add_event_cb(objects.home_wlan_button, this->ui_event_WLANButton, LV_EVENT_LONG_PRESSED, NULL); lv_obj_add_event_cb(objects.home_mqtt_button, this->ui_event_MQTTButton, LV_EVENT_ALL, NULL); lv_obj_add_event_cb(objects.home_memory_button, this->ui_event_MemoryButton, LV_EVENT_CLICKED, NULL); + + // blank screen lv_obj_add_event_cb(objects.blank_screen_button, this->ui_event_BlankScreenButton, LV_EVENT_ALL, NULL); // node and channel buttons @@ -859,6 +866,7 @@ void TFTView_320x240::ui_event_ChatDelButton(lv_event_t *e) lv_obj_del(THIS->channelGroup[ch]); THIS->channelGroup[ch] = nullptr; THIS->chats.erase(ch); + THIS->controller->removeTextMessages(THIS->ownNode, UINT32_MAX, ch); } else { uint32_t nodeNum = channelOrNode; lv_obj_delete_delayed(THIS->chats[nodeNum], 500); @@ -866,9 +874,14 @@ void TFTView_320x240::ui_event_ChatDelButton(lv_event_t *e) THIS->messages.erase(nodeNum); THIS->chats.erase(nodeNum); THIS->applyNodesFilter(nodeNum); + THIS->controller->removeTextMessages(THIS->ownNode, nodeNum, 0); } THIS->activeMsgContainer = objects.messages_container; THIS->updateActiveChats(); + if (THIS->chats.empty()) { + // last chat was deleted, now we can get rid of all logs :) + THIS->controller->removeTextMessages(0, 0, 0); + } } } @@ -1108,20 +1121,10 @@ void TFTView_320x240::ui_event_MemoryButton(lv_event_t *e) void TFTView_320x240::ui_event_BlankScreenButton(lv_event_t *e) { - static bool ignoreClicked = false; lv_event_code_t event_code = lv_event_get_code(e); if (event_code == LV_EVENT_CLICKED) { - if (ignoreClicked) { // prevent long press to enter this setting - ignoreClicked = false; - return; - } ILOG_DEBUG("screen unlocked by button"); screenUnlockRequest = true; - } else if (event_code == LV_EVENT_LONG_PRESSED) { - // currently button is disabled, see LGFXDriver.h - ILOG_DEBUG("screen locked by button"); - screenLocked = true; - ignoreClicked = true; } } @@ -1671,7 +1674,7 @@ void TFTView_320x240::ui_event_delete_channel(lv_event_t *e) void TFTView_320x240::ui_event_calibration_screen_loaded(lv_event_t *e) { uint16_t *parameters = (uint16_t *)THIS->db.uiConfig.calibration_data.bytes; - memset(parameters, 0, 8); // clear all calibration data + memset(parameters, 0, 16); // clear all calibration data bool done = THIS->displaydriver->calibrate(parameters); THIS->db.uiConfig.calibration_data.size = 16; char buf[32]; @@ -2049,7 +2052,7 @@ void TFTView_320x240::writePacketLog(const meshtastic_MeshPacket &p) curr_time = actTime; #endif tm *curr_tm = localtime(&curr_time); - if (curr_time > 1000000) { + if (VALID_TIME(curr_time)) { strftime(timebuf, 16, "%T", curr_tm); } else { strcpy(timebuf, "??:??:??"); @@ -3233,35 +3236,47 @@ void TFTView_320x240::handleAddMessage(char *msg) { // retrieve nodeNum + channel from activeMsgContainer uint32_t to = UINT32_MAX; - uint8_t ch; + uint8_t ch = 0; uint8_t hopLimit = db.config.lora.hop_limit; uint32_t requestId; uint32_t channelOrNode = (unsigned long)activeMsgContainer->user_data; bool usePkc = false; + + auto callback = [this](const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t pass) { + this->onTextMessageCallback(req, evt, pass); + }; + if (channelOrNode < c_max_channels) { ch = (uint8_t)channelOrNode; - requestId = requests.addRequest(ch, ResponseHandler::TextMessageRequest); + requestId = requests.addRequest(ch, ResponseHandler::TextMessageRequest, nullptr, callback); } else { ch = (uint8_t)(unsigned long)nodes[channelOrNode]->user_data; to = channelOrNode; usePkc = (unsigned long)nodes[to]->LV_OBJ_IDX(node_bat_idx)->user_data; // hasKey - requestId = requests.addRequest(to, ResponseHandler::TextMessageRequest, (void *)to); + requestId = requests.addRequest(to, ResponseHandler::TextMessageRequest, (void *)to, callback); // trial: hoplimit optimization for direct text messages int8_t hopsAway = (signed long)nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; if (hopsAway < 0) hopsAway = db.config.lora.hop_limit; hopLimit = (hopsAway < db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); } - controller->sendTextMessage(to, ch, hopLimit, requestId, usePkc, msg); - addMessage(requestId, msg); + + // tweak to allow multiple lines in single line text area + for (int i = 0; i < strlen(msg); i++) + if (msg[i] == CR_REPLACEMENT) + msg[i] = '\n'; + + controller->sendTextMessage(to, ch, hopLimit, actTime, requestId, usePkc, msg); + addMessage(activeMsgContainer, actTime, requestId, msg, LogMessage::eNone); } /** * display message that has just been written and sent out */ -void TFTView_320x240::addMessage(uint32_t requestId, char *msg) +void TFTView_320x240::addMessage(lv_obj_t *container, uint32_t msgTime, uint32_t requestId, char *msg, + LogMessage::MsgStatus status) { - lv_obj_t *hiddenPanel = lv_obj_create(activeMsgContainer); + lv_obj_t *hiddenPanel = lv_obj_create(container); lv_obj_set_width(hiddenPanel, lv_pct(100)); lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); @@ -3276,23 +3291,39 @@ void TFTView_320x240::addMessage(uint32_t requestId, char *msg) lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); hiddenPanel->user_data = (void *)requestId; + // add timestamp + char buf[284]; // 237 + 4 + 40 + 2 + 1 + buf[0] = '\0'; + uint32_t len = timestamp(buf, msgTime, status == LogMessage::eNone); + strcat(&buf[len], msg); + lv_obj_t *textLabel = lv_label_create(hiddenPanel); // calculate expected size of text bubble, to make it look nicer - lv_coord_t width = lv_txt_get_width(msg, strlen(msg), &ui_font_montserrat_12, 0); - lv_obj_set_width(textLabel, std::max(std::min(width + 20, 200), 40)); + lv_coord_t width = lv_txt_get_width(buf, strlen(buf), &ui_font_montserrat_12, 0); + lv_obj_set_width(textLabel, std::max(std::min(width, 200) + 10, 40)); lv_obj_set_height(textLabel, LV_SIZE_CONTENT); lv_obj_set_y(textLabel, 0); lv_obj_set_align(textLabel, LV_ALIGN_RIGHT_MID); + lv_label_set_text(textLabel, buf); - // tweak to allow multiple lines in single line text area - for (int i = 0; i < strlen(msg); i++) - if (msg[i] == CR_REPLACEMENT) - msg[i] = '\n'; - lv_label_set_text(textLabel, msg); add_style_chat_message_style(textLabel); lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); lv_obj_move_foreground(objects.message_input_area); + + switch (status) { + case LogMessage::eHeard: + lv_obj_set_style_border_color(textLabel, colorYellow, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case LogMessage::eAcked: + lv_obj_set_style_border_color(textLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case LogMessage::eFailed: + lv_obj_set_style_border_color(textLabel, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + default: + break; + } } void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, uint32_t lastHeard, @@ -3386,14 +3417,14 @@ void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShor if (userData[0] == 0x00) userData[0] = ' '; userData[1] = userShort[1]; - if (userData[0] == 0x00) - userData[0] = ' '; + if (userData[1] == 0x00) + userData[1] = ' '; userData[2] = userShort[2]; - if (userData[0] == 0x00) - userData[0] = ' '; + if (userData[2] == 0x00) + userData[2] = ' '; userData[3] = userShort[3]; - if (userData[0] == 0x00) - userData[0] = ' '; + if (userData[3] == 0x00) + userData[3] = ' '; // BatteryLabel lv_obj_t *ui_BatteryLabel = lv_label_create(p); @@ -3529,7 +3560,7 @@ void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const char *userS eRole role, bool hasKey, bool viaMqtt) { auto it = nodes.find(nodeNum); - if (it != nodes.end()) { + if (it != nodes.end() && it->second) { if (it->first == ownNode) { // update related settings buttons and store role in image user data char buf[30]; @@ -3555,14 +3586,14 @@ void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const char *userS if (userData[0] == 0x00) userData[0] = ' '; userData[1] = userShort[1]; - if (userData[0] == 0x00) - userData[0] = ' '; + if (userData[1] == 0x00) + userData[1] = ' '; userData[2] = userShort[2]; - if (userData[0] == 0x00) - userData[0] = ' '; + if (userData[2] == 0x00) + userData[2] = ' '; userData[3] = userShort[3]; - if (userData[0] == 0x00) - userData[0] = ' '; + if (userData[3] == 0x00) + userData[3] = ' '; setNodeImage(nodeNum, role, viaMqtt, it->second->LV_OBJ_IDX(node_img_idx)); @@ -3923,6 +3954,24 @@ void TFTView_320x240::updateConnectionStatus(const meshtastic_DeviceConnectionSt } } +// ResponseHandler callbacks + +void TFTView_320x240::onTextMessageCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t result) +{ + ILOG_DEBUG("onTextMessageCallback: %d %d", evt, result); + if (evt == ResponseHandler::found) { + handleTextMessageResponse((unsigned long)req.cookie, req.id, false, result); + } else if (evt == ResponseHandler::removed) { + handleTextMessageResponse((unsigned long)req.cookie, req.id, true, result); + } else { + ILOG_DEBUG("onTextMessageCallback: timeout!"); + } +} + +void TFTView_320x240::onPositionCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) {} + +void TFTView_320x240::onTracerouteCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) {} + /** * handle response from routing */ @@ -3943,6 +3992,7 @@ void TFTView_320x240::handleResponse(uint32_t from, const uint32_t id, const mes } else { ILOG_DEBUG("handleResponse request id 0x%08x", id); } + ILOG_DEBUG("routing tag variant: %d, error: %d", routing.which_variant, routing.error_reason); switch (routing.which_variant) { case meshtastic_Routing_error_reason_tag: { if (routing.error_reason == meshtastic_Routing_Error_NONE) { @@ -3958,6 +4008,9 @@ void TFTView_320x240::handleResponse(uint32_t from, const uint32_t id, const mes if (req.type == ResponseHandler::TraceRouteRequest) { handleTraceRouteResponse(routing); } + else if (req.type == ResponseHandler::TextMessageRequest) { + handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); + } } else if (routing.error_reason == meshtastic_Routing_Error_NO_RESPONSE) { if (req.type == ResponseHandler::PositionRequest) { handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); @@ -3965,6 +4018,15 @@ void TFTView_320x240::handleResponse(uint32_t from, const uint32_t id, const mes } else if (routing.error_reason == meshtastic_Routing_Error_NO_CHANNEL) { if (req.type == ResponseHandler::TextMessageRequest) { handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); + // we probably have a wrong key; mark it as bad and don't use in future + if ((unsigned long)nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data == 1) { + ILOG_DEBUG("public key mismatch"); + nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data = (void*)2; + lv_obj_set_style_border_color(nodes[from]->LV_OBJ_IDX(node_img_idx), colorRed, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } } } else { ILOG_DEBUG("got Routing_Error %d", routing.error_reason); @@ -4203,9 +4265,7 @@ bool TFTView_320x240::applyNodesFilter(uint32_t nodeNum, bool reset) hide = true; } if (lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED)) { - lv_color_t color1 = lv_obj_get_style_bg_color(panel->LV_OBJ_IDX(node_img_idx), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_color_t color2 = lv_obj_get_style_border_color(panel->LV_OBJ_IDX(node_img_idx), LV_PART_MAIN | LV_STATE_DEFAULT); - bool hasKey = color1.blue == color2.blue && color1.red == color2.red && color1.green == color2.green; + bool hasKey = (unsigned long)panel->LV_OBJ_IDX(node_bat_idx)->user_data == 1; if (!hasKey) hide = true; } @@ -4337,7 +4397,7 @@ void TFTView_320x240::messageAlert(const char *alert, bool show) } /** - * @brief mark the sent message as either heard or acknowledged + * @brief mark the sent message as either heard or acknowledged or failed * * @param channelOrNode * @param id @@ -4369,6 +4429,8 @@ void TFTView_320x240::handleTextMessageResponse(uint32_t channelOrNode, const ui : ack ? colorBlueGreen : colorYellow, LV_PART_MAIN | LV_STATE_DEFAULT); + + // store message break; } } @@ -4660,6 +4722,39 @@ void TFTView_320x240::setChannelName(const meshtastic_Channel &ch) } } +/** + * @brief write local time stamp into buffer + * if date is not current also add day/month + * Note: time string ends with linefeed + * + * @param buf allocated buffer + * @param datetime date/time to write + * @param update update with actual time, otherwise using time from parameter 'time' + * @return length of time string + */ +uint32_t TFTView_320x240::timestamp(char* buf, uint32_t datetime, bool update) +{ + time_t local = datetime; + if (update) { +#ifdef ARCH_PORTDUINO + time(&local); +#else + if (VALID_TIME(actTime)) + local = actTime; +#endif + } + if (VALID_TIME(local)) { + std::tm date_tm{}; + localtime_r(&local, &date_tm); + if (!update) + return strftime(buf, 20, "%y/%m/%d %R\n", &date_tm); + else + return strftime(buf, 20, "%R\n", &date_tm); + } + else + return 0; +} + /** * calculate percentage value from rssi and snr * Note: ranges are based on the axis values of the signal scanner @@ -4773,8 +4868,9 @@ lv_obj_t *TFTView_320x240::newMessageContainer(uint32_t from, uint32_t to, uint8 if (channelGroup[ch] != nullptr) return channelGroup[ch]; } else { - if (messages[from] != nullptr) - return messages[from]; + auto it = messages.find(from); + if (it != messages.end() && it->second) + return it->second; } // create container for new messages @@ -4814,41 +4910,28 @@ lv_obj_t *TFTView_320x240::newMessageContainer(uint32_t from, uint32_t to, uint8 /** * @brief insert a mew message that arrived into a or container * - * @param nodeNum - * @param ch - * @param msg + * @param from source node + * @param to destination node + * @param ch channel + * @param size length of msg + * @param msg text message + * @param time in/out: message time (maybe overwritten when 0) + * @param restore if restoring then skip banners and highlight */ -void TFTView_320x240::newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg) +void TFTView_320x240::newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg, uint32_t &msgTime, bool restore) { + ILOG_DEBUG("newMessage: from:0x%08x, to:0x%08x, ch:%d, time:%d", from, to, ch, msgTime); + int pos = 0; char buf[284]; // 237 + 4 + 40 + 2 + 1 - char *message = (char *)msg; lv_obj_t *container = nullptr; if (to == UINT32_MAX) { // message for group, prepend short name to msg // original short name is held in userData, extract it and add msg char *userData = (char *)&(nodes[from]->LV_OBJ_IDX(node_lbs_idx)->user_data); - int pos = 0; while (pos < 4 && userData[pos] != 0) { buf[pos] = userData[pos]; pos++; } - - // add current time - time_t curr_time; -#ifdef ARCH_PORTDUINO - time(&curr_time); -#else - curr_time = actTime; -#endif - if (curr_time > 1000000) { - tm *curr_tm = localtime(&curr_time); - size_t len = strftime(&buf[pos], 40, " %R", curr_tm); - pos += len; - } else { - buf[pos++] = ':'; - } - - sprintf(&buf[pos], "\n%s", msg); - message = buf; + buf[pos++] = ' '; container = channelGroup[ch]; } else { // message for us container = messages[from]; @@ -4859,20 +4942,28 @@ void TFTView_320x240::newMessage(uint32_t from, uint32_t to, uint8_t ch, const c container = newMessageContainer(from, to, ch); } - // place new message into container - newMessage(from, container, ch, message); + pos += timestamp(&buf[pos], msgTime, !restore); + sprintf(&buf[pos], "%s", msg); - // display msg popup if not already viewing the messages - if (container != activeMsgContainer || activePanel != objects.messages_panel) { - unreadMessages++; - updateUnreadMessages(); - if (activePanel != objects.messages_panel && db.uiConfig.alert_enabled) { - showMessagePopup(from, to, ch, lv_label_get_text(nodes[from]->LV_OBJ_IDX(node_lbl_idx))); + // place message into container + newMessage(from, container, ch, buf); + + if (!restore) { + // display msg popup if not already viewing the messages + if (container != activeMsgContainer || activePanel != objects.messages_panel) { + unreadMessages++; + updateUnreadMessages(); + if (activePanel != objects.messages_panel && db.uiConfig.alert_enabled) { + showMessagePopup(from, to, ch, lv_label_get_text(nodes[from]->LV_OBJ_IDX(node_lbl_idx))); + } + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); } - lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + if (container != activeMsgContainer) + highlightChat(from, to, ch); + } else { + if (container != activeMsgContainer) + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); } - - highlightChat(from, to, ch); } /** @@ -4900,8 +4991,8 @@ void TFTView_320x240::newMessage(uint32_t nodeNum, lv_obj_t *container, uint8_t lv_obj_t *msgLabel = lv_label_create(hiddenPanel); // calculate expected size of text bubble, to make it look nicer lv_coord_t width = lv_txt_get_width(msg, strlen(msg), &ui_font_montserrat_12, 0); - lv_obj_set_width(msgLabel, std::max(std::min((int32_t)(width) + 20, 200), 40)); - lv_obj_set_height(msgLabel, LV_SIZE_CONTENT); /// 1 + lv_obj_set_width(msgLabel, std::max(std::min((int32_t)(width), 200), 30)); + lv_obj_set_height(msgLabel, LV_SIZE_CONTENT); lv_obj_set_align(msgLabel, LV_ALIGN_LEFT_MID); lv_label_set_text(msgLabel, msg); add_style_new_message_style(msgLabel); @@ -4910,6 +5001,71 @@ void TFTView_320x240::newMessage(uint32_t nodeNum, lv_obj_t *container, uint8_t lv_obj_move_foreground(objects.message_input_area); } +/** + * restored messages from persistent log + */ +void TFTView_320x240::restoreMessage(const LogMessage &msg) +{ + ((uint8_t *)msg.bytes)[msg._size] = 0; + ILOG_DEBUG("restoring msg from:0x%08x, to:0x%08x, ch:%d, time:%d, status:%d, trash:%d, size:%d, '%s'", msg.from, msg.to, + msg.ch, msg.time, (int)msg.status, msg.trashFlag, msg._size, msg.bytes); + + if (msg.from == ownNode) { + lv_obj_t *container = nullptr; + if (msg.to == UINT32_MAX) { + if (msg.trashFlag && chats.find(msg.ch) != chats.end()) { + ILOG_DEBUG("trashFlag set for channel %d", msg.ch); + lv_obj_delete(chats[msg.ch]); + lv_obj_del(channelGroup[msg.ch]); + channelGroup[msg.ch] = nullptr; + chats.erase(msg.ch); + return; + } else { + container = newMessageContainer(msg.from, msg.to, msg.ch); + } + } else { + if (nodes.find(msg.to) != nodes.end()) { + if (msg.trashFlag && chats.find(msg.to) != chats.end()) { + ILOG_DEBUG("trashFlag set for node %08x", msg.to); + lv_obj_delete(chats[msg.to]); + lv_obj_del(messages[msg.to]); + messages.erase(msg.to); + chats.erase(msg.to); + return; + } else { + container = newMessageContainer(msg.to, msg.from, msg.ch); + } + } + else { + LOG_DEBUG("to node 0x%08x not in db", msg.to); + MeshtasticView::addOrUpdateNode(msg.to, msg.ch, 0, eRole::unknown, false, false); + } + } + if (container) { + if (container != activeMsgContainer) + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + addMessage(container, msg.time, 0, (char *)msg.bytes, msg.status); + } + } else if (nodes.find(msg.from) != nodes.end()) { + uint32_t time = msg.time ? msg.time : UINT32_MAX; // don't overwrite 0 with actual time + newMessage(msg.from, msg.to, msg.ch, (const char *)msg.bytes, time); + } + else { + // from node not in db + LOG_DEBUG("from node 0x%08x not in db", msg.from); + MeshtasticView::addOrUpdateNode(msg.from, msg.ch, 0, eRole::unknown, false, false); + + char buf[284]; // 237 + 4 + 40 + 2 + 1 + uint32_t len = timestamp(buf, msg.time, false); + memcpy(buf + len, msg.bytes, msg.size()); + buf[len + msg.size()] = 0; + + lv_obj_t *container = newMessageContainer(msg.from, msg.to, msg.ch); + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + newMessage(msg.from, container, msg.ch, buf); + } +} + /** * @brief Add a new chat to the chat panel to access the message container * @@ -4951,8 +5107,14 @@ void TFTView_320x240::addChat(uint32_t from, uint32_t to, uint8_t ch) if (to == UINT32_MAX || from == 0) { sprintf(buf, "%d: %s", (int)ch, lv_label_get_text(channel[ch])); } else { - sprintf(buf, "%s: %s", lv_label_get_text(nodes[from]->LV_OBJ_IDX(node_lbs_idx)), - lv_label_get_text(nodes[from]->LV_OBJ_IDX(node_lbl_idx))); + auto it = nodes.find(from); + if (it != nodes.end()) { + sprintf(buf, "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), + lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); + } + else { + sprintf(buf, "!%08x", from); + } } { @@ -4994,8 +5156,10 @@ void TFTView_320x240::addChat(uint32_t from, uint32_t to, uint8_t ch) chats[index] = chatBtn; updateActiveChats(); - if (index > c_max_channels) - applyNodesFilter(index); + if (index > c_max_channels) { + if (nodes.find(index) != nodes.end()) + applyNodesFilter(index); + } lv_obj_add_event_cb(chatBtn, ui_event_ChatButton, LV_EVENT_ALL, (void *)index); lv_obj_add_event_cb(chatDelBtn, ui_event_ChatDelButton, LV_EVENT_CLICKED, (void *)index); @@ -5018,6 +5182,30 @@ void TFTView_320x240::updateActiveChats(void) lv_label_set_text(objects.top_chats_label, buf); } +/** + * @brief Display banner showing to be patient while restoring messages + */ +void TFTView_320x240::notifyRestoreMessages(int32_t percentage) +{ + ILOG_DEBUG("notifyRestoreMessages: %d%", percentage); // TODO + if (percentage >= 0) { + static char buf[64]; + lv_snprintf(buf, sizeof(buf), _("Restoring messages %d%%\n...please wait..."), percentage); + lv_label_set_text(objects.msg_popup_label, buf); + lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.msg_popup_button); + } else { + lv_obj_add_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); + ILOG_DEBUG("notifyRestoreMessages finished"); + } +} + +void TFTView_320x240::notifyMessagesRestored(void) +{ + updateActiveChats(); + updateNodesFiltered(true); +} + /** * @brief display new message popup panel * @@ -5087,15 +5275,19 @@ void TFTView_320x240::showMessages(uint32_t nodeNum) if (p) { lv_label_set_text(objects.top_messages_node_label, lv_label_get_text(p->LV_OBJ_IDX(node_lbl_idx))); ui_set_active(objects.messages_button, objects.messages_panel, objects.top_messages_panel); - lv_color_t color1 = lv_obj_get_style_bg_color(p->LV_OBJ_IDX(node_img_idx), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_color_t color2 = lv_obj_get_style_border_color(p->LV_OBJ_IDX(node_img_idx), LV_PART_MAIN | LV_STATE_DEFAULT); - bool hasKey = color1.blue == color2.blue && color1.red == color2.red && color1.green == color2.green; - if (hasKey) { + switch ((unsigned long)p->LV_OBJ_IDX(node_bat_idx)->user_data) { + case 0: + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_channel_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case 1: lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_secure_image, LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_channel_image, + break; + default: + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, LV_PART_MAIN | LV_STATE_DEFAULT); + break; } unreadMessages = 0; // TODO: not all messages may be actually read updateUnreadMessages(); @@ -5350,7 +5542,7 @@ void TFTView_320x240::updateLastHeard(uint32_t nodeNum) time_t curtime; time(&curtime); auto it = nodes.find(nodeNum); - if (it != nodes.end()) { + if (it != nodes.end() && it->second) { time_t lastHeard = (time_t)it->second->LV_OBJ_IDX(node_lh_idx)->user_data; it->second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)curtime; lv_label_set_text(it->second->LV_OBJ_IDX(node_lh_idx), _("now")); @@ -5380,7 +5572,7 @@ void TFTView_320x240::updateAllLastHeard(void) { uint16_t online = 0; time_t lastHeard; - for (auto &it : nodes) { + for (auto it : nodes) { char buf[32]; if (it.first == ownNode) { // own node is always now, so do update time_t curtime; @@ -5432,7 +5624,7 @@ void TFTView_320x240::updateTime(void) tm *curr_tm = localtime(&curr_time); int len = 0; - if (curr_time > 1000000 && (unsigned long)objects.home_time_button->user_data == 0) { + if (VALID_TIME(curr_time) && (unsigned long)objects.home_time_button->user_data == 0) { len = strftime(buf, 40, "%T %Z\n%a %d-%b-%g", curr_tm); } else { uint32_t uptime = millis() / 1000; diff --git a/source/ViewController.cpp b/source/ViewController.cpp index 7f267b1..d6b0e66 100644 --- a/source/ViewController.cpp +++ b/source/ViewController.cpp @@ -1,17 +1,28 @@ #include "ViewController.h" #include "ILog.h" +#include "LogMessage.h" #include "MeshtasticView.h" #include "assert.h" #include +#if defined(ARCH_PORTDUINO) +#include "PortduinoFS.h" +fs::FS& persistentFS = PortduinoFS; +#else +#include "LittleFS.h" +fs::FS& persistentFS = LittleFS; +#endif + const size_t DATA_PAYLOAD_LEN = meshtastic_Constants_DATA_PAYLOAD_LEN; +constexpr const char *logDir = "/messages"; /** * @brief mediate between GUI view and client interface * */ ViewController::ViewController() - : view(nullptr), client(nullptr), sendId(1), myNodeNum(0), lastSetup(0), setupDone(false), requestConfigRequired(true) + : view(nullptr), log(persistentFS, logDir, sizeof(LogMessage)), client(nullptr), sendId(1), myNodeNum(0), lastSetup(0), + setupDone(false), configCompleted(false), messagesRestored(false), requestConfigRequired(true) { } @@ -24,6 +35,7 @@ void ViewController::init(MeshtasticView *gui, IClientBase *_client) client->init(); client->connect(); } + log.init(); } /** @@ -37,6 +49,9 @@ void ViewController::runOnce(void) requestConfig(); receive(); + if (configCompleted && !messagesRestored) + restoreTextMessages(); + // executed every 10s: time_t curtime; time(&curtime); @@ -412,12 +427,16 @@ void ViewController::setConfigRequested(bool required) requestConfigRequired = required; } -void ViewController::sendTextMessage(uint32_t to, uint8_t ch, uint8_t hopLimit, uint32_t requestId, bool usePkc, +void ViewController::sendTextMessage(uint32_t to, uint8_t ch, uint8_t hopLimit, uint32_t msgTime, uint32_t requestId, bool usePkc, const char *textmsg) { - assert(strlen(textmsg) <= (size_t)DATA_PAYLOAD_LEN); - send(to, ch, hopLimit, requestId, meshtastic_PortNum_TEXT_MESSAGE_APP, false, usePkc, (const uint8_t *)textmsg, - strlen(textmsg)); + size_t msgLen = strlen(textmsg); + assert(msgLen <= (size_t)DATA_PAYLOAD_LEN); + + if (send(to, ch, hopLimit, requestId, meshtastic_PortNum_TEXT_MESSAGE_APP, false, usePkc, (const uint8_t *)textmsg, msgLen)) { + ILOG_DEBUG("storing msg to:0x%08x, ch:%d, time:%d, size:%d, '%s'", to, ch, msgTime, msgLen, textmsg); + log.write(LogMessageEnv(myNodeNum, to, ch, msgTime, LogMessage::eDefault, false, msgLen, (const uint8_t *)textmsg)); + } } bool ViewController::requestPosition(uint32_t to, uint8_t ch, uint32_t requestId) @@ -560,6 +579,43 @@ void ViewController::requestAdditionalConfig(void) requestRingtone(myNodeNum); } +/** + * recover all messages from persistent log (could take a while!) + */ +void ViewController::beginRestoreTextMessages(void) +{ + configCompleted = true; + restoreTimer = millis(); + ILOG_DEBUG("loading persistent messages..."); +} + +/** + * incrementally recover messages from persistent log (could take a while!) + */ +void ViewController::restoreTextMessages(void) +{ + LogMessageEnv msg; + if (log.readNext(msg)) { + view->restoreMessage(msg); + } else { + ILOG_DEBUG("restoring log messages completed in %dms.", millis() - restoreTimer); + messagesRestored = true; + view->notifyMessagesRestored(); + } +} +/** + * write a flag into message log that chat has been deleted + * The call removeTextMessages(0,0,0) removes all logs. + */ +void ViewController::removeTextMessages(uint32_t from, uint32_t to, uint8_t ch) +{ + if (!from && !to && !ch) { + log.clear(); + } else { + log.write(LogMessageEnv(from, to, ch, 0L, LogMessage::eDefault, true, 0, nullptr)); + } +} + /** * request connection status of WLAN/BT/MQTT */ @@ -764,6 +820,7 @@ bool ViewController::handleFromRadio(const meshtastic_FromRadio &from) break; } case meshtastic_FromRadio_config_complete_id_tag: { + beginRestoreTextMessages(); view->configCompleted(); requestAdditionalConfig(); view->notifyResync(false); @@ -810,7 +867,36 @@ bool ViewController::packetReceived(const meshtastic_MeshPacket &p) switch (p.decoded.portnum) { case meshtastic_PortNum_TEXT_MESSAGE_APP: { ILOG_INFO("received text message '%s'", (const char *)p.decoded.payload.bytes); - view->newMessage(p.from, p.to, p.channel, (const char *)p.decoded.payload.bytes); + if (!messagesRestored && log.count() > 0) { + // houston we have a problem! Haven't finished restoring messages incrementally while new ones come in + // enforce loading all at once which may take a while so display some banner if it'll take longer + if (!configCompleted) { + ILOG_ERROR("cannot handle received message NOW"); + return false; // the only way out + } + + ILOG_DEBUG("loading all logs at once"); + int32_t percentage = log.current() * 100 / log.count(); + bool showPercentage = false; + if (log.count() > 2 && percentage < 50) { // TODO: was 10 + showPercentage = true; + view->notifyRestoreMessages(percentage); + } + uint32_t count = 0; + LogMessageEnv msg; + while (log.readNext(msg)) { + view->restoreMessage(msg); + if (showPercentage && (count++ % 10) == 0) { + view->notifyRestoreMessages(log.current() * 100 / log.count()); + } + } + messagesRestored = true; + view->notifyRestoreMessages(-1); + } + uint32_t time = p.rx_time; + view->newMessage(p.from, p.to, p.channel, (const char *)p.decoded.payload.bytes, time); + log.write(LogMessageEnv(p.from, p.to, p.channel, time, LogMessage::eDefault, false, p.decoded.payload.size, + (const uint8_t *)p.decoded.payload.bytes)); break; } case meshtastic_PortNum_POSITION_APP: { diff --git a/source/comms/packet/PacketServer.cpp b/source/comms/packet/PacketServer.cpp index 5b41490..e388560 100644 --- a/source/comms/packet/PacketServer.cpp +++ b/source/comms/packet/PacketServer.cpp @@ -2,7 +2,7 @@ #include "SharedQueue.h" #include -const uint32_t max_packet_queue_size = 50; +const uint32_t max_packet_queue_size = 200; SharedQueue *sharedQueue = nullptr;