From b5e952b0087bd39e64ab2709cea53437a30e3e16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 19:17:55 -0600 Subject: [PATCH 01/83] Upgrade trunk (#9128) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 705f0177c..1dcee4209 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,8 +8,8 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.496 - - renovate@42.66.14 + - checkov@3.2.497 + - renovate@42.69.2 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.37.1 @@ -21,7 +21,7 @@ lint: - markdownlint@0.47.0 - oxipng@10.0.0 - svgo@4.0.0 - - actionlint@1.7.9 + - actionlint@1.7.10 - flake8@7.3.0 - hadolint@2.14.0 - shfmt@3.6.0 From 1e914140cae3262f0341ea506c88e691d86e02ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 08:35:52 -0600 Subject: [PATCH 02/83] Update protobufs (#9148) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/admin.pb.cpp | 5 ++ src/mesh/generated/meshtastic/admin.pb.h | 50 ++++++++++- src/mesh/generated/meshtastic/mesh.pb.cpp | 5 ++ src/mesh/generated/meshtastic/mesh.pb.h | 92 +++++++++++++++++++-- src/mesh/generated/meshtastic/portnums.pb.h | 5 ++ 6 files changed, 151 insertions(+), 8 deletions(-) diff --git a/protobufs b/protobufs index 9beb80f1d..c2e45a3fc 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 9beb80f1d302f70d05f9c4bc9dd543b8f7bc8796 +Subproject commit c2e45a3fc9cda6aedb72ad3b5b88fcccfa78073e diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index 4c4d0e3d1..e358bc96d 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -12,6 +12,9 @@ PB_BIND(meshtastic_AdminMessage, meshtastic_AdminMessage, 2) PB_BIND(meshtastic_AdminMessage_InputEvent, meshtastic_AdminMessage_InputEvent, AUTO) +PB_BIND(meshtastic_AdminMessage_OTAEvent, meshtastic_AdminMessage_OTAEvent, AUTO) + + PB_BIND(meshtastic_HamParameters, meshtastic_HamParameters, AUTO) @@ -33,3 +36,5 @@ PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO) + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index a542cf29c..ec6e13e9e 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -16,6 +16,16 @@ #endif /* Enum definitions */ +/* Firmware update mode for OTA updates */ +typedef enum _meshtastic_OTAMode { + /* Do not reboot into OTA mode */ + meshtastic_OTAMode_NO_REBOOT_OTA = 0, + /* Reboot into OTA mode for BLE firmware update */ + meshtastic_OTAMode_OTA_BLE = 1, + /* Reboot into OTA mode for WiFi firmware update */ + meshtastic_OTAMode_OTA_WIFI = 2 +} meshtastic_OTAMode; + /* TODO: REPLACE */ typedef enum _meshtastic_AdminMessage_ConfigType { /* TODO: REPLACE */ @@ -103,6 +113,17 @@ typedef struct _meshtastic_AdminMessage_InputEvent { uint16_t touch_y; } meshtastic_AdminMessage_InputEvent; +typedef PB_BYTES_ARRAY_T(32) meshtastic_AdminMessage_OTAEvent_ota_hash_t; +/* User is requesting an over the air update. + Node will reboot into the OTA loader */ +typedef struct _meshtastic_AdminMessage_OTAEvent { + /* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) */ + meshtastic_OTAMode reboot_ota_mode; + /* A 32 byte hash of the OTA firmware. + Used to verify the integrity of the firmware before applying an update. */ + meshtastic_AdminMessage_OTAEvent_ota_hash_t ota_hash; +} meshtastic_AdminMessage_OTAEvent; + /* Parameters for setting up Meshtastic for ameteur radio usage */ typedef struct _meshtastic_HamParameters { /* Amateur radio call sign, eg. KD2ABC */ @@ -261,7 +282,8 @@ typedef struct _meshtastic_AdminMessage { /* Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. */ int32_t factory_reset_device; /* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) - Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. */ + Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + Deprecated in favor of reboot_ota_mode in 2.7.17 */ int32_t reboot_ota_seconds; /* This message is only supported for the simulator Portduino build. If received the simulator will exit successfully. */ @@ -275,6 +297,8 @@ typedef struct _meshtastic_AdminMessage { /* Tell the node to reset the nodedb. When true, favorites are preserved through reset. */ bool nodedb_reset; + /* Tell the node to reset into the OTA Loader */ + meshtastic_AdminMessage_OTAEvent ota_request; }; /* The node generates this key and sends it with any get_x_response packets. The client MUST include the same key with any set_x commands. Key expires after 300 seconds. @@ -288,6 +312,10 @@ extern "C" { #endif /* Helper constants for enums */ +#define _meshtastic_OTAMode_MIN meshtastic_OTAMode_NO_REBOOT_OTA +#define _meshtastic_OTAMode_MAX meshtastic_OTAMode_OTA_WIFI +#define _meshtastic_OTAMode_ARRAYSIZE ((meshtastic_OTAMode)(meshtastic_OTAMode_OTA_WIFI+1)) + #define _meshtastic_AdminMessage_ConfigType_MIN meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG #define _meshtastic_AdminMessage_ConfigType_MAX meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG #define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1)) @@ -311,6 +339,8 @@ extern "C" { #define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation +#define meshtastic_AdminMessage_OTAEvent_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode + @@ -320,12 +350,14 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0} +#define meshtastic_AdminMessage_OTAEvent_init_default {_meshtastic_OTAMode_MIN, {0, {0}}} #define meshtastic_HamParameters_init_default {"", 0, 0, ""} #define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} #define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0} #define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} +#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}} #define meshtastic_HamParameters_init_zero {"", 0, 0, ""} #define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} #define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0} @@ -336,6 +368,8 @@ extern "C" { #define meshtastic_AdminMessage_InputEvent_kb_char_tag 2 #define meshtastic_AdminMessage_InputEvent_touch_x_tag 3 #define meshtastic_AdminMessage_InputEvent_touch_y_tag 4 +#define meshtastic_AdminMessage_OTAEvent_reboot_ota_mode_tag 1 +#define meshtastic_AdminMessage_OTAEvent_ota_hash_tag 2 #define meshtastic_HamParameters_call_sign_tag 1 #define meshtastic_HamParameters_tx_power_tag 2 #define meshtastic_HamParameters_frequency_tag 3 @@ -403,6 +437,7 @@ extern "C" { #define meshtastic_AdminMessage_shutdown_seconds_tag 98 #define meshtastic_AdminMessage_factory_reset_config_tag 99 #define meshtastic_AdminMessage_nodedb_reset_tag 100 +#define meshtastic_AdminMessage_ota_request_tag 102 #define meshtastic_AdminMessage_session_passkey_tag 101 /* Struct field encoding specification for nanopb */ @@ -461,7 +496,8 @@ X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_seconds,reboot_second X(a, STATIC, ONEOF, INT32, (payload_variant,shutdown_seconds,shutdown_seconds), 98) \ X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_config,factory_reset_config), 99) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,nodedb_reset,nodedb_reset), 100) \ -X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) +X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 102) #define meshtastic_AdminMessage_CALLBACK NULL #define meshtastic_AdminMessage_DEFAULT NULL #define meshtastic_AdminMessage_payload_variant_get_channel_response_MSGTYPE meshtastic_Channel @@ -482,6 +518,7 @@ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) #define meshtastic_AdminMessage_payload_variant_store_ui_config_MSGTYPE meshtastic_DeviceUIConfig #define meshtastic_AdminMessage_payload_variant_add_contact_MSGTYPE meshtastic_SharedContact #define meshtastic_AdminMessage_payload_variant_key_verification_MSGTYPE meshtastic_KeyVerificationAdmin +#define meshtastic_AdminMessage_payload_variant_ota_request_MSGTYPE meshtastic_AdminMessage_OTAEvent #define meshtastic_AdminMessage_InputEvent_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, event_code, 1) \ @@ -491,6 +528,12 @@ X(a, STATIC, SINGULAR, UINT32, touch_y, 4) #define meshtastic_AdminMessage_InputEvent_CALLBACK NULL #define meshtastic_AdminMessage_InputEvent_DEFAULT NULL +#define meshtastic_AdminMessage_OTAEvent_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, reboot_ota_mode, 1) \ +X(a, STATIC, SINGULAR, BYTES, ota_hash, 2) +#define meshtastic_AdminMessage_OTAEvent_CALLBACK NULL +#define meshtastic_AdminMessage_OTAEvent_DEFAULT NULL + #define meshtastic_HamParameters_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, call_sign, 1) \ X(a, STATIC, SINGULAR, INT32, tx_power, 2) \ @@ -524,6 +567,7 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4) extern const pb_msgdesc_t meshtastic_AdminMessage_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg; +extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg; extern const pb_msgdesc_t meshtastic_HamParameters_msg; extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg; extern const pb_msgdesc_t meshtastic_SharedContact_msg; @@ -532,6 +576,7 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg #define meshtastic_AdminMessage_InputEvent_fields &meshtastic_AdminMessage_InputEvent_msg +#define meshtastic_AdminMessage_OTAEvent_fields &meshtastic_AdminMessage_OTAEvent_msg #define meshtastic_HamParameters_fields &meshtastic_HamParameters_msg #define meshtastic_NodeRemoteHardwarePinsResponse_fields &meshtastic_NodeRemoteHardwarePinsResponse_msg #define meshtastic_SharedContact_fields &meshtastic_SharedContact_msg @@ -540,6 +585,7 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size #define meshtastic_AdminMessage_InputEvent_size 14 +#define meshtastic_AdminMessage_OTAEvent_size 36 #define meshtastic_AdminMessage_size 511 #define meshtastic_HamParameters_size 31 #define meshtastic_KeyVerificationAdmin_size 25 diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp index 9966e52f8..d8eee1203 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.cpp +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -24,6 +24,9 @@ PB_BIND(meshtastic_Data, meshtastic_Data, 2) PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO) +PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2) + + PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO) @@ -121,6 +124,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU + + diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 0c48a7891..344c0e68a 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -92,8 +92,8 @@ typedef enum _meshtastic_HardwareModel { Less common/prototype boards listed here (needs one more byte over the air) --------------------------------------------------------------------------- */ meshtastic_HardwareModel_LORA_RELAY_V1 = 32, - /* TODO: REPLACE */ - meshtastic_HardwareModel_NRF52840DK = 33, + /* T-Echo Plus device from LilyGo */ + meshtastic_HardwareModel_T_ECHO_PLUS = 33, /* TODO: REPLACE */ meshtastic_HardwareModel_PPR = 34, /* TODO: REPLACE */ @@ -475,9 +475,28 @@ typedef enum _meshtastic_Routing_Error { meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37, /* Airtime fairness rate limit exceeded for a packet This typically enforced per portnum and is used to prevent a single node from monopolizing airtime */ - meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38 + meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38, + /* PKI encryption failed, due to no public key for the remote node + This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet */ + meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY = 39 } meshtastic_Routing_Error; +/* Enum of message types */ +typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type { + /* Send an announcement of the canonical tip of a chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE = 0, + /* Query whether a specific link is on the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_CHAIN_QUERY = 1, + /* Request the next link in the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST = 3, + /* Provide a link to add to the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE = 4, + /* If we must fragment, send the first half */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_FIRSTHALF = 5, + /* If we must fragment, send the second half */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6 +} meshtastic_StoreForwardPlusPlus_SFPP_message_type; + /* The priority of this message for sending. Higher priorities are sent first (when managing the transmit queue). This field is never sent over the air, it is only used internally inside of a local device node. @@ -782,6 +801,34 @@ typedef struct _meshtastic_KeyVerification { meshtastic_KeyVerification_hash2_t hash2; } meshtastic_KeyVerification; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_message_hash_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_commit_hash_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_root_hash_t; +typedef PB_BYTES_ARRAY_T(240) meshtastic_StoreForwardPlusPlus_message_t; +/* The actual over-the-mesh message doing store and forward++ */ +typedef struct _meshtastic_StoreForwardPlusPlus { + /* Which message type is this */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type sfpp_message_type; + /* The hash of the specific message */ + meshtastic_StoreForwardPlusPlus_message_hash_t message_hash; + /* The hash of a link on a chain */ + meshtastic_StoreForwardPlusPlus_commit_hash_t commit_hash; + /* the root hash of a chain */ + meshtastic_StoreForwardPlusPlus_root_hash_t root_hash; + /* The encrypted bytes from a message */ + meshtastic_StoreForwardPlusPlus_message_t message; + /* Message ID of the contained message */ + uint32_t encapsulated_id; + /* Destination of the contained message */ + uint32_t encapsulated_to; + /* Sender of the contained message */ + uint32_t encapsulated_from; + /* The receive time of the message in question */ + uint32_t encapsulated_rxtime; + /* Used in a LINK_REQUEST to specify the message X spots back from head */ + uint32_t chain_count; +} meshtastic_StoreForwardPlusPlus; + /* Waypoint message, used to share arbitrary locations across the mesh */ typedef struct _meshtastic_Waypoint { /* Id of the waypoint */ @@ -1307,8 +1354,12 @@ extern "C" { #define _meshtastic_Position_AltSource_ARRAYSIZE ((meshtastic_Position_AltSource)(meshtastic_Position_AltSource_ALT_BAROMETRIC+1)) #define _meshtastic_Routing_Error_MIN meshtastic_Routing_Error_NONE -#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED -#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED+1)) +#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY +#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY+1)) + +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1)) #define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET #define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX @@ -1338,6 +1389,8 @@ extern "C" { #define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum +#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type + #define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority @@ -1380,6 +1433,7 @@ extern "C" { #define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}} #define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} #define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}} +#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} @@ -1411,6 +1465,7 @@ extern "C" { #define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}} #define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} #define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}} +#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} @@ -1489,6 +1544,16 @@ extern "C" { #define meshtastic_KeyVerification_nonce_tag 1 #define meshtastic_KeyVerification_hash1_tag 2 #define meshtastic_KeyVerification_hash2_tag 3 +#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_tag 1 +#define meshtastic_StoreForwardPlusPlus_message_hash_tag 2 +#define meshtastic_StoreForwardPlusPlus_commit_hash_tag 3 +#define meshtastic_StoreForwardPlusPlus_root_hash_tag 4 +#define meshtastic_StoreForwardPlusPlus_message_tag 5 +#define meshtastic_StoreForwardPlusPlus_encapsulated_id_tag 6 +#define meshtastic_StoreForwardPlusPlus_encapsulated_to_tag 7 +#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8 +#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9 +#define meshtastic_StoreForwardPlusPlus_chain_count_tag 10 #define meshtastic_Waypoint_id_tag 1 #define meshtastic_Waypoint_latitude_i_tag 2 #define meshtastic_Waypoint_longitude_i_tag 3 @@ -1705,6 +1770,20 @@ X(a, STATIC, SINGULAR, BYTES, hash2, 3) #define meshtastic_KeyVerification_CALLBACK NULL #define meshtastic_KeyVerification_DEFAULT NULL +#define meshtastic_StoreForwardPlusPlus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, sfpp_message_type, 1) \ +X(a, STATIC, SINGULAR, BYTES, message_hash, 2) \ +X(a, STATIC, SINGULAR, BYTES, commit_hash, 3) \ +X(a, STATIC, SINGULAR, BYTES, root_hash, 4) \ +X(a, STATIC, SINGULAR, BYTES, message, 5) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_id, 6) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_to, 7) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_from, 8) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_rxtime, 9) \ +X(a, STATIC, SINGULAR, UINT32, chain_count, 10) +#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL +#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL + #define meshtastic_Waypoint_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, id, 1) \ X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \ @@ -1980,6 +2059,7 @@ extern const pb_msgdesc_t meshtastic_RouteDiscovery_msg; extern const pb_msgdesc_t meshtastic_Routing_msg; extern const pb_msgdesc_t meshtastic_Data_msg; extern const pb_msgdesc_t meshtastic_KeyVerification_msg; +extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg; extern const pb_msgdesc_t meshtastic_Waypoint_msg; extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg; extern const pb_msgdesc_t meshtastic_MeshPacket_msg; @@ -2013,6 +2093,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_Routing_fields &meshtastic_Routing_msg #define meshtastic_Data_fields &meshtastic_Data_msg #define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg +#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg #define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg #define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg #define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg @@ -2069,6 +2150,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_QueueStatus_size 23 #define meshtastic_RouteDiscovery_size 256 #define meshtastic_Routing_size 259 +#define meshtastic_StoreForwardPlusPlus_size 377 #define meshtastic_ToRadio_size 504 #define meshtastic_User_size 115 #define meshtastic_Waypoint_size 165 diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index 67adc60cc..6b89c6a37 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -86,6 +86,11 @@ typedef enum _meshtastic_PortNum { /* Paxcounter lib included in the firmware ENCODING: protobuf */ meshtastic_PortNum_PAXCOUNTER_APP = 34, + /* Store and Forward++ module included in the firmware + ENCODING: protobuf + This module is specifically for Native Linux nodes, and provides a Git-style + chain of messages. */ + meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP = 35, /* Provides a hardware serial interface to send and receive from the Meshtastic network. Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network. From 3a90781e1bb249e155d00f327c7f899cd4e270f0 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 2 Jan 2026 20:14:09 -0600 Subject: [PATCH 03/83] Add support for LilyGo T-Echo Plus (#9149) * Add t-echo plus support * T-Echo plus hw model * Cruft * Fix * Added InkHUD style touch backlight control --- src/graphics/EInkDisplay2.cpp | 2 +- src/input/ButtonThread.cpp | 9 +- src/input/ButtonThread.h | 6 + src/main.cpp | 34 +++- src/main.h | 2 +- src/modules/ExternalNotificationModule.cpp | 6 +- src/modules/SerialModule.cpp | 19 +-- src/platform/nrf52/architecture.h | 2 + variants/esp32s3/t-watch-s3/variant.h | 2 + variants/esp32s3/tlora-pager/variant.h | 2 + variants/nrf52840/t-echo-plus/nicheGraphics.h | 70 +++++++++ variants/nrf52840/t-echo-plus/platformio.ini | 26 ++++ variants/nrf52840/t-echo-plus/variant.cpp | 24 +++ variants/nrf52840/t-echo-plus/variant.h | 145 ++++++++++++++++++ 14 files changed, 330 insertions(+), 19 deletions(-) create mode 100644 variants/nrf52840/t-echo-plus/nicheGraphics.h create mode 100644 variants/nrf52840/t-echo-plus/platformio.ini create mode 100644 variants/nrf52840/t-echo-plus/variant.cpp create mode 100644 variants/nrf52840/t-echo-plus/variant.h diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 4209baf5d..1678da793 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -148,7 +148,7 @@ bool EInkDisplay::connect() #endif #endif -#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) +#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS) { auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 9f53b06f4..63fd8b0d7 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -37,6 +37,9 @@ bool ButtonThread::initButton(const ButtonConfig &config) _activeLow = config.activeLow; _touchQuirk = config.touchQuirk; _intRoutine = config.intRoutine; + _pressHandler = config.onPress; + _releaseHandler = config.onRelease; + _suppressLeadUp = config.suppressLeadUpSound; _longLongPress = config.longLongPress; userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); @@ -133,6 +136,8 @@ int32_t ButtonThread::runOnce() // Detect start of button press if (buttonCurrentlyPressed && !buttonWasPressed) { + if (_pressHandler) + _pressHandler(); buttonPressStartTime = millis(); leadUpPlayed = false; leadUpSequenceActive = false; @@ -140,7 +145,7 @@ int32_t ButtonThread::runOnce() } // Progressive lead-up sound system - if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { + if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) { // Start the progressive sequence if not already active if (!leadUpSequenceActive) { @@ -160,6 +165,8 @@ int32_t ButtonThread::runOnce() // Reset when button is released if (!buttonCurrentlyPressed && buttonWasPressed) { + if (_releaseHandler) + _releaseHandler(); leadUpSequenceActive = false; resetLeadUpSequence(); } diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h index 7de38341c..e724c3596 100644 --- a/src/input/ButtonThread.h +++ b/src/input/ButtonThread.h @@ -13,6 +13,9 @@ struct ButtonConfig { bool activePullup = true; uint32_t pullupSense = 0; voidFuncPtr intRoutine = nullptr; + voidFuncPtr onPress = nullptr; // Optional edge callbacks + voidFuncPtr onRelease = nullptr; // Optional edge callbacks + bool suppressLeadUpSound = false; input_broker_event singlePress = INPUT_BROKER_NONE; input_broker_event longPress = INPUT_BROKER_NONE; uint16_t longPressTime = 500; @@ -94,6 +97,9 @@ class ButtonThread : public Observable, public concurrency:: input_broker_event _shortLong = INPUT_BROKER_NONE; voidFuncPtr _intRoutine = nullptr; + voidFuncPtr _pressHandler = nullptr; + voidFuncPtr _releaseHandler = nullptr; + bool _suppressLeadUp = false; uint16_t _longPressTime = 500; uint16_t _longLongPressTime = 3900; int _pinNum = 0; diff --git a/src/main.cpp b/src/main.cpp index 245f06e05..97bb21475 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -107,6 +107,10 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr; #if defined(BUTTON_PIN_TOUCH) ButtonThread *TouchButtonThread = nullptr; +#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) +static bool touchBacklightWasOn = false; +static bool touchBacklightActive = false; +#endif #endif #if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) @@ -205,7 +209,7 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, /// The I2C address of our Air Quality Indicator (if found) ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 Adafruit_DRV2605 drv; #endif @@ -788,7 +792,6 @@ void setup() // We do this as early as possible because this loads preferences from flash // but we need to do this after main cpu init (esp32setup), because we need the random seed set nodeDB = new NodeDB; - #if HAS_TFT if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { tftSetup(); @@ -834,7 +837,12 @@ void setup() #endif #endif -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 +#if defined(PIN_DRV_EN) + pinMode(PIN_DRV_EN, OUTPUT); + digitalWrite(PIN_DRV_EN, HIGH); + delay(10); +#endif drv.begin(); drv.selectLibrary(1); // I2C trigger by sending 'go' command @@ -870,7 +878,7 @@ void setup() SPI.begin(); #endif #else - // ESP32 +// ESP32 #if defined(HW_SPI1_DEVICE) SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); @@ -1039,6 +1047,24 @@ void setup() }; touchConfig.singlePress = INPUT_BROKER_NONE; touchConfig.longPress = INPUT_BROKER_BACK; +#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) + // On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds + touchConfig.longPress = INPUT_BROKER_NONE; + touchConfig.suppressLeadUpSound = true; + touchConfig.onPress = []() { + touchBacklightWasOn = uiconfig.screen_brightness == 1; + if (!touchBacklightWasOn) { + digitalWrite(PIN_EINK_EN, HIGH); + } + touchBacklightActive = true; + }; + touchConfig.onRelease = []() { + if (touchBacklightActive && !touchBacklightWasOn) { + digitalWrite(PIN_EINK_EN, LOW); + } + touchBacklightActive = false; + }; +#endif TouchButtonThread->initButton(touchConfig); #endif diff --git a/src/main.h b/src/main.h index 414752b5c..7ca14d825 100644 --- a/src/main.h +++ b/src/main.h @@ -42,7 +42,7 @@ extern bool eink_found; extern bool pmu_found; extern bool isUSBPowered; -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 #include extern Adafruit_DRV2605 drv; #endif diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 6d52a3e46..3f6375a65 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -168,7 +168,7 @@ int32_t ExternalNotificationModule::runOnce() delay = EXT_NOTIFICATION_FAST_THREAD_MS; #endif -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 drv.go(); #endif } @@ -283,7 +283,7 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on) #ifdef UNPHONE unphone.rgb(red, green, blue); #endif -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 if (on) { drv.go(); } else { @@ -319,7 +319,7 @@ void ExternalNotificationModule::stopNow() externalTurnedOn[i] = 0; } setIntervalFromNow(0); -#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) +#ifdef HAS_DRV2605 drv.stop(); #endif diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 719e342b1..f6007a565 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -63,9 +63,9 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ - defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || \ - defined(MUZI_BASE) +#if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \ + defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || \ + defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; @@ -204,8 +204,9 @@ int32_t SerialModule::runOnce() Serial.begin(baud); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } -#elif !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) +#elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ + !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ + !defined(MUZI_BASE) if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 Serial2.setFIFOSize(RX_BUFFER); @@ -261,7 +262,7 @@ int32_t SerialModule::runOnce() } } -#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ +#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); @@ -536,9 +537,9 @@ ParsedLine parseLine(const char *line) */ void SerialModule::processWXSerial() { -#if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ - !defined(ARCH_STM32WL) && !defined(MUZI_BASE) +#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ + !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ + !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. static double dir_sum_sin = 0; diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index d4699cd8c..afe96963d 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -66,6 +66,8 @@ #define HW_VENDOR meshtastic_HardwareModel_T_ECHO #elif defined(T_ECHO_LITE) #define HW_VENDOR meshtastic_HardwareModel_T_ECHO_LITE +#elif defined(TTGO_T_ECHO_PLUS) +#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_PLUS #elif defined(ELECROW_ThinkNode_M1) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1 #elif defined(ELECROW_ThinkNode_M3) diff --git a/variants/esp32s3/t-watch-s3/variant.h b/variants/esp32s3/t-watch-s3/variant.h index 86b0a03c8..c2ad624c3 100644 --- a/variants/esp32s3/t-watch-s3/variant.h +++ b/variants/esp32s3/t-watch-s3/variant.h @@ -20,6 +20,8 @@ #define SCREEN_TRANSITION_FRAMERATE 5 // fps #define USE_TFTDISPLAY 1 +#define HAS_DRV2605 1 + #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 #define SCREEN_TOUCH_USE_I2C1 diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index 42cd7f502..fcccc0284 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -26,6 +26,8 @@ #define I2C_SDA SDA #define I2C_SCL SCL +#define HAS_DRV2605 1 + #define USE_POWERSAVE #define SLEEP_TIME 120 diff --git a/variants/nrf52840/t-echo-plus/nicheGraphics.h b/variants/nrf52840/t-echo-plus/nicheGraphics.h new file mode 100644 index 000000000..483e16ea4 --- /dev/null +++ b/variants/nrf52840/t-echo-plus/nicheGraphics.h @@ -0,0 +1,70 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/GDEY0154D67.h" +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" +#include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/Inputs/TwoButton.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + SPI1.begin(); + + Drivers::EInk *driver = new Drivers::GDEY0154D67; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + inkhud->setDriver(driver); + inkhud->setDisplayResilience(20, 1.5); + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + inkhud->persistence->settings.userTiles.maxCount = 2; + inkhud->persistence->settings.rotation = 3; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + inkhud->persistence->settings.optionalMenuItems.backlight = true; + + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_BL); + + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); + inkhud->addApplet("DMs", new InkHUD::DMApplet); + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); + + inkhud->begin(); + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); + + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + buttons->setWiring(1, PIN_BUTTON_TOUCH); + buttons->setTiming(1, 50, 5000); + buttons->setHandlerDown(1, [inkhud, backlight]() { + backlight->peek(); + inkhud->persistence->settings.optionalMenuItems.backlight = false; + }); + buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); + buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); + + buttons->start(); +} + +#endif diff --git a/variants/nrf52840/t-echo-plus/platformio.ini b/variants/nrf52840/t-echo-plus/platformio.ini new file mode 100644 index 000000000..b77d54748 --- /dev/null +++ b/variants/nrf52840/t-echo-plus/platformio.ini @@ -0,0 +1,26 @@ +[env:t-echo-plus] +extends = nrf52840_base +board = t-echo +board_level = pr +board_check = true +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -DTTGO_T_ECHO_PLUS + -Ivariants/nrf52840/t-echo-plus + -DEINK_DISPLAY_MODEL=GxEPD2_154_D67 + -DEINK_WIDTH=200 + -DEINK_HEIGHT=200 + -DUSE_EINK + -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted + -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -DI2C_NO_RESCAN + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-plus> + +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip + lewisxhe/PCF8563_Library@^1.0.1 + adafruit/Adafruit DRV2605 Library@1.2.4 diff --git a/variants/nrf52840/t-echo-plus/variant.cpp b/variants/nrf52840/t-echo-plus/variant.cpp new file mode 100644 index 000000000..084186bf6 --- /dev/null +++ b/variants/nrf52840/t-echo-plus/variant.cpp @@ -0,0 +1,24 @@ +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LEDs (if populated) + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); +} diff --git a/variants/nrf52840/t-echo-plus/variant.h b/variants/nrf52840/t-echo-plus/variant.h new file mode 100644 index 000000000..226f6d6fe --- /dev/null +++ b/variants/nrf52840/t-echo-plus/variant.h @@ -0,0 +1,145 @@ +#ifndef _VARIANT_T_ECHO_PLUS_ +#define _VARIANT_T_ECHO_PLUS_ + +#define VARIANT_MCK (64000000ul) +#define USE_LFXO + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Pin counts +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs (not documented on pinmap; keep defaults for compatibility) +#define PIN_LED1 (0 + 14) +#define PIN_LED2 (0 + 15) +#define PIN_LED3 (0 + 13) + +#define LED_RED PIN_LED3 +#define LED_BLUE PIN_LED1 +#define LED_GREEN PIN_LED2 + +#define LED_BUILTIN LED_BLUE +#define LED_CONN LED_GREEN + +#define LED_STATE_ON 0 + +// Buttons / touch +#define PIN_BUTTON1 (32 + 10) +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true +#define PIN_BUTTON2 (0 + 18) // reset-labelled but usable as GPIO +#define PIN_BUTTON_TOUCH (0 + 11) // capacitive touch +#define BUTTON_TOUCH_ACTIVE_LOW true +#define BUTTON_TOUCH_ACTIVE_PULLUP true + +#define BUTTON_CLICK_MS 400 +#define BUTTON_TOUCH_MS 200 + +// Analog +#define PIN_A0 (4) +#define BATTERY_PIN PIN_A0 +static const uint8_t A0 = PIN_A0; +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (2.0F) + +// NFC +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +// I2C (IMU BHI260AP, RTC, etc.) +#define WIRE_INTERFACES_COUNT 1 +#define PIN_WIRE_SDA (0 + 26) +#define PIN_WIRE_SCL (0 + 27) +#define HAS_BHI260AP + +#define TP_SER_IO (0 + 11) + +// RTC interrupt +#define PIN_RTC_INT (0 + 16) + +// QSPI flash +#define PIN_QSPI_SCK (32 + 14) +#define PIN_QSPI_CS (32 + 15) +#define PIN_QSPI_IO0 (32 + 12) +#define PIN_QSPI_IO1 (32 + 13) +#define PIN_QSPI_IO2 (0 + 7) +#define PIN_QSPI_IO3 (0 + 5) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI + +// LoRa SX1262 +#define USE_SX1262 +#define USE_SX1268 +#define SX126X_CS (0 + 24) +#define SX126X_DIO1 (0 + 20) +#define SX1262_DIO3 (0 + 21) +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL + +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) + +// E-paper (1.54" per pinmap) +// Alias PIN_EINK_EN to keep common eink power control code working +#define PIN_EINK_BL (32 + 11) // backlight / panel power switch +#define PIN_EINK_EN PIN_EINK_BL +#define PIN_EINK_CS (0 + 30) +#define PIN_EINK_BUSY (0 + 3) +#define PIN_EINK_DC (0 + 28) +#define PIN_EINK_RES (0 + 2) +#define PIN_EINK_SCLK (0 + 31) +#define PIN_EINK_MOSI (0 + 29) // also called SDI + +// Power control +#define PIN_POWER_EN (0 + 12) + +#define PIN_SPI1_MISO (32 + 7) // Placeholder MISO; keep off QSPI pins to avoid contention +#define PIN_SPI1_MOSI PIN_EINK_MOSI +#define PIN_SPI1_SCK PIN_EINK_SCLK + +// GPS (TX/RX/Wake/Reset/PPS per pinmap) +#define GPS_L76K +#define PIN_GPS_REINIT (32 + 5) // Reset +#define PIN_GPS_STANDBY (32 + 2) // Wake +#define PIN_GPS_PPS (32 + 4) +#define GPS_TX_PIN (32 + 8) +#define GPS_RX_PIN (32 + 9) +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +// Sensors / accessories +#define PIN_BUZZER (0 + 6) +#define PIN_DRV_EN (0 + 8) + +#define HAS_DRV2605 1 + +// Battery / ADC already defined above +#define HAS_RTC 1 + +#ifdef __cplusplus +} +#endif + +#endif From 21ca25404adccb0496c5063b82ab778b8fd3f4f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 12:00:38 -0600 Subject: [PATCH 04/83] chore(deps): update dorny/test-reporter action to v2.5.0 (#9167) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test_native.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index 40fcd7109..b527c2fd9 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -143,7 +143,7 @@ jobs: merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.4.0 + uses: dorny/test-reporter@v2.5.0 with: name: PlatformIO Tests path: testreport.xml From 15f5b35859b69d27f94e674083a2e53f740ef89e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 05:22:50 -0600 Subject: [PATCH 05/83] chore(deps): update meshtastic/device-ui digest to 272defc (#9166) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 5c3bb9730..dd8111346 100644 --- a/platformio.ini +++ b/platformio.ini @@ -123,7 +123,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/a8e2f947f7abaf0c5ac8e6dd189a22156335beaa.zip + https://github.com/meshtastic/device-ui/archive/272defcb35651461830ebfd1b39c9167c8f49317.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 594f27c3ffcfce3513a9de8b1f6aa15d6b06a1c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 05:27:14 -0600 Subject: [PATCH 06/83] Upgrade trunk (#9183) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 1dcee4209..b3d6d98e2 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,7 +9,7 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.69.2 + - renovate@42.71.2 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.37.1 From da11cc739d9d68350240d565f8ce1f08888887c5 Mon Sep 17 00:00:00 2001 From: Lewis He Date: Tue, 6 Jan 2026 21:25:31 +0800 Subject: [PATCH 07/83] Added support for the new SSD1306 control panel. (#9192) --- src/detect/ScanI2CTwoWire.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index db269ac64..45e7fda2d 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -68,7 +68,7 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const if (r == 0x08 || r == 0x00) { logFoundDevice("SH1106", (uint8_t)addr.address); o_probe = SCREEN_SH1106; // SH1106 - } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07) { + } else if (r == 0x03 || r == 0x04 || r == 0x06 || r == 0x07 || r == 0x05) { logFoundDevice("SSD1306", (uint8_t)addr.address); o_probe = SCREEN_SSD1306; // SSD1306 } From a6c4683ddc9f3e0c10d15cc0e0c69b760744ac48 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 05:50:17 -0600 Subject: [PATCH 08/83] Upgrade trunk (#9208) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index b3d6d98e2..d2d2ab0b0 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,7 +9,7 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.71.2 + - renovate@42.72.0 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.37.1 From 6e110788fd2eb2e44d89031ca8c72002c57ecc4d Mon Sep 17 00:00:00 2001 From: Wessel Date: Thu, 8 Jan 2026 12:42:01 +0100 Subject: [PATCH 09/83] =?UTF-8?q?=F0=9F=94=A7=20Fix=20LNA/PA=20power=20con?= =?UTF-8?q?trol=20for=20Heltec=20v4,=20wireless=20tracker=20v2=20(#9029)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix LNA/PA power control for Heltec v4, wireless tracker v2 * Stop using pin 46 as RF switch, just let DIO2 switch handle the RF path --------- Co-authored-by: Ben Meadors --- src/mesh/SX126xInterface.cpp | 20 +++++++++---- variants/esp32s3/heltec_v4/variant.h | 30 ++++++++++++++++--- .../heltec_wireless_tracker_v2/variant.h | 30 ++++++++++++++++--- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 2d7996a13..53cf3154e 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -53,13 +53,21 @@ template bool SX126xInterface::init() #endif #if defined(USE_GC1109_PA) + // GC1109 FEM chip initialization + // See variant.h for full pin mapping and control logic documentation + + // VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on) pinMode(LORA_PA_POWER, OUTPUT); digitalWrite(LORA_PA_POWER, HIGH); + // CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX pinMode(LORA_PA_EN, OUTPUT); - digitalWrite(LORA_PA_EN, LOW); + digitalWrite(LORA_PA_EN, HIGH); + + // CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care) + // Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH pinMode(LORA_PA_TX_EN, OUTPUT); - digitalWrite(LORA_PA_TX_EN, LOW); + digitalWrite(LORA_PA_TX_EN, LOW); // Start in RX-ready state #endif #ifdef RF95_FAN_EN @@ -377,13 +385,13 @@ template bool SX126xInterface::sleep() return true; } -/** Some boards require GPIO control of tx vs rx paths */ +/** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */ template void SX126xInterface::setTransmitEnable(bool txon) { #if defined(USE_GC1109_PA) - digitalWrite(LORA_PA_POWER, HIGH); - digitalWrite(LORA_PA_EN, HIGH); - digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); + digitalWrite(LORA_PA_POWER, HIGH); // Ensure LDO is on + digitalWrite(LORA_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) #endif } diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h index 6524bbc72..1c1168d94 100644 --- a/variants/esp32s3/heltec_v4/variant.h +++ b/variants/esp32s3/heltec_v4/variant.h @@ -29,10 +29,32 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define USE_GC1109_PA // We have a GC1109 power amplifier+attenuator -#define LORA_PA_POWER 7 // power en -#define LORA_PA_EN 2 -#define LORA_PA_TX_EN 46 // enable tx +// ---- GC1109 RF FRONT END CONFIGURATION ---- +// The Heltec V4 uses a GC1109 FEM chip with integrated PA and LNA +// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna +// Measured net TX gain (non-linear due to PA compression): +// +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) +// +10dB at 16-17dBm input +// +9dB at 18-19dBm input +// +7dB at 21dBm input (e.g., 21dBm in -> 28dBm out max) +// Control logic (from GC1109 datasheet): +// Shutdown: CSD=0, CTX=X, CPS=X +// Receive LNA: CSD=1, CTX=0, CPS=X (17dB gain, 2dB NF) +// Transmit bypass: CSD=1, CTX=1, CPS=0 (~1dB loss, no PA) +// Transmit PA: CSD=1, CTX=1, CPS=1 (full PA enabled) +// Pin mapping: +// CTX (pin 6) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) +// CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 +#define USE_GC1109_PA +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable +#define LORA_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) +#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) + +// GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) +// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp +// Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 #if HAS_TFT #define USE_TFTDISPLAY 1 diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h index 0ce6b3e00..a5489173d 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h @@ -73,7 +73,29 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define USE_GC1109_PA // We have a GC1109 power amplifier+attenuator -#define LORA_PA_POWER 7 // power en -#define LORA_PA_EN 4 -#define LORA_PA_TX_EN 46 // enable tx \ No newline at end of file +// ---- GC1109 RF FRONT END CONFIGURATION ---- +// The Heltec Wireless Tracker V2 uses a GC1109 FEM chip with integrated PA and LNA +// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna +// Measured net TX gain (non-linear due to PA compression): +// +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) +// +10dB at 16-17dBm input +// +9dB at 18-19dBm input +// +7dB at 21dBm input (e.g., 21dBm in -> 28dBm out max) +// Control logic (from GC1109 datasheet): +// Shutdown: CSD=0, CTX=X, CPS=X +// Receive LNA: CSD=1, CTX=0, CPS=X (17dB gain, 2dB NF) +// Transmit bypass: CSD=1, CTX=1, CPS=0 (~1dB loss, no PA) +// Transmit PA: CSD=1, CTX=1, CPS=1 (full PA enabled) +// Pin mapping: +// CTX (pin 6) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO4: Chip enable (HIGH=on, LOW=shutdown) +// CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 +#define USE_GC1109_PA +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable +#define LORA_PA_EN 4 // CSD - GC1109 chip enable (HIGH=on) +#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) + +// GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) +// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp +// Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 \ No newline at end of file From 29d0d5e5592ecdd4d6db83628bfc5469a90b9408 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 05:42:13 -0600 Subject: [PATCH 10/83] Upgrade trunk (#9219) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index d2d2ab0b0..fb2d93563 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,7 +9,7 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.72.0 + - renovate@42.74.2 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.37.1 From b4369b27307c824a7c78001aea57629a7eded69f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=F0=9F=95=B4=EF=B8=8F?= Date: Thu, 8 Jan 2026 09:04:17 -0500 Subject: [PATCH 11/83] Fix screen not sleeping due to power status updates (#9216) Co-authored-by: Ben Meadors --- src/graphics/Screen.cpp | 9 +++++++-- src/graphics/Screen.h | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0012aeb5d..91a9ed593 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1429,10 +1429,15 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) } nodeDB->updateGUI = false; break; - case STATUS_TYPE_POWER: - forceDisplay(true); + case STATUS_TYPE_POWER: { + bool currentUSB = powerStatus->getHasUSB(); + if (currentUSB != lastPowerUSBState) { + lastPowerUSBState = currentUSB; + forceDisplay(true); + } break; } + } return 0; } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 4bb808970..cfffc6731 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -715,6 +715,8 @@ class Screen : public concurrency::OSThread // Whether we are showing the regular screen (as opposed to booth screen or // Bluetooth PIN screen) bool showingNormalScreen = false; + /// Track USB power state to only wake screen on actual power state changes + bool lastPowerUSBState = false; // Implementation to Adjust Brightness uint8_t brightness = BRIGHTNESS_DEFAULT; // H = 254, MH = 192, ML = 130 L = 103 From f289b780618bfa8b3bee1c5c56dc99ca9549d34a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 8 Jan 2026 08:17:45 -0600 Subject: [PATCH 12/83] Fix rotary regression and tighten up playBeep (#9221) * Fix T-LoRA rotary regression and tighten up playBeep * Derp * Increase duration of chirp sound in playChirp --- src/buzz/BuzzerFeedbackThread.cpp | 16 ++++++++++------ src/buzz/buzz.cpp | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 0716dedd0..6bb4ef141 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -22,15 +22,19 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) // Handle different input events with appropriate buzzer feedback switch (event->inputEvent) { - case INPUT_BROKER_USER_PRESS: - case INPUT_BROKER_ALT_PRESS: - playClick(); // Low delay feedback - break; - +#ifdef INPUTDRIVER_ENCODER_TYPE case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT_LONG: - playBeep(); // Confirmation feedback + playClick(); break; +#else + case INPUT_BROKER_USER_PRESS: + case INPUT_BROKER_ALT_PRESS: + case INPUT_BROKER_SELECT: + case INPUT_BROKER_SELECT_LONG: + playBeep(); + break; +#endif case INPUT_BROKER_UP: case INPUT_BROKER_UP_LONG: diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 00ad71031..89d752c08 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -65,7 +65,7 @@ void playTones(const ToneDuration *tone_durations, int size) void playBeep() { - ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}}; + ToneDuration melody[] = {{NOTE_B3, DURATION_1_16}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } From c6e070461a3df0d8fae671223c18fe34831d58e8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 8 Jan 2026 08:44:05 -0600 Subject: [PATCH 13/83] Remove disabled gh action --- .github/workflows/trunk_format_pr.yml | 51 --------------------------- 1 file changed, 51 deletions(-) delete mode 100644 .github/workflows/trunk_format_pr.yml diff --git a/.github/workflows/trunk_format_pr.yml b/.github/workflows/trunk_format_pr.yml deleted file mode 100644 index 8fa0cc1eb..000000000 --- a/.github/workflows/trunk_format_pr.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Run Trunk Fmt on PR Comment - -on: - issue_comment: - types: [created] - -permissions: read-all - -jobs: - trunk-fmt: - if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'trunk fmt') - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Install trunk - run: curl https://get.trunk.io -fsSL | bash - - - name: Run Trunk Fmt - run: trunk fmt - - - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - id: version - - - name: Commit and push changes - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add . - git commit -m "Add firmware version ${{ steps.version.outputs.long }}" - git push - - - name: Comment on PR - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '`trunk fmt` has been run on this PR.' - }) From 792e930e45f119f69174194f716a775034e494ba Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 8 Jan 2026 12:07:14 -0600 Subject: [PATCH 14/83] Trunk --- boards/t-beam-1w.json | 83 ++++++++++------------- src/mesh/RadioInterface.cpp | 5 +- src/mesh/SX126xInterface.cpp | 6 +- variants/esp32s3/t-beam-1w/pins_arduino.h | 2 +- variants/esp32s3/t-beam-1w/variant.h | 16 ++--- 5 files changed, 51 insertions(+), 61 deletions(-) diff --git a/boards/t-beam-1w.json b/boards/t-beam-1w.json index ac1d1f15e..40f16195d 100644 --- a/boards/t-beam-1w.json +++ b/boards/t-beam-1w.json @@ -1,50 +1,39 @@ { - "build": { - "arduino": { - "ldscript": "esp32s3_out.ld", - "memory_type": "qio_opi" - }, - "core": "esp32", - "extra_flags": [ - "-DBOARD_HAS_PSRAM", - "-DLILYGO_TBEAM_1W", - "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", - "-DARDUINO_RUNNING_CORE=1", - "-DARDUINO_EVENT_RUNNING_CORE=1" - ], - "f_cpu": "240000000L", - "f_flash": "80000000L", - "flash_mode": "qio", - "psram_type": "opi", - "hwids": [ - [ - "0x303A", - "0x1001" - ] - ], - "mcu": "esp32s3", - "variant": "t-beam-1w" + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" }, - "connectivity": [ - "wifi", - "bluetooth", - "lora" + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DLILYGO_TBEAM_1W", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" ], - "debug": { - "openocd_target": "esp32s3.cfg" - }, - "frameworks": [ - "arduino" - ], - "name": "LilyGo TBeam-1W", - "upload": { - "flash_size": "16MB", - "maximum_ram_size": 327680, - "maximum_size": 16777216, - "require_upload_port": true, - "speed": 921600 - }, - "url": "http://www.lilygo.cn/", - "vendor": "LilyGo" -} \ No newline at end of file + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "t-beam-1w" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino"], + "name": "LilyGo TBeam-1W", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 6660549eb..aaaca719e 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -246,8 +246,9 @@ uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool rece /** The delay to use for retransmitting dropped packets */ uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) { - size_t numbytes =p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? - pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded) : p->encrypted.size+MESHTASTIC_HEADER_LENGTH; + size_t numbytes = p->which_payload_variant == meshtastic_MeshPacket_decoded_tag + ? pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded) + : p->encrypted.size + MESHTASTIC_HEADER_LENGTH; uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader)); // Make sure enough time has elapsed for this packet to be sent and an ACK is received. // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec); diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 53cf3154e..0e3069c14 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -389,9 +389,9 @@ template bool SX126xInterface::sleep() template void SX126xInterface::setTransmitEnable(bool txon) { #if defined(USE_GC1109_PA) - digitalWrite(LORA_PA_POWER, HIGH); // Ensure LDO is on - digitalWrite(LORA_PA_EN, HIGH); // CSD=1: Chip enabled - digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) + digitalWrite(LORA_PA_POWER, HIGH); // Ensure LDO is on + digitalWrite(LORA_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) #endif } diff --git a/variants/esp32s3/t-beam-1w/pins_arduino.h b/variants/esp32s3/t-beam-1w/pins_arduino.h index 92b74615d..c4591878b 100644 --- a/variants/esp32s3/t-beam-1w/pins_arduino.h +++ b/variants/esp32s3/t-beam-1w/pins_arduino.h @@ -14,7 +14,7 @@ static const uint8_t SDA = 8; static const uint8_t SCL = 9; // Default SPI mapped to Radio/SD -static const uint8_t SS = 15; // LoRa CS +static const uint8_t SS = 15; // LoRa CS static const uint8_t MOSI = 11; static const uint8_t MISO = 12; static const uint8_t SCK = 13; diff --git a/variants/esp32s3/t-beam-1w/variant.h b/variants/esp32s3/t-beam-1w/variant.h index 01404afcb..00301fdfd 100644 --- a/variants/esp32s3/t-beam-1w/variant.h +++ b/variants/esp32s3/t-beam-1w/variant.h @@ -1,4 +1,4 @@ -// LilyGo T-Beam-1W variant.h +// LilyGo T-Beam-1W variant.h // Configuration based on LilyGO utilities.h and RF documentation // I2C for OLED display (SH1106 at 0x3C) @@ -9,13 +9,13 @@ #define GPS_RX_PIN 5 #define GPS_TX_PIN 6 #define GPS_1PPS_PIN 7 -#define GPS_WAKEUP_PIN 16 // GPS_EN_PIN in LilyGO code +#define GPS_WAKEUP_PIN 16 // GPS_EN_PIN in LilyGO code #define HAS_GPS 1 #define GPS_BAUDRATE 9600 // Buttons -#define BUTTON_PIN 0 // BUTTON 1 -#define BUTTON_PIN_ALT 17 // BUTTON 2 +#define BUTTON_PIN 0 // BUTTON 1 +#define BUTTON_PIN_ALT 17 // BUTTON 2 // SPI (shared by LoRa and SD) #define SPI_MOSI 11 @@ -32,7 +32,7 @@ #define USE_SX1262 #define LORA_SCK SPI_SCK -#define LORA_MISO SPI_MISO +#define LORA_MISO SPI_MISO #define LORA_MOSI SPI_MOSI #define LORA_CS 15 #define LORA_RESET 3 @@ -58,7 +58,7 @@ // Truth table: DIO2=1,CTRL=0 → TX (PA on, LNA off) // DIO2=0,CTRL=1 → RX (PA off, LNA on) #define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_RXEN 21 // LNA enable - HIGH during RX +#define SX126X_RXEN 21 // LNA enable - HIGH during RX // TCXO voltage - required for radio init #define SX126X_DIO3_TCXO_VOLTAGE 1.8 @@ -68,7 +68,7 @@ // LED #define LED_PIN 18 -#define LED_STATE_ON 1 // HIGH = ON +#define LED_STATE_ON 1 // HIGH = ON // Battery ADC #define BATTERY_PIN 4 @@ -89,7 +89,7 @@ #define SX126X_PA_RAMP_US 0x05 // Display - SH1106 OLED (128x64) -#define USE_SH1106 +#define USE_SH1106 #define OLED_WIDTH 128 #define OLED_HEIGHT 64 From 390f0c8248a4aa6f12a4d0e595a4c0a0ca3c3a5f Mon Sep 17 00:00:00 2001 From: Jason P Date: Thu, 8 Jan 2026 16:44:05 -0600 Subject: [PATCH 15/83] Screenless Devices want to mute too! (#9210) * Screenless Devices want to mute too! * Add logging for actions * Gate to screenless devices only * WisMesh Tag was missing HAS_SCREEN 0 --------- Co-authored-by: Ben Meadors --- src/buzz/buzz.cpp | 22 ++++++++++++++++++++++ src/buzz/buzz.h | 2 ++ src/input/ButtonThread.cpp | 16 +++++++++++++++- variants/nrf52840/rak_wismeshtag/variant.h | 2 ++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 89d752c08..6fb28a6ac 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -35,6 +35,14 @@ struct ToneDuration { #define NOTE_G6 1568 #define NOTE_E7 2637 +#define NOTE_C4 262 +#define NOTE_E4 330 +#define NOTE_G4 392 +#define NOTE_A4 440 +#define NOTE_C5 523 +#define NOTE_E5 659 +#define NOTE_G5 784 + const int DURATION_1_16 = 62; // 1/16 note const int DURATION_1_8 = 125; // 1/8 note const int DURATION_1_4 = 250; // 1/4 note @@ -189,3 +197,17 @@ void playComboTune() }; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } + +void play4ClickDown() +{ + ToneDuration melody[] = {{NOTE_G5, 55}, {NOTE_E5, 55}, {NOTE_C5, 60}, {NOTE_A4, 55}, {NOTE_G4, 55}, + {NOTE_E4, 65}, {NOTE_C4, 80}, {NOTE_G3, 120}, {NOTE_E3, 160}, {NOTE_SILENT, 120}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void play4ClickUp() +{ + // Quick high-pitched notes with trills + ToneDuration melody[] = {{NOTE_F5, 50}, {NOTE_G6, 45}, {NOTE_E7, 60}}; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} \ No newline at end of file diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index fa0c8c496..1b97e24de 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -7,6 +7,8 @@ void playShutdownMelody(); void playGPSEnableBeep(); void playGPSDisableBeep(); void playComboTune(); +void play4ClickDown(); +void play4ClickUp(); void playBoop(); void playChirp(); void playClick(); diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 63fd8b0d7..0d835a3a9 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -248,7 +248,21 @@ int32_t ButtonThread::runOnce() this->notifyObservers(&evt); playComboTune(); break; - +#if !HAS_SCREEN + case 4: + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) + if (externalNotificationModule->getMute()) { + LOG_INFO("Temporarily Muted"); + play4ClickDown(); // Disable tone + } else { + LOG_INFO("Unmuted"); + play4ClickUp(); // Enable tone + } + } + break; +#endif // No valid multipress action default: break; diff --git a/variants/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index 159cabf07..fa3e252ab 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -234,6 +234,8 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define RAK_4631 1 +#define HAS_SCREEN 0 + #ifdef __cplusplus } #endif From 489de09375b0591138f7b548775573bb1dc8fc86 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 8 Jan 2026 19:19:17 -0600 Subject: [PATCH 16/83] Use correct name for ALT_BUTTON_PIN (#9225) --- variants/esp32s3/t-beam-1w/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/esp32s3/t-beam-1w/variant.h b/variants/esp32s3/t-beam-1w/variant.h index 00301fdfd..dbe1620e2 100644 --- a/variants/esp32s3/t-beam-1w/variant.h +++ b/variants/esp32s3/t-beam-1w/variant.h @@ -15,7 +15,7 @@ // Buttons #define BUTTON_PIN 0 // BUTTON 1 -#define BUTTON_PIN_ALT 17 // BUTTON 2 +#define ALT_BUTTON_PIN 17 // BUTTON 2 // SPI (shared by LoRa and SD) #define SPI_MOSI 11 From 6b8e5e9d7b1aeb421c0e96e14e181cef7e7d840b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 05:41:19 -0600 Subject: [PATCH 17/83] Upgrade trunk (#9229) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index fb2d93563..54a803206 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,14 +9,14 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.74.2 + - renovate@42.75.0 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.37.1 - bandit@1.9.2 - trivy@0.68.2 - taplo@0.10.0 - - ruff@0.14.10 + - ruff@0.14.11 - isort@7.0.0 - markdownlint@0.47.0 - oxipng@10.0.0 From b12acba44f08a607ad48aa6c596a4a0a70410a25 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 9 Jan 2026 05:42:00 -0600 Subject: [PATCH 18/83] CH341 MAC address derivation from serial and product string (#9226) Updated MAC address derivation logic to include product string in hashing. --- src/platform/portduino/PortduinoGlue.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index f2b58f333..3fa473d2d 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -419,9 +419,11 @@ void portduinoSetup() ch341Hal->getProductString(product_string, 95); std::cout << "CH341 Product " << product_string << std::endl; if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) { - uint8_t hash[32] = {0}; + std::cout << "Deriving MAC address from Serial and Product String" << std::endl; + uint8_t hash[104] = {0}; memcpy(hash, serial, 8); - crypto->hash(hash, 8); + memcpy(hash + 8, product_string, strlen(product_string)); + crypto->hash(hash, 8 + strlen(product_string)); dmac[0] = (hash[0] << 4) | 2; dmac[1] = hash[1]; dmac[2] = hash[2]; From ff8d6aa9c3e49b75e2340140ded4bca52719a42f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 9 Jan 2026 08:35:37 -0600 Subject: [PATCH 19/83] Increase default position broadcast intervals and enforce minimums for default channels --- src/mesh/Default.h | 5 ++++- src/mesh/NodeDB.cpp | 19 ++++++++++++++++++- src/modules/PositionModule.h | 7 ++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/mesh/Default.h b/src/mesh/Default.h index a60e3af9b..e206d8277 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -13,7 +13,10 @@ #define min_default_telemetry_interval_secs 30 * 60 #define default_gps_update_interval IF_ROUTER(ONE_DAY, 2 * 60) #define default_telemetry_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) -#define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 15 * 60) +#define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) +#define default_broadcast_smart_minimum_interval_secs 5 * 60 +#define min_default_broadcast_interval_secs 60 * 60 +#define min_default_broadcast_smart_minimum_interval_secs 5 * 60 #define default_wait_bluetooth_secs IF_ROUTER(1, 60) #define default_sds_secs IF_ROUTER(ONE_DAY, UINT32_MAX) // Default to forever super deep sleep #define default_ls_secs IF_ROUTER(ONE_DAY, 5 * 60) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 3c408f01f..8913e0019 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -335,6 +335,23 @@ NodeDB::NodeDB() moduleConfig.telemetry.health_update_interval = Default::getConfiguredOrMinimumValue( moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs); } + // Enforce position broadcast minimums if we would send positions over a default channel + // Check channels the same way PositionModule::sendOurPosition() does - first channel with position_precision set + bool positionUsesDefaultChannel = false; + for (uint8_t i = 0; i < channels.getNumChannels(); i++) { + if (channels.getByIndex(i).settings.has_module_settings && + channels.getByIndex(i).settings.module_settings.position_precision != 0) { + positionUsesDefaultChannel = channels.isDefaultChannel(i); + break; + } + } + if (positionUsesDefaultChannel) { + LOG_DEBUG("Coerce position broadcasts to min of 1 hour and smart broadcast min of 5 minutes on defaults"); + config.position.position_broadcast_secs = + Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, min_default_broadcast_interval_secs); + config.position.broadcast_smart_minimum_interval_secs = Default::getConfiguredOrMinimumValue( + config.position.broadcast_smart_minimum_interval_secs, min_default_broadcast_smart_minimum_interval_secs); + } // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched if (config.device.node_info_broadcast_secs > MAX_INTERVAL) config.device.node_info_broadcast_secs = MAX_INTERVAL; @@ -644,7 +661,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.position.position_broadcast_smart_enabled = true; #endif config.position.broadcast_smart_minimum_distance = 100; - config.position.broadcast_smart_minimum_interval_secs = 30; + config.position.broadcast_smart_minimum_interval_secs = default_broadcast_smart_minimum_interval_secs; if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; config.security.serial_enabled = true; diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index 32e499531..d0a4d4603 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -69,10 +69,11 @@ class PositionModule : public ProtobufModule, private concu // In event mode we want to prevent excessive position broadcasts // we set the minimum interval to 5m const uint32_t minimumTimeThreshold = - max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30)); + max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, + default_broadcast_smart_minimum_interval_secs)); #else - const uint32_t minimumTimeThreshold = - Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); + const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, + default_broadcast_smart_minimum_interval_secs); #endif }; From 925381ef7b809c347e6f86e52890c950b25fc9db Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 9 Jan 2026 10:30:49 -0600 Subject: [PATCH 20/83] Fix TFT_MESH settings across setting and recalling (#9234) * Fix TFT_MESH settings across setting and recalling * Fix a word in documentation * Update src/graphics/Screen.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/graphics/Screen.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/graphics/Screen.cpp | 11 +++++++++++ src/graphics/draw/MenuHandler.cpp | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 91a9ed593..28f17f962 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -312,6 +312,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O // Only validate the combined value once if (rawRGB > 0 && rawRGB <= 255255255) { + LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB); // Extract each component as a normal int first int r = (rawRGB >> 16) & 0xFF; int g = (rawRGB >> 8) & 0xFF; @@ -319,6 +320,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); } +#ifdef TFT_MESH_OVERRIDE + } else if (rawRGB == 0) { + LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE); + // Default to TFT_MESH_OVERRIDE if available + TFT_MESH = TFT_MESH_OVERRIDE; +#endif + } else { + // Default best readable yellow color + LOG_INFO("Setting screen RGB color to default: (255,255,128)"); + TFT_MESH = COLOR565(255, 255, 128); } #if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 138995ebe..7c17c8b92 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1840,7 +1840,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) static const ScreenColorOption colorOptions[] = { {"Back", OptionsAction::Back}, {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, - {"Meshtastic Green", OptionsAction::Select, ScreenColor(103, 234, 148)}, + {"Meshtastic Green", OptionsAction::Select, ScreenColor(0x67, 0xEA, 0x94)}, {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, @@ -1890,7 +1890,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) #ifdef TFT_MESH_OVERRIDE TFT_MESH = TFT_MESH_OVERRIDE; #else - TFT_MESH = COLOR565(0x67, 0xEA, 0x94); + TFT_MESH = COLOR565(255, 255, 128); #endif } else { TFT_MESH = COLOR565(r, g, b); From b002844aa014fe9c965b8099080efe335277a552 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 9 Jan 2026 11:36:53 -0600 Subject: [PATCH 21/83] Add Rak 6421 autoconf (#9010) * Add Rak 6421 autoconf * Minor memory safety hardening --- bin/config.d/lora-hat-rak-6421-pi-hat.yaml | 11 +++++++ src/platform/portduino/PortduinoGlue.cpp | 35 +++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 bin/config.d/lora-hat-rak-6421-pi-hat.yaml diff --git a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml new file mode 100644 index 000000000..066e36a10 --- /dev/null +++ b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml @@ -0,0 +1,11 @@ +Lora: + + ### RAK13300in Slot 1 + Module: sx1262 + IRQ: 22 #IO6 + Reset: 16 # IO4 + Busy: 24 # IO5 + # Ant_sw: 13 # IO3 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index ea9e2de67..986e1c340 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -6,6 +6,7 @@ #include "target_specific.h" #include "PortduinoGlue.h" +#include "SHA256.h" #include "api/ServerAPI.h" #include "linux/gpio/LinuxGPIOPin.h" #include "meshUtils.h" @@ -270,7 +271,39 @@ void portduinoSetup() } std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" << std::endl; - found_hat = true; + + // potential TODO: Validate that this is a real UUID + std::ifstream hatUUID("/proc/device-tree/hat/uuid"); + char uuid[38] = {0}; + if (hatUUID.is_open()) { + hatUUID.read(uuid, 37); + hatUUID.close(); + std::cout << "autoconf: UUID " << uuid << std::endl; + SHA256 uuid_hash; + uint8_t uuid_hash_bytes[32] = {0}; + + uuid_hash.reset(); + uuid_hash.update(uuid, 37); + uuid_hash.finalize(uuid_hash_bytes, 32); + + for (int j = 0; j < 16; j++) { + portduino_config.device_id[j] = uuid_hash_bytes[j]; + } + portduino_config.has_device_id = true; + uint8_t dmac[6] = {0}; + dmac[0] = (uuid_hash_bytes[17] << 4) | 2; + dmac[1] = uuid_hash_bytes[18]; + dmac[2] = uuid_hash_bytes[19]; + dmac[3] = uuid_hash_bytes[20]; + dmac[4] = uuid_hash_bytes[21]; + dmac[5] = uuid_hash_bytes[22]; + char macBuf[13] = {0}; + snprintf(macBuf, sizeof(macBuf), "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], + dmac[5]); + portduino_config.mac_address = macBuf; + found_hat = true; + } + } else { std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl; } From 214c76ce1b58856371cb26210154353119947e07 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:48:27 +0100 Subject: [PATCH 22/83] T-Watch S3 Plus GPS support (#9235) * Upgrade trunk (#9229) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> * support T-Watch S3 Plus GPS --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- src/Power.cpp | 6 +++--- src/gps/GPS.cpp | 5 ++++- variants/esp32s3/t-watch-s3/variant.h | 7 ++++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index fb2d93563..54a803206 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,14 +9,14 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.74.2 + - renovate@42.75.0 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.37.1 - bandit@1.9.2 - trivy@0.68.2 - taplo@0.10.0 - - ruff@0.14.10 + - ruff@0.14.11 - isort@7.0.0 - markdownlint@0.47.0 - oxipng@10.0.0 diff --git a/src/Power.cpp b/src/Power.cpp index 33dda8e11..e9cde0eb6 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1149,11 +1149,11 @@ bool Power::axpChipInit() PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); PMU->enablePowerOutput(XPOWERS_ALDO1); - // sdcard power channel + // sdcard (T-Beam S3) / gnns (T-Watch S3 Plus) power channel PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); +#ifndef T_WATCH_S3 PMU->enablePowerOutput(XPOWERS_BLDO1); - -#ifdef T_WATCH_S3 +#else // DRV2605 power channel PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); PMU->enablePowerOutput(XPOWERS_BLDO2); diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index a61a71dde..f53ffe5e4 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -934,8 +934,11 @@ void GPS::setPowerPMU(bool on) // t-beam v1.2 GNSS power channel on ? PMU->enablePowerOutput(XPOWERS_ALDO3) : PMU->disablePowerOutput(XPOWERS_ALDO3); } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) { - // t-beam-s3-core GNSS power channel + // t-beam-s3-core GNSS power channel on ? PMU->enablePowerOutput(XPOWERS_ALDO4) : PMU->disablePowerOutput(XPOWERS_ALDO4); + } else if (HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { + // t-watch-s3-plus GNSS power channel + on ? PMU->enablePowerOutput(XPOWERS_BLDO1) : PMU->disablePowerOutput(XPOWERS_BLDO1); } } else if (model == XPOWERS_AXP192) { // t-beam v1.1 GNSS power channel diff --git a/variants/esp32s3/t-watch-s3/variant.h b/variants/esp32s3/t-watch-s3/variant.h index 7a302f978..dfd219391 100644 --- a/variants/esp32s3/t-watch-s3/variant.h +++ b/variants/esp32s3/t-watch-s3/variant.h @@ -53,9 +53,10 @@ #define HAS_BMA423 1 #define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor -#define HAS_GPS 0 -#undef GPS_RX_PIN -#undef GPS_TX_PIN +#define GPS_DEFAULT_NOT_PRESENT 1 +#define GPS_BAUDRATE 38400 +#define GPS_RX_PIN 42 +#define GPS_TX_PIN 41 #define USE_SX1262 #define USE_SX1268 From be024d8d4e3812d35281e19a387353444a3c9d2c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 9 Jan 2026 17:14:35 -0600 Subject: [PATCH 23/83] Add copilot-instructions.md for better contextual hints --- .github/copilot-instructions.md | 314 ++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..14601b058 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,314 @@ +# Meshtastic Firmware - Copilot Instructions + +This document provides context and guidelines for AI assistants working with the Meshtastic firmware codebase. + +## Project Overview + +Meshtastic is an open-source LoRa mesh networking project for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware enables text messaging, location sharing, and telemetry over a decentralized mesh network. + +### Supported Hardware Platforms + +- **ESP32** (ESP32, ESP32-S3, ESP32-C3) - Most common platform +- **nRF52** (nRF52840, nRF52833) - Low power Nordic chips +- **RP2040/RP2350** - Raspberry Pi Pico variants +- **STM32WL** - STM32 with integrated LoRa +- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.) + +### Supported Radio Chips + +- **SX1262/SX1268** - Sub-GHz LoRa (868/915 MHz regions) +- **SX1280** - 2.4 GHz LoRa +- **LR1110/LR1120/LR1121** - Wideband radios (sub-GHz and 2.4 GHz capable, but not simultaneously) +- **RF95** - Legacy RFM95 modules +- **LLCC68** - Low-cost LoRa + +### MQTT Integration + +MQTT provides a bridge between Meshtastic mesh networks and the internet, enabling nodes with network connectivity to share messages with remote meshes or external services. + +#### Key Components + +- **`src/mqtt/MQTT.cpp`** - Main MQTT client singleton, handles connection and message routing +- **`src/mqtt/ServiceEnvelope.cpp`** - Protobuf wrapper for mesh packets sent over MQTT +- **`moduleConfig.mqtt`** - MQTT module configuration + +#### MQTT Topic Structure + +Messages are published/subscribed using a hierarchical topic format: + +``` +{root}/{channel_id}/{gateway_id} +``` + +- `root` - Configurable prefix (default: `msh`) +- `channel_id` - Channel name/identifier +- `gateway_id` - Node ID of the publishing gateway + +#### Configuration Defaults (from `Default.h`) + +```cpp +#define default_mqtt_address "mqtt.meshtastic.org" +#define default_mqtt_username "meshdev" +#define default_mqtt_password "large4cats" +#define default_mqtt_root "msh" +#define default_mqtt_encryption_enabled true +#define default_mqtt_tls_enabled false +``` + +#### Key Concepts + +- **Uplink** - Mesh packets sent TO the MQTT broker (controlled by `uplink_enabled` per channel) +- **Downlink** - MQTT messages received and injected INTO the mesh (controlled by `downlink_enabled` per channel) +- **Encryption** - When `encryption_enabled` is true, only encrypted packets are sent; plaintext JSON is disabled +- **ServiceEnvelope** - Protobuf wrapper containing packet + channel_id + gateway_id for routing +- **JSON Support** - Optional JSON encoding for integration with external systems (disabled on nRF52 by default) + +#### PKI Messages + +PKI (Public Key Infrastructure) messages have special handling: + +- Accepted on a special "PKI" channel +- Allow encrypted DMs between nodes that discovered each other on downlink-enabled channels + +## Project Structure + +``` +firmware/ +├── src/ # Main source code +│ ├── main.cpp # Application entry point +│ ├── mesh/ # Core mesh networking +│ │ ├── NodeDB.* # Node database management +│ │ ├── Router.* # Packet routing +│ │ ├── Channels.* # Channel management +│ │ ├── *Interface.* # Radio interface implementations +│ │ └── generated/ # Protobuf generated code +│ ├── modules/ # Feature modules (Position, Telemetry, etc.) +│ ├── gps/ # GPS handling +│ ├── graphics/ # Display drivers and UI +│ ├── platform/ # Platform-specific code +│ ├── input/ # Input device handling +│ └── concurrency/ # Threading utilities +├── variants/ # Hardware variant definitions +│ ├── esp32/ # ESP32 variants +│ ├── esp32s3/ # ESP32-S3 variants +│ ├── nrf52/ # nRF52 variants +│ └── rp2xxx/ # RP2040/RP2350 variants +├── protobufs/ # Protocol buffer definitions +├── boards/ # Custom PlatformIO board definitions +└── bin/ # Build and utility scripts +``` + +## Coding Conventions + +### General Style + +- Follow existing code style - run `trunk fmt` before commits +- Prefer `LOG_DEBUG`, `LOG_INFO`, `LOG_WARN`, `LOG_ERROR` for logging +- Use `assert()` for invariants that should never fail + +### Naming Conventions + +- Classes: `PascalCase` (e.g., `PositionModule`, `NodeDB`) +- Functions/Methods: `camelCase` (e.g., `sendOurPosition`, `getNodeNum`) +- Constants/Defines: `UPPER_SNAKE_CASE` (e.g., `MAX_INTERVAL`, `ONE_DAY`) +- Member variables: `camelCase` (e.g., `lastGpsSend`, `nodeDB`) +- Config defines: `USERPREFS_*` for user-configurable options + +### Key Patterns + +#### Module System + +Modules inherit from `MeshModule` or `ProtobufModule` and implement: + +- `handleReceivedProtobuf()` - Process incoming packets +- `allocReply()` - Generate response packets +- `runOnce()` - Periodic task execution (returns next run interval in ms) + +```cpp +class MyModule : public ProtobufModule +{ + protected: + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_MyMessage *msg) override; + virtual int32_t runOnce() override; +}; +``` + +#### Configuration Access + +- `config.*` - Device configuration (LoRa, position, power, etc.) +- `moduleConfig.*` - Module-specific configuration +- `channels.*` - Channel configuration and management + +#### Default Values + +Use the `Default` class helpers in `src/mesh/Default.h`: + +- `Default::getConfiguredOrDefaultMs(configured, default)` - Returns ms, using default if configured is 0 +- `Default::getConfiguredOrMinimumValue(configured, min)` - Enforces minimum values +- `Default::getConfiguredOrDefaultMsScaled(configured, default, numNodes)` - Scales based on network size + +#### Thread Safety + +- Use `concurrency::Lock` for mutex protection +- Radio SPI access uses `SPILock` +- Prefer `OSThread` for background tasks + +### Hardware Variants + +Each hardware variant has: + +- `variant.h` - Pin definitions and hardware capabilities +- `platformio.ini` - Build configuration +- Optional: `pins_arduino.h`, `rfswitch.h` + +Key defines in variant.h: + +```cpp +#define USE_SX1262 // Radio chip selection +#define HAS_GPS 1 // Hardware capabilities +#define LORA_CS 36 // Pin assignments +#define SX126X_DIO1 14 // Radio-specific pins +``` + +### Protobuf Messages + +- Defined in `protobufs/meshtastic/*.proto` +- Generated code in `src/mesh/generated/` +- Regenerate with `bin/regen-protos.sh` +- Message types prefixed with `meshtastic_` + +### Conditional Compilation + +```cpp +#if !MESHTASTIC_EXCLUDE_GPS // Feature exclusion +#ifdef ARCH_ESP32 // Architecture-specific +#if defined(USE_SX1262) // Radio-specific +#ifdef HAS_SCREEN // Hardware capability +#if USERPREFS_EVENT_MODE // User preferences +``` + +## Build System + +Uses **PlatformIO** with custom scripts: + +- `bin/platformio-pre.py` - Pre-build script +- `bin/platformio-custom.py` - Custom build logic + +Build commands: + +```bash +pio run -e tbeam # Build specific target +pio run -e tbeam -t upload # Build and upload +pio run -e native # Build native/Linux version +``` + +## Common Tasks + +### Adding a New Module + +1. Create `src/modules/MyModule.cpp` and `.h` +2. Inherit from appropriate base class +3. Register in `src/modules/Modules.cpp` +4. Add protobuf messages if needed in `protobufs/` + +### Adding a New Hardware Variant + +1. Create directory under `variants///` +2. Add `variant.h` with pin definitions +3. Add `platformio.ini` with build config +4. Reference common configs with `extends` + +### Modifying Configuration Defaults + +- Check `src/mesh/Default.h` for default value defines +- Check `src/mesh/NodeDB.cpp` for initialization logic +- Consider `isDefaultChannel()` checks for public channel restrictions + +## Important Considerations + +### Traffic Management + +The mesh network has limited bandwidth. When modifying broadcast intervals: + +- Respect minimum intervals on default/public channels +- Use `Default::getConfiguredOrMinimumValue()` to enforce minimums +- Consider `numOnlineNodes` scaling for congestion control + +### Power Management + +Many devices are battery-powered: + +- Use `IF_ROUTER(routerVal, normalVal)` for role-based defaults +- Check `config.power.is_power_saving` for power-saving modes +- Implement proper `sleep()` methods in radio interfaces + +### Channel Security + +- `channels.isDefaultChannel(index)` - Check if using default/public settings +- Default channels get stricter rate limits to prevent abuse +- Private channels may have relaxed limits + +## GitHub Actions CI/CD + +The project uses GitHub Actions extensively for CI/CD. Key workflows are in `.github/workflows/`: + +### Core CI Workflows + +- **`main_matrix.yml`** - Main CI pipeline, runs on push to `master`/`develop` and PRs + - Uses `bin/generate_ci_matrix.py` to dynamically generate build targets + - Builds all supported hardware variants + - PRs build a subset (`--level pr`) for faster feedback + +- **`trunk_check.yml`** - Code quality checks on PRs + - Runs Trunk.io for linting and formatting + - Must pass before merge + +- **`tests.yml`** - End-to-end and hardware tests + - Runs daily on schedule + - Includes native tests and hardware-in-the-loop testing + +- **`test_native.yml`** - Native platform unit tests + - Runs `pio test -e native` + +### Release Workflows + +- **`release_channels.yml`** - Triggered on GitHub release publish + - Builds Docker images + - Packages for PPA (Ubuntu), OBS (openSUSE), and COPR (Fedora) + - Handles Alpha/Beta/Stable release channels + +- **`nightly.yml`** - Nightly builds from develop branch + +- **`docker_build.yml`** / **`docker_manifest.yml`** - Docker image builds + +### Build Matrix Generation + +The CI uses `bin/generate_ci_matrix.py` to dynamically select which targets to build: + +```bash +# Generate full build matrix +./bin/generate_ci_matrix.py all + +# Generate PR-level matrix (subset for faster builds) +./bin/generate_ci_matrix.py all --level pr +``` + +Variants can specify their support level in `platformio.ini`: + +- `custom_meshtastic_support_level = 1` - Actively supported, built on every PR +- `custom_meshtastic_support_level = 2` - Supported, built on merge to main branches +- `board_level = extra` - Extra builds, only on full releases + +### Running Workflows Locally + +Most workflows can be triggered manually via `workflow_dispatch` for testing. + +## Testing + +- Unit tests in `test/` directory +- Run with `pio test -e native` +- Use `bin/test-simulator.sh` for simulation testing + +## Resources + +- [Documentation](https://meshtastic.org/docs/) From 04e755aa480f8e583a32b1d437b20718ed7d5de9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 06:15:55 -0600 Subject: [PATCH 24/83] Update protobufs (#9242) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/admin.pb.h | 4 ++++ src/mesh/generated/meshtastic/deviceonly.pb.h | 5 +++-- src/mesh/generated/meshtastic/localonly.pb.h | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 15 +++++++++++---- src/mesh/generated/meshtastic/module_config.pb.h | 12 ++++++++---- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/protobufs b/protobufs index c2e45a3fc..1a63a3d0d 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit c2e45a3fc9cda6aedb72ad3b5b88fcccfa78073e +Subproject commit 1a63a3d0d2ff5b2df97a1476fb20cc579e144842 diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index ec6e13e9e..26b4343e9 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -270,6 +270,8 @@ typedef struct _meshtastic_AdminMessage { uint32_t set_ignored_node; /* Set specified node-num to be un-ignored on the NodeDB on the device */ uint32_t remove_ignored_node; + /* Set specified node-num to be muted */ + uint32_t toggle_muted_node; /* Begins an edit transaction for config, module config, owner, and channel settings changes This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) */ bool begin_edit_settings; @@ -426,6 +428,7 @@ extern "C" { #define meshtastic_AdminMessage_store_ui_config_tag 46 #define meshtastic_AdminMessage_set_ignored_node_tag 47 #define meshtastic_AdminMessage_remove_ignored_node_tag 48 +#define meshtastic_AdminMessage_toggle_muted_node_tag 49 #define meshtastic_AdminMessage_begin_edit_settings_tag 64 #define meshtastic_AdminMessage_commit_edit_settings_tag 65 #define meshtastic_AdminMessage_add_contact_tag 66 @@ -485,6 +488,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,get_ui_config_response,get_u X(a, STATIC, ONEOF, MESSAGE, (payload_variant,store_ui_config,store_ui_config), 46) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,set_ignored_node,set_ignored_node), 47) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,remove_ignored_node,remove_ignored_node), 48) \ +X(a, STATIC, ONEOF, UINT32, (payload_variant,toggle_muted_node,toggle_muted_node), 49) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_edit_settings), 64) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,add_contact,add_contact), 66) \ diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 7fab82ff7..409805d24 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -97,7 +97,8 @@ typedef struct _meshtastic_NodeInfoLite { /* Last byte of the node number of the node that should be used as the next hop to reach this node. */ uint8_t next_hop; /* Bitfield for storing booleans. - LSB 0 is_key_manually_verified */ + LSB 0 is_key_manually_verified + LSB 1 is_muted */ uint32_t bitfield; } meshtastic_NodeInfoLite; @@ -360,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2277 +#define meshtastic_BackupPreferences_size 2279 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index 3ab6f02c1..2b44d0c9a 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -188,7 +188,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size #define meshtastic_LocalConfig_size 749 -#define meshtastic_LocalModuleConfig_size 673 +#define meshtastic_LocalModuleConfig_size 675 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 344c0e68a..1cec2ab6c 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -294,6 +294,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_THINKNODE_M4 = 119, /* Elecrow ThinkNode M6 */ meshtastic_HardwareModel_THINKNODE_M6 = 120, + /* Elecrow Meshstick 1262 */ + meshtastic_HardwareModel_MESHSTICK_1262 = 121, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ @@ -1013,6 +1015,9 @@ typedef struct _meshtastic_NodeInfo { Persists between NodeDB internal clean ups LSB 0 of the bitfield */ bool is_key_manually_verified; + /* True if node has been muted + Persistes between NodeDB internal clean ups */ + bool is_muted; } meshtastic_NodeInfo; typedef PB_BYTES_ARRAY_T(16) meshtastic_MyNodeInfo_device_id_t; @@ -1437,7 +1442,7 @@ extern "C" { #define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} -#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0} +#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0} #define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_default {0, 0, 0, 0} @@ -1469,7 +1474,7 @@ extern "C" { #define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} -#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0} +#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0} #define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} @@ -1599,6 +1604,7 @@ extern "C" { #define meshtastic_NodeInfo_is_favorite_tag 10 #define meshtastic_NodeInfo_is_ignored_tag 11 #define meshtastic_NodeInfo_is_key_manually_verified_tag 12 +#define meshtastic_NodeInfo_is_muted_tag 13 #define meshtastic_MyNodeInfo_my_node_num_tag 1 #define meshtastic_MyNodeInfo_reboot_count_tag 8 #define meshtastic_MyNodeInfo_min_app_version_tag 11 @@ -1842,7 +1848,8 @@ X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \ X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \ X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \ X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \ -X(a, STATIC, SINGULAR, BOOL, is_key_manually_verified, 12) +X(a, STATIC, SINGULAR, BOOL, is_key_manually_verified, 12) \ +X(a, STATIC, SINGULAR, BOOL, is_muted, 13) #define meshtastic_NodeInfo_CALLBACK NULL #define meshtastic_NodeInfo_DEFAULT NULL #define meshtastic_NodeInfo_user_MSGTYPE meshtastic_User @@ -2144,7 +2151,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_MyNodeInfo_size 83 #define meshtastic_NeighborInfo_size 258 #define meshtastic_Neighbor_size 22 -#define meshtastic_NodeInfo_size 323 +#define meshtastic_NodeInfo_size 325 #define meshtastic_NodeRemoteHardwarePin_size 29 #define meshtastic_Position_size 144 #define meshtastic_QueueStatus_size 23 diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 47d3b5baa..2b7b54949 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -359,6 +359,8 @@ typedef struct _meshtastic_ModuleConfig_TelemetryConfig { /* Enable/Disable the device telemetry module to send metrics to the mesh Note: We will still send telemtry to the connected phone / client every minute over the API */ bool device_telemetry_enabled; + /* Enable/Disable the air quality telemetry measurement module on-device display */ + bool air_quality_screen_enabled; } meshtastic_ModuleConfig_TelemetryConfig; /* Canned Messages Module Config */ @@ -526,7 +528,7 @@ extern "C" { #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0} -#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} #define meshtastic_RemoteHardwarePin_init_default {0, "", _meshtastic_RemoteHardwarePinType_MIN} @@ -542,7 +544,7 @@ extern "C" { #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0} -#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} #define meshtastic_RemoteHardwarePin_init_zero {0, "", _meshtastic_RemoteHardwarePinType_MIN} @@ -631,6 +633,7 @@ extern "C" { #define meshtastic_ModuleConfig_TelemetryConfig_health_update_interval_tag 12 #define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13 #define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14 +#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15 #define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1 #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2 #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3 @@ -830,7 +833,8 @@ X(a, STATIC, SINGULAR, BOOL, power_screen_enabled, 10) \ X(a, STATIC, SINGULAR, BOOL, health_measurement_enabled, 11) \ X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \ X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \ -X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) +X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \ +X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15) #define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL #define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL @@ -915,7 +919,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 -#define meshtastic_ModuleConfig_TelemetryConfig_size 48 +#define meshtastic_ModuleConfig_TelemetryConfig_size 50 #define meshtastic_ModuleConfig_size 227 #define meshtastic_RemoteHardwarePin_size 21 From acb6eb704bb85306f488e184954b7b6aeca6b7ed Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 10 Jan 2026 16:43:42 +0300 Subject: [PATCH 25/83] Update diy_promicro platformio.ini (#9245) Thus PR updates extra_scripts definition. Current env.extra_scripts don't triggers `extra_scripts/nrf52_extra.py` and user have to convert hex to uf2 manually. --- variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini index 006f5c875..82a4e1953 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini @@ -36,5 +36,5 @@ lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} extra_scripts = - ${env.extra_scripts} + ${nrf52840_base.extra_scripts} variants/nrf52840/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays From c520d3aae7e211264ec759bf0041b1251be5eb80 Mon Sep 17 00:00:00 2001 From: apo-mak Date: Sat, 10 Jan 2026 20:43:17 +0200 Subject: [PATCH 26/83] Gr language specific font (#8808) * add Greek special font * add Greek fonts with with proper Greek glyphs * lint fix ( run trunk fmt) --------- Co-authored-by: Ben Meadors --- src/graphics/Screen.h | 36 ++ src/graphics/ScreenFonts.h | 16 + src/graphics/fonts/OLEDDisplayFontsGR.cpp | 429 ++++++++++++++ src/graphics/fonts/OLEDDisplayFontsGR.h | 22 + .../niche/Fonts/FreeSans12pt_Win1253.h | 527 ++++++++++++++++++ .../niche/Fonts/FreeSans6pt_Win1253.h | 527 ++++++++++++++++++ .../niche/Fonts/FreeSans9pt_Win1253.h | 527 ++++++++++++++++++ src/graphics/niche/InkHUD/AppletFont.cpp | 95 ++++ src/graphics/niche/InkHUD/AppletFont.h | 9 + .../heltec_mesh_pocket/nicheGraphics.h | 6 +- 10 files changed, 2191 insertions(+), 3 deletions(-) create mode 100644 src/graphics/fonts/OLEDDisplayFontsGR.cpp create mode 100644 src/graphics/fonts/OLEDDisplayFontsGR.h create mode 100644 src/graphics/niche/Fonts/FreeSans12pt_Win1253.h create mode 100644 src/graphics/niche/Fonts/FreeSans6pt_Win1253.h create mode 100644 src/graphics/niche/Fonts/FreeSans9pt_Win1253.h diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index cfffc6731..31ddf1c84 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -558,6 +558,42 @@ class Screen : public concurrency::OSThread if (ch == 0xC2 || ch == 0xC3 || ch == 0xC4 || ch == 0xC5) return (uint8_t)0; +#endif + +#if defined(OLED_GR) + + switch (last) { + case 0xC3: { + SKIPREST = false; + return (uint8_t)(ch | 0xC0); + } + // Map UTF-8 Greek chars to Windows-1253 (CP-1253) ASCII codes + case 0xCE: { + SKIPREST = false; + // Uppercase Greek: Α-Ρ (U+0391-U+03A1) -> CP-1253 193-209 + if (ch >= 145 && ch <= 161) + return (uint8_t)(ch + 48); + // Uppercase Greek: Σ-Ω (U+03A3-U+03A9) -> CP-1253 211-217 + else if (ch >= 163 && ch <= 169) + return (uint8_t)(ch + 48); + // Lowercase Greek: α-ρ (U+03B1-U+03C1) -> CP-1253 225-241 + else if (ch >= 177 && ch <= 193) + return (uint8_t)(ch + 48); + break; + } + case 0xCF: { + SKIPREST = false; + // Lowercase Greek: ς-ω (U+03C2-U+03C9) -> CP-1253 242-249 + if (ch >= 130 && ch <= 137) + return (uint8_t)(ch + 112); + break; + } + } + + // We want to strip out prefix chars for two-byte Greek char formats + if (ch == 0xC2 || ch == 0xC3 || ch == 0xCE || ch == 0xCF) + return (uint8_t)0; + #endif // If we already returned an unconvertable-character symbol for this unconvertable-character sequence, return NULs for the diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index d54fc9958..ed2e200bb 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -16,10 +16,17 @@ #include "graphics/fonts/OLEDDisplayFontsCS.h" #endif +#ifdef OLED_GR +#include "graphics/fonts/OLEDDisplayFontsGR.h" +#endif + #if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) #include "graphics/fonts/EinkDisplayFonts.h" #endif +#ifdef OLED_GR +#define FONT_SMALL_LOCAL ArialMT_Plain_10_GR // Height: 13 +#else #ifdef OLED_PL #define FONT_SMALL_LOCAL ArialMT_Plain_10_PL #else @@ -37,6 +44,10 @@ #endif #endif #endif +#endif +#ifdef OLED_GR +#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_GR // Height: 19 +#else #ifdef OLED_PL #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19 #else @@ -54,6 +65,10 @@ #endif #endif #endif +#endif +#ifdef OLED_GR +#define FONT_LARGE_LOCAL ArialMT_Plain_24_GR // Height: 28 +#else #ifdef OLED_PL #define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28 #else @@ -71,6 +86,7 @@ #endif #endif #endif +#endif #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ diff --git a/src/graphics/fonts/OLEDDisplayFontsGR.cpp b/src/graphics/fonts/OLEDDisplayFontsGR.cpp new file mode 100644 index 000000000..71f89ea6e --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsGR.cpp @@ -0,0 +1,429 @@ +#ifdef OLED_GR + +#include "OLEDDisplayFontsGR.h" + +/** + * Greek font for OLED displays - ArialMT Plain 10pt + * Contains ASCII 32-127 + Greek characters mapped to CP-1253 positions (192-254) + * + * Generated using ThingPulse OLED font converter + * Font: Arial, Size: 10px + * Character set: Basic Latin + Greek (Α-Ω, α-ω, accented) + * + * CP-1253 Greek character mapping: + * 193-209: Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ + * 211-217: Σ Τ Υ Φ Χ Ψ Ω + * 225-241: α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ + * 242-249: ς σ τ υ φ χ ψ ω + */ +const uint8_t ArialMT_Plain_10_GR[] PROGMEM = { + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x20, // First char: 32 + 0xE0, // Number of chars: 224 + + // Jump Table (4 bytes per character: offset high, offset low, size, width) + // Characters 32-127: Standard ASCII + 0xFF, 0xFF, 0x00, 0x03, // 32 space + 0x00, 0x00, 0x04, 0x03, // 33 ! + 0x00, 0x04, 0x05, 0x04, // 34 " + 0x00, 0x09, 0x09, 0x06, // 35 # + 0x00, 0x12, 0x0A, 0x06, // 36 $ + 0x00, 0x1C, 0x10, 0x09, // 37 % + 0x00, 0x2C, 0x0E, 0x08, // 38 & + 0x00, 0x3A, 0x01, 0x02, // 39 ' + 0x00, 0x3B, 0x06, 0x04, // 40 ( + 0x00, 0x41, 0x06, 0x04, // 41 ) + 0x00, 0x47, 0x05, 0x04, // 42 * + 0x00, 0x4C, 0x09, 0x06, // 43 + + 0x00, 0x55, 0x04, 0x03, // 44 , + 0x00, 0x59, 0x03, 0x03, // 45 - + 0x00, 0x5C, 0x04, 0x03, // 46 . + 0x00, 0x60, 0x05, 0x04, // 47 / + 0x00, 0x65, 0x0A, 0x06, // 48 0 + 0x00, 0x6F, 0x08, 0x05, // 49 1 + 0x00, 0x77, 0x0A, 0x06, // 50 2 + 0x00, 0x81, 0x0A, 0x06, // 51 3 + 0x00, 0x8B, 0x0B, 0x07, // 52 4 + 0x00, 0x96, 0x0A, 0x06, // 53 5 + 0x00, 0xA0, 0x0A, 0x06, // 54 6 + 0x00, 0xAA, 0x09, 0x06, // 55 7 + 0x00, 0xB3, 0x0A, 0x06, // 56 8 + 0x00, 0xBD, 0x0A, 0x06, // 57 9 + 0x00, 0xC7, 0x04, 0x03, // 58 : + 0x00, 0xCB, 0x04, 0x03, // 59 ; + 0x00, 0xCF, 0x0A, 0x06, // 60 < + 0x00, 0xD9, 0x09, 0x06, // 61 = + 0x00, 0xE2, 0x09, 0x06, // 62 > + 0x00, 0xEB, 0x0B, 0x07, // 63 ? + 0x00, 0xF6, 0x14, 0x0B, // 64 @ + 0x01, 0x0A, 0x0E, 0x08, // 65 A + 0x01, 0x18, 0x0C, 0x07, // 66 B + 0x01, 0x24, 0x0C, 0x07, // 67 C + 0x01, 0x30, 0x0B, 0x07, // 68 D + 0x01, 0x3B, 0x0C, 0x07, // 69 E + 0x01, 0x47, 0x09, 0x06, // 70 F + 0x01, 0x50, 0x0D, 0x08, // 71 G + 0x01, 0x5D, 0x0C, 0x07, // 72 H + 0x01, 0x69, 0x04, 0x03, // 73 I + 0x01, 0x6D, 0x08, 0x05, // 74 J + 0x01, 0x75, 0x0E, 0x08, // 75 K + 0x01, 0x83, 0x0C, 0x07, // 76 L + 0x01, 0x8F, 0x10, 0x09, // 77 M + 0x01, 0x9F, 0x0C, 0x07, // 78 N + 0x01, 0xAB, 0x0E, 0x08, // 79 O + 0x01, 0xB9, 0x0B, 0x07, // 80 P + 0x01, 0xC4, 0x0E, 0x08, // 81 Q + 0x01, 0xD2, 0x0C, 0x07, // 82 R + 0x01, 0xDE, 0x0C, 0x07, // 83 S + 0x01, 0xEA, 0x0B, 0x07, // 84 T + 0x01, 0xF5, 0x0C, 0x07, // 85 U + 0x02, 0x01, 0x0D, 0x08, // 86 V + 0x02, 0x0E, 0x11, 0x0A, // 87 W + 0x02, 0x1F, 0x0E, 0x08, // 88 X + 0x02, 0x2D, 0x0D, 0x08, // 89 Y + 0x02, 0x3A, 0x0C, 0x07, // 90 Z + 0x02, 0x46, 0x06, 0x04, // 91 [ + 0x02, 0x4C, 0x06, 0x04, // 92 backslash + 0x02, 0x52, 0x04, 0x03, // 93 ] + 0x02, 0x56, 0x09, 0x06, // 94 ^ + 0x02, 0x5F, 0x0C, 0x07, // 95 _ + 0x02, 0x6B, 0x03, 0x03, // 96 ` + 0x02, 0x6E, 0x0A, 0x06, // 97 a + 0x02, 0x78, 0x0A, 0x06, // 98 b + 0x02, 0x82, 0x0A, 0x06, // 99 c + 0x02, 0x8C, 0x0A, 0x06, // 100 d + 0x02, 0x96, 0x0A, 0x06, // 101 e + 0x02, 0xA0, 0x05, 0x04, // 102 f + 0x02, 0xA5, 0x0A, 0x06, // 103 g + 0x02, 0xAF, 0x0A, 0x06, // 104 h + 0x02, 0xB9, 0x04, 0x03, // 105 i + 0x02, 0xBD, 0x04, 0x03, // 106 j + 0x02, 0xC1, 0x08, 0x05, // 107 k + 0x02, 0xC9, 0x04, 0x03, // 108 l + 0x02, 0xCD, 0x10, 0x09, // 109 m + 0x02, 0xDD, 0x0A, 0x06, // 110 n + 0x02, 0xE7, 0x0A, 0x06, // 111 o + 0x02, 0xF1, 0x0A, 0x06, // 112 p + 0x02, 0xFB, 0x0A, 0x06, // 113 q + 0x03, 0x05, 0x05, 0x04, // 114 r + 0x03, 0x0A, 0x08, 0x05, // 115 s + 0x03, 0x12, 0x06, 0x04, // 116 t + 0x03, 0x18, 0x0A, 0x06, // 117 u + 0x03, 0x22, 0x09, 0x06, // 118 v + 0x03, 0x2B, 0x0E, 0x08, // 119 w + 0x03, 0x39, 0x0A, 0x06, // 120 x + 0x03, 0x43, 0x09, 0x06, // 121 y + 0x03, 0x4C, 0x0A, 0x06, // 122 z + 0x03, 0x56, 0x06, 0x04, // 123 { + 0x03, 0x5C, 0x04, 0x03, // 124 | + 0x03, 0x60, 0x05, 0x04, // 125 } + 0x03, 0x65, 0x09, 0x06, // 126 ~ + 0xFF, 0xFF, 0x00, 0x03, // 127 + // Characters 128-191: Placeholders (extended ASCII) + 0xFF, 0xFF, 0x00, 0x03, // 128 + 0xFF, 0xFF, 0x00, 0x03, // 129 + 0xFF, 0xFF, 0x00, 0x03, // 130 + 0xFF, 0xFF, 0x00, 0x03, // 131 + 0xFF, 0xFF, 0x00, 0x03, // 132 + 0xFF, 0xFF, 0x00, 0x03, // 133 + 0xFF, 0xFF, 0x00, 0x03, // 134 + 0xFF, 0xFF, 0x00, 0x03, // 135 + 0xFF, 0xFF, 0x00, 0x03, // 136 + 0xFF, 0xFF, 0x00, 0x03, // 137 + 0xFF, 0xFF, 0x00, 0x03, // 138 + 0xFF, 0xFF, 0x00, 0x03, // 139 + 0xFF, 0xFF, 0x00, 0x03, // 140 + 0xFF, 0xFF, 0x00, 0x03, // 141 + 0xFF, 0xFF, 0x00, 0x03, // 142 + 0xFF, 0xFF, 0x00, 0x03, // 143 + 0xFF, 0xFF, 0x00, 0x03, // 144 + 0xFF, 0xFF, 0x00, 0x03, // 145 + 0xFF, 0xFF, 0x00, 0x03, // 146 + 0xFF, 0xFF, 0x00, 0x03, // 147 + 0xFF, 0xFF, 0x00, 0x03, // 148 + 0xFF, 0xFF, 0x00, 0x03, // 149 + 0xFF, 0xFF, 0x00, 0x03, // 150 + 0xFF, 0xFF, 0x00, 0x03, // 151 + 0xFF, 0xFF, 0x00, 0x03, // 152 + 0xFF, 0xFF, 0x00, 0x03, // 153 + 0xFF, 0xFF, 0x00, 0x03, // 154 + 0xFF, 0xFF, 0x00, 0x03, // 155 + 0xFF, 0xFF, 0x00, 0x03, // 156 + 0xFF, 0xFF, 0x00, 0x03, // 157 + 0xFF, 0xFF, 0x00, 0x03, // 158 + 0xFF, 0xFF, 0x00, 0x03, // 159 + 0xFF, 0xFF, 0x00, 0x03, // 160 + 0xFF, 0xFF, 0x00, 0x03, // 161 + 0xFF, 0xFF, 0x00, 0x03, // 162 + 0xFF, 0xFF, 0x00, 0x03, // 163 + 0xFF, 0xFF, 0x00, 0x03, // 164 + 0xFF, 0xFF, 0x00, 0x03, // 165 + 0xFF, 0xFF, 0x00, 0x03, // 166 + 0xFF, 0xFF, 0x00, 0x03, // 167 + 0xFF, 0xFF, 0x00, 0x03, // 168 + 0xFF, 0xFF, 0x00, 0x03, // 169 + 0xFF, 0xFF, 0x00, 0x03, // 170 + 0xFF, 0xFF, 0x00, 0x03, // 171 + 0xFF, 0xFF, 0x00, 0x03, // 172 + 0xFF, 0xFF, 0x00, 0x03, // 173 + 0xFF, 0xFF, 0x00, 0x03, // 174 + 0xFF, 0xFF, 0x00, 0x03, // 175 + 0xFF, 0xFF, 0x00, 0x03, // 176 + 0xFF, 0xFF, 0x00, 0x03, // 177 + 0xFF, 0xFF, 0x00, 0x03, // 178 + 0xFF, 0xFF, 0x00, 0x03, // 179 + 0xFF, 0xFF, 0x00, 0x03, // 180 + 0xFF, 0xFF, 0x00, 0x03, // 181 + 0xFF, 0xFF, 0x00, 0x03, // 182 + 0xFF, 0xFF, 0x00, 0x03, // 183 + 0xFF, 0xFF, 0x00, 0x03, // 184 + 0xFF, 0xFF, 0x00, 0x03, // 185 + 0xFF, 0xFF, 0x00, 0x03, // 186 + 0xFF, 0xFF, 0x00, 0x03, // 187 + 0xFF, 0xFF, 0x00, 0x03, // 188 + 0xFF, 0xFF, 0x00, 0x03, // 189 + 0xFF, 0xFF, 0x00, 0x03, // 190 + 0xFF, 0xFF, 0x00, 0x03, // 191 + // Characters 192-255: Greek letters (CP-1253 positions) + 0xFF, 0xFF, 0x00, 0x03, // 192 (unused) + 0x03, 0x6E, 0x0E, 0x08, // 193 Α Alpha + 0x03, 0x7C, 0x0C, 0x07, // 194 Β Beta + 0x03, 0x88, 0x09, 0x06, // 195 Γ Gamma + 0x03, 0x91, 0x0C, 0x07, // 196 Δ Delta + 0x03, 0x9D, 0x0C, 0x07, // 197 Ε Epsilon + 0x03, 0xA9, 0x0A, 0x06, // 198 Ζ Zeta + 0x03, 0xB3, 0x0C, 0x07, // 199 Η Eta + 0x03, 0xBF, 0x0E, 0x08, // 200 Θ Theta + 0x03, 0xCD, 0x04, 0x03, // 201 Ι Iota + 0x03, 0xD1, 0x0E, 0x08, // 202 Κ Kappa + 0x03, 0xDF, 0x0E, 0x08, // 203 Λ Lambda + 0x03, 0xED, 0x10, 0x09, // 204 Μ Mu + 0x03, 0xFD, 0x0C, 0x07, // 205 Ν Nu + 0x04, 0x09, 0x0C, 0x07, // 206 Ξ Xi + 0x04, 0x15, 0x0E, 0x08, // 207 Ο Omicron + 0x04, 0x23, 0x0C, 0x07, // 208 Π Pi + 0x04, 0x2F, 0x0B, 0x07, // 209 Ρ Rho + 0xFF, 0xFF, 0x00, 0x03, // 210 (unused) + 0x04, 0x3A, 0x0C, 0x07, // 211 Σ Sigma + 0x04, 0x46, 0x0B, 0x07, // 212 Τ Tau + 0x04, 0x51, 0x0D, 0x08, // 213 Υ Upsilon + 0x04, 0x5E, 0x0E, 0x08, // 214 Φ Phi + 0x04, 0x6C, 0x0E, 0x08, // 215 Χ Chi + 0x04, 0x7A, 0x0E, 0x08, // 216 Ψ Psi + 0x04, 0x88, 0x0E, 0x08, // 217 Ω Omega + 0xFF, 0xFF, 0x00, 0x03, // 218 + 0xFF, 0xFF, 0x00, 0x03, // 219 + 0xFF, 0xFF, 0x00, 0x03, // 220 + 0xFF, 0xFF, 0x00, 0x03, // 221 + 0xFF, 0xFF, 0x00, 0x03, // 222 + 0xFF, 0xFF, 0x00, 0x03, // 223 + 0xFF, 0xFF, 0x00, 0x03, // 224 + 0x04, 0x96, 0x0A, 0x06, // 225 α alpha + 0x04, 0xA0, 0x0A, 0x06, // 226 β beta + 0x04, 0xAA, 0x09, 0x06, // 227 γ gamma + 0x04, 0xB3, 0x0A, 0x06, // 228 δ delta + 0x04, 0xBD, 0x08, 0x05, // 229 ε epsilon + 0x04, 0xC5, 0x08, 0x05, // 230 ζ zeta + 0x04, 0xCD, 0x0A, 0x06, // 231 η eta + 0x04, 0xD7, 0x0A, 0x06, // 232 θ theta + 0x04, 0xE1, 0x04, 0x03, // 233 ι iota + 0x04, 0xE5, 0x08, 0x05, // 234 κ kappa + 0x04, 0xED, 0x0A, 0x06, // 235 λ lambda + 0x04, 0xF7, 0x0A, 0x06, // 236 μ mu + 0x05, 0x01, 0x08, 0x05, // 237 ν nu + 0x05, 0x09, 0x0A, 0x06, // 238 ξ xi + 0x05, 0x13, 0x0A, 0x06, // 239 ο omicron + 0x05, 0x1D, 0x0A, 0x06, // 240 π pi + 0x05, 0x27, 0x0A, 0x06, // 241 ρ rho + 0x05, 0x31, 0x08, 0x05, // 242 ς final sigma + 0x05, 0x39, 0x0A, 0x06, // 243 σ sigma + 0x05, 0x43, 0x06, 0x04, // 244 τ tau + 0x05, 0x49, 0x0A, 0x06, // 245 υ upsilon + 0x05, 0x53, 0x0C, 0x07, // 246 φ phi + 0x05, 0x5F, 0x0A, 0x06, // 247 χ chi + 0x05, 0x69, 0x0C, 0x07, // 248 ψ psi + 0x05, 0x75, 0x0C, 0x07, // 249 ω omega + 0xFF, 0xFF, 0x00, 0x03, // 250 + 0xFF, 0xFF, 0x00, 0x03, // 251 + 0xFF, 0xFF, 0x00, 0x03, // 252 + 0xFF, 0xFF, 0x00, 0x03, // 253 + 0xFF, 0xFF, 0x00, 0x03, // 254 + 0xFF, 0xFF, 0x00, 0x03, // 255 + + // Font Data - Basic ASCII (32-127) + 0x00, 0x00, 0xF8, 0x02, // 33 ! + 0x38, 0x00, 0x00, 0x00, 0x38, // 34 " + 0xA0, 0x03, 0xE0, 0x00, 0xB8, 0x03, 0xE0, 0x00, 0xB8, // 35 # + 0x30, 0x01, 0x28, 0x02, 0xF8, 0x07, 0x48, 0x02, 0x90, 0x01, // 36 $ + 0x00, 0x00, 0x30, 0x00, 0x48, 0x00, 0x30, 0x03, 0xC0, 0x00, 0xB0, 0x01, 0x48, 0x02, 0x80, 0x01, // 37 % + 0x80, 0x01, 0x50, 0x02, 0x68, 0x02, 0xA8, 0x02, 0x18, 0x01, 0x80, 0x03, 0x80, 0x02, // 38 & + 0x38, // 39 ' + 0xE0, 0x03, 0x10, 0x04, 0x08, 0x08, // 40 ( + 0x08, 0x08, 0x10, 0x04, 0xE0, 0x03, // 41 ) + 0x28, 0x00, 0x18, 0x00, 0x28, // 42 * + 0x40, 0x00, 0x40, 0x00, 0xF0, 0x01, 0x40, 0x00, 0x40, // 43 + + 0x00, 0x00, 0x00, 0x06, // 44 , + 0x80, 0x00, 0x80, // 45 - + 0x00, 0x00, 0x00, 0x02, // 46 . + 0x00, 0x03, 0xE0, 0x00, 0x18, // 47 / + 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 48 0 + 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0xF8, 0x03, // 49 1 + 0x10, 0x02, 0x08, 0x03, 0x88, 0x02, 0x48, 0x02, 0x30, 0x02, // 50 2 + 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 51 3 + 0xC0, 0x00, 0xA0, 0x00, 0x90, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x80, // 52 4 + 0x60, 0x01, 0x38, 0x02, 0x28, 0x02, 0x28, 0x02, 0xC8, 0x01, // 53 5 + 0xF0, 0x01, 0x28, 0x02, 0x28, 0x02, 0x28, 0x02, 0xD0, 0x01, // 54 6 + 0x08, 0x00, 0x08, 0x03, 0xC8, 0x00, 0x38, 0x00, 0x08, // 55 7 + 0xB0, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xB0, 0x01, // 56 8 + 0x70, 0x01, 0x88, 0x02, 0x88, 0x02, 0x88, 0x02, 0xF0, 0x01, // 57 9 + 0x00, 0x00, 0x20, 0x02, // 58 : + 0x00, 0x00, 0x20, 0x06, // 59 ; + 0x00, 0x00, 0x40, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x10, 0x01, // 60 < + 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, // 61 = + 0x00, 0x00, 0x10, 0x01, 0xA0, 0x00, 0xA0, 0x00, 0x40, // 62 > + 0x10, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC8, 0x02, 0x48, 0x00, 0x30, // 63 ? + 0x00, 0x00, 0xC0, 0x03, 0x30, 0x04, 0xD0, 0x09, 0x28, 0x0A, 0x28, 0x0A, 0xC8, 0x0B, 0x68, 0x0A, 0x10, 0x05, 0xE0, + 0x04, // 64 @ + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // 65 A + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // 66 B + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, // 67 C + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x02, 0x08, 0x02, 0x10, 0x01, 0xE0, // 68 D + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // 69 E + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x08, // 70 F + 0x00, 0x00, 0xE0, 0x00, 0x10, 0x01, 0x08, 0x02, 0x48, 0x02, 0x50, 0x01, 0xC0, // 71 G + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // 72 H + 0x00, 0x00, 0xF8, 0x03, // 73 I + 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 74 J + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 75 K + 0x00, 0x00, 0xF8, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, // 76 L + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // 77 M + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // 78 N + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // 79 O + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // 80 P + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0x08, 0x03, 0xF0, 0x02, // 81 Q + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0xC8, 0x00, 0x30, 0x03, // 82 R + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // 83 S + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // 84 T + 0x00, 0x00, 0xF8, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0xF8, 0x01, // 85 U + 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, // 86 V + 0x18, 0x00, 0xE0, 0x01, 0x00, 0x02, 0xF0, 0x01, 0x08, 0x00, 0xF0, 0x01, 0x00, 0x02, 0xE0, 0x01, 0x18, // 87 W + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // 88 X + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // 89 Y + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, 0x18, 0x02, // 90 Z + 0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, // 91 [ + 0x18, 0x00, 0xE0, 0x00, 0x00, 0x03, // 92 backslash + 0x08, 0x08, 0xF8, 0x0F, // 93 ] + 0x40, 0x00, 0x30, 0x00, 0x08, 0x00, 0x30, 0x00, 0x40, // 94 ^ + 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, // 95 _ + 0x08, 0x00, 0x10, // 96 ` + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // 97 a + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 98 b + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, 0x01, // 99 c + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xF8, 0x03, // 100 d + 0x00, 0x00, 0xC0, 0x01, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x02, // 101 e + 0x20, 0x00, 0xF0, 0x03, 0x28, // 102 f + 0x00, 0x00, 0xC0, 0x05, 0x20, 0x0A, 0x20, 0x0A, 0xE0, 0x07, // 103 g + 0x00, 0x00, 0xF8, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 104 h + 0x00, 0x00, 0xE8, 0x03, // 105 i + 0x00, 0x08, 0xE8, 0x07, // 106 j + 0xF8, 0x03, 0x80, 0x00, 0xC0, 0x01, 0x20, 0x02, // 107 k + 0x00, 0x00, 0xF8, 0x03, // 108 l + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 109 m + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x03, // 110 n + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 111 o + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // 112 p + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xE0, 0x0F, // 113 q + 0x00, 0x00, 0xE0, 0x03, 0x20, // 114 r + 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0x20, 0x01, // 115 s + 0x20, 0x00, 0xF8, 0x03, 0x20, 0x02, // 116 t + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // 117 u + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, // 118 v + 0xE0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x01, // 119 w + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // 120 x + 0x20, 0x00, 0xC0, 0x09, 0x00, 0x06, 0xC0, 0x01, 0x20, // 121 y + 0x20, 0x02, 0x20, 0x03, 0xA0, 0x02, 0x60, 0x02, 0x20, 0x02, // 122 z + 0x80, 0x00, 0x78, 0x0F, 0x08, 0x08, // 123 { + 0x00, 0x00, 0xF8, 0x0F, // 124 | + 0x08, 0x08, 0x78, 0x0F, 0x80, // 125 } + 0xC0, 0x00, 0x40, 0x00, 0xC0, 0x00, 0x80, 0x00, 0xC0, // 126 ~ + + // Greek uppercase letters (193-217 in CP-1253) + 0x00, 0x02, 0xC0, 0x01, 0xB0, 0x00, 0x88, 0x00, 0xB0, 0x00, 0xC0, 0x01, 0x00, 0x02, // Α Alpha (same as A) + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0xF0, 0x01, // Β Beta (same as B) + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x18, // Γ Gamma + 0x00, 0x02, 0x80, 0x01, 0x60, 0x00, 0x10, 0x00, 0x60, 0x00, 0x80, 0x01, 0x00, 0x02, // Δ Delta + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, // Ε Epsilon (same as E) + 0x08, 0x03, 0x88, 0x02, 0xC8, 0x02, 0x68, 0x02, 0x38, 0x02, // Ζ Zeta (same as Z) + 0x00, 0x00, 0xF8, 0x03, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xF8, 0x03, // Η Eta (same as H) + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x48, 0x02, 0x48, 0x02, 0x08, 0x02, 0xF0, 0x01, // Θ Theta + 0x00, 0x00, 0xF8, 0x03, // Ι Iota (same as I) + 0x00, 0x00, 0xF8, 0x03, 0x80, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // Κ Kappa (same as K) + 0x00, 0x02, 0x80, 0x01, 0x70, 0x00, 0x08, 0x00, 0x70, 0x00, 0x80, 0x01, 0x00, 0x02, // Λ Lambda + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xC0, 0x01, 0x30, 0x00, 0xF8, 0x03, // Μ Mu (same as M) + 0x00, 0x00, 0xF8, 0x03, 0x30, 0x00, 0x40, 0x00, 0x80, 0x01, 0xF8, 0x03, // Ν Nu (same as N) + 0x00, 0x00, 0x48, 0x02, 0x48, 0x02, 0xF8, 0x03, 0x48, 0x02, 0x48, 0x02, // Ξ Xi + 0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, // Ο Omicron (same as O) + 0x00, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, // Π Pi + 0x00, 0x00, 0xF8, 0x03, 0x48, 0x00, 0x48, 0x00, 0x48, 0x00, 0x30, // Ρ Rho (same as P) + 0x00, 0x00, 0x30, 0x01, 0x48, 0x02, 0x48, 0x02, 0x48, 0x02, 0x90, 0x01, // Σ Sigma + 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0xF8, 0x03, 0x08, 0x00, 0x08, // Τ Tau (same as T) + 0x08, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x03, 0x20, 0x00, 0x10, 0x00, 0x08, // Υ Upsilon (same as Y) + 0x00, 0x00, 0x70, 0x00, 0x88, 0x00, 0xF8, 0x03, 0x88, 0x00, 0x70, 0x00, 0x00, // Φ Phi + 0x00, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x00, 0x02, // Χ Chi (same as X) + 0x00, 0x00, 0x08, 0x00, 0xF0, 0x01, 0x08, 0x02, 0xF8, 0x03, 0x08, 0x02, 0xF0, 0x01, // Ψ Psi + 0x00, 0x00, 0x08, 0x02, 0xF0, 0x01, 0x08, 0x02, 0x08, 0x02, 0xF0, 0x01, 0x08, 0x02, // Ω Omega + + // Greek lowercase letters (225-249 in CP-1253) + 0x00, 0x00, 0x00, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // α alpha + 0x00, 0x00, 0xF8, 0x07, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // β beta + 0x00, 0x04, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x00, 0x20, // γ gamma + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x50, 0x01, // δ delta + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0x40, // ε epsilon + 0x00, 0x04, 0x00, 0x03, 0xE0, 0x00, 0x18, // ζ zeta + 0x00, 0x00, 0xE0, 0x05, 0x20, 0x0A, 0x20, 0x02, 0xC0, 0x01, // η eta + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0xA0, 0x02, 0xC0, 0x01, // θ theta + 0x00, 0x00, 0xE0, 0x03, // ι iota + 0xE0, 0x03, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // κ kappa + 0x00, 0x02, 0x80, 0x01, 0x40, 0x00, 0x20, 0x00, 0xE0, 0x03, // λ lambda + 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // μ mu + 0x20, 0x00, 0xC0, 0x01, 0x00, 0x02, 0xE0, 0x03, // ν nu + 0x00, 0x04, 0xC0, 0x03, 0xA0, 0x02, 0xA0, 0x02, 0xC0, 0x01, // ξ xi + 0x00, 0x00, 0xC0, 0x01, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // ο omicron + 0x00, 0x00, 0xE0, 0x03, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x03, // π pi + 0x00, 0x00, 0xE0, 0x0F, 0x20, 0x02, 0x20, 0x02, 0xC0, 0x01, // ρ rho + 0x00, 0x04, 0x00, 0x03, 0xA0, 0x02, 0x40, 0x01, // ς final sigma + 0x00, 0x00, 0x40, 0x02, 0xA0, 0x02, 0xA0, 0x02, 0xE0, 0x03, // σ sigma + 0x20, 0x00, 0xE0, 0x03, 0x20, // τ tau + 0x00, 0x00, 0xE0, 0x01, 0x00, 0x02, 0x00, 0x02, 0xE0, 0x03, // υ upsilon + 0x00, 0x00, 0xC0, 0x00, 0x20, 0x01, 0xE0, 0x03, 0x20, 0x01, 0xC0, // φ phi + 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x40, 0x01, 0x20, 0x02, // χ chi + 0x00, 0x00, 0x20, 0x00, 0xC0, 0x05, 0x20, 0x02, 0xE0, 0x03, 0x20, // ψ psi + 0x00, 0x00, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x02, 0xC0, 0x01, 0x20, 0x02, // ω omega +}; + +// Placeholder for 16pt font - needs to be generated with font converter tool +const uint8_t ArialMT_Plain_16_GR[] PROGMEM = { + 0x10, // Width: 16 + 0x13, // Height: 19 + 0x20, // First Char: 32 + 0x01, // Number of chars: 1 (placeholder) + // Minimal placeholder - replace with full font data + 0xFF, 0xFF, 0x00, 0x04, // 32 space + // Font Data: + // (empty placeholder) +}; + +// Placeholder for 24pt font - needs to be generated with font converter tool +const uint8_t ArialMT_Plain_24_GR[] PROGMEM = { + 0x18, // Width: 24 + 0x1C, // Height: 28 + 0x20, // First Char: 32 + 0x01, // Number of chars: 1 (placeholder) + // Minimal placeholder - replace with full font data + 0xFF, 0xFF, 0x00, 0x06, // 32 space + // Font Data: + // (empty placeholder) +}; + +#endif // OLED_GR diff --git a/src/graphics/fonts/OLEDDisplayFontsGR.h b/src/graphics/fonts/OLEDDisplayFontsGR.h new file mode 100644 index 000000000..83a2adda6 --- /dev/null +++ b/src/graphics/fonts/OLEDDisplayFontsGR.h @@ -0,0 +1,22 @@ +#ifndef OLEDDISPLAYFONTSGR_h +#define OLEDDISPLAYFONTSGR_h + +#ifdef ARDUINO +#include +#elif __MBED__ +#define PROGMEM +#endif + +/** + * Localization for Greek language containing glyphs for the Greek alphabet. + * Uses Windows-1253 (CP-1253) encoding for Greek characters. + * + * Supported characters: + * - Uppercase Greek: Α-Ω (U+0391 to U+03A9) + * - Lowercase Greek: α-ω (U+03B1 to U+03C9) + * - Accented Greek: ά, έ, ή, ί, ό, ύ, ώ, etc. + */ +extern const uint8_t ArialMT_Plain_10_GR[] PROGMEM; +extern const uint8_t ArialMT_Plain_16_GR[] PROGMEM; +extern const uint8_t ArialMT_Plain_24_GR[] PROGMEM; +#endif diff --git a/src/graphics/niche/Fonts/FreeSans12pt_Win1253.h b/src/graphics/niche/Fonts/FreeSans12pt_Win1253.h new file mode 100644 index 000000000..a2a41dd1b --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans12pt_Win1253.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans12pt_Win1253 +*/ +const uint8_t FreeSans12pt_Win1253Bitmaps[] PROGMEM = { +/* 0x01 */ 0x00, 0x30, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x24, 0x00, 0x04, 0x80, 0x01, 0x90, 0x00, 0x62, 0x00, 0x30, 0xFE, 0x04, 0x10, 0x5F, 0x02, 0x0B, 0x00, 0x7F, 0xE0, 0x0C, 0x1C, 0x02, 0x83, 0x81, 0x9F, 0xF0, 0x02, 0x1E, 0x00, 0x41, 0xC0, 0x0E, 0x7F, 0x81, 0x78, 0x18, 0x62, 0x00, 0xFF, 0xC0, +/* 0x02 */ 0x00, 0xFF, 0x80, 0x61, 0x13, 0xF0, 0x62, 0x60, 0x07, 0xFC, 0x00, 0x83, 0x80, 0x10, 0xF0, 0x33, 0xF6, 0x01, 0x41, 0xC0, 0x18, 0x38, 0x03, 0xFF, 0xE0, 0x47, 0x02, 0x08, 0x20, 0x61, 0xC4, 0x06, 0x17, 0x00, 0x22, 0x00, 0x02, 0x40, 0x00, 0x48, 0x00, 0x09, 0x00, 0x01, 0x20, 0x00, 0x3C, 0x00, +/* 0x03 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x04, 0x08, 0x48, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x08, 0x10, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA1, 0x81, 0x8D, 0x87, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x04 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x10, 0x02, 0x48, 0xE0, 0x61, 0xC1, 0xCC, 0x0E, 0x78, 0x1C, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xFC, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x05 */ 0x00, 0x18, 0x00, 0x00, 0x40, 0x01, 0x90, 0x01, 0xF4, 0x08, 0x12, 0x23, 0xC1, 0x91, 0x2C, 0x1C, 0x8A, 0xC3, 0x64, 0x64, 0x13, 0x22, 0x41, 0x98, 0x26, 0x2C, 0xC4, 0x22, 0x60, 0x42, 0x13, 0x04, 0x30, 0x80, 0x61, 0xA4, 0x02, 0x18, 0x20, 0x03, 0x41, 0x00, 0x20, 0x08, 0x02, 0x00, 0x60, 0x40, 0x03, 0xF8, +/* 0x06 */ 0x00, 0x10, 0x00, 0x03, 0x00, 0x1C, 0x48, 0x00, 0xB4, 0x80, 0x09, 0xF9, 0xC0, 0xE0, 0xE4, 0x0C, 0x02, 0x8F, 0x80, 0x38, 0x88, 0x01, 0x0D, 0x00, 0x18, 0x30, 0x01, 0x60, 0x80, 0x13, 0x18, 0x03, 0xF2, 0xC0, 0x20, 0x26, 0x06, 0x07, 0xFF, 0xA0, 0x02, 0x39, 0x00, 0x14, 0x70, 0x01, 0xC3, 0x00, 0x18, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x10, 0x37, 0xE2, 0x61, 0x00, 0x0C, 0xC6, 0x10, 0x98, 0x0C, 0x63, 0x00, 0x00, 0xC6, 0x00, +/* 0x09 */ 0x00, 0x1F, 0x80, 0x00, 0x60, 0x80, 0x01, 0x00, 0x80, 0x06, 0x00, 0x80, 0x3C, 0x01, 0x01, 0x8C, 0x02, 0x02, 0x08, 0x04, 0x04, 0x08, 0x0C, 0x38, 0x00, 0x04, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x2E, 0xC0, 0x01, 0x83, 0x7E, 0x0C, 0x00, 0x37, 0xE0, +/* 0x0A */ +/* 0x0B */ 0x1F, 0x07, 0xC1, 0x86, 0x41, 0x10, 0x0C, 0x04, 0x80, 0x40, 0x18, 0x00, 0x00, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x30, 0x00, 0x01, 0x40, 0x00, 0x0A, 0x00, 0x00, 0x88, 0x00, 0x04, 0x40, 0x00, 0x41, 0x00, 0x02, 0x04, 0x00, 0x20, 0x20, 0x02, 0x00, 0x80, 0x20, 0x02, 0x02, 0x00, 0x08, 0x20, 0x00, 0x22, 0x00, 0x00, 0xE0, 0x00, +/* 0x0C */ 0x01, 0x00, 0x00, 0x38, 0x00, 0x04, 0xC0, 0x01, 0x08, 0x00, 0x18, 0x80, 0x1C, 0x10, 0x02, 0x07, 0x80, 0x81, 0x10, 0x1F, 0xC2, 0x02, 0x00, 0x60, 0x80, 0x1A, 0x20, 0x1C, 0x42, 0x1C, 0x08, 0xFE, 0x03, 0xA0, 0x01, 0x8C, 0x01, 0xC1, 0x43, 0xD0, 0x27, 0x81, 0xF8, +/* 0x0D */ +/* 0x0E */ 0x00, 0xE0, 0x00, 0x11, 0x00, 0x01, 0x10, 0x00, 0x0B, 0x00, 0x03, 0xF8, 0x00, 0x60, 0x60, 0x09, 0x02, 0x00, 0xA0, 0x10, 0x16, 0x01, 0x01, 0x40, 0x10, 0x10, 0x01, 0x01, 0x00, 0x08, 0x10, 0x00, 0x82, 0x1F, 0x08, 0x3F, 0x90, 0x44, 0x00, 0x06, 0xBF, 0xFF, 0xAF, 0xF0, 0xFF, 0xFF, 0x0F, 0xE3, 0xFB, 0xFC, +/* 0x0F */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x40, 0x12, 0x34, 0x00, 0x69, 0x40, 0x01, 0x49, 0xE0, 0xF1, 0xCD, 0x06, 0x8E, 0x28, 0x14, 0x71, 0x40, 0xA3, 0x8B, 0xFD, 0x14, 0x50, 0x68, 0xA2, 0x81, 0x4D, 0x97, 0xFA, 0x44, 0xBF, 0xD6, 0x31, 0x02, 0xE0, 0xC8, 0x16, 0x08, 0x61, 0x08, 0x21, 0xF0, 0x80, 0xF8, 0x78, 0x00, +/* 0x10 */ 0x00, 0xF0, 0x00, 0x3A, 0x00, 0x07, 0xC0, 0x00, 0xA8, 0x00, 0x1F, 0x00, 0x02, 0xB0, 0x00, 0x52, 0x00, 0x0A, 0x40, 0x02, 0x48, 0x00, 0x49, 0x00, 0x09, 0x30, 0x01, 0x22, 0x01, 0xC4, 0x70, 0xF0, 0x85, 0xE1, 0x10, 0x88, 0x37, 0x20, 0x03, 0x9C, 0x00, 0x37, 0x00, 0x06, 0x40, 0x01, 0x86, 0x00, +/* 0x11 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x60, 0x02, 0x36, 0x00, 0x09, 0x04, 0x0C, 0x48, 0x60, 0xC1, 0xC3, 0x0F, 0x0E, 0x00, 0x08, 0x70, 0x00, 0x23, 0x80, 0x63, 0x84, 0x01, 0x9F, 0x20, 0x0C, 0xFD, 0x80, 0x27, 0xE4, 0x03, 0x3F, 0x30, 0x33, 0xE0, 0xC0, 0x00, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x12 */ 0x00, 0xC2, 0x00, 0x1C, 0x24, 0x02, 0x18, 0x60, 0x64, 0x02, 0x02, 0x40, 0x20, 0x00, 0xF2, 0x03, 0x89, 0xE0, 0x7C, 0x80, 0x0E, 0x25, 0x80, 0xE1, 0x00, 0x1A, 0x08, 0x71, 0xB0, 0xC4, 0x39, 0x84, 0xC2, 0xCC, 0x40, 0x76, 0x7C, 0x05, 0xBB, 0x80, 0x4C, 0xE0, 0x0A, 0x78, 0x00, 0x9C, 0x00, 0x0F, 0x00, 0x00, +/* 0x13 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x60, 0xC1, 0xC6, 0xC9, 0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x14, 0xFF, 0xF8, 0xA6, 0x00, 0xCD, 0x9F, 0xFE, 0x44, 0x71, 0xE6, 0x30, 0xFC, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x14 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x20, 0x22, 0x33, 0x01, 0x89, 0x20, 0x02, 0x48, 0x60, 0xE1, 0xC8, 0x80, 0x8E, 0x46, 0x46, 0x72, 0x32, 0x33, 0x9F, 0x9F, 0x94, 0x78, 0x78, 0xA0, 0x00, 0x0D, 0x80, 0x00, 0x44, 0x0E, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x15 */ 0x03, 0xFC, 0x20, 0x38, 0x1C, 0x81, 0x80, 0x1D, 0x08, 0x00, 0x32, 0x60, 0x00, 0x89, 0x00, 0x02, 0x18, 0x00, 0x08, 0x61, 0xC3, 0x22, 0x8D, 0x93, 0x72, 0x00, 0x00, 0x48, 0x00, 0x01, 0x20, 0x00, 0x04, 0x9F, 0xFF, 0x92, 0x60, 0x0E, 0x44, 0xFF, 0xF2, 0x11, 0xC3, 0x88, 0x21, 0xF8, 0x40, 0x40, 0x02, 0x00, 0xC0, 0x30, 0x00, 0xFF, 0x00, +/* 0x16 */ 0x03, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0xF0, 0x03, 0xF0, 0x27, 0xF0, 0x6F, 0x70, 0x6E, 0x60, 0xFC, 0x60, 0xFC, 0x7E, 0xFC, 0x7E, 0xFC, 0x3F, 0xF4, 0x1F, 0xF4, 0x1F, 0xF0, 0x0E, 0x70, 0x0E, 0x30, 0x1C, 0x38, 0x38, 0x0F, 0xF0, +/* 0x17 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x48, 0x00, 0x21, 0xC0, 0x02, 0x8E, 0x20, 0xF4, 0x70, 0x84, 0x11, 0x82, 0x40, 0x84, 0x01, 0x03, 0x20, 0x0F, 0x85, 0x80, 0x03, 0x04, 0x00, 0x04, 0x30, 0x78, 0x10, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x18 */ 0x00, 0xFC, 0x00, 0x02, 0x06, 0x00, 0x08, 0x24, 0x00, 0x21, 0xA4, 0x00, 0x4C, 0x48, 0x00, 0xA0, 0x50, 0x01, 0x92, 0x60, 0x03, 0x24, 0xC0, 0x06, 0x01, 0x81, 0x28, 0x03, 0x49, 0x6C, 0xC4, 0xAD, 0xD8, 0x16, 0xA4, 0xCC, 0xC4, 0x44, 0x86, 0x13, 0x05, 0x00, 0x28, 0x0A, 0x00, 0x50, 0x14, 0x00, 0x90, 0x48, 0x01, 0x20, 0x90, 0x02, 0x41, 0x20, 0x00, 0x00, +/* 0x19 */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x00, 0x02, 0x30, 0x00, 0x09, 0x00, 0x00, 0x49, 0xC3, 0x81, 0xC0, 0x00, 0x0E, 0x78, 0xF0, 0x77, 0xEF, 0xC3, 0xA7, 0x4E, 0x15, 0x0A, 0x10, 0xA7, 0x8F, 0x0D, 0x80, 0x00, 0x44, 0x00, 0x06, 0x33, 0xF0, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1A */ 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0C, 0x3E, 0x18, 0x82, 0x32, 0x02, 0x64, 0x04, 0xC8, 0x09, 0x80, 0x23, 0x00, 0x86, 0x02, 0x0C, 0x08, 0x18, 0x10, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x81, 0x80, 0x03, 0x00, 0x07, 0xFF, 0xF8, +/* 0x1B */ 0x00, 0xFE, 0x00, 0x03, 0x81, 0x80, 0x04, 0x00, 0x60, 0x08, 0x00, 0x30, 0x10, 0x00, 0x10, 0x30, 0x07, 0x88, 0x23, 0xC8, 0x08, 0x22, 0x00, 0x04, 0x60, 0x00, 0x44, 0x60, 0x00, 0x84, 0x63, 0x03, 0x04, 0x61, 0xFC, 0x04, 0x6B, 0x00, 0x9E, 0xA5, 0x01, 0x6A, 0xD5, 0x01, 0x43, 0xA8, 0x81, 0x05, 0xD0, 0x82, 0x0A, 0xA0, 0x82, 0x05, 0xC0, 0x82, 0x02, 0x61, 0xFF, 0x0C, 0x1E, 0x00, 0xF0, +/* 0x1C */ 0x01, 0xFC, 0x00, 0x38, 0x18, 0x02, 0x00, 0x30, 0x20, 0x00, 0xC2, 0x30, 0x02, 0x32, 0x00, 0x09, 0x00, 0x00, 0x48, 0x20, 0x61, 0xC3, 0x84, 0x0E, 0x1C, 0x78, 0x70, 0x40, 0x03, 0x80, 0x00, 0x14, 0x00, 0x00, 0xA0, 0x03, 0x0D, 0x83, 0xF0, 0x44, 0x00, 0x06, 0x30, 0x00, 0x60, 0xC0, 0x06, 0x03, 0x80, 0x60, 0x07, 0xFC, 0x00, +/* 0x1D */ 0x01, 0xFE, 0x00, 0x3A, 0x1C, 0x03, 0x00, 0x30, 0x23, 0x1E, 0xC3, 0x38, 0x03, 0x10, 0xC3, 0x09, 0x00, 0x18, 0x68, 0x00, 0xC1, 0x40, 0x00, 0x0A, 0x07, 0x80, 0x50, 0x46, 0x02, 0x80, 0x00, 0x1A, 0x1E, 0x00, 0xCB, 0x10, 0x0D, 0x03, 0x00, 0x48, 0x60, 0x06, 0x40, 0x00, 0x22, 0x0C, 0x02, 0x10, 0x60, 0x60, 0x43, 0xFC, 0x01, 0xE0, 0x00, 0x00, +/* 0x1E */ 0x01, 0xF0, 0x00, 0xEA, 0xC0, 0x31, 0x5F, 0x04, 0x5F, 0x88, 0x80, 0xA0, 0x48, 0x0E, 0x02, 0x8F, 0x40, 0x3C, 0x10, 0x21, 0x66, 0x87, 0x15, 0x98, 0x71, 0x41, 0x02, 0x14, 0x00, 0x01, 0x40, 0x00, 0x14, 0x00, 0x01, 0x21, 0xFE, 0x12, 0x00, 0x02, 0x10, 0x00, 0x60, 0x80, 0x0C, 0x06, 0x01, 0x80, 0x3F, 0xE0, +/* 0x1F */ 0x0E, 0x00, 0x13, 0x00, 0x23, 0x00, 0xF3, 0x01, 0x31, 0x01, 0x11, 0x03, 0xD3, 0x06, 0xF2, 0x30, 0x34, 0xC7, 0x25, 0x33, 0x2B, 0xC2, 0x57, 0x04, 0x3A, 0x08, 0x72, 0x30, 0xA3, 0xC3, 0x40, 0x04, 0x40, 0x18, 0x40, 0x60, 0x7F, 0x80, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, +/* '"' 0x22 */ 0xCF, 0x3C, 0xF3, 0x8A, 0x20, +/* '#' 0x23 */ 0x06, 0x30, 0x31, 0x03, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC7, 0xFF, 0xBF, 0xFC, 0x31, 0x01, 0x18, 0x18, 0xC0, 0xC6, 0x06, 0x30, +/* '$' 0x24 */ 0x04, 0x03, 0xE1, 0xFF, 0x72, 0x7C, 0x47, 0x88, 0xF1, 0x07, 0xA0, 0x7E, 0x03, 0xF0, 0x17, 0x02, 0x7C, 0x47, 0x88, 0xF1, 0x1B, 0x26, 0x7F, 0xC3, 0xE0, 0x10, 0x02, 0x00, +/* '%' 0x25 */ 0x00, 0x06, 0x03, 0xC0, 0x40, 0x7E, 0x0C, 0x0E, 0x70, 0x80, 0xC3, 0x18, 0x0C, 0x31, 0x00, 0xE7, 0x30, 0x07, 0xE6, 0x00, 0x3C, 0x40, 0x00, 0x0C, 0x7C, 0x00, 0x8F, 0xE0, 0x19, 0xC7, 0x01, 0x18, 0x30, 0x31, 0x83, 0x02, 0x1C, 0x70, 0x40, 0xFE, 0x04, 0x07, 0xC0, +/* '&' 0x26 */ 0x0F, 0x00, 0x7E, 0x03, 0x9C, 0x0C, 0x30, 0x30, 0xC0, 0xE7, 0x01, 0xF8, 0x03, 0x80, 0x3E, 0x01, 0xCC, 0x6E, 0x39, 0xB0, 0x7C, 0xC0, 0xF3, 0x03, 0xCE, 0x1F, 0x9F, 0xE6, 0x3E, 0x1C, +/* ''' 0x27 */ 0xFF, 0xA0, +/* '(' 0x28 */ 0x08, 0x8C, 0x46, 0x31, 0x98, 0xC6, 0x31, 0x8C, 0x63, 0x08, 0x63, 0x08, 0x61, 0x0C, 0x20, +/* ')' 0x29 */ 0x82, 0x18, 0xC3, 0x18, 0xC3, 0x18, 0xC6, 0x31, 0x8C, 0x62, 0x31, 0x88, 0xC4, 0x62, 0x00, +/* '*' 0x2A */ 0x10, 0x23, 0x5B, 0xE3, 0x8D, 0x91, 0x00, +/* '+' 0x2B */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, +/* ',' 0x2C */ 0xF5, 0x60, +/* '-' 0x2D */ 0xFF, 0xF0, +/* '.' 0x2E */ 0xF0, +/* '/' 0x2F */ 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x02, 0x0C, 0x10, 0x20, 0xC1, 0x00, +/* '0' 0x30 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x0F, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3E, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* '1' 0x31 */ 0x08, 0xCF, 0xFF, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, +/* '2' 0x32 */ 0x1F, 0x0F, 0xF9, 0x87, 0x60, 0x7C, 0x06, 0x00, 0xC0, 0x18, 0x07, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x06, 0x00, 0xC0, 0x30, 0x07, 0xFF, 0xFF, 0xE0, +/* '3' 0x33 */ 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0C, 0x01, 0x80, 0x60, 0x78, 0x0F, 0x80, 0x18, 0x01, 0x80, 0x3C, 0x07, 0x80, 0xD8, 0x73, 0xFC, 0x3F, 0x00, +/* '4' 0x34 */ 0x01, 0x80, 0x70, 0x0E, 0x03, 0xC0, 0xD8, 0x1B, 0x06, 0x61, 0x8C, 0x21, 0x8C, 0x33, 0x06, 0x7F, 0xFF, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, +/* '5' 0x35 */ 0x3F, 0xCF, 0xF9, 0x80, 0x30, 0x06, 0x00, 0xDE, 0x1F, 0xE7, 0x0E, 0x00, 0xE0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x81, 0xB8, 0x73, 0xFC, 0x1F, 0x00, +/* '6' 0x36 */ 0x0F, 0x07, 0xF9, 0xC3, 0x30, 0x74, 0x01, 0x80, 0x33, 0xC7, 0xFE, 0xF1, 0xDC, 0x1F, 0x01, 0xE0, 0x3C, 0x06, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* '7' 0x37 */ 0xFF, 0xFF, 0xFC, 0x01, 0x00, 0x60, 0x18, 0x02, 0x00, 0xC0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x04, 0x01, 0x80, 0x30, 0x06, 0x01, 0x80, 0x30, 0x00, +/* '8' 0x38 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x66, 0x0C, 0xC1, 0x8C, 0x61, 0xF8, 0x3F, 0x8E, 0x3B, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xD8, 0x31, 0xFC, 0x1F, 0x00, +/* '9' 0x39 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x07, 0x61, 0xEF, 0xFC, 0x79, 0x80, 0x30, 0x05, 0xC1, 0x98, 0x73, 0xFC, 0x1E, 0x00, +/* ':' 0x3A */ 0xF0, 0x00, 0x03, 0xC0, +/* ';' 0x3B */ 0xF0, 0x00, 0x0F, 0x56, +/* '<' 0x3C */ 0x00, 0x70, 0x1E, 0x0F, 0x83, 0xC0, 0xF0, 0x0E, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x10, +/* '=' 0x3D */ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, +/* '>' 0x3E */ 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF0, 0x07, 0x01, 0xE0, 0xF0, 0x3C, 0x0F, 0x00, 0x80, 0x00, +/* '?' 0x3F */ 0x3F, 0x1F, 0xEE, 0x1F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x06, 0x03, 0x81, 0xC0, 0xE0, 0x30, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x0C, 0x03, 0x00, +/* '@' 0x40 */ 0x00, 0xFE, 0x00, 0x0F, 0xFE, 0x00, 0xF0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x38, 0xC1, 0xE0, 0x66, 0x0F, 0xD9, 0xD8, 0x61, 0xC3, 0xC3, 0x07, 0x0F, 0x1C, 0x1C, 0x3C, 0x60, 0x60, 0xF1, 0x81, 0x83, 0xC6, 0x06, 0x1B, 0x18, 0x38, 0xEE, 0x71, 0xE7, 0x18, 0xFD, 0xF8, 0x71, 0xE7, 0xC0, 0xE0, 0x00, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x01, 0xFC, 0x00, +/* 'A' 0x41 */ 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0C, 0xC0, 0x33, 0x01, 0xCE, 0x06, 0x18, 0x18, 0x60, 0xE1, 0xC3, 0x03, 0x0F, 0xFC, 0x7F, 0xF9, 0x80, 0x66, 0x01, 0xB8, 0x07, 0xC0, 0x0F, 0x00, 0x30, +/* 'B' 0x42 */ 0xFF, 0xC7, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x0C, 0xFF, 0xC7, 0xFF, 0x30, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x00, +/* 'C' 0x43 */ 0x07, 0xE0, 0x3F, 0xF0, 0xE0, 0x73, 0x80, 0x76, 0x00, 0x6C, 0x00, 0x30, 0x00, 0x60, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x6C, 0x00, 0xDC, 0x03, 0x1E, 0x0E, 0x1F, 0xF8, 0x0F, 0xC0, +/* 'D' 0x44 */ 0xFF, 0xC3, 0xFF, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1B, 0x00, 0x7C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x1F, 0x00, 0x6C, 0x03, 0xB0, 0x1C, 0xFF, 0xE3, 0xFE, 0x00, +/* 'E' 0x45 */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xEF, 0xFE, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, +/* 'F' 0x46 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, +/* 'G' 0x47 */ 0x07, 0xF0, 0x1F, 0xFC, 0x3C, 0x1E, 0x70, 0x07, 0x60, 0x03, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xC0, 0x7F, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x30, 0x0F, 0x3C, 0x1F, 0x1F, 0xFB, 0x07, 0xE1, +/* 'H' 0x48 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xC0, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'J' 0x4A */ 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x3C, 0x1E, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 'K' 0x4B */ 0xC0, 0x3E, 0x03, 0xB0, 0x39, 0x83, 0x8C, 0x38, 0x63, 0x83, 0x38, 0x19, 0xC0, 0xDE, 0x07, 0xB8, 0x38, 0xE1, 0x83, 0x0C, 0x1C, 0x60, 0x73, 0x01, 0x98, 0x0E, 0xC0, 0x3E, 0x00, 0xC0, +/* 'L' 0x4C */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFF, 0xFF, 0xF0, +/* 'M' 0x4D */ 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xD0, 0x0F, 0xD8, 0x1B, 0xD8, 0x1B, 0xD8, 0x1B, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xC6, 0x63, 0xC6, 0x63, 0xC6, 0x63, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC1, 0x83, +/* 'N' 0x4E */ 0xE0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x79, 0xC3, 0xC6, 0x1E, 0x18, 0xF0, 0xE7, 0x83, 0x3C, 0x1D, 0xE0, 0x6F, 0x01, 0xF8, 0x0F, 0xC0, 0x3E, 0x01, 0xC0, +/* 'O' 0x4F */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x00, 0x18, 0xC0, 0x18, 0x78, 0x3C, 0x1F, 0xFC, 0x03, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x8F, 0xFE, 0xC0, 0x6C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x06, 0xFF, 0xEF, 0xFC, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, +/* 'Q' 0x51 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x86, 0x00, 0xC6, 0x00, 0x33, 0x00, 0x1B, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x33, 0x01, 0x98, 0xC0, 0xFC, 0x78, 0x3C, 0x1F, 0xFF, 0x03, 0xF9, 0x80, 0x00, 0x40, +/* 'R' 0x52 */ 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x0C, 0xFF, 0xE3, 0xFF, 0xCC, 0x03, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x6C, 0x01, 0xB0, 0x06, 0xC0, 0x1B, 0x00, 0x70, +/* 'S' 0x53 */ 0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x98, 0x07, 0x60, 0x0D, 0x80, 0x07, 0x00, 0x1E, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x0F, 0x80, 0x07, 0xC0, 0x0F, 0x00, 0x3E, 0x00, 0xDE, 0x0E, 0x3F, 0xF0, 0x3F, 0x80, +/* 'T' 0x54 */ 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 'U' 0x55 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x80, 0xEE, 0x0E, 0x3F, 0xE0, 0xFC, 0x00, +/* 'V' 0x56 */ 0xC0, 0x0F, 0x00, 0x7E, 0x01, 0x98, 0x06, 0x60, 0x39, 0xC0, 0xC3, 0x03, 0x0C, 0x1C, 0x38, 0x60, 0x61, 0x81, 0x8E, 0x07, 0x30, 0x0C, 0xC0, 0x37, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x00, +/* 'W' 0x57 */ 0xE0, 0x30, 0x1D, 0x80, 0xE0, 0x76, 0x07, 0x81, 0xDC, 0x1E, 0x06, 0x70, 0x7C, 0x18, 0xC1, 0xB0, 0xE3, 0x0C, 0xC3, 0x8C, 0x33, 0x0C, 0x38, 0xC6, 0x30, 0x67, 0x18, 0xC1, 0x98, 0x67, 0x06, 0x61, 0xD8, 0x1D, 0x83, 0x60, 0x3C, 0x0D, 0x80, 0xF0, 0x3E, 0x03, 0xC0, 0x70, 0x0F, 0x01, 0xC0, 0x18, 0x07, 0x00, +/* 'X' 0x58 */ 0xE0, 0x1D, 0x80, 0xE7, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0x30, 0x1C, 0xE0, 0xE1, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1B, 0x00, 0x70, +/* 'Y' 0x59 */ 0xC0, 0x0F, 0x80, 0x76, 0x01, 0x9C, 0x0C, 0x38, 0x70, 0x61, 0x81, 0xCE, 0x03, 0x30, 0x0F, 0x80, 0x1E, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x03, 0x80, 0x18, 0x01, 0xC0, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCF, 0xF0, +/* '\' 0x5C */ 0x81, 0x81, 0x02, 0x06, 0x04, 0x08, 0x18, 0x10, 0x20, 0x60, 0x40, 0x81, 0x81, 0x02, 0x06, 0x04, +/* ']' 0x5D */ 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xF0, +/* '^' 0x5E */ 0x0C, 0x0E, 0x05, 0x86, 0xC3, 0x21, 0x19, 0x8C, 0x83, 0xC1, 0x80, +/* '_' 0x5F */ 0xFF, 0xFE, +/* '`' 0x60 */ 0xE3, 0x8C, 0x30, +/* 'a' 0x61 */ 0x3F, 0x07, 0xF8, 0xE1, 0xCC, 0x0C, 0x00, 0xC0, 0x1C, 0x3F, 0xCF, 0x8C, 0xC0, 0xCC, 0x0C, 0xE3, 0xC7, 0xEF, 0x3C, 0x70, +/* 'b' 0x62 */ 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xF8, 0xDF, 0xCF, 0x0E, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xE0, 0x6F, 0x0E, 0xDF, 0xCC, 0xF8, +/* 'c' 0x63 */ 0x1F, 0x0F, 0xE6, 0x1F, 0x83, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x38, 0x37, 0x1C, 0xFE, 0x1F, 0x00, +/* 'd' 0x64 */ 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x3C, 0xCF, 0xFB, 0x8F, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF8, 0x3B, 0x8F, 0x3F, 0x63, 0xCC, +/* 'e' 0x65 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x00, 0xC0, 0x1C, 0x0D, 0xC3, 0x1F, 0xC1, 0xF0, +/* 'f' 0x66 */ 0x3B, 0xD8, 0xC6, 0x7F, 0xEC, 0x63, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x00, +/* 'g' 0x67 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xB1, 0xE6, 0x00, 0xC0, 0x3E, 0x0E, 0x7F, 0xC7, 0xE0, +/* 'h' 0x68 */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xCD, 0xFB, 0xC7, 0xE0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, +/* 'i' 0x69 */ 0xF0, 0x3F, 0xFF, 0xFF, 0xF0, +/* 'j' 0x6A */ 0x33, 0x00, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, +/* 'k' 0x6B */ 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0xB8, 0xC6, 0x31, 0xCC, 0x3B, 0x06, 0xC1, 0xF0, 0x30, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 'm' 0x6D */ 0xCF, 0x1F, 0x6F, 0xDF, 0xFC, 0x78, 0xFC, 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0F, 0x03, 0x07, 0x81, 0x83, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 0x18, 0x3C, 0x0C, 0x18, +/* 'n' 0x6E */ 0xCF, 0x37, 0xEF, 0x1F, 0x83, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, +/* 'o' 0x6F */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, +/* 'p' 0x70 */ 0xCF, 0x8D, 0xFC, 0xF0, 0xEE, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3E, 0x06, 0xF0, 0xEF, 0xFC, 0xCF, 0x8C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 'q' 0x71 */ 0x1E, 0x67, 0xFD, 0xC7, 0xF0, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x9F, 0xF1, 0xE6, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, +/* 'r' 0x72 */ 0xCF, 0x7F, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 's' 0x73 */ 0x3E, 0x1F, 0xEE, 0x1B, 0x00, 0xC0, 0x3C, 0x07, 0xF0, 0x3F, 0x01, 0xF0, 0x3E, 0x1D, 0xFE, 0x3F, 0x00, +/* 't' 0x74 */ 0x63, 0x19, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0xC6, 0x31, 0xE7, +/* 'u' 0x75 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x7E, 0x3D, 0xFB, 0x3C, 0xC0, +/* 'v' 0x76 */ 0xE0, 0x6C, 0x0D, 0x81, 0xB8, 0x63, 0x0C, 0x61, 0x8E, 0x60, 0xCC, 0x19, 0x83, 0xE0, 0x3C, 0x07, 0x00, 0xE0, +/* 'w' 0x77 */ 0xC1, 0xC1, 0xB0, 0xE1, 0xD8, 0x70, 0xCC, 0x2C, 0x66, 0x36, 0x31, 0x9B, 0x18, 0xCD, 0x98, 0x64, 0x6C, 0x16, 0x36, 0x0F, 0x1A, 0x07, 0x8F, 0x03, 0x83, 0x80, 0xC1, 0xC0, +/* 'x' 0x78 */ 0xC1, 0xF8, 0x66, 0x30, 0xCC, 0x3E, 0x07, 0x00, 0xC0, 0x78, 0x36, 0x0C, 0xC6, 0x3B, 0x06, 0xC0, 0xC0, +/* 'y' 0x79 */ 0xE0, 0x6C, 0x0D, 0x83, 0x38, 0x63, 0x0C, 0x63, 0x0C, 0x60, 0xCC, 0x1B, 0x03, 0x60, 0x3C, 0x07, 0x00, 0xE0, 0x18, 0x03, 0x00, 0xE0, 0x78, 0x0E, 0x00, +/* 'z' 0x7A */ 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0x03, 0x81, 0xC0, 0x60, 0x30, 0x18, 0x0E, 0x03, 0xFF, 0xFF, 0xC0, +/* '{' 0x7B */ 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x31, 0x99, 0x86, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x1C, 0x60, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, +/* '}' 0x7D */ 0xC7, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x0C, 0x33, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x73, 0x00, +/* '~' 0x7E */ 0x70, 0x3E, 0x09, 0xE4, 0x1F, 0x03, 0x80, +/* 0x7F */ +/* 0x80 */ 0x01, 0xF0, 0x1F, 0xF0, 0xE0, 0xC7, 0x00, 0x18, 0x00, 0xC0, 0x07, 0xFF, 0x3F, 0xFC, 0x30, 0x01, 0xFF, 0x8F, 0xFC, 0x0C, 0x00, 0x18, 0x00, 0x70, 0x00, 0xE0, 0x81, 0xFE, 0x03, 0xF0, +/* 0x81 */ +/* 0x82 */ 0xF5, 0x80, +/* 0x83 */ 0x1C, 0xF3, 0x0C, 0x31, 0xF7, 0xCC, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x33, 0xCE, 0x00, +/* 0x84 */ 0xCF, 0x34, 0x51, 0x88, +/* 0x85 */ 0xC6, 0x3C, 0x63, +/* 0x86 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, +/* 0x87 */ 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x0F, 0xFF, 0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0x88 */ 0x38, 0xD9, 0xB6, 0x30, +/* 0x89 */ 0x38, 0x18, 0x00, 0xF8, 0x30, 0x03, 0x18, 0xC0, 0x04, 0x11, 0x80, 0x0C, 0x66, 0x00, 0x0F, 0x8C, 0x00, 0x0E, 0x30, 0x00, 0x00, 0x40, 0x00, 0x01, 0x80, 0x00, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x31, 0xC0, 0xE0, 0x67, 0xC3, 0xC1, 0x98, 0xCC, 0xC3, 0x20, 0x90, 0x8C, 0x63, 0x33, 0x10, 0x7C, 0x3C, 0x60, 0x70, 0x38, +/* 0x8A */ 0x0C, 0x40, 0x1F, 0x00, 0x38, 0x03, 0xF8, 0x1F, 0xF0, 0xE0, 0xE6, 0x01, 0xD8, 0x03, 0x60, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0xE0, 0x0F, 0xF0, 0x03, 0xE0, 0x01, 0xF0, 0x03, 0xC0, 0x0F, 0x80, 0x37, 0x83, 0x8F, 0xFC, 0x0F, 0xE0, +/* 0x8B */ 0x2F, 0x49, 0x99, +/* 0x8C */ 0x07, 0xCF, 0xFC, 0x7F, 0xFF, 0xF3, 0x83, 0xC0, 0x18, 0x07, 0x00, 0x60, 0x0C, 0x03, 0x00, 0x30, 0x0C, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0F, 0xFF, 0x00, 0x3F, 0xFC, 0x00, 0xC0, 0x30, 0x03, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x30, 0x07, 0x01, 0xC0, 0x0E, 0x0F, 0x00, 0x1F, 0xEF, 0xFC, 0x1F, 0x3F, 0xF0, +/* 0x8D */ +/* 0x8E */ 0x0C, 0xC0, 0x3C, 0x00, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60, 0x07, 0x00, 0x70, 0x07, 0x00, 0x30, 0x03, 0x80, 0x38, 0x01, 0xFF, 0xFF, 0xFF, 0x80, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0x6A, 0xF0, +/* 0x92 */ 0xF5, 0x60, +/* 0x93 */ 0x4E, 0x28, 0xA2, 0xCF, 0x30, +/* 0x94 */ 0xCF, 0x34, 0x51, 0x4E, 0x20, +/* 0x95 */ 0x7B, 0xFF, 0xFF, 0xFD, 0xE0, +/* 0x96 */ 0xFF, 0xFF, 0xF0, +/* 0x97 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, +/* 0x98 */ 0x63, 0xFE, 0x70, +/* 0x99 */ 0xFF, 0x70, 0x1F, 0xFD, 0xC0, 0x71, 0x87, 0x83, 0xC6, 0x1E, 0x0F, 0x18, 0x68, 0x3C, 0x61, 0xB1, 0xB1, 0x86, 0xC6, 0xC6, 0x19, 0x1B, 0x18, 0x66, 0xCC, 0x61, 0x9B, 0x31, 0x86, 0x3C, 0xC6, 0x18, 0xE3, 0x18, 0x63, 0x8C, +/* 0x9A */ 0x63, 0x0D, 0x83, 0x60, 0x70, 0x00, 0x0F, 0x87, 0xFB, 0x86, 0xC0, 0x30, 0x0F, 0x01, 0xFC, 0x0F, 0xC0, 0x7C, 0x0F, 0x87, 0x7F, 0x8F, 0xC0, +/* 0x9B */ 0x99, 0x92, 0xF4, +/* 0x9C */ 0x1F, 0x0F, 0x83, 0xF9, 0xFC, 0x71, 0xF8, 0x6E, 0x0F, 0x03, 0xC0, 0x60, 0x3C, 0x07, 0xFF, 0xC0, 0x7F, 0xFC, 0x06, 0x00, 0xC0, 0x60, 0x0E, 0x0F, 0x03, 0x71, 0xF8, 0x63, 0xF9, 0xFC, 0x1F, 0x0F, 0x80, +/* 0x9D */ +/* 0x9E */ 0x63, 0x0C, 0x83, 0x60, 0x70, 0x00, 0x3F, 0xFF, 0xFC, 0x06, 0x03, 0x01, 0xC0, 0xE0, 0x70, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFF, 0xFF, 0xF0, +/* 0x9F */ 0x0C, 0xC0, 0x33, 0x00, 0x00, 0x30, 0x03, 0xE0, 0x1D, 0x80, 0x67, 0x03, 0x0E, 0x1C, 0x18, 0x60, 0x73, 0x80, 0xCC, 0x03, 0xE0, 0x07, 0x80, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, +/* 0xA0 */ +/* 0xA1 */ 0xF0, 0xBF, 0xFF, 0xFF, 0xF0, +/* 0xA2 */ 0x04, 0x00, 0x80, 0x7C, 0x1F, 0xE7, 0x4C, 0xC8, 0xF1, 0x1E, 0x20, 0xC4, 0x18, 0x83, 0x10, 0x72, 0x37, 0x4E, 0x7F, 0x87, 0xC0, 0x20, 0x04, 0x00, +/* 0xA3 */ 0x0F, 0xC1, 0xFE, 0x38, 0x76, 0x03, 0x60, 0x36, 0x00, 0x70, 0x03, 0x80, 0xFF, 0x0F, 0xF0, 0x1C, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x10, 0x02, 0xF1, 0x7F, 0xF6, 0x1F, +/* 0xA4 */ 0xDD, 0xFF, 0xD8, 0xD8, 0x3C, 0x1E, 0x0F, 0x8D, 0xFF, 0xDD, 0x80, +/* 0xA5 */ 0xC0, 0x3E, 0x06, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x19, 0x80, 0xF0, 0x0F, 0x07, 0xFE, 0x06, 0x00, 0x60, 0x7F, 0xE0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 0xA6 */ 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, +/* 0xA7 */ 0x0F, 0x03, 0xF0, 0xE7, 0x18, 0x63, 0x0C, 0x70, 0x07, 0x03, 0xF8, 0xC3, 0x98, 0x3B, 0x03, 0xF0, 0x37, 0x06, 0x78, 0xC7, 0xB0, 0x7C, 0x03, 0x80, 0x39, 0x83, 0x30, 0x67, 0x1C, 0x7F, 0x07, 0xC0, +/* 0xA8 */ 0xCF, 0x30, +/* 0xA9 */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xE3, 0x1C, 0x73, 0xF3, 0x99, 0x86, 0x6C, 0xC1, 0x8F, 0x30, 0x03, 0xCC, 0x00, 0xF3, 0x00, 0x3C, 0xC1, 0x8D, 0x98, 0x66, 0x77, 0xF3, 0x8E, 0x79, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAA */ 0x79, 0x08, 0x11, 0xEE, 0x50, 0xA3, 0x3B, 0x00, 0x03, 0xF8, +/* 0xAB */ 0x21, 0x63, 0xE7, 0x84, 0x84, 0xE7, 0x63, 0x21, +/* 0xAC */ 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x03, 0x00, 0x30, 0x03, +/* 0xAD */ 0xFF, 0xF0, +/* 0xAE */ 0x03, 0xF0, 0x03, 0xFF, 0x01, 0xE0, 0xE0, 0xFF, 0x1C, 0x7F, 0xF3, 0x9B, 0x04, 0x6C, 0xC1, 0x8F, 0x30, 0x43, 0xCF, 0xF0, 0xF3, 0xFC, 0x3C, 0xC1, 0x0D, 0xB0, 0x66, 0x7C, 0x1B, 0x8F, 0x07, 0xC1, 0xC0, 0xE0, 0x3F, 0xF0, 0x03, 0xF0, 0x00, +/* 0xAF */ 0xFF, 0xF0, +/* 0xB0 */ 0x38, 0xFB, 0x1C, 0x18, 0x38, 0xDF, 0x1C, +/* 0xB1 */ 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x7F, 0xE7, 0xFE, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xB2 */ 0x7D, 0x8F, 0x18, 0x30, 0xC6, 0x18, 0x60, 0xFF, 0xFC, +/* 0xB3 */ 0x7D, 0x8F, 0x18, 0x31, 0x80, 0xC1, 0xE3, 0xC6, 0xF8, +/* 0xB4 */ 0x3B, 0x99, 0x80, +/* 0xB5 */ 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x0C, 0xC0, 0xCC, 0x1C, 0xE3, 0xCF, 0xEF, 0xFC, 0x7C, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x00, +/* 0xB6 */ 0x1F, 0xE7, 0xFD, 0xF3, 0x7E, 0x6F, 0xCD, 0xF9, 0xBF, 0x37, 0xE6, 0x7C, 0xCF, 0x98, 0xF3, 0x06, 0x60, 0xCC, 0x19, 0x83, 0x30, 0x66, 0x0C, 0xC1, 0x98, 0x33, 0x06, 0x60, 0xCC, +/* 0xB7 */ 0xF0, +/* 0xB8 */ 0x10, 0xF0, 0xE3, 0x78, +/* 0xB9 */ 0x2F, 0xB6, 0xDB, 0x6C, +/* 0xBA */ 0x79, 0x38, 0x61, 0x86, 0x1C, 0xDE, 0x00, 0x0F, 0xC0, +/* 0xBB */ 0x88, 0xC6, 0xE7, 0x21, 0x21, 0xE7, 0xC6, 0x88, +/* 0xBC */ 0x20, 0x08, 0x30, 0x0C, 0x38, 0x04, 0x0C, 0x06, 0x06, 0x02, 0x03, 0x02, 0x01, 0x81, 0x00, 0xC1, 0x06, 0x61, 0x87, 0x30, 0x83, 0x80, 0xC2, 0xC0, 0x42, 0x60, 0x43, 0x30, 0x21, 0xFC, 0x20, 0x0C, 0x30, 0x06, 0x10, 0x03, 0x00, +/* 0xBD */ 0x20, 0x00, 0x08, 0x02, 0x06, 0x01, 0x83, 0x80, 0x40, 0x60, 0x20, 0x18, 0x18, 0x06, 0x04, 0x01, 0x83, 0x00, 0x61, 0x9F, 0x98, 0x4E, 0x76, 0x33, 0x0C, 0x08, 0x03, 0x04, 0x03, 0x83, 0x01, 0x80, 0x81, 0x80, 0x60, 0xC0, 0x30, 0x3F, 0xC8, 0x0F, 0xF0, +/* 0xBE */ 0x7C, 0x00, 0x18, 0xC0, 0x43, 0x18, 0x18, 0x03, 0x02, 0x00, 0x60, 0xC0, 0x30, 0x10, 0x01, 0x84, 0x00, 0x31, 0x80, 0xC6, 0x20, 0xD8, 0xC8, 0x39, 0xF1, 0x07, 0x00, 0x41, 0x60, 0x18, 0x4C, 0x02, 0x11, 0x80, 0x83, 0xF8, 0x10, 0x06, 0x04, 0x00, 0xC1, 0x00, 0x18, +/* 0xBF */ 0x0C, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x60, 0x60, 0x30, 0x30, 0x38, 0x38, 0x18, 0x0C, 0x06, 0x0F, 0x07, 0xC7, 0x7F, 0x1F, 0x00, +/* 0xC0 */ 0x0C, 0xDB, 0xD3, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0xC1 */ 0x03, 0x80, 0x07, 0x00, 0x1B, 0x00, 0x36, 0x00, 0xEE, 0x01, 0x8C, 0x03, 0x18, 0x0C, 0x18, 0x18, 0x30, 0x30, 0x60, 0xFF, 0xE1, 0xFF, 0xC7, 0x01, 0xCC, 0x01, 0x98, 0x03, 0x60, 0x03, 0xC0, 0x06, +/* 0xC2 */ 0xFF, 0x87, 0xFF, 0x30, 0x1D, 0x80, 0x6C, 0x03, 0x60, 0x1B, 0x01, 0x9F, 0xFC, 0xFF, 0xE6, 0x03, 0xB0, 0x0F, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0xDF, 0xFE, 0xFF, 0xC0, +/* 0xC3 */ 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x00, +/* 0xC4 */ 0x01, 0xC0, 0x01, 0xC0, 0x03, 0x60, 0x03, 0x60, 0x07, 0x60, 0x06, 0x30, 0x06, 0x30, 0x0C, 0x18, 0x0C, 0x18, 0x1C, 0x18, 0x18, 0x0C, 0x18, 0x0C, 0x30, 0x06, 0x30, 0x06, 0x70, 0x06, 0x7F, 0xFF, 0x7F, 0xFF, +/* 0xC5 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xFE, 0xFF, 0xF6, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x1F, 0xFF, 0xFF, 0xF8, +/* 0xC6 */ 0x7F, 0xFD, 0xFF, 0xF0, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x80, 0x1C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xE0, 0x07, 0x00, 0x18, 0x00, 0xC0, 0x06, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, +/* 0xC7 */ 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x7F, 0xFF, 0xFF, 0xFE, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x18, +/* 0xC8 */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x8E, 0x00, 0xE6, 0x00, 0x37, 0x00, 0x1F, 0x00, 0x07, 0x9F, 0xF3, 0xCF, 0xF9, 0xE0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x76, 0x00, 0x33, 0x80, 0x38, 0xF0, 0x78, 0x3F, 0xF8, 0x07, 0xF0, 0x00, +/* 0xC9 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, +/* 0xCA */ 0xC0, 0x3B, 0x01, 0xCC, 0x0E, 0x30, 0x70, 0xC3, 0x83, 0x1C, 0x0C, 0xE0, 0x37, 0x80, 0xFF, 0x03, 0xDC, 0x0E, 0x38, 0x30, 0x70, 0xC0, 0xE3, 0x03, 0x8C, 0x07, 0x30, 0x0E, 0xC0, 0x1C, +/* 0xCB */ 0x01, 0xC0, 0x00, 0xE0, 0x00, 0xD8, 0x00, 0x6C, 0x00, 0x37, 0x00, 0x31, 0x80, 0x18, 0xC0, 0x18, 0x30, 0x0C, 0x18, 0x0E, 0x0E, 0x06, 0x03, 0x03, 0x01, 0x83, 0x00, 0x61, 0x80, 0x31, 0xC0, 0x1C, 0xC0, 0x06, 0x60, 0x03, 0x00, +/* 0xCC */ 0xE0, 0x0F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80, 0xFD, 0x83, 0x7B, 0x06, 0xF6, 0x0D, 0xE4, 0x13, 0xCC, 0x67, 0x98, 0xCF, 0x31, 0x9E, 0x36, 0x3C, 0x6C, 0x78, 0xD8, 0xF0, 0xA1, 0xE1, 0xC3, 0xC3, 0x86, +/* 0xCD */ 0xC0, 0x1F, 0x00, 0xFC, 0x07, 0xE0, 0x3D, 0x81, 0xEE, 0x0F, 0x30, 0x78, 0xC3, 0xC7, 0x1E, 0x18, 0xF0, 0x67, 0x83, 0xBC, 0x0D, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xC0, 0x18, +/* 0xCE */ 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFE, 0x7F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, +/* 0xCF */ 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0x07, 0x8E, 0x00, 0xE6, 0x00, 0x37, 0x00, 0x1F, 0x00, 0x07, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x76, 0x00, 0x33, 0x80, 0x38, 0xF0, 0x78, 0x3F, 0xF8, 0x07, 0xF0, 0x00, +/* 0xD0 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x18, +/* 0xD1 */ 0xFF, 0xC7, 0xFF, 0xB0, 0x0D, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x06, 0xFF, 0xF7, 0xFE, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x18, 0x00, 0xC0, 0x00, +/* 0xD2 */ +/* 0xD3 */ 0xFF, 0xEF, 0xFE, 0xC0, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x0F, 0xFF, 0xFF, 0xF0, +/* 0xD4 */ 0xFF, 0xFF, 0xFF, 0xF0, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, +/* 0xD5 */ 0xE0, 0x07, 0x60, 0x0E, 0x30, 0x1C, 0x38, 0x1C, 0x1C, 0x38, 0x0E, 0x70, 0x06, 0x60, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, +/* 0xD6 */ 0x01, 0x80, 0x01, 0x80, 0x0F, 0xF0, 0x3F, 0xFC, 0x71, 0x8E, 0x61, 0x86, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x86, 0x71, 0x8E, 0x3F, 0xFC, 0x0F, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, +/* 0xD7 */ 0x70, 0x1C, 0x70, 0x70, 0x61, 0xC0, 0xE3, 0x80, 0xEE, 0x00, 0xD8, 0x01, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x0D, 0x80, 0x3B, 0x80, 0x77, 0x01, 0xC7, 0x07, 0x07, 0x0E, 0x06, 0x38, 0x0E, 0xE0, 0x0E, +/* 0xD8 */ 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x86, 0x79, 0x9E, 0x3F, 0xFC, 0x0F, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, +/* 0xD9 */ 0x07, 0xE0, 0x1F, 0xF8, 0x38, 0x3C, 0x70, 0x0E, 0x60, 0x06, 0xE0, 0x07, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, 0x06, 0x30, 0x0C, 0x1C, 0x38, 0xFE, 0x7F, 0xFE, 0x7F, +/* 0xDA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xDB */ 0x06, 0x60, 0x06, 0x60, 0x00, 0x00, 0xE0, 0x07, 0x60, 0x0E, 0x30, 0x1C, 0x38, 0x1C, 0x1C, 0x38, 0x0E, 0x70, 0x06, 0x60, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, +/* 0xDC */ 0x03, 0x80, 0x30, 0x06, 0x00, 0x00, 0x1E, 0x33, 0xFB, 0x71, 0xFE, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x6E, 0x0E, 0x71, 0xF3, 0xFF, 0x1F, 0x30, +/* 0xDD */ 0x0C, 0x0C, 0x04, 0x00, 0x03, 0xE3, 0xFF, 0x8D, 0x80, 0xE0, 0x3E, 0x1F, 0x1C, 0x0C, 0x06, 0x0B, 0x8E, 0xFE, 0x3E, 0x00, +/* 0xDE */ 0x07, 0x01, 0x80, 0xC0, 0x00, 0xCF, 0x3F, 0xEE, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0xDF */ 0x76, 0xC0, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 0xE0 */ 0x0C, 0x1B, 0x66, 0x98, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, +/* 0xE1 */ 0x1E, 0x33, 0xFB, 0x71, 0xFE, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x6E, 0x0E, 0x71, 0xF3, 0xFF, 0x1F, 0x30, +/* 0xE2 */ 0x1F, 0x0F, 0xF1, 0x87, 0x60, 0x6C, 0x0D, 0x83, 0x33, 0x86, 0x7C, 0xC1, 0xD8, 0x1F, 0x01, 0xE0, 0x3C, 0x07, 0xC0, 0xFC, 0x36, 0xFE, 0xCF, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x00, +/* 0xE3 */ 0x60, 0x66, 0x06, 0x60, 0x63, 0x0C, 0x30, 0xC3, 0x8C, 0x19, 0x81, 0x98, 0x1F, 0x80, 0xF0, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xE4 */ 0x7F, 0xCF, 0xF8, 0xE0, 0x07, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* 0xE5 */ 0x3E, 0x3F, 0xF8, 0xD8, 0x0E, 0x03, 0xE1, 0xF1, 0xC0, 0xC0, 0x60, 0xB8, 0xEF, 0xE3, 0xE0, +/* 0xE6 */ 0x3F, 0x9F, 0xC0, 0xC1, 0xC1, 0xC0, 0xC0, 0xC0, 0xC0, 0x60, 0x70, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x80, 0xFC, 0x3F, 0x80, 0xC0, 0x60, 0x70, 0xF0, 0x70, +/* 0xE7 */ 0xCF, 0x3F, 0xEE, 0x1F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, +/* 0xE8 */ 0x1F, 0x07, 0xF1, 0xC7, 0x30, 0x6C, 0x07, 0x80, 0xF0, 0x1F, 0xFF, 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3C, 0x0E, 0xC1, 0x9C, 0x71, 0xFC, 0x1F, 0x00, +/* 0xE9 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 0xEA */ 0xC1, 0xB0, 0xCC, 0x63, 0x30, 0xD8, 0x3C, 0x0F, 0x83, 0x60, 0xCC, 0x31, 0x8C, 0x73, 0x0C, 0xC1, 0x80, +/* 0xEB */ 0x0C, 0x00, 0x60, 0x01, 0x80, 0x0C, 0x00, 0x60, 0x01, 0x80, 0x1C, 0x00, 0xF0, 0x0D, 0x80, 0x6C, 0x06, 0x30, 0x31, 0x83, 0x8E, 0x18, 0x30, 0xC1, 0x8C, 0x06, 0x60, 0x30, +/* 0xEC */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF8, 0x7E, 0x1F, 0xFF, 0xDE, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x00, +/* 0xED */ 0xC0, 0x78, 0x0D, 0x83, 0x30, 0x66, 0x0C, 0x63, 0x0C, 0x60, 0xD8, 0x1B, 0x03, 0x60, 0x38, 0x07, 0x00, 0x40, +/* 0xEE */ 0x1F, 0x1F, 0x9C, 0x0C, 0x07, 0x01, 0xF8, 0x3C, 0x70, 0x70, 0x30, 0x30, 0x18, 0x0C, 0x06, 0x01, 0xC0, 0xFC, 0x1F, 0x00, 0xC0, 0x60, 0x70, 0xF0, 0x70, +/* 0xEF */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1D, 0xC7, 0x1F, 0xC1, 0xF0, +/* 0xF0 */ 0xFF, 0xFF, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0xF1 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x6C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x7C, 0x1F, 0xC7, 0x7F, 0xCD, 0xF1, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x00, +/* 0xF2 */ 0x07, 0xE3, 0xFC, 0xE0, 0x30, 0x06, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x1C, 0x01, 0xC0, 0x1F, 0x80, 0xF8, 0x03, 0x80, 0x30, 0x0E, 0x0F, 0x81, 0xE0, +/* 0xF3 */ 0x1F, 0xF9, 0xFF, 0xDC, 0x39, 0xC0, 0xCC, 0x03, 0x60, 0x1B, 0x00, 0xD8, 0x06, 0xC0, 0x37, 0x03, 0x9C, 0x38, 0x7F, 0x81, 0xF8, 0x00, +/* 0xF4 */ 0xFF, 0xF3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, +/* 0xF5 */ 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, +/* 0xF6 */ 0x19, 0xE0, 0xEF, 0xC6, 0x31, 0xB8, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xE3, 0x1D, 0x8C, 0x67, 0xB7, 0x0F, 0xF8, 0x0F, 0xC0, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 0xF7 */ 0x60, 0x33, 0x03, 0x8C, 0x18, 0x71, 0xC1, 0x8C, 0x0E, 0xC0, 0x36, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xDC, 0x0C, 0x60, 0xE3, 0x86, 0x0C, 0x70, 0x33, 0x01, 0x80, +/* 0xF8 */ 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x8C, 0x77, 0x33, 0x8F, 0xFC, 0x1F, 0xE0, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, +/* 0xF9 */ 0x30, 0x0C, 0x60, 0x06, 0x60, 0x06, 0xE1, 0x87, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xE1, 0x87, 0x63, 0xC6, 0x7E, 0x7E, 0x3C, 0x38, +/* 0xFA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xFB */ 0x33, 0x0C, 0xC0, 0x03, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xD8, 0x67, 0xF8, 0x78, +/* 0xFC */ 0x07, 0x00, 0xC0, 0x30, 0x00, 0x01, 0xF0, 0x7F, 0x1C, 0x77, 0x07, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xC1, 0xDC, 0x71, 0xFC, 0x1F, 0x00, +/* 0xFD */ 0x06, 0x03, 0x00, 0x80, 0x00, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x36, 0x19, 0xFE, 0x1E, 0x00, +/* 0xFE */ 0x00, 0xC0, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x30, 0x0C, 0x60, 0x06, 0x60, 0x06, 0xE1, 0x87, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xC1, 0x83, 0xE1, 0x87, 0x63, 0xC6, 0x7E, 0x7E, 0x3C, 0x38, +/* 0xFF */ +}; + +const GFXglyph FreeSans12pt_Win1253Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 19, 20, 21, 1, -17 }, +/* 0x02 */ { 48, 19, 20, 21, 1, -17 }, +/* 0x03 */ { 96, 21, 20, 23, 1, -17 }, +/* 0x04 */ { 149, 21, 20, 23, 1, -17 }, +/* 0x05 */ { 202, 20, 20, 22, 1, -17 }, +/* 0x06 */ { 252, 20, 20, 22, 1, -17 }, +/* 0x07 */ { 302, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 302, 23, 20, 25, 1, -17 }, +/* 0x09 */ { 360, 23, 16, 25, 1, -16 }, +/* 0x0A */ { 406, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 406, 21, 20, 23, 1, -17 }, +/* 0x0C */ { 459, 19, 18, 21, 1, -15 }, +/* 0x0D */ { 502, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 502, 20, 20, 22, 1, -17 }, +/* 0x0F */ { 552, 21, 21, 23, 1, -18 }, +/* 0x10 */ { 608, 19, 20, 21, 1, -17 }, +/* 0x11 */ { 656, 21, 20, 23, 1, -17 }, +/* 0x12 */ { 709, 20, 20, 22, 1, -17 }, +/* 0x13 */ { 759, 21, 20, 23, 1, -17 }, +/* 0x14 */ { 812, 21, 20, 23, 1, -17 }, +/* 0x15 */ { 865, 22, 20, 24, 1, -17 }, +/* 0x16 */ { 920, 16, 20, 18, 1, -17 }, +/* 0x17 */ { 960, 21, 20, 23, 1, -17 }, +/* 0x18 */ { 1013, 23, 20, 25, 1, -17 }, +/* 0x19 */ { 1071, 21, 20, 23, 1, -17 }, +/* 0x1A */ { 1124, 15, 19, 17, 1, -16 }, +/* 0x1B */ { 1160, 24, 21, 26, 1, -18 }, +/* 0x1C */ { 1223, 21, 20, 23, 1, -17 }, +/* 0x1D */ { 1276, 21, 21, 23, 1, -18 }, +/* 0x1E */ { 1332, 20, 20, 22, 1, -17 }, +/* 0x1F */ { 1382, 15, 20, 17, 1, -17 }, +/* ' ' 0x20 */ { 1420, 0, 0, 6, 0, 0 }, +/* '!' 0x21 */ { 1420, 2, 18, 8, 3, -16 }, +/* '"' 0x22 */ { 1425, 6, 6, 8, 1, -15 }, +/* '#' 0x23 */ { 1430, 13, 16, 13, 0, -14 }, +/* '$' 0x24 */ { 1456, 11, 20, 13, 1, -16 }, +/* '%' 0x25 */ { 1484, 20, 17, 21, 1, -15 }, +/* '&' 0x26 */ { 1527, 14, 17, 16, 1, -15 }, +/* ''' 0x27 */ { 1557, 2, 6, 5, 1, -15 }, +/* '(' 0x28 */ { 1559, 5, 23, 8, 2, -16 }, +/* ')' 0x29 */ { 1574, 5, 23, 8, 1, -16 }, +/* '*' 0x2A */ { 1589, 7, 7, 9, 1, -16 }, +/* '+' 0x2B */ { 1596, 10, 11, 14, 2, -9 }, +/* ',' 0x2C */ { 1610, 2, 6, 7, 2, 0 }, +/* '-' 0x2D */ { 1612, 6, 2, 8, 1, -6 }, +/* '.' 0x2E */ { 1614, 2, 2, 6, 2, 0 }, +/* '/' 0x2F */ { 1615, 7, 18, 7, 0, -16 }, +/* '0' 0x30 */ { 1631, 11, 17, 13, 1, -15 }, +/* '1' 0x31 */ { 1655, 5, 17, 13, 3, -15 }, +/* '2' 0x32 */ { 1666, 11, 17, 13, 1, -15 }, +/* '3' 0x33 */ { 1690, 11, 17, 13, 1, -15 }, +/* '4' 0x34 */ { 1714, 11, 17, 13, 1, -15 }, +/* '5' 0x35 */ { 1738, 11, 17, 13, 1, -15 }, +/* '6' 0x36 */ { 1762, 11, 17, 13, 1, -15 }, +/* '7' 0x37 */ { 1786, 11, 17, 13, 1, -15 }, +/* '8' 0x38 */ { 1810, 11, 17, 13, 1, -15 }, +/* '9' 0x39 */ { 1834, 11, 17, 13, 1, -15 }, +/* ':' 0x3A */ { 1858, 2, 13, 6, 2, -11 }, +/* ';' 0x3B */ { 1862, 2, 16, 6, 2, -10 }, +/* '<' 0x3C */ { 1866, 12, 11, 14, 1, -9 }, +/* '=' 0x3D */ { 1883, 12, 6, 14, 1, -7 }, +/* '>' 0x3E */ { 1892, 12, 11, 14, 1, -9 }, +/* '?' 0x3F */ { 1909, 10, 18, 13, 2, -16 }, +/* '@' 0x40 */ { 1932, 22, 21, 24, 1, -16 }, +/* 'A' 0x41 */ { 1990, 14, 18, 16, 1, -16 }, +/* 'B' 0x42 */ { 2022, 13, 18, 16, 2, -16 }, +/* 'C' 0x43 */ { 2052, 15, 18, 17, 1, -16 }, +/* 'D' 0x44 */ { 2086, 14, 18, 17, 2, -16 }, +/* 'E' 0x45 */ { 2118, 12, 18, 15, 2, -16 }, +/* 'F' 0x46 */ { 2145, 11, 18, 14, 2, -16 }, +/* 'G' 0x47 */ { 2170, 16, 18, 18, 1, -16 }, +/* 'H' 0x48 */ { 2206, 13, 18, 17, 2, -16 }, +/* 'I' 0x49 */ { 2236, 2, 18, 7, 2, -16 }, +/* 'J' 0x4A */ { 2241, 9, 18, 13, 1, -16 }, +/* 'K' 0x4B */ { 2262, 13, 18, 16, 2, -16 }, +/* 'L' 0x4C */ { 2292, 10, 18, 14, 2, -16 }, +/* 'M' 0x4D */ { 2315, 16, 18, 20, 2, -16 }, +/* 'N' 0x4E */ { 2351, 13, 18, 18, 2, -16 }, +/* 'O' 0x4F */ { 2381, 17, 18, 19, 1, -16 }, +/* 'P' 0x50 */ { 2420, 12, 18, 16, 2, -16 }, +/* 'Q' 0x51 */ { 2447, 17, 19, 19, 1, -16 }, +/* 'R' 0x52 */ { 2488, 14, 18, 17, 2, -16 }, +/* 'S' 0x53 */ { 2520, 14, 18, 16, 1, -16 }, +/* 'T' 0x54 */ { 2552, 12, 18, 15, 1, -16 }, +/* 'U' 0x55 */ { 2579, 13, 18, 17, 2, -16 }, +/* 'V' 0x56 */ { 2609, 14, 18, 15, 1, -16 }, +/* 'W' 0x57 */ { 2641, 22, 18, 22, 0, -16 }, +/* 'X' 0x58 */ { 2691, 14, 18, 16, 1, -16 }, +/* 'Y' 0x59 */ { 2723, 14, 18, 16, 1, -16 }, +/* 'Z' 0x5A */ { 2755, 13, 18, 15, 1, -16 }, +/* '[' 0x5B */ { 2785, 4, 23, 7, 2, -16 }, +/* '\' 0x5C */ { 2797, 7, 18, 7, 0, -16 }, +/* ']' 0x5D */ { 2813, 4, 23, 7, 1, -16 }, +/* '^' 0x5E */ { 2825, 9, 9, 11, 1, -15 }, +/* '_' 0x5F */ { 2836, 15, 1, 13, -1, 5 }, +/* '`' 0x60 */ { 2838, 5, 4, 6, 1, -16 }, +/* 'a' 0x61 */ { 2841, 12, 13, 13, 1, -11 }, +/* 'b' 0x62 */ { 2861, 12, 18, 13, 1, -16 }, +/* 'c' 0x63 */ { 2888, 10, 13, 12, 1, -11 }, +/* 'd' 0x64 */ { 2905, 11, 18, 13, 1, -16 }, +/* 'e' 0x65 */ { 2930, 11, 13, 13, 1, -11 }, +/* 'f' 0x66 */ { 2948, 5, 18, 7, 1, -16 }, +/* 'g' 0x67 */ { 2960, 11, 18, 13, 1, -11 }, +/* 'h' 0x68 */ { 2985, 10, 18, 13, 1, -16 }, +/* 'i' 0x69 */ { 3008, 2, 18, 5, 2, -16 }, +/* 'j' 0x6A */ { 3013, 4, 23, 6, 0, -16 }, +/* 'k' 0x6B */ { 3025, 10, 18, 12, 1, -16 }, +/* 'l' 0x6C */ { 3048, 2, 18, 5, 1, -16 }, +/* 'm' 0x6D */ { 3053, 17, 13, 19, 1, -11 }, +/* 'n' 0x6E */ { 3081, 10, 13, 13, 1, -11 }, +/* 'o' 0x6F */ { 3098, 11, 13, 13, 1, -11 }, +/* 'p' 0x70 */ { 3116, 12, 17, 13, 1, -11 }, +/* 'q' 0x71 */ { 3142, 11, 17, 13, 1, -11 }, +/* 'r' 0x72 */ { 3166, 6, 13, 8, 1, -11 }, +/* 's' 0x73 */ { 3176, 10, 13, 12, 1, -11 }, +/* 't' 0x74 */ { 3193, 5, 16, 7, 1, -14 }, +/* 'u' 0x75 */ { 3203, 10, 13, 13, 1, -11 }, +/* 'v' 0x76 */ { 3220, 11, 13, 12, 0, -11 }, +/* 'w' 0x77 */ { 3238, 17, 13, 17, 0, -11 }, +/* 'x' 0x78 */ { 3266, 10, 13, 11, 1, -11 }, +/* 'y' 0x79 */ { 3283, 11, 18, 11, 0, -11 }, +/* 'z' 0x7A */ { 3308, 10, 13, 12, 1, -11 }, +/* '{' 0x7B */ { 3325, 5, 23, 8, 1, -16 }, +/* '|' 0x7C */ { 3340, 2, 23, 6, 2, -16 }, +/* '}' 0x7D */ { 3346, 5, 23, 8, 2, -16 }, +/* '~' 0x7E */ { 3361, 10, 5, 12, 1, -9 }, +/* 0x7F */ { 3368, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 3368, 14, 17, 16, 1, -15 }, +/* 0x81 */ { 3398, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 3398, 2, 5, 6, 2, 0 }, +/* 0x83 */ { 3400, 6, 23, 7, 0, -16 }, +/* 0x84 */ { 3418, 6, 5, 10, 2, 0 }, +/* 0x85 */ { 3422, 12, 2, 16, 2, 0 }, +/* 0x86 */ { 3425, 10, 21, 13, 2, -15 }, +/* 0x87 */ { 3452, 10, 20, 13, 2, -15 }, +/* 0x88 */ { 3477, 7, 4, 8, 0, -16 }, +/* 0x89 */ { 3481, 23, 18, 24, 0, -16 }, +/* 0x8A */ { 3533, 14, 21, 16, 1, -19 }, +/* 0x8B */ { 3570, 3, 8, 6, 1, -9 }, +/* 0x8C */ { 3573, 22, 18, 24, 1, -16 }, +/* 0x8D */ { 3623, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 3623, 13, 21, 15, 1, -19 }, +/* 0x8F */ { 3658, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 3658, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 3658, 2, 6, 6, 2, -16 }, +/* 0x92 */ { 3660, 2, 6, 6, 2, -16 }, +/* 0x93 */ { 3662, 6, 6, 10, 2, -16 }, +/* 0x94 */ { 3667, 6, 6, 10, 2, -16 }, +/* 0x95 */ { 3672, 6, 6, 10, 2, -9 }, +/* 0x96 */ { 3677, 10, 2, 12, 1, -6 }, +/* 0x97 */ { 3680, 22, 2, 24, 1, -6 }, +/* 0x98 */ { 3686, 7, 3, 8, 0, -16 }, +/* 0x99 */ { 3689, 22, 13, 24, 2, -16 }, +/* 0x9A */ { 3725, 10, 18, 12, 1, -16 }, +/* 0x9B */ { 3748, 3, 8, 6, 2, -8 }, +/* 0x9C */ { 3751, 20, 13, 22, 1, -11 }, +/* 0x9D */ { 3784, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 3784, 10, 18, 12, 1, -16 }, +/* 0x9F */ { 3807, 14, 21, 16, 1, -19 }, +/* 0xA0 */ { 3844, 0, 0, 7, 0, 0 }, +/* 0xA1 */ { 3844, 2, 18, 8, 3, -11 }, +/* 0xA2 */ { 3849, 11, 17, 13, 1, -13 }, +/* 0xA3 */ { 3873, 12, 18, 13, 0, -16 }, +/* 0xA4 */ { 3900, 9, 9, 13, 2, -11 }, +/* 0xA5 */ { 3911, 12, 17, 13, 1, -15 }, +/* 0xA6 */ { 3937, 2, 23, 6, 2, -16 }, +/* 0xA7 */ { 3943, 11, 23, 13, 1, -16 }, +/* 0xA8 */ { 3975, 6, 2, 8, 1, -15 }, +/* 0xA9 */ { 3977, 18, 17, 19, 1, -15 }, +/* 0xAA */ { 4016, 7, 11, 9, 1, -16 }, +/* 0xAB */ { 4026, 8, 8, 12, 2, -9 }, +/* 0xAC */ { 4034, 12, 6, 14, 1, -7 }, +/* 0xAD */ { 4043, 6, 2, 8, 1, -6 }, +/* 0xAE */ { 4045, 18, 17, 19, 1, -15 }, +/* 0xAF */ { 4084, 6, 2, 8, 1, -15 }, +/* 0xB0 */ { 4086, 7, 8, 15, 4, -15 }, +/* 0xB1 */ { 4093, 12, 15, 14, 1, -13 }, +/* 0xB2 */ { 4116, 7, 10, 8, 1, -17 }, +/* 0xB3 */ { 4125, 7, 10, 8, 1, -17 }, +/* 0xB4 */ { 4134, 5, 4, 8, 2, -16 }, +/* 0xB5 */ { 4137, 12, 17, 13, 2, -11 }, +/* 0xB6 */ { 4163, 11, 21, 13, 2, -16 }, +/* 0xB7 */ { 4192, 2, 2, 6, 2, -6 }, +/* 0xB8 */ { 4193, 6, 5, 8, 1, 2 }, +/* 0xB9 */ { 4197, 3, 10, 8, 3, -18 }, +/* 0xBA */ { 4201, 6, 11, 9, 1, -16 }, +/* 0xBB */ { 4210, 8, 8, 12, 2, -8 }, +/* 0xBC */ { 4218, 17, 17, 21, 3, -15 }, +/* 0xBD */ { 4255, 18, 18, 21, 3, -16 }, +/* 0xBE */ { 4296, 19, 18, 21, 1, -16 }, +/* 0xBF */ { 4339, 9, 18, 13, 3, -11 }, +/* 0xC0 */ { 4360, 8, 18, 6, -1, -18 }, +/* 0xC1 */ { 4378, 15, 17, 15, 0, -17 }, +/* 0xC2 */ { 4410, 13, 17, 16, 2, -17 }, +/* 0xC3 */ { 4438, 11, 17, 13, 2, -17 }, +/* 0xC4 */ { 4462, 16, 17, 16, -1, -17 }, +/* 0xC5 */ { 4496, 13, 17, 16, 2, -17 }, +/* 0xC6 */ { 4524, 14, 17, 15, 0, -17 }, +/* 0xC7 */ { 4554, 13, 17, 17, 2, -17 }, +/* 0xC8 */ { 4582, 17, 17, 19, 1, -17 }, +/* 0xC9 */ { 4619, 2, 17, 6, 2, -17 }, +/* 0xCA */ { 4624, 14, 17, 16, 2, -17 }, +/* 0xCB */ { 4654, 17, 17, 16, -1, -17 }, +/* 0xCC */ { 4691, 15, 17, 19, 2, -17 }, +/* 0xCD */ { 4723, 13, 17, 17, 2, -17 }, +/* 0xCE */ { 4751, 14, 17, 16, 1, -17 }, +/* 0xCF */ { 4781, 17, 17, 19, 1, -17 }, +/* 0xD0 */ { 4818, 13, 17, 17, 2, -17 }, +/* 0xD1 */ { 4846, 13, 17, 16, 2, -17 }, +/* 0xD2 */ { 4874, 0, 0, 5, 0, 0 }, +/* 0xD3 */ { 4874, 12, 17, 15, 2, -17 }, +/* 0xD4 */ { 4900, 14, 17, 14, 0, -17 }, +/* 0xD5 */ { 4930, 16, 17, 16, 0, -17 }, +/* 0xD6 */ { 4964, 16, 17, 18, 1, -17 }, +/* 0xD7 */ { 4998, 15, 17, 15, 0, -17 }, +/* 0xD8 */ { 5030, 16, 17, 19, 2, -17 }, +/* 0xD9 */ { 5064, 16, 17, 18, 1, -17 }, +/* 0xDA */ { 5098, 6, 20, 6, 0, -20 }, +/* 0xDB */ { 5113, 16, 20, 16, 0, -20 }, +/* 0xDC */ { 5153, 12, 17, 14, 1, -17 }, +/* 0xDD */ { 5179, 9, 17, 11, 1, -17 }, +/* 0xDE */ { 5199, 10, 22, 14, 2, -17 }, +/* 0xDF */ { 5227, 4, 17, 6, 1, -17 }, +/* 0xE0 */ { 5236, 10, 17, 14, 2, -17 }, +/* 0xE1 */ { 5258, 12, 13, 14, 1, -13 }, +/* 0xE2 */ { 5278, 11, 22, 14, 2, -17 }, +/* 0xE3 */ { 5309, 12, 18, 11, -1, -13 }, +/* 0xE4 */ { 5336, 11, 17, 13, 1, -17 }, +/* 0xE5 */ { 5360, 9, 13, 11, 1, -13 }, +/* 0xE6 */ { 5375, 9, 22, 11, 1, -17 }, +/* 0xE7 */ { 5400, 10, 18, 14, 2, -13 }, +/* 0xE8 */ { 5423, 11, 17, 13, 1, -17 }, +/* 0xE9 */ { 5447, 2, 13, 6, 2, -13 }, +/* 0xEA */ { 5451, 10, 13, 12, 2, -13 }, +/* 0xEB */ { 5468, 13, 17, 12, -1, -17 }, +/* 0xEC */ { 5496, 10, 18, 14, 2, -13 }, +/* 0xED */ { 5519, 11, 13, 11, 0, -13 }, +/* 0xEE */ { 5537, 9, 22, 11, 1, -17 }, +/* 0xEF */ { 5562, 11, 13, 13, 1, -13 }, +/* 0xF0 */ { 5580, 16, 13, 17, 0, -13 }, +/* 0xF1 */ { 5606, 11, 18, 14, 2, -13 }, +/* 0xF2 */ { 5631, 11, 18, 12, 1, -13 }, +/* 0xF3 */ { 5656, 13, 13, 15, 1, -13 }, +/* 0xF4 */ { 5678, 6, 13, 9, 1, -13 }, +/* 0xF5 */ { 5688, 10, 13, 14, 2, -13 }, +/* 0xF6 */ { 5705, 14, 18, 16, 1, -13 }, +/* 0xF7 */ { 5737, 13, 18, 13, 0, -13 }, +/* 0xF8 */ { 5767, 14, 18, 18, 2, -13 }, +/* 0xF9 */ { 5799, 16, 13, 18, 1, -13 }, +/* 0xFA */ { 5825, 6, 16, 6, 0, -16 }, +/* 0xFB */ { 5837, 10, 16, 14, 2, -16 }, +/* 0xFC */ { 5857, 11, 17, 13, 1, -17 }, +/* 0xFD */ { 5881, 10, 17, 14, 2, -17 }, +/* 0xFE */ { 5903, 16, 17, 18, 1, -17 }, +/* 0xFF */ { 5937, 0, 0, 5, 0, 0 }, +}; + +const GFXfont FreeSans12pt_Win1253 PROGMEM = { +(uint8_t*)FreeSans12pt_Win1253Bitmaps, +(GFXglyph*)FreeSans12pt_Win1253Glyphs, +0x01, 0xFF, 19 +}; diff --git a/src/graphics/niche/Fonts/FreeSans6pt_Win1253.h b/src/graphics/niche/Fonts/FreeSans6pt_Win1253.h new file mode 100644 index 000000000..440d136fa --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans6pt_Win1253.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans6pt_Win1253 +*/ +const uint8_t FreeSans6pt_Win1253Bitmaps[] PROGMEM = { +/* 0x01 */ 0x1C, 0x0A, 0x05, 0x04, 0xFE, 0x08, 0x1C, 0x02, 0x07, 0xE0, 0x9F, 0xC0, +/* 0x02 */ 0x3F, 0xF0, 0x40, 0xE0, 0x10, 0x3F, 0x04, 0x9E, 0x28, 0x14, 0x0E, 0x00, +/* 0x03 */ 0x3F, 0x10, 0x28, 0x06, 0x49, 0x80, 0x60, 0x19, 0x26, 0x31, 0x40, 0x8F, 0xC0, +/* 0x04 */ 0x3F, 0x10, 0x2A, 0x16, 0x49, 0xA1, 0x60, 0x19, 0xE6, 0x31, 0x40, 0x8F, 0xC0, +/* 0x05 */ 0x28, 0x15, 0x2A, 0xB5, 0x55, 0xA8, 0x54, 0x12, 0x04, 0x41, 0x08, 0x81, 0xC0, +/* 0x06 */ 0x04, 0x08, 0x88, 0x82, 0x07, 0x01, 0x11, 0xA2, 0xC4, 0x40, 0x70, 0x20, 0x88, 0x88, 0x10, 0x00, +/* 0x07 */ +/* 0x08 */ 0x03, 0x83, 0x44, 0x48, 0x28, 0x01, 0x80, 0x17, 0xFE, 0x08, 0x45, 0x28, 0x84, 0x00, +/* 0x09 */ 0x01, 0xC0, 0x68, 0x82, 0x41, 0x10, 0x02, 0x80, 0x06, 0x00, 0x14, 0x00, 0x8F, 0xFC, +/* 0x0A */ +/* 0x0B */ 0x22, 0x2A, 0xA2, 0x30, 0x18, 0x0A, 0x09, 0x04, 0x44, 0x14, 0x04, 0x00, +/* 0x0C */ 0x46, 0x00, 0x19, 0x03, 0x21, 0x20, 0x93, 0x04, 0x20, 0x11, 0x80, 0x50, 0x02, 0x7F, 0xE0, +/* 0x0D */ +/* 0x0E */ 0x08, 0x0E, 0x08, 0x88, 0x24, 0x12, 0x09, 0x05, 0x01, 0xFF, 0x8A, 0x02, 0x00, +/* 0x0F */ 0x3F, 0x14, 0xAA, 0x16, 0x01, 0x92, 0x60, 0x18, 0xC6, 0x49, 0x40, 0x8F, 0xC0, +/* 0x10 */ 0x1B, 0x02, 0xA0, 0x54, 0x12, 0x42, 0x48, 0x49, 0x31, 0x1E, 0x23, 0xEA, 0xFE, 0x3C, +/* 0x11 */ 0x3F, 0x02, 0x00, 0x20, 0x6D, 0x27, 0xF8, 0x3F, 0xC1, 0xFE, 0x37, 0xD0, 0xBE, 0x40, 0xE1, 0xE2, 0x00, +/* 0x12 */ 0x12, 0x42, 0x20, 0x24, 0xC0, 0x29, 0x99, 0x05, 0x23, 0x30, 0xB0, 0x30, 0x00, +/* 0x13 */ 0x3F, 0x88, 0x0A, 0x44, 0xD5, 0x58, 0x03, 0x00, 0x67, 0xCC, 0x71, 0x40, 0x47, 0xF0, +/* 0x14 */ 0x3F, 0x18, 0x69, 0x26, 0x85, 0xA1, 0x6C, 0xD8, 0x06, 0x31, 0x40, 0x8F, 0xC0, +/* 0x15 */ 0x3F, 0x11, 0x00, 0xE8, 0x03, 0xA0, 0x1F, 0xB3, 0x7E, 0x00, 0xE9, 0xE0, 0x23, 0x00, 0x40, 0x40, 0xFE, 0x00, +/* 0x16 */ 0x30, 0x38, 0x3A, 0x3E, 0x6E, 0xEB, 0xC3, 0xC3, 0x66, 0x3C, +/* 0x17 */ 0x3F, 0x04, 0x00, 0x82, 0x88, 0x5C, 0xA4, 0x49, 0x22, 0x81, 0x98, 0xC4, 0x40, 0xA3, 0xF0, +/* 0x18 */ 0x07, 0x80, 0x42, 0x04, 0x08, 0x21, 0x41, 0x42, 0x60, 0x0E, 0x8C, 0xB2, 0x89, 0x50, 0x52, 0x82, 0x80, +/* 0x19 */ 0x3F, 0xC4, 0x02, 0x80, 0x18, 0x01, 0xB3, 0x1B, 0xB9, 0x80, 0x19, 0xE1, 0x40, 0x23, 0xFC, +/* 0x1A */ 0xFF, 0xC0, 0x67, 0x34, 0x58, 0x4C, 0x46, 0x03, 0x11, 0x80, 0xFF, 0xC0, +/* 0x1B */ 0x0F, 0xC0, 0x40, 0x82, 0x49, 0x08, 0x04, 0x00, 0x00, 0x12, 0x02, 0x31, 0x34, 0x0B, 0x88, 0x45, 0x00, 0x20, +/* 0x1C */ 0x3F, 0x88, 0x0A, 0x44, 0xC9, 0x19, 0x3B, 0x00, 0x60, 0x4C, 0x71, 0x40, 0x47, 0xF0, +/* 0x1D */ 0x3F, 0x8B, 0x0A, 0x00, 0xC8, 0x18, 0x13, 0x00, 0x48, 0xCA, 0xC1, 0x44, 0x53, 0x30, +/* 0x1E */ 0x19, 0xC2, 0x02, 0x50, 0x1E, 0x49, 0x80, 0x12, 0x01, 0x27, 0x92, 0x01, 0x10, 0x20, 0xFC, +/* 0x1F */ 0x30, 0x1C, 0x0C, 0x3E, 0x7E, 0xCF, 0x07, 0xC7, 0x7F, 0x3F, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFC, 0x80, +/* '"' 0x22 */ 0xB6, 0x80, +/* '#' 0x23 */ 0x24, 0x51, 0xF9, 0x42, 0x9F, 0x92, 0x28, +/* '$' 0x24 */ 0x10, 0xE5, 0x55, 0x50, 0xE1, 0x65, 0x55, 0xE1, 0x00, +/* '%' 0x25 */ 0x71, 0x24, 0x89, 0x22, 0x50, 0x74, 0x02, 0x70, 0xA4, 0x49, 0x11, 0xC0, +/* '&' 0x26 */ 0x71, 0x24, 0x9C, 0x62, 0x58, 0xA7, 0xF4, +/* ''' 0x27 */ 0xE0, +/* '(' 0x28 */ 0x5A, 0xAA, 0x94, +/* ')' 0x29 */ 0x89, 0x12, 0x49, 0x29, 0x00, +/* '*' 0x2A */ 0x5E, 0x80, +/* '+' 0x2B */ 0x21, 0x3E, 0x42, 0x00, +/* ',' 0x2C */ 0xE0, +/* '-' 0x2D */ 0xC0, +/* '.' 0x2E */ 0x80, +/* '/' 0x2F */ 0x24, 0xA4, 0xA4, 0x80, +/* '0' 0x30 */ 0x76, 0xE3, 0x18, 0xC6, 0x3B, 0x70, +/* '1' 0x31 */ 0x27, 0x92, 0x49, 0x20, +/* '2' 0x32 */ 0x79, 0x10, 0x41, 0x08, 0xC6, 0x10, 0xFC, +/* '3' 0x33 */ 0x79, 0x30, 0x43, 0x18, 0x10, 0x71, 0x78, +/* '4' 0x34 */ 0x08, 0x61, 0x8A, 0x49, 0x2F, 0xC2, 0x08, +/* '5' 0x35 */ 0xFC, 0x21, 0xE8, 0x84, 0x31, 0xF0, +/* '6' 0x36 */ 0x74, 0x61, 0xE8, 0xC6, 0x31, 0x70, +/* '7' 0x37 */ 0xF8, 0x44, 0x22, 0x11, 0x08, 0x40, +/* '8' 0x38 */ 0x39, 0x34, 0x53, 0x39, 0x1C, 0x51, 0x38, +/* '9' 0x39 */ 0x39, 0x3C, 0x71, 0x4C, 0xF0, 0x53, 0x78, +/* ':' 0x3A */ 0x82, +/* ';' 0x3B */ 0x87, +/* '<' 0x3C */ 0x3E, 0x30, 0x60, 0x80, +/* '=' 0x3D */ 0xF8, 0x3E, +/* '>' 0x3E */ 0xE0, 0xC6, 0xC8, 0x00, +/* '?' 0x3F */ 0x74, 0x42, 0x11, 0x10, 0x80, 0x20, +/* '@' 0x40 */ 0x0F, 0x86, 0x19, 0x9A, 0xA4, 0xD9, 0x13, 0x22, 0x56, 0xDA, 0x6E, 0x60, 0x06, 0x00, 0x3C, 0x00, +/* 'A' 0x41 */ 0x18, 0x18, 0x24, 0x24, 0x24, 0x7E, 0x42, 0x42, 0xC3, +/* 'B' 0x42 */ 0xFA, 0x18, 0x61, 0xFA, 0x18, 0x61, 0xFC, +/* 'C' 0x43 */ 0x3E, 0x63, 0x40, 0x40, 0xC0, 0x40, 0x41, 0x63, 0x3E, +/* 'D' 0x44 */ 0xF9, 0x0A, 0x1C, 0x18, 0x30, 0x61, 0xC2, 0xF8, +/* 'E' 0x45 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, +/* 'F' 0x46 */ 0xFE, 0x08, 0x20, 0xFA, 0x08, 0x20, 0x80, +/* 'G' 0x47 */ 0x1E, 0x61, 0x40, 0x40, 0xC7, 0x41, 0x41, 0x63, 0x1D, +/* 'H' 0x48 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, +/* 'I' 0x49 */ 0xFF, 0x80, +/* 'J' 0x4A */ 0x08, 0x42, 0x10, 0x87, 0x29, 0x70, +/* 'K' 0x4B */ 0x85, 0x12, 0x45, 0x0D, 0x13, 0x22, 0x42, 0x86, +/* 'L' 0x4C */ 0x84, 0x21, 0x08, 0x42, 0x10, 0xF8, +/* 'M' 0x4D */ 0xC3, 0xC3, 0xC3, 0xA5, 0xA5, 0xA5, 0x99, 0x99, 0x99, +/* 'N' 0x4E */ 0x83, 0x86, 0x8D, 0x19, 0x33, 0x62, 0xC3, 0x86, +/* 'O' 0x4F */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x06, 0xC6, 0x1E, 0x00, +/* 'P' 0x50 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, +/* 'Q' 0x51 */ 0x1E, 0x31, 0x90, 0x68, 0x1C, 0x0A, 0x05, 0x16, 0xC6, 0x1F, 0x00, 0x40, +/* 'R' 0x52 */ 0xFD, 0x0E, 0x1C, 0x2F, 0x90, 0xA1, 0x42, 0x86, +/* 'S' 0x53 */ 0x7A, 0x18, 0x30, 0x78, 0x38, 0x61, 0x78, +/* 'T' 0x54 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, +/* 'U' 0x55 */ 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xE2, 0x78, +/* 'V' 0x56 */ 0xC2, 0x85, 0x0B, 0x22, 0x44, 0x8E, 0x0C, 0x18, +/* 'W' 0x57 */ 0xC4, 0x28, 0xCD, 0x29, 0x25, 0x24, 0xA4, 0x52, 0x8C, 0x61, 0x8C, 0x31, 0x80, +/* 'X' 0x58 */ 0x87, 0x34, 0x8C, 0x30, 0xC4, 0xA3, 0x84, +/* 'Y' 0x59 */ 0xC3, 0x42, 0x24, 0x34, 0x18, 0x08, 0x08, 0x08, 0x08, +/* 'Z' 0x5A */ 0x7E, 0x0C, 0x30, 0x41, 0x06, 0x18, 0x20, 0xFE, +/* '[' 0x5B */ 0xEA, 0xAA, 0xAB, +/* '\' 0x5C */ 0x92, 0x24, 0x89, 0x20, +/* ']' 0x5D */ 0xD5, 0x55, 0x57, +/* '^' 0x5E */ 0x46, 0xA9, +/* '_' 0x5F */ 0xFE, +/* '`' 0x60 */ 0x80, +/* 'a' 0x61 */ 0x79, 0x20, 0x4F, 0xC6, 0x37, 0x40, +/* 'b' 0x62 */ 0x84, 0x3D, 0x18, 0xC6, 0x31, 0xF0, +/* 'c' 0x63 */ 0x39, 0x3C, 0x20, 0xC1, 0x33, 0x80, +/* 'd' 0x64 */ 0x04, 0x13, 0xD3, 0xC6, 0x1C, 0x53, 0x3C, +/* 'e' 0x65 */ 0x39, 0x38, 0x7F, 0x81, 0x13, 0x80, +/* 'f' 0x66 */ 0x6B, 0xA4, 0x92, 0x40, +/* 'g' 0x67 */ 0x35, 0x3C, 0x61, 0xC5, 0x33, 0x41, 0x4D, 0xE0, +/* 'h' 0x68 */ 0x84, 0x3D, 0x38, 0xC6, 0x31, 0x88, +/* 'i' 0x69 */ 0xBF, 0x80, +/* 'j' 0x6A */ 0x45, 0x55, 0x57, +/* 'k' 0x6B */ 0x84, 0x25, 0x4E, 0x52, 0xD2, 0x88, +/* 'l' 0x6C */ 0xFF, 0x80, +/* 'm' 0x6D */ 0xF7, 0x99, 0x91, 0x91, 0x91, 0x91, 0x91, +/* 'n' 0x6E */ 0xF4, 0x63, 0x18, 0xC6, 0x20, +/* 'o' 0x6F */ 0x39, 0x3C, 0x61, 0xC5, 0x33, 0x80, +/* 'p' 0x70 */ 0xF4, 0x63, 0x18, 0xC7, 0xD0, 0x80, +/* 'q' 0x71 */ 0x3D, 0x3C, 0x61, 0xC5, 0x37, 0x41, 0x04, +/* 'r' 0x72 */ 0xF2, 0x49, 0x20, +/* 's' 0x73 */ 0x7A, 0x50, 0xE0, 0xE5, 0xE0, +/* 't' 0x74 */ 0x5D, 0x24, 0x93, +/* 'u' 0x75 */ 0x8C, 0x63, 0x18, 0xCF, 0xA0, +/* 'v' 0x76 */ 0x85, 0x24, 0x92, 0x30, 0xC3, 0x00, +/* 'w' 0x77 */ 0x89, 0x59, 0x59, 0x55, 0x56, 0x26, 0x26, +/* 'x' 0x78 */ 0x4A, 0x4C, 0x43, 0x27, 0x20, +/* 'y' 0x79 */ 0x8A, 0x52, 0xA5, 0x18, 0x84, 0x22, 0x00, +/* 'z' 0x7A */ 0x78, 0x44, 0x46, 0x23, 0xE0, +/* '{' 0x7B */ 0x6A, 0xAA, 0xA9, +/* '|' 0x7C */ 0xFF, 0xE0, +/* '}' 0x7D */ 0x95, 0x55, 0x56, +/* '~' 0x7E */ 0x66, 0x60, +/* 0x7F */ +/* 0x80 */ 0x1C, 0x45, 0x07, 0xE4, 0x1F, 0x10, 0x10, 0x1E, +/* 0x81 */ +/* 0x82 */ 0xE0, +/* 0x83 */ 0x6B, 0xA4, 0x92, 0x49, 0x60, +/* 0x84 */ 0xB6, 0x80, +/* 0x85 */ 0xA8, +/* 0x86 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0x21, 0x08, +/* 0x87 */ 0x21, 0x09, 0xF2, 0x10, 0x84, 0xF9, 0x08, +/* 0x88 */ 0x54, +/* 0x89 */ 0x62, 0x09, 0x40, 0x98, 0x06, 0x80, 0x10, 0x01, 0x66, 0x29, 0x92, 0x99, 0x06, 0x60, +/* 0x8A */ 0x28, 0x47, 0xA1, 0x83, 0x07, 0x83, 0x87, 0x17, 0x80, +/* 0x8B */ 0x64, +/* 0x8C */ 0x3B, 0xE8, 0xC2, 0x08, 0x41, 0x08, 0x3F, 0x04, 0x20, 0x82, 0x30, 0x3B, 0xE0, +/* 0x8D */ +/* 0x8E */ 0x14, 0x11, 0xF8, 0x30, 0xC1, 0x04, 0x18, 0x61, 0xFC, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0xE0, +/* 0x92 */ 0xE0, +/* 0x93 */ 0xB6, 0x80, +/* 0x94 */ 0xB6, 0x80, +/* 0x95 */ 0xFF, 0x80, +/* 0x96 */ 0xFC, +/* 0x97 */ 0xFF, 0xF0, +/* 0x98 */ 0xDB, +/* 0x99 */ 0xE6, 0x28, 0xCD, 0x19, 0xA3, 0x34, 0x6A, 0x8B, 0x51, 0x68, +/* 0x9A */ 0x52, 0x69, 0x8E, 0x19, 0x60, +/* 0x9B */ 0x98, +/* 0x9C */ 0x7B, 0xD9, 0xCE, 0x10, 0xC3, 0xF8, 0x41, 0x9C, 0x5E, 0xF0, +/* 0x9D */ +/* 0x9E */ 0x51, 0x1E, 0x11, 0x11, 0x88, 0xF8, +/* 0x9F */ 0x29, 0x05, 0x12, 0x22, 0x87, 0x04, 0x08, 0x10, 0x20, +/* 0xA0 */ +/* 0xA1 */ 0xBF, 0x80, +/* 0xA2 */ 0x23, 0xAB, 0x4A, 0x52, 0xAE, 0x20, +/* 0xA3 */ 0x39, 0x14, 0x10, 0xF0, 0x82, 0x1C, 0x4C, +/* 0xA4 */ 0xFC, 0x63, 0xF0, +/* 0xA5 */ 0x8C, 0x54, 0xAF, 0x93, 0xE4, 0x20, +/* 0xA6 */ 0xF9, 0xF0, +/* 0xA7 */ 0x32, 0x91, 0xC9, 0x47, 0x26, 0x14, 0xA4, 0xC0, +/* 0xA8 */ 0xA0, +/* 0xA9 */ 0x3E, 0x3F, 0xB8, 0xF4, 0x1A, 0x0D, 0x17, 0x76, 0xC6, 0x3E, 0x00, +/* 0xAA */ 0x61, 0x79, 0x60, +/* 0xAB */ 0x5A, 0xA5, +/* 0xAC */ 0xFC, 0x10, 0x40, +/* 0xAD */ +/* 0xAE */ 0x3E, 0x31, 0xB7, 0x72, 0x99, 0xCC, 0xC7, 0x56, 0xC6, 0x3E, 0x00, +/* 0xAF */ 0xE0, +/* 0xB0 */ 0x69, 0x96, +/* 0xB1 */ 0x21, 0x3E, 0x42, 0x03, 0xE0, +/* 0xB2 */ 0x69, 0x3C, 0xF0, +/* 0xB3 */ 0x79, 0x29, 0x70, +/* 0xB4 */ 0x80, +/* 0xB5 */ 0x8A, 0x28, 0xA2, 0x8A, 0x6E, 0xE0, 0x80, +/* 0xB6 */ 0x7F, 0xAE, 0xBA, 0x68, 0xA2, 0x8A, 0x28, 0xA0, +/* 0xB7 */ 0x80, +/* 0xB8 */ 0x67, 0x80, +/* 0xB9 */ 0x75, 0x50, +/* 0xBA */ 0x69, 0x96, 0xF0, +/* 0xBB */ 0xA5, 0x5A, +/* 0xBC */ 0x42, 0x30, 0x84, 0x41, 0x10, 0x48, 0x82, 0x61, 0x28, 0x8F, 0x20, 0x80, +/* 0xBD */ 0x40, 0x63, 0x11, 0x09, 0x74, 0xA8, 0x84, 0x44, 0x44, 0x43, 0x80, +/* 0xBE */ 0x71, 0x24, 0x82, 0x20, 0x50, 0x98, 0x9A, 0x61, 0x28, 0x4F, 0x20, 0x80, +/* 0xBF */ 0x20, 0x08, 0x44, 0x42, 0x11, 0x70, +/* 0xC0 */ 0x2D, 0x02, 0x22, 0x22, 0x22, +/* 0xC1 */ 0x10, 0x50, 0xA1, 0x44, 0x4F, 0x91, 0x41, 0x82, +/* 0xC2 */ 0xFA, 0x18, 0x61, 0xFE, 0x18, 0x61, 0xF8, +/* 0xC3 */ 0xFE, 0x08, 0x20, 0x82, 0x08, 0x20, 0x80, +/* 0xC4 */ 0x08, 0x0A, 0x05, 0x02, 0x82, 0x21, 0x11, 0x04, 0x82, 0x7F, 0x00, +/* 0xC5 */ 0xFE, 0x08, 0x20, 0xFE, 0x08, 0x20, 0xFC, +/* 0xC6 */ 0x7E, 0x08, 0x20, 0x41, 0x04, 0x08, 0x20, 0xFE, +/* 0xC7 */ 0x83, 0x06, 0x0C, 0x1F, 0xF0, 0x60, 0xC1, 0x82, +/* 0xC8 */ 0x38, 0x8A, 0x0C, 0x1B, 0xB0, 0x60, 0xA2, 0x38, +/* 0xC9 */ 0xFF, 0x80, +/* 0xCA */ 0x83, 0x0A, 0x24, 0x8A, 0x1A, 0x22, 0x42, 0x82, +/* 0xCB */ 0x08, 0x0A, 0x05, 0x02, 0x82, 0x21, 0x11, 0x04, 0x82, 0x41, 0x00, +/* 0xCC */ 0x83, 0x8F, 0x1D, 0x5A, 0xB5, 0x6A, 0xC9, 0x92, +/* 0xCD */ 0x83, 0x86, 0x8D, 0x19, 0x31, 0x62, 0xC3, 0x82, +/* 0xCE */ 0xFC, 0x00, 0x00, 0x78, 0x00, 0x00, 0xFC, +/* 0xCF */ 0x38, 0x8A, 0x0C, 0x18, 0x30, 0x60, 0xA2, 0x38, +/* 0xD0 */ 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x82, +/* 0xD1 */ 0xFA, 0x18, 0x61, 0xFA, 0x08, 0x20, 0x80, +/* 0xD2 */ +/* 0xD3 */ 0xFE, 0x04, 0x08, 0x10, 0x84, 0x20, 0xFC, +/* 0xD4 */ 0xFE, 0x20, 0x40, 0x81, 0x02, 0x04, 0x08, 0x10, +/* 0xD5 */ 0x82, 0x89, 0x11, 0x41, 0x02, 0x04, 0x08, 0x10, +/* 0xD6 */ 0x10, 0xFA, 0x4C, 0x99, 0x32, 0x64, 0xBE, 0x10, +/* 0xD7 */ 0x82, 0x89, 0x11, 0x41, 0x05, 0x11, 0x22, 0x82, +/* 0xD8 */ 0x93, 0x26, 0x4C, 0x99, 0x2F, 0x84, 0x08, 0x10, +/* 0xD9 */ 0x38, 0x8A, 0x0C, 0x18, 0x30, 0x60, 0xA2, 0xEE, +/* 0xDA */ 0xA1, 0x24, 0x92, 0x49, 0x00, +/* 0xDB */ 0x28, 0x02, 0x0A, 0x24, 0x45, 0x04, 0x08, 0x10, 0x20, 0x40, +/* 0xDC */ 0x11, 0x00, 0xD9, 0x4A, 0x52, 0x93, 0x40, +/* 0xDD */ 0x11, 0x00, 0xF8, 0x41, 0x90, 0x83, 0xC0, +/* 0xDE */ 0x11, 0x01, 0x6C, 0xC6, 0x31, 0x8C, 0x42, 0x10, +/* 0xDF */ 0x62, 0xAA, 0xA0, +/* 0xE0 */ 0x25, 0x81, 0x18, 0xC6, 0x31, 0x8B, 0x80, +/* 0xE1 */ 0x6C, 0xA5, 0x29, 0x49, 0xA0, +/* 0xE2 */ 0x74, 0x63, 0x1B, 0x46, 0x39, 0xB4, 0x20, +/* 0xE3 */ 0x44, 0x89, 0x11, 0x42, 0x85, 0x04, 0x08, 0x10, +/* 0xE4 */ 0x71, 0x1D, 0x18, 0xC6, 0x31, 0x70, +/* 0xE5 */ 0x7C, 0x20, 0xC8, 0x41, 0xE0, +/* 0xE6 */ 0x72, 0x44, 0x88, 0x88, 0x71, 0x20, +/* 0xE7 */ 0xB6, 0x63, 0x18, 0xC6, 0x21, 0x08, +/* 0xE8 */ 0x74, 0x63, 0x1F, 0xC6, 0x31, 0x70, +/* 0xE9 */ 0xFE, +/* 0xEA */ 0x8A, 0x4A, 0x38, 0x92, 0x48, 0x80, +/* 0xEB */ 0x20, 0x41, 0x04, 0x28, 0xA2, 0x91, 0x44, +/* 0xEC */ 0x8C, 0x63, 0x18, 0xC7, 0xF0, 0x80, +/* 0xED */ 0x8C, 0x54, 0xA5, 0x10, 0x80, +/* 0xEE */ 0x68, 0x86, 0x48, 0x88, 0x71, 0x20, +/* 0xEF */ 0x74, 0x63, 0x18, 0xC5, 0xC0, +/* 0xF0 */ 0xFF, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, +/* 0xF1 */ 0x74, 0x63, 0x18, 0xC7, 0xD0, 0x80, +/* 0xF2 */ 0x34, 0x88, 0x88, 0x71, 0x60, +/* 0xF3 */ 0x7F, 0x12, 0x24, 0x48, 0x91, 0x1C, 0x00, +/* 0xF4 */ 0xE9, 0x24, 0x90, +/* 0xF5 */ 0x8C, 0x63, 0x18, 0xC5, 0xC0, +/* 0xF6 */ 0x5A, 0x59, 0x65, 0x95, 0x53, 0x84, 0x10, +/* 0xF7 */ 0x49, 0x24, 0x8C, 0x30, 0xC4, 0x92, 0x48, +/* 0xF8 */ 0x93, 0x26, 0x4C, 0x99, 0x32, 0x5F, 0x08, 0x10, +/* 0xF9 */ 0x45, 0x06, 0x4C, 0x99, 0x32, 0x5B, 0x00, +/* 0xFA */ 0xA1, 0x24, 0x92, 0x40, +/* 0xFB */ 0x50, 0x23, 0x18, 0xC6, 0x31, 0x70, +/* 0xFC */ 0x11, 0x00, 0xE8, 0xC6, 0x31, 0x8B, 0x80, +/* 0xFD */ 0x21, 0x01, 0x18, 0xC6, 0x31, 0x8B, 0x80, +/* 0xFE */ 0x08, 0x20, 0x02, 0x28, 0x32, 0x64, 0xC9, 0x92, 0xD8, +/* 0xFF */ +}; + +const GFXglyph FreeSans6pt_Win1253Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 9, 10, 11, 1, -9 }, +/* 0x02 */ { 12, 9, 10, 11, 1, -8 }, +/* 0x03 */ { 24, 10, 10, 12, 1, -8 }, +/* 0x04 */ { 37, 10, 10, 12, 1, -8 }, +/* 0x05 */ { 50, 10, 10, 12, 1, -9 }, +/* 0x06 */ { 63, 11, 11, 13, 1, -9 }, +/* 0x07 */ { 79, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 79, 12, 9, 14, 1, -8 }, +/* 0x09 */ { 93, 14, 8, 16, 1, -7 }, +/* 0x0A */ { 107, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 107, 9, 10, 11, 1, -9 }, +/* 0x0C */ { 119, 13, 9, 15, 1, -8 }, +/* 0x0D */ { 134, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 134, 9, 11, 11, 1, -9 }, +/* 0x0F */ { 147, 10, 10, 12, 1, -9 }, +/* 0x10 */ { 160, 11, 10, 13, 1, -9 }, +/* 0x11 */ { 174, 13, 10, 15, 1, -9 }, +/* 0x12 */ { 191, 10, 10, 12, 1, -9 }, +/* 0x13 */ { 204, 11, 10, 13, 1, -9 }, +/* 0x14 */ { 218, 10, 10, 12, 1, -9 }, +/* 0x15 */ { 231, 14, 10, 16, 1, -9 }, +/* 0x16 */ { 249, 8, 10, 10, 1, -9 }, +/* 0x17 */ { 259, 12, 10, 14, 1, -9 }, +/* 0x18 */ { 274, 13, 10, 15, 1, -9 }, +/* 0x19 */ { 291, 12, 10, 14, 1, -9 }, +/* 0x1A */ { 306, 9, 10, 11, 1, -8 }, +/* 0x1B */ { 318, 14, 10, 16, 1, -9 }, +/* 0x1C */ { 336, 11, 10, 13, 1, -9 }, +/* 0x1D */ { 350, 11, 10, 13, 1, -9 }, +/* 0x1E */ { 364, 12, 10, 14, 1, -9 }, +/* 0x1F */ { 379, 8, 10, 11, 2, -9 }, +/* ' ' 0x20 */ { 389, 0, 0, 3, 0, 0 }, +/* '!' 0x21 */ { 389, 1, 9, 4, 2, -8 }, +/* '"' 0x22 */ { 391, 3, 3, 4, 0, -8 }, +/* '#' 0x23 */ { 393, 7, 8, 7, 0, -7 }, +/* '$' 0x24 */ { 400, 6, 11, 7, 0, -9 }, +/* '%' 0x25 */ { 409, 10, 9, 11, 0, -8 }, +/* '&' 0x26 */ { 421, 6, 9, 8, 1, -8 }, +/* ''' 0x27 */ { 428, 1, 3, 2, 1, -8 }, +/* '(' 0x28 */ { 429, 2, 11, 4, 1, -8 }, +/* ')' 0x29 */ { 432, 3, 11, 4, 0, -8 }, +/* '*' 0x2A */ { 437, 3, 3, 5, 1, -8 }, +/* '+' 0x2B */ { 439, 5, 5, 7, 1, -4 }, +/* ',' 0x2C */ { 443, 1, 3, 3, 1, 0 }, +/* '-' 0x2D */ { 444, 2, 1, 4, 1, -3 }, +/* '.' 0x2E */ { 445, 1, 1, 3, 1, 0 }, +/* '/' 0x2F */ { 446, 3, 9, 3, 0, -8 }, +/* '0' 0x30 */ { 450, 5, 9, 7, 1, -8 }, +/* '1' 0x31 */ { 456, 3, 9, 7, 1, -8 }, +/* '2' 0x32 */ { 460, 6, 9, 7, 0, -8 }, +/* '3' 0x33 */ { 467, 6, 9, 7, 0, -8 }, +/* '4' 0x34 */ { 474, 6, 9, 7, 0, -8 }, +/* '5' 0x35 */ { 481, 5, 9, 7, 1, -8 }, +/* '6' 0x36 */ { 487, 5, 9, 7, 1, -8 }, +/* '7' 0x37 */ { 493, 5, 9, 7, 1, -8 }, +/* '8' 0x38 */ { 499, 6, 9, 7, 0, -8 }, +/* '9' 0x39 */ { 506, 6, 9, 7, 0, -8 }, +/* ':' 0x3A */ { 513, 1, 7, 3, 1, -6 }, +/* ';' 0x3B */ { 514, 1, 8, 3, 1, -5 }, +/* '<' 0x3C */ { 515, 5, 5, 7, 1, -4 }, +/* '=' 0x3D */ { 519, 5, 3, 7, 1, -3 }, +/* '>' 0x3E */ { 521, 5, 5, 7, 1, -4 }, +/* '?' 0x3F */ { 525, 5, 9, 7, 1, -8 }, +/* '@' 0x40 */ { 531, 11, 11, 12, 0, -8 }, +/* 'A' 0x41 */ { 547, 8, 9, 8, 0, -8 }, +/* 'B' 0x42 */ { 556, 6, 9, 8, 1, -8 }, +/* 'C' 0x43 */ { 563, 8, 9, 9, 0, -8 }, +/* 'D' 0x44 */ { 572, 7, 9, 8, 1, -8 }, +/* 'E' 0x45 */ { 580, 6, 9, 8, 1, -8 }, +/* 'F' 0x46 */ { 587, 6, 9, 7, 1, -8 }, +/* 'G' 0x47 */ { 594, 8, 9, 9, 0, -8 }, +/* 'H' 0x48 */ { 603, 7, 9, 9, 1, -8 }, +/* 'I' 0x49 */ { 611, 1, 9, 3, 1, -8 }, +/* 'J' 0x4A */ { 613, 5, 9, 6, 0, -8 }, +/* 'K' 0x4B */ { 619, 7, 9, 8, 1, -8 }, +/* 'L' 0x4C */ { 627, 5, 9, 7, 1, -8 }, +/* 'M' 0x4D */ { 633, 8, 9, 10, 1, -8 }, +/* 'N' 0x4E */ { 642, 7, 9, 9, 1, -8 }, +/* 'O' 0x4F */ { 650, 9, 9, 9, 0, -8 }, +/* 'P' 0x50 */ { 661, 6, 9, 8, 1, -8 }, +/* 'Q' 0x51 */ { 668, 9, 10, 9, 0, -8 }, +/* 'R' 0x52 */ { 680, 7, 9, 9, 1, -8 }, +/* 'S' 0x53 */ { 688, 6, 9, 8, 1, -8 }, +/* 'T' 0x54 */ { 695, 7, 9, 8, 0, -8 }, +/* 'U' 0x55 */ { 703, 7, 9, 9, 1, -8 }, +/* 'V' 0x56 */ { 711, 7, 9, 8, 0, -8 }, +/* 'W' 0x57 */ { 719, 11, 9, 11, 0, -8 }, +/* 'X' 0x58 */ { 732, 6, 9, 8, 1, -8 }, +/* 'Y' 0x59 */ { 739, 8, 9, 8, 0, -8 }, +/* 'Z' 0x5A */ { 748, 7, 9, 7, 0, -8 }, +/* '[' 0x5B */ { 756, 2, 12, 3, 1, -8 }, +/* '\' 0x5C */ { 759, 3, 9, 3, 0, -8 }, +/* ']' 0x5D */ { 763, 2, 12, 3, 0, -8 }, +/* '^' 0x5E */ { 766, 4, 4, 6, 1, -8 }, +/* '_' 0x5F */ { 768, 7, 1, 7, 0, 2 }, +/* '`' 0x60 */ { 769, 1, 1, 3, 1, -8 }, +/* 'a' 0x61 */ { 770, 6, 7, 7, 0, -6 }, +/* 'b' 0x62 */ { 776, 5, 9, 7, 1, -8 }, +/* 'c' 0x63 */ { 782, 6, 7, 6, 0, -6 }, +/* 'd' 0x64 */ { 788, 6, 9, 7, 0, -8 }, +/* 'e' 0x65 */ { 795, 6, 7, 6, 0, -6 }, +/* 'f' 0x66 */ { 801, 3, 9, 3, 0, -8 }, +/* 'g' 0x67 */ { 805, 6, 10, 7, 0, -6 }, +/* 'h' 0x68 */ { 813, 5, 9, 6, 1, -8 }, +/* 'i' 0x69 */ { 819, 1, 9, 3, 1, -8 }, +/* 'j' 0x6A */ { 821, 2, 12, 3, 0, -8 }, +/* 'k' 0x6B */ { 824, 5, 9, 6, 1, -8 }, +/* 'l' 0x6C */ { 830, 1, 9, 3, 1, -8 }, +/* 'm' 0x6D */ { 832, 8, 7, 10, 1, -6 }, +/* 'n' 0x6E */ { 839, 5, 7, 6, 1, -6 }, +/* 'o' 0x6F */ { 844, 6, 7, 6, 0, -6 }, +/* 'p' 0x70 */ { 850, 5, 9, 7, 1, -6 }, +/* 'q' 0x71 */ { 856, 6, 9, 7, 0, -6 }, +/* 'r' 0x72 */ { 863, 3, 7, 4, 1, -6 }, +/* 's' 0x73 */ { 866, 5, 7, 6, 0, -6 }, +/* 't' 0x74 */ { 871, 3, 8, 3, 0, -7 }, +/* 'u' 0x75 */ { 874, 5, 7, 6, 1, -6 }, +/* 'v' 0x76 */ { 879, 6, 7, 6, 0, -6 }, +/* 'w' 0x77 */ { 885, 8, 7, 9, 0, -6 }, +/* 'x' 0x78 */ { 892, 5, 7, 6, 0, -6 }, +/* 'y' 0x79 */ { 897, 5, 10, 6, 0, -6 }, +/* 'z' 0x7A */ { 904, 5, 7, 6, 0, -6 }, +/* '{' 0x7B */ { 909, 2, 12, 4, 1, -8 }, +/* '|' 0x7C */ { 912, 1, 11, 3, 1, -8 }, +/* '}' 0x7D */ { 914, 2, 12, 4, 1, -8 }, +/* '~' 0x7E */ { 917, 6, 2, 6, 0, -4 }, +/* 0x7F */ { 919, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 919, 7, 9, 8, 0, -8 }, +/* 0x81 */ { 927, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 927, 1, 3, 3, 1, 0 }, +/* 0x83 */ { 928, 3, 12, 3, 0, -8 }, +/* 0x84 */ { 933, 3, 3, 5, 1, 0 }, +/* 0x85 */ { 935, 5, 1, 7, 1, 0 }, +/* 0x86 */ { 936, 5, 11, 7, 1, -8 }, +/* 0x87 */ { 943, 5, 11, 7, 1, -8 }, +/* 0x88 */ { 950, 3, 2, 4, 0, -9 }, +/* 0x89 */ { 951, 12, 9, 12, 0, -8 }, +/* 0x8A */ { 965, 6, 11, 8, 1, -9 }, +/* 0x8B */ { 974, 2, 3, 4, 1, -4 }, +/* 0x8C */ { 975, 11, 9, 12, 0, -8 }, +/* 0x8D */ { 988, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 988, 7, 10, 7, 0, -9 }, +/* 0x8F */ { 997, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 997, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 997, 1, 3, 3, 1, -8 }, +/* 0x92 */ { 998, 1, 3, 2, 1, -8 }, +/* 0x93 */ { 999, 3, 3, 5, 1, -8 }, +/* 0x94 */ { 1001, 3, 3, 5, 1, -8 }, +/* 0x95 */ { 1003, 3, 3, 5, 1, -5 }, +/* 0x96 */ { 1005, 6, 1, 6, 0, -3 }, +/* 0x97 */ { 1006, 12, 1, 12, 0, -3 }, +/* 0x98 */ { 1008, 4, 2, 4, 0, -8 }, +/* 0x99 */ { 1009, 11, 7, 12, 1, -8 }, +/* 0x9A */ { 1019, 4, 9, 6, 1, -8 }, +/* 0x9B */ { 1024, 2, 3, 3, 1, -4 }, +/* 0x9C */ { 1025, 11, 7, 11, 0, -6 }, +/* 0x9D */ { 1035, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 1035, 5, 9, 6, 0, -8 }, +/* 0x9F */ { 1041, 7, 10, 8, 1, -9 }, +/* 0xA0 */ { 1050, 0, 0, 3, 0, 0 }, +/* 0xA1 */ { 1050, 1, 9, 4, 1, -5 }, +/* 0xA2 */ { 1052, 5, 9, 7, 1, -7 }, +/* 0xA3 */ { 1058, 6, 9, 7, 0, -8 }, +/* 0xA4 */ { 1065, 5, 4, 7, 1, -5 }, +/* 0xA5 */ { 1068, 5, 9, 7, 1, -8 }, +/* 0xA6 */ { 1074, 1, 12, 3, 1, -8 }, +/* 0xA7 */ { 1076, 5, 12, 7, 1, -8 }, +/* 0xA8 */ { 1084, 3, 1, 4, 0, -7 }, +/* 0xA9 */ { 1085, 9, 9, 10, 0, -8 }, +/* 0xAA */ { 1096, 4, 5, 4, 0, -8 }, +/* 0xAB */ { 1099, 4, 4, 6, 1, -4 }, +/* 0xAC */ { 1101, 6, 3, 7, 1, -4 }, +/* 0xAD */ { 1104, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 1104, 9, 9, 10, 0, -8 }, +/* 0xAF */ { 1115, 3, 1, 4, 0, -8 }, +/* 0xB0 */ { 1116, 4, 4, 7, 2, -8 }, +/* 0xB1 */ { 1118, 5, 7, 7, 1, -6 }, +/* 0xB2 */ { 1123, 4, 5, 4, 0, -9 }, +/* 0xB3 */ { 1126, 4, 5, 4, 0, -9 }, +/* 0xB4 */ { 1129, 1, 1, 4, 1, -8 }, +/* 0xB5 */ { 1130, 6, 9, 7, 1, -6 }, +/* 0xB6 */ { 1137, 6, 10, 6, 1, -8 }, +/* 0xB7 */ { 1145, 1, 1, 3, 1, -2 }, +/* 0xB8 */ { 1146, 3, 3, 4, 1, 1 }, +/* 0xB9 */ { 1148, 2, 6, 4, 1, -9 }, +/* 0xBA */ { 1150, 4, 5, 4, 0, -8 }, +/* 0xBB */ { 1153, 4, 4, 6, 1, -5 }, +/* 0xBC */ { 1155, 10, 9, 10, 1, -8 }, +/* 0xBD */ { 1167, 9, 9, 10, 1, -8 }, +/* 0xBE */ { 1178, 10, 9, 11, 0, -8 }, +/* 0xBF */ { 1190, 5, 9, 7, 1, -5 }, +/* 0xC0 */ { 1196, 4, 10, 3, -1, -10 }, +/* 0xC1 */ { 1201, 7, 9, 7, 0, -9 }, +/* 0xC2 */ { 1209, 6, 9, 8, 1, -9 }, +/* 0xC3 */ { 1216, 6, 9, 7, 1, -9 }, +/* 0xC4 */ { 1223, 9, 9, 7, -1, -9 }, +/* 0xC5 */ { 1234, 6, 9, 8, 1, -9 }, +/* 0xC6 */ { 1241, 7, 9, 7, 0, -9 }, +/* 0xC7 */ { 1249, 7, 9, 9, 1, -9 }, +/* 0xC8 */ { 1257, 7, 9, 9, 1, -9 }, +/* 0xC9 */ { 1265, 1, 9, 3, 1, -9 }, +/* 0xCA */ { 1267, 7, 9, 8, 1, -9 }, +/* 0xCB */ { 1275, 9, 9, 7, -1, -9 }, +/* 0xCC */ { 1286, 7, 9, 9, 1, -9 }, +/* 0xCD */ { 1294, 7, 9, 9, 1, -9 }, +/* 0xCE */ { 1302, 6, 9, 8, 1, -9 }, +/* 0xCF */ { 1309, 7, 9, 9, 1, -9 }, +/* 0xD0 */ { 1317, 7, 9, 9, 1, -9 }, +/* 0xD1 */ { 1325, 6, 9, 8, 1, -9 }, +/* 0xD2 */ { 1332, 0, 0, 5, 0, 0 }, +/* 0xD3 */ { 1332, 6, 9, 7, 1, -9 }, +/* 0xD4 */ { 1339, 7, 9, 7, 0, -9 }, +/* 0xD5 */ { 1347, 7, 9, 7, 0, -9 }, +/* 0xD6 */ { 1355, 7, 9, 9, 1, -9 }, +/* 0xD7 */ { 1363, 7, 9, 7, 0, -9 }, +/* 0xD8 */ { 1371, 7, 9, 9, 1, -9 }, +/* 0xD9 */ { 1379, 7, 9, 9, 1, -9 }, +/* 0xDA */ { 1387, 3, 11, 3, 0, -11 }, +/* 0xDB */ { 1392, 7, 11, 7, 0, -11 }, +/* 0xDC */ { 1402, 5, 10, 7, 1, -10 }, +/* 0xDD */ { 1409, 5, 10, 5, 0, -10 }, +/* 0xDE */ { 1416, 5, 12, 7, 1, -10 }, +/* 0xDF */ { 1424, 2, 10, 3, 1, -10 }, +/* 0xE0 */ { 1427, 5, 10, 7, 1, -10 }, +/* 0xE1 */ { 1434, 5, 7, 7, 1, -7 }, +/* 0xE2 */ { 1439, 5, 11, 7, 1, -9 }, +/* 0xE3 */ { 1446, 7, 9, 5, -1, -7 }, +/* 0xE4 */ { 1454, 5, 9, 7, 1, -9 }, +/* 0xE5 */ { 1460, 5, 7, 5, 0, -7 }, +/* 0xE6 */ { 1465, 4, 11, 5, 1, -9 }, +/* 0xE7 */ { 1471, 5, 9, 7, 1, -7 }, +/* 0xE8 */ { 1477, 5, 9, 7, 1, -9 }, +/* 0xE9 */ { 1483, 1, 7, 3, 1, -7 }, +/* 0xEA */ { 1484, 6, 7, 7, 1, -7 }, +/* 0xEB */ { 1490, 6, 9, 5, -1, -9 }, +/* 0xEC */ { 1497, 5, 9, 7, 1, -7 }, +/* 0xED */ { 1503, 5, 7, 5, 0, -7 }, +/* 0xEE */ { 1508, 4, 11, 5, 1, -9 }, +/* 0xEF */ { 1514, 5, 7, 7, 1, -7 }, +/* 0xF0 */ { 1519, 8, 7, 8, 0, -7 }, +/* 0xF1 */ { 1526, 5, 9, 7, 1, -7 }, +/* 0xF2 */ { 1532, 4, 9, 6, 1, -7 }, +/* 0xF3 */ { 1537, 7, 7, 7, 1, -7 }, +/* 0xF4 */ { 1544, 3, 7, 5, 1, -7 }, +/* 0xF5 */ { 1547, 5, 7, 7, 1, -7 }, +/* 0xF6 */ { 1552, 6, 9, 8, 1, -7 }, +/* 0xF7 */ { 1559, 6, 9, 6, 0, -7 }, +/* 0xF8 */ { 1566, 7, 9, 9, 1, -7 }, +/* 0xF9 */ { 1574, 7, 7, 9, 1, -7 }, +/* 0xFA */ { 1581, 3, 9, 3, 0, -9 }, +/* 0xFB */ { 1585, 5, 9, 7, 1, -9 }, +/* 0xFC */ { 1591, 5, 10, 7, 1, -10 }, +/* 0xFD */ { 1598, 5, 10, 7, 1, -10 }, +/* 0xFE */ { 1605, 7, 10, 9, 1, -10 }, +/* 0xFF */ { 1614, 0, 0, 5, 0, 0 }, +}; + +const GFXfont FreeSans6pt_Win1253 PROGMEM = { +(uint8_t*)FreeSans6pt_Win1253Bitmaps, +(GFXglyph*)FreeSans6pt_Win1253Glyphs, +0x01, 0xFF, 10 +}; diff --git a/src/graphics/niche/Fonts/FreeSans9pt_Win1253.h b/src/graphics/niche/Fonts/FreeSans9pt_Win1253.h new file mode 100644 index 000000000..e9ff547cb --- /dev/null +++ b/src/graphics/niche/Fonts/FreeSans9pt_Win1253.h @@ -0,0 +1,527 @@ +// trunk-ignore-all(clang-format) +#pragma once +/* PROPERTIES + +FONT_NAME FreeSans9pt_Win1253 +*/ +const uint8_t FreeSans9pt_Win1253Bitmaps[] PROGMEM = { +/* 0x01 */ 0x07, 0x00, 0x0A, 0x00, 0x24, 0x00, 0x48, 0x01, 0x10, 0x04, 0x40, 0x10, 0xFF, 0x20, 0x02, 0x81, 0xFD, 0x00, 0x06, 0x07, 0xF4, 0x08, 0x24, 0x0F, 0x88, 0x11, 0x0F, 0xDC, 0x00, +/* 0x02 */ 0x3F, 0x70, 0x81, 0x11, 0x03, 0xE4, 0x08, 0x28, 0x1F, 0xD0, 0x00, 0x60, 0x7F, 0x20, 0x02, 0x43, 0xFC, 0x44, 0x00, 0x44, 0x00, 0x48, 0x00, 0x90, 0x00, 0xA0, 0x01, 0xC0, 0x00, +/* 0x03 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x8C, 0x63, 0x18, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x20, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x04 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x30, 0x88, 0x62, 0x08, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x05 */ 0x0B, 0x10, 0x14, 0xA8, 0x12, 0x50, 0x29, 0x42, 0x24, 0xA5, 0x32, 0x95, 0x5A, 0x09, 0x48, 0x09, 0x24, 0x01, 0x10, 0x01, 0x48, 0x02, 0xA4, 0x02, 0x42, 0x04, 0x01, 0x98, 0x00, 0x60, +/* 0x06 */ 0x00, 0x80, 0x22, 0x80, 0x65, 0x00, 0xBE, 0xE1, 0x82, 0x4E, 0x03, 0x24, 0x04, 0x28, 0x06, 0x30, 0x12, 0x20, 0x3C, 0xA0, 0xC3, 0xFE, 0x80, 0x4D, 0x00, 0xA6, 0x01, 0x80, 0x00, +/* 0x07 */ +/* 0x08 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x02, 0x00, 0x03, 0x01, 0x00, 0x09, 0x88, 0x0C, 0x0C, +/* 0x09 */ 0x00, 0xF8, 0x00, 0x82, 0x00, 0x80, 0x83, 0xE0, 0x41, 0x10, 0x21, 0x04, 0x1B, 0x00, 0x03, 0x00, 0x01, 0x80, 0x00, 0xE0, 0x00, 0x4F, 0xE1, 0xC0, 0x0F, 0x00, +/* 0x0A */ +/* 0x0B */ 0x1C, 0x1C, 0x31, 0xB1, 0x90, 0x50, 0x50, 0x10, 0x18, 0x00, 0x0C, 0x00, 0x06, 0x00, 0x02, 0x80, 0x02, 0x40, 0x01, 0x10, 0x01, 0x04, 0x01, 0x01, 0x01, 0x00, 0x41, 0x00, 0x11, 0x00, 0x07, 0x00, 0x01, 0x00, +/* 0x0C */ 0x06, 0x00, 0x0A, 0x00, 0x12, 0x00, 0x32, 0x01, 0x84, 0x04, 0x10, 0x08, 0x98, 0x1C, 0x18, 0x40, 0x48, 0x82, 0x11, 0xF0, 0x74, 0x02, 0x18, 0x70, 0x2F, 0x9F, 0x80, +/* 0x0D */ +/* 0x0E */ 0x01, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x3E, 0x00, 0x82, 0x02, 0x82, 0x06, 0x04, 0x10, 0x04, 0x20, 0x08, 0x40, 0x10, 0xFF, 0x22, 0x00, 0x29, 0xFF, 0x3F, 0x8F, 0xDF, 0x9F, 0x01, 0xC0, +/* 0x0F */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x82, 0x36, 0x03, 0x60, 0x00, 0xCC, 0x19, 0xA4, 0x4B, 0x00, 0x06, 0x8E, 0x2B, 0x22, 0x66, 0x7C, 0xCC, 0x71, 0x98, 0x03, 0x00, +/* 0x10 */ 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x54, 0x00, 0xA8, 0x01, 0x50, 0x02, 0xA0, 0x05, 0x20, 0x32, 0x61, 0xC4, 0x74, 0x49, 0x10, 0x6C, 0x00, 0xD8, 0x01, 0x10, 0x00, +/* 0x11 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x40, 0x29, 0x00, 0x31, 0x84, 0x63, 0x18, 0xC0, 0x00, 0x80, 0x15, 0x03, 0x7E, 0x02, 0xFA, 0x04, 0xE4, 0x18, 0x84, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x12 */ 0x02, 0x08, 0x01, 0x08, 0x40, 0x10, 0xC0, 0x08, 0xC0, 0x60, 0x80, 0x28, 0x04, 0x12, 0x4C, 0x10, 0x80, 0x08, 0x23, 0x0E, 0x08, 0xC4, 0x82, 0x04, 0x20, 0x83, 0x09, 0x82, 0x47, 0x01, 0x1C, 0x01, 0x30, 0x00, 0xE0, 0x00, 0x00, +/* 0x13 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x31, 0x08, 0x65, 0x28, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x14 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x22, 0x29, 0x83, 0x30, 0x00, 0x65, 0x14, 0xD3, 0x4D, 0xBA, 0xEB, 0x38, 0xE6, 0x00, 0x0A, 0x00, 0x24, 0x38, 0x44, 0x01, 0x07, 0x1C, 0x01, 0xC0, +/* 0x15 */ 0x07, 0xC0, 0x30, 0x18, 0x80, 0x32, 0x00, 0xF8, 0x01, 0xF1, 0x09, 0xA5, 0x28, 0x40, 0x01, 0x80, 0x03, 0x00, 0x06, 0x3F, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x16 */ 0x0C, 0x00, 0xC0, 0x1C, 0x03, 0x80, 0xF8, 0xBB, 0x36, 0xC7, 0x99, 0xF3, 0xFE, 0x3F, 0xC3, 0xF0, 0x7E, 0x0E, 0xC1, 0x8E, 0xE0, 0x20, +/* 0x17 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x10, 0x01, 0x20, 0x1D, 0x44, 0x42, 0x84, 0x85, 0x00, 0x86, 0x00, 0xC4, 0x00, 0x44, 0x7C, 0x44, 0x00, 0x06, 0x0C, 0x03, 0xE0, +/* 0x18 */ 0x01, 0xE0, 0x00, 0x84, 0x00, 0x40, 0x80, 0x20, 0x10, 0x08, 0x24, 0x02, 0x41, 0x00, 0x86, 0x03, 0x12, 0x03, 0xB4, 0x03, 0x52, 0x81, 0x23, 0x80, 0x70, 0xA0, 0x14, 0x28, 0x05, 0x0A, 0x01, 0x42, 0x80, 0x50, +/* 0x19 */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x00, 0x28, 0x00, 0x33, 0x18, 0x60, 0x00, 0xDC, 0xE1, 0xB9, 0xC3, 0x7B, 0xC6, 0x63, 0x0A, 0x00, 0x24, 0xF0, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1A */ 0xFF, 0xFC, 0x00, 0x63, 0xE3, 0x31, 0x99, 0x04, 0xC8, 0x66, 0x06, 0x30, 0x61, 0x82, 0x0C, 0x10, 0x60, 0x03, 0x04, 0x18, 0x00, 0xFF, 0xFC, +/* 0x1B */ 0x07, 0xF0, 0x06, 0x0C, 0x04, 0x01, 0x04, 0x00, 0x44, 0x22, 0x12, 0x2A, 0x89, 0x00, 0x04, 0x80, 0x02, 0x44, 0x11, 0x01, 0xF0, 0x04, 0x01, 0x0D, 0x01, 0x6A, 0x41, 0x2C, 0x00, 0x05, 0xC0, 0x0E, 0x18, 0x18, +/* 0x1C */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0xC0, 0x2A, 0x00, 0x33, 0x00, 0x66, 0x00, 0xCC, 0x39, 0x80, 0x83, 0x00, 0x06, 0x00, 0x8C, 0x3E, 0x14, 0x00, 0x44, 0x01, 0x06, 0x0C, 0x03, 0xE0, +/* 0x1D */ 0x07, 0xC0, 0x30, 0x60, 0x80, 0x22, 0x70, 0x28, 0x00, 0x31, 0x80, 0x63, 0x18, 0xC0, 0x31, 0x80, 0x03, 0x00, 0x06, 0x60, 0x0D, 0x33, 0x12, 0x10, 0x48, 0x21, 0x23, 0x8C, 0x00, +/* 0x1E */ 0x03, 0x00, 0x07, 0x9E, 0x07, 0x00, 0x86, 0x00, 0x27, 0xC0, 0x0F, 0xC0, 0x07, 0x8C, 0x62, 0x06, 0x31, 0x20, 0x00, 0x90, 0x00, 0x48, 0x00, 0x24, 0x3E, 0x11, 0x00, 0x10, 0x40, 0x10, 0x18, 0x30, 0x03, 0xE0, +/* 0x1F */ 0x18, 0x02, 0x80, 0x4C, 0x16, 0x41, 0x24, 0x3C, 0x88, 0x6E, 0x65, 0xF2, 0x78, 0x46, 0x88, 0xCF, 0x18, 0x02, 0x80, 0x8C, 0x60, 0x70, +/* ' ' 0x20 */ +/* '!' 0x21 */ 0xFF, 0xFF, 0xF0, 0xC0, +/* '"' 0x22 */ 0xDE, 0xF7, 0x20, +/* '#' 0x23 */ 0x09, 0x86, 0x41, 0x91, 0xFF, 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, +/* '$' 0x24 */ 0x10, 0x1F, 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, 0x33, 0xF0, 0x40, 0x20, +/* '%' 0x25 */ 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, +/* '&' 0x26 */ 0x0E, 0x0C, 0xC3, 0x30, 0xCC, 0x1E, 0x03, 0x03, 0xC1, 0x9B, 0xC2, 0xF0, 0xEC, 0x19, 0x8F, 0x3C, 0x40, +/* ''' 0x27 */ 0xFE, +/* '(' 0x28 */ 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, +/* ')' 0x29 */ 0x8C, 0x46, 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, +/* '*' 0x2A */ 0x25, 0x7E, 0xA5, 0x00, +/* '+' 0x2B */ 0x30, 0xC3, 0x3F, 0x30, 0xC3, 0x0C, +/* ',' 0x2C */ 0xD6, +/* '-' 0x2D */ 0xF0, +/* '.' 0x2E */ 0xC0, +/* '/' 0x2F */ 0x08, 0x44, 0x21, 0x10, 0x84, 0x42, 0x11, 0x08, 0x00, +/* '0' 0x30 */ 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, +/* '1' 0x31 */ 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, 0x30, +/* '2' 0x32 */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, 0x10, 0x08, 0x07, 0xF8, +/* '3' 0x33 */ 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, 0x03, 0xC3, 0xC3, 0x66, 0x3C, +/* '4' 0x34 */ 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, 0xFE, 0x18, 0x30, 0x60, 0xC0, +/* '5' 0x35 */ 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, +/* '6' 0x36 */ 0x1E, 0x31, 0x98, 0x78, 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, +/* '7' 0x37 */ 0xFF, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, +/* '8' 0x38 */ 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, 0x6C, 0x63, 0xE0, +/* '9' 0x39 */ 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC2, 0x66, 0x3C, +/* ':' 0x3A */ 0xC0, 0x00, 0x30, +/* ';' 0x3B */ 0xC0, 0x00, 0x00, 0x64, 0xA0, +/* '<' 0x3C */ 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, +/* '=' 0x3D */ 0xFF, 0x80, 0x00, 0x1F, 0xF0, +/* '>' 0x3E */ 0xE0, 0x1C, 0x03, 0x80, 0x30, 0x70, 0xE3, 0x81, 0x00, +/* '?' 0x3F */ 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, +/* '@' 0x40 */ 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, 0x70, 0x40, 0x0F, 0xE0, +/* 'A' 0x41 */ 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, +/* 'B' 0x42 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, +/* 'C' 0x43 */ 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, +/* 'D' 0x44 */ 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, +/* 'E' 0x45 */ 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, +/* 'F' 0x46 */ 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 'G' 0x47 */ 0x0F, 0x83, 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, +/* 'H' 0x48 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 'I' 0x49 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'J' 0x4A */ 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1E, 0x27, 0x80, +/* 'K' 0x4B */ 0xC0, 0xF0, 0x6C, 0x33, 0x18, 0xCC, 0x37, 0x0F, 0xC3, 0x98, 0xC3, 0x30, 0xCC, 0x1B, 0x03, 0xC0, 0xC0, +/* 'L' 0x4C */ 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, +/* 'M' 0x4D */ 0xE0, 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, +/* 'N' 0x4E */ 0xE0, 0x7C, 0x0F, 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, 0xE0, 0x7C, 0x0E, +/* 'O' 0x4F */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, 0x00, +/* 'P' 0x50 */ 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 'Q' 0x51 */ 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, 0x60, 0xC0, 0xFB, 0x00, 0x08, +/* 'R' 0x52 */ 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, 0x70, +/* 'S' 0x53 */ 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, +/* 'T' 0x54 */ 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, +/* 'U' 0x55 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xB0, 0x61, 0xF0, +/* 'V' 0x56 */ 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, +/* 'W' 0x57 */ 0xC1, 0x81, 0x61, 0xC3, 0x61, 0xC3, 0x61, 0x43, 0x62, 0x62, 0x22, 0x66, 0x32, 0x26, 0x36, 0x26, 0x14, 0x34, 0x14, 0x34, 0x1C, 0x1C, 0x18, 0x1C, 0x08, 0x18, +/* 'X' 0x58 */ 0xC0, 0xD8, 0x66, 0x18, 0xCC, 0x1E, 0x07, 0x00, 0xC0, 0x78, 0x32, 0x0C, 0xC6, 0x1B, 0x07, 0xC0, 0xC0, +/* 'Y' 0x59 */ 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 'Z' 0x5A */ 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, +/* '[' 0x5B */ 0xFB, 0x6D, 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, +/* '\' 0x5C */ 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x80, +/* ']' 0x5D */ 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, +/* '^' 0x5E */ 0x30, 0x60, 0xA2, 0x44, 0xD8, 0xA1, 0x80, +/* '_' 0x5F */ 0xFF, 0xC0, +/* '`' 0x60 */ 0xC6, 0x30, +/* 'a' 0x61 */ 0x7E, 0x71, 0xB0, 0xC0, 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, +/* 'b' 0x62 */ 0xC0, 0x60, 0x30, 0x1B, 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, +/* 'c' 0x63 */ 0x3C, 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'd' 0x64 */ 0x03, 0x03, 0x03, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, +/* 'e' 0x65 */ 0x3C, 0x66, 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, +/* 'f' 0x66 */ 0x36, 0x6F, 0x66, 0x66, 0x66, 0x66, 0x60, +/* 'g' 0x67 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0xC6, 0x7C, +/* 'h' 0x68 */ 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'i' 0x69 */ 0xC3, 0xFF, 0xFF, 0xC0, +/* 'j' 0x6A */ 0x30, 0x03, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, +/* 'k' 0x6B */ 0xC0, 0xC0, 0xC0, 0xC2, 0xC4, 0xCC, 0xD8, 0xF8, 0xEC, 0xC4, 0xC6, 0xC3, 0xC3, +/* 'l' 0x6C */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 'm' 0x6D */ 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, +/* 'n' 0x6E */ 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, +/* 'o' 0x6F */ 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, +/* 'p' 0x70 */ 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, +/* 'q' 0x71 */ 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, +/* 'r' 0x72 */ 0xDF, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, +/* 's' 0x73 */ 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 't' 0x74 */ 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, +/* 'u' 0x75 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7B, +/* 'v' 0x76 */ 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, 0x28, 0x1C, 0x0C, 0x00, +/* 'w' 0x77 */ 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, +/* 'x' 0x78 */ 0x87, 0x89, 0xB1, 0xC3, 0x07, 0x1E, 0x26, 0xC5, 0x0C, +/* 'y' 0x79 */ 0xC1, 0x43, 0x63, 0x62, 0x26, 0x36, 0x34, 0x1C, 0x1C, 0x18, 0x18, 0x18, 0x10, 0x60, +/* 'z' 0x7A */ 0xFE, 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, +/* '{' 0x7B */ 0x36, 0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x66, 0x30, +/* '|' 0x7C */ 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, +/* '}' 0x7D */ 0xC6, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, +/* '~' 0x7E */ 0x61, 0x24, 0x38, +/* 0x7F */ +/* 0x80 */ 0x07, 0xC6, 0x13, 0x00, 0xC0, 0x60, 0x3F, 0xE6, 0x03, 0xFC, 0x60, 0x0C, 0x03, 0x00, 0x61, 0x07, 0xC0, +/* 0x81 */ +/* 0x82 */ 0xDC, +/* 0x83 */ 0x19, 0x8C, 0xF3, 0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0xE0, +/* 0x84 */ 0xDA, 0x76, +/* 0x85 */ 0xCC, 0xC0, +/* 0x86 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0x87 */ 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, +/* 0x88 */ 0x72, 0xA2, +/* 0x89 */ 0x70, 0x80, 0x22, 0x20, 0x08, 0x90, 0x02, 0x24, 0x00, 0x72, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x10, 0x00, 0x09, 0xC7, 0x84, 0x8B, 0x31, 0x22, 0x84, 0x88, 0xB3, 0x21, 0xC7, 0x80, +/* 0x8A */ 0x1B, 0x03, 0x80, 0x00, 0xFC, 0x61, 0xB0, 0x3C, 0x0F, 0x00, 0x78, 0x07, 0xC0, 0x38, 0x03, 0xC0, 0xF0, 0x36, 0x18, 0xFC, +/* 0x8B */ 0x69, +/* 0x8C */ 0x1E, 0xFE, 0x43, 0x81, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x3F, 0xE0, 0x60, 0xC0, 0xC1, 0x81, 0x81, 0x83, 0x01, 0x8E, 0x01, 0xEF, 0xE0, +/* 0x8D */ +/* 0x8E */ 0x1B, 0x03, 0x80, 0x03, 0xFF, 0x01, 0x80, 0xC0, 0x30, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x18, 0x0C, 0x03, 0xFF, +/* 0x8F */ +/* 0x90 */ +/* 0x91 */ 0x6B, +/* 0x92 */ 0xD6, +/* 0x93 */ 0x4C, 0xA5, 0xB0, +/* 0x94 */ 0xDA, 0x53, 0x20, +/* 0x95 */ 0x6F, 0xFF, 0x60, +/* 0x96 */ 0xFE, +/* 0x97 */ 0xFF, 0xFF, +/* 0x98 */ 0x4D, 0xC0, +/* 0x99 */ 0xFC, 0xE1, 0xCC, 0x38, 0x73, 0x0E, 0x1C, 0xC3, 0x8F, 0x30, 0xD2, 0xCC, 0x34, 0xB3, 0x0D, 0x6C, 0xC3, 0x53, 0x30, 0xCC, 0xCC, 0x33, 0x30, +/* 0x9A */ 0x24, 0x3C, 0x18, 0x7E, 0xE3, 0xC0, 0xC0, 0x60, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, +/* 0x9B */ 0x96, +/* 0x9C */ 0x3C, 0xF8, 0xCF, 0x1B, 0x0C, 0x1E, 0x18, 0x3C, 0x3F, 0xF8, 0x60, 0x30, 0xC0, 0x61, 0x83, 0x67, 0x8C, 0x79, 0xF0, +/* 0x9D */ +/* 0x9E */ 0x48, 0xF0, 0xC7, 0xF0, 0x61, 0x86, 0x0C, 0x30, 0xC1, 0x06, 0x0F, 0xE0, +/* 0x9F */ 0x19, 0x80, 0x00, 0xC0, 0x36, 0x06, 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xA0 */ +/* 0xA1 */ 0xCF, 0xFF, 0xFF, 0xC0, +/* 0xA2 */ 0x08, 0x04, 0x0F, 0x8D, 0x6C, 0x9E, 0x43, 0x21, 0x90, 0xC8, 0x64, 0xDA, 0xC7, 0xC0, 0x80, 0x40, +/* 0xA3 */ 0x1F, 0x0C, 0x66, 0x0D, 0x83, 0x60, 0x0C, 0x0F, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x01, 0xF1, 0x43, 0xC0, +/* 0xA4 */ 0xFF, 0xDF, 0x1E, 0x3E, 0xFF, 0xC0, +/* 0xA5 */ 0xC3, 0x42, 0x42, 0x24, 0x24, 0x3C, 0x18, 0x7E, 0x18, 0x7E, 0x18, 0x18, 0x18, +/* 0xA6 */ 0xFF, 0xFC, 0x0F, 0xFF, 0xC0, +/* 0xA7 */ 0x0C, 0x09, 0x0C, 0xC6, 0x63, 0x81, 0xE3, 0x19, 0x87, 0xE1, 0xB8, 0xC6, 0x41, 0xC0, 0x73, 0x19, 0x8C, 0x66, 0x1E, 0x00, +/* 0xA8 */ 0xCC, +/* 0xA9 */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9B, 0xC6, 0xD9, 0x8F, 0x60, 0x3D, 0x00, 0xF4, 0x03, 0xD8, 0x0D, 0xE6, 0x67, 0xF3, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAA */ 0x74, 0x8D, 0xA9, 0x7C, 0x1F, +/* 0xAB */ 0x22, 0xCF, 0x26, 0x46, 0x64, 0x40, +/* 0xAC */ 0xFF, 0x80, 0xC0, 0x60, 0x30, 0x18, +/* 0xAD */ +/* 0xAE */ 0x0F, 0xC0, 0x61, 0x87, 0x03, 0x9F, 0xE6, 0xD0, 0x8F, 0x42, 0x3D, 0xF0, 0xF4, 0x23, 0xD0, 0x8D, 0xC2, 0x67, 0x0B, 0x86, 0x18, 0x0F, 0xC0, +/* 0xAF */ 0xF8, +/* 0xB0 */ 0x74, 0x63, 0x17, 0x00, +/* 0xB1 */ 0x0C, 0x06, 0x03, 0x07, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x00, 0x00, 0x3F, 0xE0, +/* 0xB2 */ 0x7B, 0x30, 0xC3, 0x11, 0x84, 0x3F, +/* 0xB3 */ 0x7D, 0x8C, 0x18, 0xC0, 0x60, 0xF1, 0xBE, +/* 0xB4 */ 0x36, 0xC0, +/* 0xB5 */ 0xC3, 0x61, 0xB0, 0xD8, 0x6C, 0x36, 0x1B, 0x0D, 0x86, 0xE7, 0x7D, 0xF0, 0x18, 0x0C, 0x00, +/* 0xB6 */ 0x3F, 0x7E, 0xF2, 0xF2, 0xF2, 0xF2, 0xF2, 0x72, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, +/* 0xB7 */ 0xE0, +/* 0xB8 */ 0x21, 0xC7, 0xE0, +/* 0xB9 */ 0x3D, 0xB6, 0xD8, +/* 0xBA */ 0x74, 0x63, 0x18, 0xB8, 0x1F, +/* 0xBB */ 0x89, 0x98, 0x99, 0x3C, 0xD1, 0x00, +/* 0xBC */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x84, 0x06, 0x21, 0x80, 0x86, 0x04, 0x78, 0x32, 0x60, 0x87, 0xC4, 0x06, 0x10, 0x18, +/* 0xBD */ 0x20, 0x43, 0x81, 0x06, 0x08, 0x18, 0x20, 0x61, 0x01, 0x8D, 0xE6, 0x2C, 0xC1, 0x03, 0x0C, 0x0C, 0x20, 0x41, 0x86, 0x0C, 0x30, 0x20, 0xFC, +/* 0xBE */ 0x78, 0x11, 0x98, 0x40, 0x31, 0x00, 0x82, 0x00, 0xC8, 0x01, 0x90, 0x33, 0x43, 0x3D, 0x06, 0x02, 0x3C, 0x08, 0x98, 0x10, 0xF8, 0x40, 0x61, 0x00, 0xC0, +/* 0xBF */ 0x0C, 0x00, 0x00, 0x01, 0x80, 0xC0, 0xC0, 0xE0, 0xC0, 0xC0, 0x60, 0xF0, 0x6C, 0x63, 0xE0, +/* 0xC0 */ 0x0C, 0xDB, 0xD3, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, +/* 0xC1 */ 0x0E, 0x01, 0xC0, 0x6C, 0x0D, 0x81, 0xB0, 0x63, 0x0C, 0x61, 0xFC, 0x7F, 0xCC, 0x19, 0x83, 0x60, 0x3C, 0x06, +/* 0xC2 */ 0xFF, 0x3F, 0xEC, 0x0F, 0x03, 0xC0, 0xFF, 0xEF, 0xFB, 0x03, 0xC0, 0xF0, 0x3C, 0x1F, 0xFE, 0xFF, 0x00, +/* 0xC3 */ 0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xC4 */ 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xD8, 0x0C, 0x60, 0x63, 0x03, 0x18, 0x30, 0x61, 0x83, 0x18, 0x0C, 0xFF, 0xE7, 0xFF, 0x00, +/* 0xC5 */ 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0xC0, 0x3F, 0xEF, 0xFB, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0xFF, 0xFF, 0xC0, +/* 0xC6 */ 0x7F, 0xDF, 0xF0, 0x18, 0x0C, 0x07, 0x01, 0x80, 0xC0, 0x60, 0x38, 0x0C, 0x06, 0x03, 0xFF, 0xFF, 0xC0, +/* 0xC7 */ 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 0xC8 */ 0x0F, 0x03, 0xFC, 0x70, 0xE6, 0x06, 0xC0, 0x3C, 0xF3, 0xCF, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x70, 0xE3, 0xFC, 0x1F, 0x80, +/* 0xC9 */ 0xFF, 0xFF, 0xFF, 0xC0, +/* 0xCA */ 0xC1, 0xD8, 0x73, 0x1C, 0x67, 0x0D, 0xC1, 0xF0, 0x3F, 0x07, 0x70, 0xC7, 0x18, 0x63, 0x0E, 0x60, 0xEC, 0x0E, +/* 0xCB */ 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1B, 0x00, 0xD8, 0x0C, 0x60, 0x63, 0x03, 0x18, 0x30, 0x61, 0x83, 0x1C, 0x1C, 0xC0, 0x66, 0x03, 0x00, +/* 0xCC */ 0xE0, 0x3F, 0x83, 0xFC, 0x1F, 0xE0, 0xFD, 0x8D, 0xEC, 0x6F, 0x63, 0x79, 0x13, 0xCD, 0x9E, 0x6C, 0xF3, 0x67, 0x8E, 0x3C, 0x71, 0x80, +/* 0xCD */ 0xC0, 0x7C, 0x0F, 0xC1, 0xF8, 0x3D, 0x87, 0x98, 0xF3, 0x9E, 0x33, 0xC3, 0x78, 0x3F, 0x07, 0xE0, 0x7C, 0x06, +/* 0xCE */ 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x1F, 0xE7, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xC0, +/* 0xCF */ 0x0F, 0x83, 0xFC, 0x70, 0xE6, 0x06, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x36, 0x06, 0x70, 0xE3, 0xFC, 0x0F, 0x00, +/* 0xD0 */ 0xFF, 0xFF, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, +/* 0xD1 */ 0xFF, 0x3F, 0xEC, 0x1F, 0x03, 0xC0, 0xF0, 0x7F, 0xFB, 0xFC, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, +/* 0xD2 */ +/* 0xD3 */ 0xFF, 0xFF, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0xFF, 0xFF, +/* 0xD4 */ 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, +/* 0xD5 */ 0xE0, 0x76, 0x06, 0x30, 0xC3, 0x9C, 0x19, 0x80, 0xF0, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 0xD6 */ 0x06, 0x00, 0x60, 0x1F, 0x87, 0xFE, 0xE6, 0x7C, 0x63, 0xC6, 0x3C, 0x63, 0xE6, 0x77, 0xFE, 0x1F, 0x80, 0x60, 0x06, 0x00, +/* 0xD7 */ 0x71, 0xC6, 0x30, 0x6C, 0x0D, 0x80, 0xE0, 0x1C, 0x03, 0x80, 0xD8, 0x1B, 0x07, 0x70, 0xC6, 0x30, 0x6E, 0x0E, +/* 0xD8 */ 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0x46, 0x66, 0x66, 0x3F, 0xC0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, +/* 0xD9 */ 0x1F, 0x07, 0xF1, 0xC7, 0x70, 0x7C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0x40, 0x4C, 0x18, 0xEE, 0x7D, 0xFF, 0xBE, +/* 0xDA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xDB */ 0x19, 0x81, 0x98, 0x00, 0x0E, 0x07, 0x60, 0x63, 0x0C, 0x39, 0xC1, 0x98, 0x0F, 0x00, 0xF0, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, +/* 0xDC */ 0x06, 0x0C, 0x00, 0x3B, 0x7B, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xEE, 0x7B, 0x3B, +/* 0xDD */ 0x18, 0x20, 0x03, 0xCF, 0xF8, 0xB0, 0x38, 0x71, 0x83, 0x17, 0xF7, 0x80, +/* 0xDE */ 0x0C, 0x18, 0x00, 0xDE, 0xFF, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x03, 0x03, 0x03, 0x03, +/* 0xDF */ 0x78, 0x6D, 0xB6, 0xDB, 0x6C, +/* 0xE0 */ 0x0C, 0xDB, 0xD3, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xE1 */ 0x3B, 0x7B, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xEE, 0x7B, 0x3B, +/* 0xE2 */ 0x3C, 0x7E, 0xC6, 0xC6, 0xC4, 0xD8, 0xDE, 0xC7, 0xC3, 0xC3, 0xE7, 0xFE, 0xDC, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xE3 */ 0x61, 0x98, 0x66, 0x18, 0xCC, 0x33, 0x0C, 0xC1, 0xE0, 0x78, 0x1E, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, +/* 0xE4 */ 0x7E, 0x7E, 0x30, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xE5 */ 0x79, 0xFF, 0x16, 0x07, 0x0E, 0x30, 0x62, 0xFE, 0xF0, +/* 0xE6 */ 0x7E, 0xFC, 0x30, 0xC3, 0x0C, 0x18, 0x60, 0xC1, 0x83, 0x07, 0xE7, 0xE0, 0xC1, 0x83, 0x0C, +/* 0xE7 */ 0xDE, 0xFF, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x03, 0x03, 0x03, 0x03, +/* 0xE8 */ 0x3C, 0x7E, 0x66, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0x66, 0x7E, 0x3C, +/* 0xE9 */ 0xFF, 0xFF, 0xF0, +/* 0xEA */ 0xC3, 0x63, 0x33, 0x1B, 0x0F, 0x06, 0xC3, 0x31, 0x8C, 0xC6, 0x61, 0x80, +/* 0xEB */ 0x30, 0x0C, 0x06, 0x03, 0x01, 0xC1, 0xE0, 0xD0, 0x6C, 0x36, 0x33, 0x18, 0xCC, 0x66, 0x30, +/* 0xEC */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0xFF, 0xDB, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xED */ 0xC1, 0xE0, 0xD8, 0xCC, 0x66, 0x31, 0xB0, 0xD8, 0x38, 0x1C, 0x04, 0x00, +/* 0xEE */ 0x7D, 0xFB, 0x06, 0x07, 0xC7, 0x9C, 0x70, 0xC1, 0x83, 0x83, 0xE3, 0xE0, 0xC1, 0x8E, 0x18, +/* 0xEF */ 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xF0 */ 0xFF, 0xFF, 0xFF, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, +/* 0xF1 */ 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0xFE, 0xDC, 0xC0, 0xC0, 0xC0, 0xC0, +/* 0xF2 */ 0x1E, 0xFD, 0x86, 0x0C, 0x18, 0x30, 0x70, 0x7C, 0x7C, 0x18, 0x33, 0xE7, 0x00, +/* 0xF3 */ 0x3F, 0xDF, 0xFE, 0x63, 0x0C, 0xC3, 0x30, 0xCC, 0x33, 0x9C, 0x7E, 0x0F, 0x00, +/* 0xF4 */ 0xFF, 0xF3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, +/* 0xF5 */ 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xF6 */ 0x2F, 0x1B, 0xEC, 0xDF, 0x33, 0xCC, 0xF3, 0x3C, 0xCD, 0xB6, 0x7F, 0x8F, 0x80, 0xC0, 0x30, 0x0C, 0x03, 0x00, +/* 0xF7 */ 0x63, 0x31, 0x8D, 0x86, 0xC3, 0x60, 0xE0, 0x70, 0x38, 0x1C, 0x1B, 0x0D, 0x86, 0xC6, 0x33, 0x18, +/* 0xF8 */ 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0x6D, 0x8F, 0xC0, 0xC0, 0x30, 0x0C, 0x03, 0x00, +/* 0xF9 */ 0x30, 0xC6, 0x06, 0x66, 0x6C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3E, 0xF7, 0x79, 0xE3, 0x9C, +/* 0xFA */ 0xCF, 0x30, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, +/* 0xFB */ 0x66, 0x66, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xFC */ 0x0C, 0x18, 0x00, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xFD */ 0x08, 0x10, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, +/* 0xFE */ 0x03, 0x00, 0x60, 0x00, 0x03, 0x0C, 0x60, 0x66, 0x66, 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xEF, 0x77, 0x9E, 0x39, 0xC0, +/* 0xFF */ +}; + +const GFXglyph FreeSans9pt_Win1253Glyphs[] PROGMEM = { +/* 0x01 */ { 0, 15, 15, 17, 1, -13 }, +/* 0x02 */ { 29, 15, 15, 17, 1, -13 }, +/* 0x03 */ { 58, 15, 16, 17, 1, -14 }, +/* 0x04 */ { 88, 15, 16, 17, 1, -14 }, +/* 0x05 */ { 118, 16, 15, 18, 1, -13 }, +/* 0x06 */ { 148, 15, 15, 17, 1, -13 }, +/* 0x07 */ { 177, 0, 0, 8, 0, 0 }, +/* 0x08 */ { 177, 17, 16, 19, 1, -14 }, +/* 0x09 */ { 211, 17, 12, 19, 1, -12 }, +/* 0x0A */ { 237, 0, 0, 8, 0, 0 }, +/* 0x0B */ { 237, 17, 16, 19, 1, -14 }, +/* 0x0C */ { 271, 15, 14, 17, 1, -12 }, +/* 0x0D */ { 298, 0, 0, 8, 0, 0 }, +/* 0x0E */ { 298, 15, 16, 17, 1, -14 }, +/* 0x0F */ { 328, 15, 15, 17, 1, -13 }, +/* 0x10 */ { 357, 15, 15, 17, 1, -13 }, +/* 0x11 */ { 386, 15, 16, 17, 1, -14 }, +/* 0x12 */ { 416, 17, 17, 19, 1, -15 }, +/* 0x13 */ { 453, 15, 16, 17, 1, -14 }, +/* 0x14 */ { 483, 15, 16, 17, 1, -14 }, +/* 0x15 */ { 513, 15, 16, 17, 1, -14 }, +/* 0x16 */ { 543, 11, 16, 13, 1, -14 }, +/* 0x17 */ { 565, 15, 16, 17, 1, -14 }, +/* 0x18 */ { 595, 18, 15, 20, 1, -13 }, +/* 0x19 */ { 629, 15, 16, 17, 1, -14 }, +/* 0x1A */ { 659, 13, 14, 15, 1, -12 }, +/* 0x1B */ { 682, 17, 16, 19, 1, -14 }, +/* 0x1C */ { 716, 15, 16, 17, 1, -14 }, +/* 0x1D */ { 746, 15, 15, 17, 1, -13 }, +/* 0x1E */ { 775, 17, 16, 19, 1, -14 }, +/* 0x1F */ { 809, 11, 16, 13, 1, -14 }, +/* ' ' 0x20 */ { 831, 0, 0, 5, 0, 0 }, +/* '!' 0x21 */ { 831, 2, 13, 6, 2, -12 }, +/* '"' 0x22 */ { 835, 5, 4, 6, 1, -12 }, +/* '#' 0x23 */ { 838, 10, 12, 10, 0, -11 }, +/* '$' 0x24 */ { 853, 9, 16, 10, 1, -13 }, +/* '%' 0x25 */ { 871, 16, 13, 16, 1, -12 }, +/* '&' 0x26 */ { 897, 10, 13, 12, 1, -12 }, +/* ''' 0x27 */ { 914, 2, 4, 4, 1, -12 }, +/* '(' 0x28 */ { 915, 4, 17, 6, 1, -12 }, +/* ')' 0x29 */ { 924, 4, 17, 6, 1, -12 }, +/* '*' 0x2A */ { 933, 5, 5, 7, 1, -12 }, +/* '+' 0x2B */ { 937, 6, 8, 11, 3, -7 }, +/* ',' 0x2C */ { 943, 2, 4, 5, 2, 0 }, +/* '-' 0x2D */ { 944, 4, 1, 6, 1, -4 }, +/* '.' 0x2E */ { 945, 2, 1, 5, 1, 0 }, +/* '/' 0x2F */ { 946, 5, 13, 5, 0, -12 }, +/* '0' 0x30 */ { 955, 8, 13, 10, 1, -12 }, +/* '1' 0x31 */ { 968, 4, 13, 10, 3, -12 }, +/* '2' 0x32 */ { 975, 9, 13, 10, 1, -12 }, +/* '3' 0x33 */ { 990, 8, 13, 10, 1, -12 }, +/* '4' 0x34 */ { 1003, 7, 13, 10, 2, -12 }, +/* '5' 0x35 */ { 1015, 9, 13, 10, 1, -12 }, +/* '6' 0x36 */ { 1030, 9, 13, 10, 1, -12 }, +/* '7' 0x37 */ { 1045, 8, 13, 10, 0, -12 }, +/* '8' 0x38 */ { 1058, 9, 13, 10, 1, -12 }, +/* '9' 0x39 */ { 1073, 8, 13, 10, 1, -12 }, +/* ':' 0x3A */ { 1086, 2, 10, 5, 1, -9 }, +/* ';' 0x3B */ { 1089, 3, 12, 5, 1, -8 }, +/* '<' 0x3C */ { 1094, 9, 9, 11, 1, -8 }, +/* '=' 0x3D */ { 1105, 9, 4, 11, 1, -5 }, +/* '>' 0x3E */ { 1110, 9, 8, 11, 1, -7 }, +/* '?' 0x3F */ { 1119, 9, 13, 10, 1, -12 }, +/* '@' 0x40 */ { 1134, 17, 16, 18, 1, -12 }, +/* 'A' 0x41 */ { 1168, 12, 13, 12, 0, -12 }, +/* 'B' 0x42 */ { 1188, 11, 13, 12, 1, -12 }, +/* 'C' 0x43 */ { 1206, 11, 13, 13, 1, -12 }, +/* 'D' 0x44 */ { 1224, 11, 13, 13, 1, -12 }, +/* 'E' 0x45 */ { 1242, 9, 13, 11, 1, -12 }, +/* 'F' 0x46 */ { 1257, 8, 13, 11, 1, -12 }, +/* 'G' 0x47 */ { 1270, 12, 13, 14, 1, -12 }, +/* 'H' 0x48 */ { 1290, 11, 13, 13, 1, -12 }, +/* 'I' 0x49 */ { 1308, 2, 13, 5, 2, -12 }, +/* 'J' 0x4A */ { 1312, 7, 13, 10, 1, -12 }, +/* 'K' 0x4B */ { 1324, 10, 13, 12, 1, -12 }, +/* 'L' 0x4C */ { 1341, 8, 13, 10, 1, -12 }, +/* 'M' 0x4D */ { 1354, 13, 13, 15, 1, -12 }, +/* 'N' 0x4E */ { 1376, 11, 13, 13, 1, -12 }, +/* 'O' 0x4F */ { 1394, 13, 13, 14, 1, -12 }, +/* 'P' 0x50 */ { 1416, 10, 13, 12, 1, -12 }, +/* 'Q' 0x51 */ { 1433, 13, 14, 14, 1, -12 }, +/* 'R' 0x52 */ { 1456, 12, 13, 13, 1, -12 }, +/* 'S' 0x53 */ { 1476, 10, 13, 12, 1, -12 }, +/* 'T' 0x54 */ { 1493, 9, 13, 11, 1, -12 }, +/* 'U' 0x55 */ { 1508, 11, 13, 13, 1, -12 }, +/* 'V' 0x56 */ { 1526, 11, 13, 11, 0, -12 }, +/* 'W' 0x57 */ { 1544, 16, 13, 17, 0, -12 }, +/* 'X' 0x58 */ { 1570, 10, 13, 12, 1, -12 }, +/* 'Y' 0x59 */ { 1587, 12, 13, 12, 0, -12 }, +/* 'Z' 0x5A */ { 1607, 10, 13, 11, 1, -12 }, +/* '[' 0x5B */ { 1624, 3, 17, 5, 1, -12 }, +/* '\' 0x5C */ { 1631, 5, 13, 5, 0, -12 }, +/* ']' 0x5D */ { 1640, 3, 17, 5, 0, -12 }, +/* '^' 0x5E */ { 1647, 7, 7, 8, 1, -12 }, +/* '_' 0x5F */ { 1654, 10, 1, 10, 0, 3 }, +/* '`' 0x60 */ { 1656, 4, 3, 5, 0, -12 }, +/* 'a' 0x61 */ { 1658, 9, 10, 10, 1, -9 }, +/* 'b' 0x62 */ { 1670, 9, 13, 10, 1, -12 }, +/* 'c' 0x63 */ { 1685, 8, 10, 9, 1, -9 }, +/* 'd' 0x64 */ { 1695, 8, 13, 10, 1, -12 }, +/* 'e' 0x65 */ { 1708, 8, 10, 10, 1, -9 }, +/* 'f' 0x66 */ { 1718, 4, 13, 5, 1, -12 }, +/* 'g' 0x67 */ { 1725, 8, 14, 10, 1, -9 }, +/* 'h' 0x68 */ { 1739, 8, 13, 10, 1, -12 }, +/* 'i' 0x69 */ { 1752, 2, 13, 4, 1, -12 }, +/* 'j' 0x6A */ { 1756, 4, 17, 4, 0, -12 }, +/* 'k' 0x6B */ { 1765, 8, 13, 9, 1, -12 }, +/* 'l' 0x6C */ { 1778, 2, 13, 4, 1, -12 }, +/* 'm' 0x6D */ { 1782, 13, 10, 15, 1, -9 }, +/* 'n' 0x6E */ { 1799, 8, 10, 10, 1, -9 }, +/* 'o' 0x6F */ { 1809, 8, 10, 10, 1, -9 }, +/* 'p' 0x70 */ { 1819, 9, 13, 10, 1, -9 }, +/* 'q' 0x71 */ { 1834, 8, 13, 10, 1, -9 }, +/* 'r' 0x72 */ { 1847, 5, 10, 6, 1, -9 }, +/* 's' 0x73 */ { 1854, 8, 10, 9, 1, -9 }, +/* 't' 0x74 */ { 1864, 4, 12, 5, 1, -11 }, +/* 'u' 0x75 */ { 1870, 8, 10, 10, 1, -9 }, +/* 'v' 0x76 */ { 1880, 9, 10, 9, 0, -9 }, +/* 'w' 0x77 */ { 1892, 13, 10, 13, 0, -9 }, +/* 'x' 0x78 */ { 1909, 7, 10, 9, 1, -9 }, +/* 'y' 0x79 */ { 1918, 8, 14, 9, 0, -9 }, +/* 'z' 0x7A */ { 1932, 7, 10, 9, 1, -9 }, +/* '{' 0x7B */ { 1941, 4, 17, 6, 1, -12 }, +/* '|' 0x7C */ { 1950, 2, 17, 4, 2, -12 }, +/* '}' 0x7D */ { 1955, 4, 17, 6, 1, -12 }, +/* '~' 0x7E */ { 1964, 7, 3, 9, 1, -7 }, +/* 0x7F */ { 1967, 0, 0, 0, 0, 0 }, +/* 0x80 */ { 1967, 10, 13, 12, 1, -12 }, +/* 0x81 */ { 1984, 0, 0, 8, 0, 0 }, +/* 0x82 */ { 1984, 2, 3, 5, 1, 0 }, +/* 0x83 */ { 1985, 5, 17, 5, 0, -12 }, +/* 0x84 */ { 1996, 5, 3, 7, 1, 0 }, +/* 0x85 */ { 1998, 10, 1, 12, 1, 0 }, +/* 0x86 */ { 2000, 8, 16, 10, 1, -12 }, +/* 0x87 */ { 2016, 8, 16, 10, 1, -12 }, +/* 0x88 */ { 2032, 5, 3, 6, 0, -12 }, +/* 0x89 */ { 2034, 18, 13, 18, 0, -12 }, +/* 0x8A */ { 2064, 10, 16, 12, 1, -15 }, +/* 0x8B */ { 2084, 2, 4, 4, 1, -6 }, +/* 0x8C */ { 2085, 15, 13, 18, 1, -12 }, +/* 0x8D */ { 2110, 0, 0, 8, 0, 0 }, +/* 0x8E */ { 2110, 10, 16, 11, 1, -15 }, +/* 0x8F */ { 2130, 0, 0, 8, 0, 0 }, +/* 0x90 */ { 2130, 0, 0, 8, 0, 0 }, +/* 0x91 */ { 2130, 2, 4, 4, 2, -12 }, +/* 0x92 */ { 2131, 2, 4, 4, 1, -12 }, +/* 0x93 */ { 2132, 5, 4, 7, 2, -12 }, +/* 0x94 */ { 2135, 5, 4, 7, 1, -12 }, +/* 0x95 */ { 2138, 4, 5, 7, 1, -8 }, +/* 0x96 */ { 2141, 7, 1, 9, 1, -4 }, +/* 0x97 */ { 2142, 16, 1, 18, 1, -4 }, +/* 0x98 */ { 2144, 5, 2, 6, 0, -12 }, +/* 0x99 */ { 2146, 18, 10, 18, 1, -13 }, +/* 0x9A */ { 2169, 8, 13, 9, 1, -12 }, +/* 0x9B */ { 2182, 2, 4, 5, 2, -6 }, +/* 0x9C */ { 2183, 15, 10, 17, 1, -9 }, +/* 0x9D */ { 2202, 0, 0, 8, 0, 0 }, +/* 0x9E */ { 2202, 7, 13, 9, 1, -12 }, +/* 0x9F */ { 2214, 12, 14, 12, 0, -13 }, +/* 0xA0 */ { 2235, 0, 0, 5, 0, 0 }, +/* 0xA1 */ { 2235, 2, 13, 6, 2, -8 }, +/* 0xA2 */ { 2239, 9, 14, 10, 1, -11 }, +/* 0xA3 */ { 2255, 10, 13, 10, 0, -12 }, +/* 0xA4 */ { 2272, 7, 6, 10, 2, -8 }, +/* 0xA5 */ { 2278, 8, 13, 10, 1, -12 }, +/* 0xA6 */ { 2291, 2, 17, 5, 2, -12 }, +/* 0xA7 */ { 2296, 9, 17, 10, 1, -12 }, +/* 0xA8 */ { 2316, 6, 1, 6, 0, -11 }, +/* 0xA9 */ { 2317, 14, 13, 14, 1, -12 }, +/* 0xAA */ { 2340, 5, 8, 7, 1, -12 }, +/* 0xAB */ { 2345, 7, 6, 9, 1, -7 }, +/* 0xAC */ { 2351, 9, 5, 11, 2, -5 }, +/* 0xAD */ { 2357, 0, 0, 0, 0, 0 }, +/* 0xAE */ { 2357, 14, 13, 14, 1, -12 }, +/* 0xAF */ { 2380, 5, 1, 6, 0, -12 }, +/* 0xB0 */ { 2381, 5, 5, 11, 3, -11 }, +/* 0xB1 */ { 2385, 9, 11, 11, 1, -10 }, +/* 0xB2 */ { 2398, 6, 8, 6, 1, -13 }, +/* 0xB3 */ { 2404, 7, 8, 6, 0, -13 }, +/* 0xB4 */ { 2411, 4, 3, 6, 2, -12 }, +/* 0xB5 */ { 2413, 9, 13, 10, 1, -9 }, +/* 0xB6 */ { 2428, 8, 16, 10, 2, -12 }, +/* 0xB7 */ { 2444, 3, 1, 5, 1, -4 }, +/* 0xB8 */ { 2445, 5, 4, 6, 1, 1 }, +/* 0xB9 */ { 2448, 3, 7, 6, 2, -13 }, +/* 0xBA */ { 2451, 5, 8, 7, 1, -12 }, +/* 0xBB */ { 2456, 7, 6, 9, 1, -7 }, +/* 0xBC */ { 2462, 14, 13, 16, 2, -12 }, +/* 0xBD */ { 2485, 14, 13, 16, 2, -12 }, +/* 0xBE */ { 2508, 15, 13, 16, 1, -12 }, +/* 0xBF */ { 2533, 9, 13, 10, 1, -8 }, +/* 0xC0 */ { 2548, 8, 15, 4, -2, -15 }, +/* 0xC1 */ { 2563, 11, 13, 11, 0, -13 }, +/* 0xC2 */ { 2581, 10, 13, 12, 1, -13 }, +/* 0xC3 */ { 2598, 8, 13, 10, 2, -13 }, +/* 0xC4 */ { 2611, 13, 13, 12, -1, -13 }, +/* 0xC5 */ { 2633, 10, 13, 12, 1, -13 }, +/* 0xC6 */ { 2650, 10, 13, 11, 0, -13 }, +/* 0xC7 */ { 2667, 11, 13, 13, 1, -13 }, +/* 0xC8 */ { 2685, 12, 13, 14, 1, -13 }, +/* 0xC9 */ { 2705, 2, 13, 4, 1, -13 }, +/* 0xCA */ { 2709, 11, 13, 12, 1, -13 }, +/* 0xCB */ { 2727, 13, 13, 12, -1, -13 }, +/* 0xCC */ { 2749, 13, 13, 15, 1, -13 }, +/* 0xCD */ { 2771, 11, 13, 13, 1, -13 }, +/* 0xCE */ { 2789, 10, 13, 12, 1, -13 }, +/* 0xCF */ { 2806, 12, 13, 14, 1, -13 }, +/* 0xD0 */ { 2826, 11, 13, 13, 1, -13 }, +/* 0xD1 */ { 2844, 10, 13, 12, 1, -13 }, +/* 0xD2 */ { 2861, 0, 0, 5, 0, 0 }, +/* 0xD3 */ { 2861, 8, 13, 11, 2, -13 }, +/* 0xD4 */ { 2874, 10, 13, 12, 1, -13 }, +/* 0xD5 */ { 2891, 12, 13, 12, 0, -13 }, +/* 0xD6 */ { 2911, 12, 13, 14, 1, -13 }, +/* 0xD7 */ { 2931, 11, 13, 11, 0, -13 }, +/* 0xD8 */ { 2949, 12, 13, 14, 1, -13 }, +/* 0xD9 */ { 2969, 11, 13, 13, 1, -13 }, +/* 0xDA */ { 2987, 6, 16, 4, -1, -16 }, +/* 0xDB */ { 2999, 12, 16, 12, 0, -16 }, +/* 0xDC */ { 3023, 8, 13, 10, 1, -13 }, +/* 0xDD */ { 3036, 7, 13, 8, 1, -13 }, +/* 0xDE */ { 3048, 8, 17, 10, 1, -13 }, +/* 0xDF */ { 3065, 3, 13, 4, 1, -13 }, +/* 0xE0 */ { 3070, 8, 14, 10, 1, -14 }, +/* 0xE1 */ { 3084, 8, 10, 10, 1, -10 }, +/* 0xE2 */ { 3094, 8, 17, 10, 1, -13 }, +/* 0xE3 */ { 3111, 10, 14, 8, -1, -10 }, +/* 0xE4 */ { 3129, 8, 13, 10, 1, -13 }, +/* 0xE5 */ { 3142, 7, 10, 8, 1, -10 }, +/* 0xE6 */ { 3151, 7, 17, 8, 1, -13 }, +/* 0xE7 */ { 3166, 8, 14, 10, 1, -10 }, +/* 0xE8 */ { 3180, 8, 13, 10, 1, -13 }, +/* 0xE9 */ { 3193, 2, 10, 4, 1, -10 }, +/* 0xEA */ { 3196, 9, 10, 9, 1, -10 }, +/* 0xEB */ { 3208, 9, 13, 9, 0, -13 }, +/* 0xEC */ { 3223, 8, 14, 10, 1, -10 }, +/* 0xED */ { 3237, 9, 10, 9, 0, -10 }, +/* 0xEE */ { 3249, 7, 17, 8, 1, -13 }, +/* 0xEF */ { 3264, 8, 10, 10, 1, -10 }, +/* 0xF0 */ { 3274, 12, 10, 12, 0, -10 }, +/* 0xF1 */ { 3289, 8, 14, 10, 1, -10 }, +/* 0xF2 */ { 3303, 7, 14, 9, 1, -10 }, +/* 0xF3 */ { 3316, 10, 10, 11, 1, -10 }, +/* 0xF4 */ { 3329, 6, 10, 8, 1, -10 }, +/* 0xF5 */ { 3337, 8, 10, 10, 1, -10 }, +/* 0xF6 */ { 3347, 10, 14, 12, 1, -10 }, +/* 0xF7 */ { 3365, 9, 14, 9, 0, -10 }, +/* 0xF8 */ { 3381, 10, 14, 12, 1, -10 }, +/* 0xF9 */ { 3399, 12, 10, 14, 1, -10 }, +/* 0xFA */ { 3414, 6, 13, 4, -1, -13 }, +/* 0xFB */ { 3424, 8, 13, 10, 1, -13 }, +/* 0xFC */ { 3437, 8, 13, 10, 1, -13 }, +/* 0xFD */ { 3450, 8, 13, 10, 1, -13 }, +/* 0xFE */ { 3463, 12, 13, 14, 1, -13 }, +/* 0xFF */ { 3483, 0, 0, 5, 0, 0 }, +}; + +const GFXfont FreeSans9pt_Win1253 PROGMEM = { +(uint8_t*)FreeSans9pt_Win1253Bitmaps, +(GFXglyph*)FreeSans9pt_Win1253Glyphs, +0x01, 0xFF, 16 +}; diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index 6c7a7b491..93a621ee8 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -616,6 +616,101 @@ char InkHUD::AppletFont::applyEncoding(std::string utf8) } } + else if (encoding == WINDOWS_1253) { + // Greek + // 1-Byte chars: no remapping + if (utf8.length() == 1) + return utf8.at(0); + + // Multi-byte chars: + switch (toUtf32(utf8)) { + // Windows-1253 special characters (0x80-0xBF range) + REMAP(0x20AC, 0x80) // EURO SIGN + REMAP(0x2018, 0x91) // LEFT SINGLE QUOTATION MARK + REMAP(0x2019, 0x92) // RIGHT SINGLE QUOTATION MARK + REMAP(0x201C, 0x93) // LEFT DOUBLE QUOTATION MARK + REMAP(0x201D, 0x94) // RIGHT DOUBLE QUOTATION MARK + REMAP(0x2022, 0x95) // BULLET + REMAP(0x2013, 0x96) // EN DASH + REMAP(0x2014, 0x97) // EM DASH + + // Greek accented capitals + REMAP(0x0386, 0xA2) // GREEK CAPITAL LETTER ALPHA WITH TONOS + REMAP(0x0388, 0xB8) // GREEK CAPITAL LETTER EPSILON WITH TONOS + REMAP(0x0389, 0xB9) // GREEK CAPITAL LETTER ETA WITH TONOS + REMAP(0x038A, 0xBA) // GREEK CAPITAL LETTER IOTA WITH TONOS + REMAP(0x038C, 0xBC) // GREEK CAPITAL LETTER OMICRON WITH TONOS + REMAP(0x038E, 0xBE) // GREEK CAPITAL LETTER UPSILON WITH TONOS + REMAP(0x038F, 0xBF) // GREEK CAPITAL LETTER OMEGA WITH TONOS + + // Greek capital letters (U+0391-U+03A9 -> 0xC1-0xD1, with gaps) + REMAP(0x0391, 0xC1) // GREEK CAPITAL LETTER ALPHA + REMAP(0x0392, 0xC2) // GREEK CAPITAL LETTER BETA + REMAP(0x0393, 0xC3) // GREEK CAPITAL LETTER GAMMA + REMAP(0x0394, 0xC4) // GREEK CAPITAL LETTER DELTA + REMAP(0x0395, 0xC5) // GREEK CAPITAL LETTER EPSILON + REMAP(0x0396, 0xC6) // GREEK CAPITAL LETTER ZETA + REMAP(0x0397, 0xC7) // GREEK CAPITAL LETTER ETA + REMAP(0x0398, 0xC8) // GREEK CAPITAL LETTER THETA + REMAP(0x0399, 0xC9) // GREEK CAPITAL LETTER IOTA + REMAP(0x039A, 0xCA) // GREEK CAPITAL LETTER KAPPA + REMAP(0x039B, 0xCB) // GREEK CAPITAL LETTER LAMDA + REMAP(0x039C, 0xCC) // GREEK CAPITAL LETTER MU + REMAP(0x039D, 0xCD) // GREEK CAPITAL LETTER NU + REMAP(0x039E, 0xCE) // GREEK CAPITAL LETTER XI + REMAP(0x039F, 0xCF) // GREEK CAPITAL LETTER OMICRON + REMAP(0x03A0, 0xD0) // GREEK CAPITAL LETTER PI + REMAP(0x03A1, 0xD1) // GREEK CAPITAL LETTER RHO + REMAP(0x03A3, 0xD3) // GREEK CAPITAL LETTER SIGMA + REMAP(0x03A4, 0xD4) // GREEK CAPITAL LETTER TAU + REMAP(0x03A5, 0xD5) // GREEK CAPITAL LETTER UPSILON + REMAP(0x03A6, 0xD6) // GREEK CAPITAL LETTER PHI + REMAP(0x03A7, 0xD7) // GREEK CAPITAL LETTER CHI + REMAP(0x03A8, 0xD8) // GREEK CAPITAL LETTER PSI + REMAP(0x03A9, 0xD9) // GREEK CAPITAL LETTER OMEGA + + // Greek small letters with tonos (accented) + REMAP(0x03AC, 0xDC) // GREEK SMALL LETTER ALPHA WITH TONOS + REMAP(0x03AD, 0xDD) // GREEK SMALL LETTER EPSILON WITH TONOS + REMAP(0x03AE, 0xDE) // GREEK SMALL LETTER ETA WITH TONOS + REMAP(0x03AF, 0xDF) // GREEK SMALL LETTER IOTA WITH TONOS + + // Greek small letters (U+03B1-U+03C9 -> 0xE1-0xF9) + REMAP(0x03B1, 0xE1) // GREEK SMALL LETTER ALPHA + REMAP(0x03B2, 0xE2) // GREEK SMALL LETTER BETA + REMAP(0x03B3, 0xE3) // GREEK SMALL LETTER GAMMA + REMAP(0x03B4, 0xE4) // GREEK SMALL LETTER DELTA + REMAP(0x03B5, 0xE5) // GREEK SMALL LETTER EPSILON + REMAP(0x03B6, 0xE6) // GREEK SMALL LETTER ZETA + REMAP(0x03B7, 0xE7) // GREEK SMALL LETTER ETA + REMAP(0x03B8, 0xE8) // GREEK SMALL LETTER THETA + REMAP(0x03B9, 0xE9) // GREEK SMALL LETTER IOTA + REMAP(0x03BA, 0xEA) // GREEK SMALL LETTER KAPPA + REMAP(0x03BB, 0xEB) // GREEK SMALL LETTER LAMDA + REMAP(0x03BC, 0xEC) // GREEK SMALL LETTER MU + REMAP(0x03BD, 0xED) // GREEK SMALL LETTER NU + REMAP(0x03BE, 0xEE) // GREEK SMALL LETTER XI + REMAP(0x03BF, 0xEF) // GREEK SMALL LETTER OMICRON + REMAP(0x03C0, 0xF0) // GREEK SMALL LETTER PI + REMAP(0x03C1, 0xF1) // GREEK SMALL LETTER RHO + REMAP(0x03C2, 0xF2) // GREEK SMALL LETTER FINAL SIGMA + REMAP(0x03C3, 0xF3) // GREEK SMALL LETTER SIGMA + REMAP(0x03C4, 0xF4) // GREEK SMALL LETTER TAU + REMAP(0x03C5, 0xF5) // GREEK SMALL LETTER UPSILON + REMAP(0x03C6, 0xF6) // GREEK SMALL LETTER PHI + REMAP(0x03C7, 0xF7) // GREEK SMALL LETTER CHI + REMAP(0x03C8, 0xF8) // GREEK SMALL LETTER PSI + REMAP(0x03C9, 0xF9) // GREEK SMALL LETTER OMEGA + + // More accented small letters + REMAP(0x03CA, 0xFA) // GREEK SMALL LETTER IOTA WITH DIALYTIKA + REMAP(0x03CB, 0xFB) // GREEK SMALL LETTER UPSILON WITH DIALYTIKA + REMAP(0x03CC, 0xFC) // GREEK SMALL LETTER OMICRON WITH TONOS + REMAP(0x03CD, 0xFD) // GREEK SMALL LETTER UPSILON WITH TONOS + REMAP(0x03CE, 0xFE) // GREEK SMALL LETTER OMEGA WITH TONOS + } + } + else /*ASCII or Unhandled*/ { if (utf8.length() == 1) return utf8.at(0); diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index e1fe37974..02ba13c31 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -26,6 +26,7 @@ class AppletFont WINDOWS_1250, WINDOWS_1251, WINDOWS_1252, + WINDOWS_1253, }; AppletFont(); @@ -84,4 +85,12 @@ class AppletFont #define FREESANS_9PT_WIN1252 InkHUD::AppletFont(FreeSans9pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -2, -1) #define FREESANS_6PT_WIN1252 InkHUD::AppletFont(FreeSans6pt_Win1252, InkHUD::AppletFont::WINDOWS_1252, -1, -2) +// Greek +#include "graphics/niche/Fonts/FreeSans12pt_Win1253.h" +#include "graphics/niche/Fonts/FreeSans6pt_Win1253.h" +#include "graphics/niche/Fonts/FreeSans9pt_Win1253.h" +#define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1) +#define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1) +#define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2) + #endif \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h index f8202debb..10f628d56 100644 --- a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h @@ -50,9 +50,9 @@ void setupNicheGraphics() inkhud->setDisplayResilience(10, 1.5); // Select fonts - InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; - InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1252; - InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1253; + InkHUD::Applet::fontMedium = FREESANS_9PT_WIN1253; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1253; // Customize default settings inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? From c45c217743584ce47a268ce55d6afd21be260c58 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 10 Jan 2026 19:36:40 -0600 Subject: [PATCH 27/83] Remove a strlcpy reference (#9249) --- src/mqtt/MQTT.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 7c33f0360..4c2c0fe1b 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -494,7 +494,8 @@ bool MQTT::publish(const char *topic, const uint8_t *payload, size_t length, boo if (moduleConfig.mqtt.proxy_to_client_enabled) { meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); msg->which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; - strlcpy(msg->topic, topic, sizeof(msg->topic)); + strncpy(msg->topic, topic, sizeof(msg->topic)); + msg->topic[sizeof(msg->topic) - 1] = '\0'; // Ensure null termination if (length > sizeof(msg->payload_variant.data.bytes)) length = sizeof(msg->payload_variant.data.bytes); msg->payload_variant.data.size = length; From 727b1b3e85e92241e94b6dbe158cae6b8f6a0298 Mon Sep 17 00:00:00 2001 From: "Ted W." Date: Sat, 10 Jan 2026 20:39:11 -0500 Subject: [PATCH 28/83] Add support for sevice start wrapper (#8676) Co-authored-by: Ben Meadors Co-authored-by: Austin --- bin/meshtasticd-start.sh | 32 ++++++++++++++++++++++++++++++++ bin/meshtasticd.service | 4 ++-- bin/native-install.sh | 1 + debian/meshtasticd.install | 1 + meshtasticd.spec.rpkg | 3 +++ 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100755 bin/meshtasticd-start.sh diff --git a/bin/meshtasticd-start.sh b/bin/meshtasticd-start.sh new file mode 100755 index 000000000..b58d92085 --- /dev/null +++ b/bin/meshtasticd-start.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh + +INSTANCE=$1 +CONF_DIR="/etc/meshtasticd/config.d" +VFS_DIR="/var/lib" + +# If no instance ID provided, start bare daemon and exit +echo "no instance ID provided, starting bare meshtasticd service" +if [ -z "${INSTANCE}" ]; then + /usr/bin/meshtasticd + exit 0 +fi + +# Make VFS dir if it does not exist +if [ ! -d "${VFS_DIR}/meshtasticd-${INSTANCE}" ]; then + echo "vfs for ${INSTANCE} does not exist, creating it." + mkdir "${VFS_DIR}/meshtasticd-${INSTANCE}" +fi + +# Abort if config for $INSTANCE does not exist +if [ ! -f "${CONF_DIR}/config-${INSTANCE}.yaml" ]; then + echo "no config for ${INSTANCE} found in ${CONF_DIR}. refusing to start" >&2 + exit 1 +fi + +# Start meshtasticd with instance parameters +printf "starting meshtasticd-%s..., ${INSTANCE}" +if /usr/bin/meshtasticd --config="${CONF_DIR}/config-${INSTANCE}.yaml" --fsdir="${VFS_DIR}/meshtasticd-${INSTANCE}"; then + echo "ok" +else + echo "failed" +fi diff --git a/bin/meshtasticd.service b/bin/meshtasticd.service index 63430bae8..8ca32a8aa 100644 --- a/bin/meshtasticd.service +++ b/bin/meshtasticd.service @@ -1,5 +1,5 @@ [Unit] -Description=Meshtastic Native Daemon +Description=Meshtastic %i Daemon After=network-online.target StartLimitInterval=200 StartLimitBurst=5 @@ -9,7 +9,7 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE User=meshtasticd Group=meshtasticd Type=simple -ExecStart=/usr/bin/meshtasticd +ExecStart=/usr/bin/meshtasticd-start.sh %i Restart=always RestartSec=3 diff --git a/bin/native-install.sh b/bin/native-install.sh index 18cd9205b..dfcb7b40d 100755 --- a/bin/native-install.sh +++ b/bin/native-install.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash cp "release/meshtasticd_linux_$(uname -m)" /usr/bin/meshtasticd +cp "bin/meshtasticd-start.sh" /usr/bin/meshtasticd-start.sh mkdir -p /etc/meshtasticd if [[ -f "/etc/meshtasticd/config.yaml" ]]; then cp bin/config-dist.yaml /etc/meshtasticd/config-upgrade.yaml diff --git a/debian/meshtasticd.install b/debian/meshtasticd.install index 3c68b42b1..62c0150db 100644 --- a/debian/meshtasticd.install +++ b/debian/meshtasticd.install @@ -4,5 +4,6 @@ bin/config.yaml etc/meshtasticd bin/config.d/* etc/meshtasticd/available.d bin/meshtasticd.service lib/systemd/system +bin/meshtasticd-start.sh usr/bin web/* usr/share/meshtasticd/web diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index 3456001f0..0819d5f8d 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -95,6 +95,9 @@ cp -r bin/config.d/* %{buildroot}%{_sysconfdir}/meshtasticd/available.d # Install systemd service install -D -m 0644 bin/meshtasticd.service %{buildroot}%{_unitdir}/meshtasticd.service +# Install meshtasticd start wrapper +install -D -m 0755 bin/meshtasticd-start.sh %{buildroot}%{_bindir}/meshtasticd-start.sh + # Install the web files under /usr/share/meshtasticd/web mkdir -p %{buildroot}%{_datadir}/meshtasticd/web cp -r web/* %{buildroot}%{_datadir}/meshtasticd/web From 5dba5c82c039b1725b60f3106f1b75d68939fd72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:39:45 -0600 Subject: [PATCH 29/83] Update INA226 to v0.6.6 (#9247) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index dd8111346..293b5def1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -171,7 +171,7 @@ lib_deps = # renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip # renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226 - robtillaart/INA226@0.6.5 + robtillaart/INA226@0.6.6 # renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2 # renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library From 6f62748916996acf1471d635e2e16b6266a58067 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 05:34:56 -0600 Subject: [PATCH 30/83] Update protobufs (#9254) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/module_config.pb.h | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/protobufs b/protobufs index 1a63a3d0d..aa48faf5b 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 1a63a3d0d2ff5b2df97a1476fb20cc579e144842 +Subproject commit aa48faf5b5cd5fd1b58503efc6e0a262ae34abcd diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 2b7b54949..dd0151e3f 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -84,8 +84,11 @@ typedef enum _meshtastic_ModuleConfig_SerialConfig_Serial_Mode { https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable */ meshtastic_ModuleConfig_SerialConfig_Serial_Mode_VE_DIRECT = 7, /* Used to configure and view some parameters of MeshSolar. -https://heltec.org/project/meshsolar/ */ - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG = 8 + https://heltec.org/project/meshsolar/ */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG = 8, + /* Logs mesh traffic to the serial pins, ideal for logging via openLog or similar. */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOG = 9, /* includes other packets */ + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOGTEXT = 10 /* only text (channel & DM) */ } meshtastic_ModuleConfig_SerialConfig_Serial_Mode; /* TODO: REPLACE */ @@ -483,8 +486,8 @@ extern "C" { #define _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Baud)(meshtastic_ModuleConfig_SerialConfig_Serial_Baud_BAUD_921600+1)) #define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT -#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MAX meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG -#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Mode)(meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG+1)) +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MAX meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOGTEXT +#define _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_ARRAYSIZE ((meshtastic_ModuleConfig_SerialConfig_Serial_Mode)(meshtastic_ModuleConfig_SerialConfig_Serial_Mode_LOGTEXT+1)) #define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE #define _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MAX meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK From 8cb8540ef6b911c885a37073ac576d674608447d Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 11 Jan 2026 12:08:39 -0600 Subject: [PATCH 31/83] Add release notes generation and publishing workflow (#9255) --- .github/workflows/main_matrix.yml | 30 ++- .github/workflows/release_channels.yml | 31 +++ bin/generate_release_notes.py | 276 +++++++++++++++++++++++++ 3 files changed, 335 insertions(+), 2 deletions(-) create mode 100755 bin/generate_release_notes.py diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index d7bde7bc5..04731f335 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -247,6 +247,24 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: 3.x + + - name: Generate release notes + id: release_notes + run: | + chmod +x ./bin/generate_release_notes.py + NOTES=$(./bin/generate_release_notes.py ${{ needs.version.outputs.long }}) + echo "notes<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release uses: softprops/action-gh-release@v2 @@ -256,8 +274,7 @@ jobs: prerelease: true name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha tag_name: v${{ needs.version.outputs.long }} - body: | - Autogenerated by github action, developer should edit as required before publishing... + body: ${{ steps.release_notes.outputs.notes }} - name: Download source deb uses: actions/download-artifact@v7 @@ -381,6 +398,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v6 @@ -400,6 +419,13 @@ jobs: pattern: manifest-${{ needs.version.outputs.long }} path: ./publish + - name: Generate release notes + run: | + chmod +x ./bin/generate_release_notes.py + ./bin/generate_release_notes.py ${{ needs.version.outputs.long }} > ./publish/release_notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish firmware to meshtastic.github.io uses: peaceiris/actions-gh-pages@v4 env: diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index badbb31d4..7f925b67c 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -48,6 +48,37 @@ jobs: ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }} secrets: inherit + publish-release-notes: + if: github.event.action == 'published' + runs-on: ubuntu-latest + steps: + - name: Get release version + id: version + run: | + # Extract version from tag (e.g., v2.7.15.567b8ea -> 2.7.15.567b8ea) + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Get release notes + run: | + mkdir -p ./publish + gh release view ${{ github.event.release.tag_name }} --json body --jq '.body' > ./publish/release_notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish release notes to meshtastic.github.io + uses: peaceiris/actions-gh-pages@v4 + with: + deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} + external_repository: meshtastic/meshtastic.github.io + publish_branch: master + publish_dir: ./publish + destination_dir: firmware-${{ steps.version.outputs.version }} + user_name: github-actions[bot] + user_email: github-actions[bot]@users.noreply.github.com + commit_message: Release notes for ${{ steps.version.outputs.version }} + enable_jekyll: true + # Create a PR to bump version when a release is Published bump-version: if: github.event.action == 'published' diff --git a/bin/generate_release_notes.py b/bin/generate_release_notes.py new file mode 100755 index 000000000..7c9ecb420 --- /dev/null +++ b/bin/generate_release_notes.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +""" +Generate release notes from merged PRs on develop and master branches. +Categorizes PRs into Enhancements and Bug Fixes/Maintenance sections. +""" + +import subprocess +import re +import json +import sys +from datetime import datetime + + +def get_last_release_tag(): + """Get the most recent release tag.""" + result = subprocess.run( + ["git", "describe", "--tags", "--abbrev=0"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +def get_merged_prs_since_tag(tag, branch): + """Get all merged PRs since the given tag on the specified branch.""" + # Get commits since tag on the branch - look for PR numbers in parentheses + result = subprocess.run( + [ + "git", + "log", + f"{tag}..origin/{branch}", + "--oneline", + ], + capture_output=True, + text=True, + ) + + prs = [] + seen_pr_numbers = set() + + for line in result.stdout.strip().split("\n"): + if not line: + continue + + # Extract PR number from commit message - format: "Title (#1234)" + pr_match = re.search(r"\(#(\d+)\)", line) + if pr_match: + pr_number = pr_match.group(1) + if pr_number not in seen_pr_numbers: + seen_pr_numbers.add(pr_number) + prs.append(pr_number) + + return prs + + +def get_pr_details(pr_number): + """Get PR details from GitHub API via gh CLI.""" + try: + result = subprocess.run( + [ + "gh", + "pr", + "view", + pr_number, + "--json", + "title,author,labels,url", + ], + capture_output=True, + text=True, + check=True, + ) + return json.loads(result.stdout) + except subprocess.CalledProcessError: + return None + + +def should_exclude_pr(pr_details): + """Check if PR should be excluded from release notes.""" + if not pr_details: + return True + + title = pr_details.get("title", "").lower() + + # Exclude trunk update PRs + if "upgrade trunk" in title or "update trunk" in title or "trunk update" in title: + return True + + # Exclude protobuf update PRs + if "update protobufs" in title or "update protobuf" in title: + return True + + # Exclude automated version bump PRs + if "bump release version" in title or "bump version" in title: + return True + + # Exclude automated dependency digest updates (chore(deps): update X digest to Y) + if re.match(r"^chore\(deps\):\s*update .+ digest to [a-f0-9]+$", title, re.IGNORECASE): + return True + + # Exclude generic "Update X digest to Y" patterns + if re.match(r"^update .+ digest to [a-f0-9]+$", title, re.IGNORECASE): + return True + + return False + + +def is_enhancement(pr_details): + """Determine if PR is an enhancement based on labels and title.""" + labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])] + + # Check labels first + enhancement_labels = ["enhancement", "feature", "feat", "new feature"] + for label in labels: + if any(enh in label for enh in enhancement_labels): + return True + + # Check title prefixes + title = pr_details.get("title", "") + enhancement_prefixes = ["feat:", "feature:", "add:"] + title_lower = title.lower() + for prefix in enhancement_prefixes: + if title_lower.startswith(prefix) or f" {prefix}" in title_lower: + return True + + return False + + +def clean_title(title): + """Clean up PR title for release notes.""" + # Remove common prefixes + prefixes_to_remove = [ + r"^fix:\s*", + r"^feat:\s*", + r"^feature:\s*", + r"^bug:\s*", + r"^bugfix:\s*", + r"^chore:\s*", + r"^chore\([^)]+\):\s*", + r"^refactor:\s*", + r"^docs:\s*", + r"^ci:\s*", + r"^build:\s*", + r"^perf:\s*", + r"^style:\s*", + r"^test:\s*", + ] + + cleaned = title + for prefix in prefixes_to_remove: + cleaned = re.sub(prefix, "", cleaned, flags=re.IGNORECASE) + + # Ensure first letter is capitalized + if cleaned: + cleaned = cleaned[0].upper() + cleaned[1:] + + return cleaned.strip() + + +def format_pr_line(pr_details): + """Format a PR as a markdown bullet point.""" + title = clean_title(pr_details.get("title", "Unknown")) + author = pr_details.get("author", {}).get("login", "unknown") + url = pr_details.get("url", "") + + return f"- {title} by @{author} in {url}" + + +def get_new_contributors(pr_details_list, tag): + """Find contributors who made their first contribution in this release.""" + # Exclude bots from new contributors + bot_authors = {"github-actions", "renovate", "dependabot", "app/renovate", "app/github-actions", "app/dependabot"} + + new_contributors = [] + seen_authors = set() + + for pr in pr_details_list: + author = pr.get("author", {}).get("login", "") + if not author or author in seen_authors: + continue + + # Skip bots + if author.lower() in bot_authors or author.startswith("app/"): + continue + + seen_authors.add(author) + + # Check if this author appears in git history before tag + author_check = subprocess.run( + ["git", "log", f"{tag}", f"--author={author}", "--oneline", "-1"], + capture_output=True, + text=True, + ) + if not author_check.stdout.strip(): + new_contributors.append((author, pr.get("url", ""))) + + return new_contributors + + +def main(): + if len(sys.argv) < 2: + print("Usage: generate_release_notes.py ", file=sys.stderr) + sys.exit(1) + + new_version = sys.argv[1] + + # Get last release tag + try: + last_tag = get_last_release_tag() + except subprocess.CalledProcessError: + print("Error: Could not find last release tag", file=sys.stderr) + sys.exit(1) + + # Collect PRs from both branches + all_pr_numbers = set() + + for branch in ["develop", "master"]: + try: + prs = get_merged_prs_since_tag(last_tag, branch) + all_pr_numbers.update(prs) + except Exception as e: + print(f"Warning: Could not get PRs from {branch}: {e}", file=sys.stderr) + + # Get details for all PRs + enhancements = [] + bug_fixes = [] + all_pr_details = [] + + for pr_number in sorted(all_pr_numbers, key=int): + details = get_pr_details(pr_number) + if details and not should_exclude_pr(details): + all_pr_details.append(details) + if is_enhancement(details): + enhancements.append(details) + else: + bug_fixes.append(details) + + # Generate release notes + output = [] + + if enhancements: + output.append("## 🚀 Enhancements\n") + for pr in enhancements: + output.append(format_pr_line(pr)) + output.append("") + + if bug_fixes: + output.append("## 🐛 Bug fixes and maintenance\n") + for pr in bug_fixes: + output.append(format_pr_line(pr)) + output.append("") + + # Find new contributors + new_contributors = get_new_contributors(all_pr_details, last_tag) + if new_contributors: + output.append("## New Contributors\n") + for author, url in new_contributors: + # Find first PR URL for this contributor + first_pr_url = url + for pr in all_pr_details: + if pr.get("author", {}).get("login") == author: + first_pr_url = pr.get("url", url) + break + output.append(f"- @{author} made their first contribution in {first_pr_url}") + output.append("") + + # Add full changelog link + output.append( + f"**Full Changelog**: https://github.com/meshtastic/firmware/compare/{last_tag}...v{new_version}" + ) + + print("\n".join(output)) + + +if __name__ == "__main__": + main() From bafdeb4275d3aea96494e97e016b8d593b89b08f Mon Sep 17 00:00:00 2001 From: Austin Date: Sun, 11 Jan 2026 15:30:42 -0500 Subject: [PATCH 32/83] CI: Unified ESP32 OTA firmware + manifests (#9258) --- .github/workflows/build_firmware.yml | 41 +++++++++++++++------------- bin/platformio-custom.py | 5 +++- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 19381e211..2777d86cd 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -29,23 +29,6 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - name: Set OTA firmware source and target - if: startsWith(inputs.platform, 'esp32') - id: ota_dir - env: - PIO_PLATFORM: ${{ inputs.platform }} - run: | - if [ "$PIO_PLATFORM" = "esp32s3" ]; then - echo "src=firmware-s3.bin" >> $GITHUB_OUTPUT - echo "tgt=release/bleota-s3.bin" >> $GITHUB_OUTPUT - elif [ "$PIO_PLATFORM" = "esp32c3" ] || [ "$PIO_PLATFORM" = "esp32c6" ]; then - echo "src=firmware-c3.bin" >> $GITHUB_OUTPUT - echo "tgt=release/bleota-c3.bin" >> $GITHUB_OUTPUT - elif [ "$PIO_PLATFORM" = "esp32" ]; then - echo "src=firmware.bin" >> $GITHUB_OUTPUT - echo "tgt=release/bleota.bin" >> $GITHUB_OUTPUT - fi - - name: Build ${{ inputs.platform }} id: build uses: meshtastic/gh-action-firmware@main @@ -53,8 +36,28 @@ jobs: pio_platform: ${{ inputs.platform }} pio_env: ${{ inputs.pio_env }} pio_target: build - ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }} - ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }} + + - name: ESP32 - Download Unified OTA firmware + # Currently only esp32 and esp32s3 use the unified ota + if: inputs.platform == 'esp32' || inputs.platform == 'esp32s3' + id: dl-ota-unified + env: + PIO_PLATFORM: ${{ inputs.platform }} + PIO_ENV: ${{ inputs.pio_env }} + OTA_URL: https://github.com/meshtastic/esp32-unified-ota/releases/latest/download/mt-${{ inputs.platform }}-ota.bin + working-directory: release + run: | + curl -L -o "mt-$PIO_PLATFORM-ota.bin" $OTA_URL + + - name: ESP32-C* - Download BLE-Only OTA firmware + if: inputs.platform == 'esp32c3' || inputs.platform == 'esp32c6' + id: dl-ota-ble + env: + PIO_ENV: ${{ inputs.pio_env }} + OTA_URL: https://github.com/meshtastic/firmware-ota/releases/latest/download/firmware-c3.bin + working-directory: release + run: | + curl -L -o bleota-c3.bin $OTA_URL - name: Job summary env: diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index 90d733ca7..7481500db 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -58,6 +58,7 @@ def manifest_gather(source, target, env): manifest_ran = True out = [] board_platform = env.BoardConfig().get("platform") + board_mcu = env.BoardConfig().get("build.mcu").lower() needs_ota_suffix = board_platform == "nordicnrf52" check_paths = [ progname, @@ -69,7 +70,9 @@ def manifest_gather(source, target, env): f"{progname}.uf2", f"{progname}.factory.uf2", f"{progname}.zip", - lfsbin + lfsbin, + f"mt-{board_mcu}-ota.bin", + "bleota-c3.bin" ] for p in check_paths: f = env.File(env.subst(f"$BUILD_DIR/{p}")) From 8cf8fbb8e0283259f38d2cecdca61f8bb6a9ed4b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 11 Jan 2026 16:20:47 -0600 Subject: [PATCH 33/83] Add unified OTA to manifest (#9261) --- .github/workflows/build_firmware.yml | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 2777d86cd..77260cafe 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -59,6 +59,44 @@ jobs: run: | curl -L -o bleota-c3.bin $OTA_URL + - name: Update manifest with OTA file + if: inputs.platform == 'esp32' || inputs.platform == 'esp32s3' || inputs.platform == 'esp32c3' || inputs.platform == 'esp32c6' + working-directory: release + env: + PIO_PLATFORM: ${{ inputs.platform }} + run: | + # Determine OTA filename based on platform + if [[ "$PIO_PLATFORM" == "esp32" || "$PIO_PLATFORM" == "esp32s3" ]]; then + OTA_FILE="mt-${PIO_PLATFORM}-ota.bin" + else + OTA_FILE="bleota-c3.bin" + fi + + # Check if OTA file exists + if [[ ! -f "$OTA_FILE" ]]; then + echo "OTA file $OTA_FILE not found, skipping manifest update" + exit 0 + fi + + # Calculate MD5 and size + if command -v md5sum &> /dev/null; then + OTA_MD5=$(md5sum "$OTA_FILE" | cut -d' ' -f1) + else + OTA_MD5=$(md5 -q "$OTA_FILE") + fi + OTA_SIZE=$(stat -f%z "$OTA_FILE" 2>/dev/null || stat -c%s "$OTA_FILE") + + # Find and update manifest file + for manifest in firmware-*.mt.json; do + if [[ -f "$manifest" ]]; then + echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)" + # Add OTA entry to files array if not already present + jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" \ + 'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes}] else . end' \ + "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest" + fi + done + - name: Job summary env: PIO_ENV: ${{ inputs.pio_env }} From c0f60ad664e9fb6ab35dc689c51010376f13ee76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:28:42 -0600 Subject: [PATCH 34/83] chore(deps): update meshtastic/device-ui digest to 12f8cdd (#9263) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 1260d56b6..94175e855 100644 --- a/platformio.ini +++ b/platformio.ini @@ -119,7 +119,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/272defcb35651461830ebfd1b39c9167c8f49317.zip + https://github.com/meshtastic/device-ui/archive/12f8cddc1e2908e1988da21e3500c695668e8d92.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From f38b4c1a983541da3091c4fde4c1a64d52404b0b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:30:27 -0600 Subject: [PATCH 35/83] chore(deps): update meshtastic-gxepd2 digest to a05c11c (#9264) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini | 2 +- variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini | 6 +++--- variants/esp32s3/heltec_vision_master_e213/platformio.ini | 2 +- variants/esp32s3/heltec_vision_master_e290/platformio.ini | 2 +- variants/esp32s3/heltec_wireless_paper/platformio.ini | 2 +- variants/esp32s3/heltec_wireless_paper_v1/platformio.ini | 2 +- variants/esp32s3/tlora_t3s3_epaper/platformio.ini | 2 +- variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini | 2 +- variants/nrf52840/heltec_mesh_pocket/platformio.ini | 4 ++-- variants/nrf52840/meshlink/platformio.ini | 2 +- variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini | 2 +- variants/nrf52840/t-echo/platformio.ini | 2 +- 12 files changed, 15 insertions(+), 15 deletions(-) diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini index 59ac625b6..9994cf665 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini @@ -27,6 +27,6 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip # renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino maxpromer/PCA9557-arduino@1.0.0 diff --git a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini index 315a53ffd..7a0bd31b4 100644 --- a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini +++ b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini @@ -26,7 +26,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:crowpanel-esp32s3-4-epaper] extends = esp32s3_base @@ -56,7 +56,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:crowpanel-esp32s3-2-epaper] extends = esp32s3_base @@ -86,4 +86,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip diff --git a/variants/esp32s3/heltec_vision_master_e213/platformio.ini b/variants/esp32s3/heltec_vision_master_e213/platformio.ini index a03755970..4ace5a45a 100644 --- a/variants/esp32s3/heltec_vision_master_e213/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e213/platformio.ini @@ -29,7 +29,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-vision-master-e213-inkhud] diff --git a/variants/esp32s3/heltec_vision_master_e290/platformio.ini b/variants/esp32s3/heltec_vision_master_e290/platformio.ini index 4cc913668..e86746b67 100644 --- a/variants/esp32s3/heltec_vision_master_e290/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e290/platformio.ini @@ -32,7 +32,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/448c8538129fde3d02a7cb5e6fc81971ad92547f.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-vision-master-e290-inkhud] diff --git a/variants/esp32s3/heltec_wireless_paper/platformio.ini b/variants/esp32s3/heltec_wireless_paper/platformio.ini index ac32fb219..673c834ea 100644 --- a/variants/esp32s3/heltec_wireless_paper/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper/platformio.ini @@ -29,7 +29,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 [env:heltec-wireless-paper-inkhud] diff --git a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini index a4a21c55c..8543e414f 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini @@ -26,5 +26,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip upload_speed = 115200 diff --git a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini index fdf3f7814..256cdc0d0 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini @@ -31,7 +31,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:tlora-t3s3-epaper-inkhud] extends = esp32s3_base, inkhud diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini index 041d3b76f..a4687669b 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini @@ -33,7 +33,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM khoih-prog/nRF52_PWM@1.0.1 ;upload_protocol = fs diff --git a/variants/nrf52840/heltec_mesh_pocket/platformio.ini b/variants/nrf52840/heltec_mesh_pocket/platformio.ini index 4dc8b78e7..646304a5a 100644 --- a/variants/nrf52840/heltec_mesh_pocket/platformio.ini +++ b/variants/nrf52840/heltec_mesh_pocket/platformio.ini @@ -38,7 +38,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud @@ -101,7 +101,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini index 26a999fbb..e2631affe 100644 --- a/variants/nrf52840/meshlink/platformio.ini +++ b/variants/nrf52840/meshlink/platformio.ini @@ -47,7 +47,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlin lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index 6b36a79c3..60d83b95a 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -34,7 +34,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_w lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip debug_tool = jlink [env:seeed_wio_tracker_L1_eink-inkhud] diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index a8fc027c8..9a66890a7 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -30,7 +30,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip + https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.3 ;upload_protocol = fs From b6b129650af37b6c67f980e6d9d36b78e45a0c2c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 11 Jan 2026 18:27:06 -0600 Subject: [PATCH 36/83] Extra pins (#9260) * Maybe add working extra GPIO pins to portduino * Fix typo and add config.yaml example for ExtraPins * Write extra pins back out with -y flag --- bin/config-dist.yaml | 2 ++ src/platform/portduino/PortduinoGlue.cpp | 28 ++++++++++++++++++++++++ src/platform/portduino/PortduinoGlue.h | 16 ++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index adf804ba9..3c996051e 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -105,6 +105,8 @@ Lora: GPS: # SerialPath: /dev/ttyS0 +# ExtraPins: +# - 22 ### Specify I2C device, or leave blank for none diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index af7e275c6..7430c2eae 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -487,6 +487,11 @@ void portduinoSetup() max_GPIO = i->pin; } + for (auto i : portduino_config.extra_pins) { + if (i.enabled && i.pin > max_GPIO) + max_GPIO = i.pin; + } + gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. // Need to bind all the configured GPIO pins so they're not simulated @@ -504,6 +509,19 @@ void portduinoSetup() } } } + for (auto i : portduino_config.extra_pins) { + // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora + // Those GPIO are handled in our usermode driver instead. + if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { + continue; + } + if (i.enabled) { + if (initGPIOPin(i.pin, gpioChipName + std::to_string(i.gpiochip), i.line) != ERRNO_OK) { + printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i.line); + exit(EXIT_FAILURE); + } + } + } // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { @@ -717,6 +735,16 @@ bool loadConfig(const char *configPath) portduino_config.has_gps = 1; } } + if (yamlConfig["GPIO"]["ExtraPins"]) { + for (auto extra_pin : yamlConfig["GPIO"]["ExtraPins"]) { + portduino_config.extra_pins.push_back(pinMapping()); + portduino_config.extra_pins.back().config_section = "GPIO"; + portduino_config.extra_pins.back().config_name = "ExtraPins"; + portduino_config.extra_pins.back().enabled = true; + readGPIOFromYaml(extra_pin, portduino_config.extra_pins.back()); + } + } + if (yamlConfig["I2C"]) { portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as(""); } diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 9335be90a..8992f5f1a 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "LR11x0Interface.h" #include "Module.h" @@ -97,6 +98,7 @@ extern struct portduino_config_struct { pinMapping lora_txen_pin = {"Lora", "TXen"}; pinMapping lora_rxen_pin = {"Lora", "RXen"}; pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; + std::vector extra_pins = {}; // GPS bool has_gps = false; @@ -300,6 +302,20 @@ extern struct portduino_config_struct { } out << YAML::EndMap; // Lora + if (!extra_pins.empty()) { + out << YAML::Key << "GPIO" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "ExtraPins" << YAML::Value << YAML::BeginSeq; + for (auto extra : extra_pins) { + out << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << extra.pin; + out << YAML::Key << "line" << YAML::Value << extra.line; + out << YAML::Key << "gpiochip" << YAML::Value << extra.gpiochip; + out << YAML::EndMap; + } + out << YAML::EndSeq; + out << YAML::EndMap; // GPIO + } + if (i2cdev != "") { out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev; From f805aec86749dbe7ed3bfd2a3d938143cf138ac1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 18:36:37 -0600 Subject: [PATCH 37/83] Update GxEPD2 to v1.6.5 (#9266) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32s3/t-deck-pro/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index 28fef86ba..b2c91dcf5 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -29,7 +29,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.4 + zinggjm/GxEPD2@1.6.5 # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 From e9bdd2b031b51a4984bb469c4aa0c03ddca14308 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 18:36:47 -0600 Subject: [PATCH 38/83] Update ArduinoJson to v6.21.5 (#9265) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/nrf52840/heltec_mesh_solar/platformio.ini | 2 +- variants/nrf52840/rak4631_eth_gw/platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini index 2ad699544..69264f0df 100644 --- a/variants/nrf52840/heltec_mesh_solar/platformio.ini +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -16,7 +16,7 @@ lib_deps = # renovate: datasource=git-refs depName=NMIoT-meshsolar packageName=https://github.com/NMIoT/meshsolar gitBranch=main https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson - bblanchon/ArduinoJson@6.21.4 + bblanchon/ArduinoJson@6.21.5 [env:heltec-mesh-solar] custom_meshtastic_hw_model = 108 diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini index bfa4924ce..e06a271aa 100644 --- a/variants/nrf52840/rak4631_eth_gw/platformio.ini +++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini @@ -36,7 +36,7 @@ lib_deps = # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson - bblanchon/ArduinoJson@6.21.4 + bblanchon/ArduinoJson@6.21.5 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds ;upload_protocol = jlink From e1605d126f202ed8a9bf23175762495dd88ee186 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 11 Jan 2026 19:35:41 -0600 Subject: [PATCH 39/83] Fix warning and exclude powermon by default --- platformio.ini | 1 + src/nimble/NimbleBluetooth.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 94175e855..546de2df3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -54,6 +54,7 @@ build_flags = -Wno-missing-field-initializers -DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1 -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 + -DMESHTASTIC_EXCLUDE_POWERMON=1 -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 3b98eca3d..fc1f27ea2 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -313,11 +313,11 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { PhoneAPI::onNowHasData(fromRadioNum); +#ifdef DEBUG_NIMBLE_NOTIFY + int currentNotifyCount = notifyCount.fetch_add(1); uint8_t cc = bleServer->getConnectedCount(); - -#ifdef DEBUG_NIMBLE_NOTIFY // This logging slows things down when there are lots of packets going to the phone, like initial connection: LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); #endif From d4045dff2cf9877aee0661970e50316cfc2e8c4a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 11 Jan 2026 19:37:20 -0600 Subject: [PATCH 40/83] Remove INTERRUPT_ATTR from disableInterrupt methods on interfaces --- src/mesh/LR11x0Interface.cpp | 2 +- src/mesh/RF95Interface.cpp | 2 +- src/mesh/SX126xInterface.cpp | 2 +- src/mesh/SX128xInterface.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index af6dd92e9..341afe78d 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -186,7 +186,7 @@ template bool LR11x0Interface::reconfigure() return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR LR11x0Interface::disableInterrupt() +template void LR11x0Interface::disableInterrupt() { lora.clearIrqAction(); } diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index da0039d38..5588fc348 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -193,7 +193,7 @@ bool RF95Interface::init() return res == RADIOLIB_ERR_NONE; } -void INTERRUPT_ATTR RF95Interface::disableInterrupt() +void RF95Interface::disableInterrupt() { lora->clearDio0Action(); } diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 0e3069c14..498496a3b 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -256,7 +256,7 @@ template bool SX126xInterface::reconfigure() return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX126xInterface::disableInterrupt() +template void SX126xInterface::disableInterrupt() { lora.clearDio1Action(); } diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 80872af07..b4278c636 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -155,7 +155,7 @@ template bool SX128xInterface::reconfigure() return RADIOLIB_ERR_NONE; } -template void INTERRUPT_ATTR SX128xInterface::disableInterrupt() +template void SX128xInterface::disableInterrupt() { lora.clearDio1Action(); } From 5ce821c7751af922c374152bf9f1a71812bd6e6d Mon Sep 17 00:00:00 2001 From: Ford Jones <107664313+ford-jones@users.noreply.github.com> Date: Mon, 12 Jan 2026 04:59:51 +0000 Subject: [PATCH 41/83] Mute specific nodes (#9209) * Regen protobufs * Ensure mute state is set when node is ignored * Added mechanism for toggling muted state * Implement the ability to mute specific nodes * Switch boolean value for bitmask * Correctly toggle bitfield position 2 on-change to mute state * Dont push submodule refs * Log correct info * Trunk fmt * Update protobuf ref to master branch of base * Update src/modules/ExternalNotificationModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Re-sync generated files --------- Co-authored-by: Jason P Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Bennett --- src/mesh/NodeDB.h | 2 ++ src/mesh/TypeConversions.cpp | 1 + src/modules/AdminModule.cpp | 10 ++++++++++ src/modules/ExternalNotificationModule.cpp | 12 +++++++++--- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 817e31617..adf2b42ea 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -378,6 +378,8 @@ extern meshtastic_CriticalErrorCode error_code; extern uint32_t error_address; #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0 #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT) +#define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1 +#define NODEINFO_BITFIELD_IS_MUTED_MASK (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT) #define Module_Config_size \ (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ diff --git a/src/mesh/TypeConversions.cpp b/src/mesh/TypeConversions.cpp index 17cd92851..75195bd42 100644 --- a/src/mesh/TypeConversions.cpp +++ b/src/mesh/TypeConversions.cpp @@ -14,6 +14,7 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo info.is_favorite = lite->is_favorite; info.is_ignored = lite->is_ignored; info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + info.is_muted = lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK; if (lite->has_hops_away) { info.has_hops_away = true; diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 5f0c27fff..5eac64a62 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -383,6 +383,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta } break; } + case meshtastic_AdminMessage_toggle_muted_node_tag: { + LOG_INFO("Client received toggle_muted_node command"); + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node); + if (node != NULL) { + node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT); + saveChanges(SEGMENT_NODEDATABASE, false); + } + break; + } + case meshtastic_AdminMessage_set_fixed_position_tag: { LOG_INFO("Client received set_fixed_position command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 3f6375a65..04fcd8e73 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -459,7 +459,13 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } + meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); + bool mutedNode = false; + if (sender) { + mutedNode = (sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK); + } meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); + if (moduleConfig.external_notification.alert_bell) { if (containsBell) { LOG_INFO("externalNotificationModule - Notification Bell"); @@ -510,7 +516,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_message && + if (moduleConfig.external_notification.alert_message && !mutedNode && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { LOG_INFO("externalNotificationModule - Notification Module"); isNagging = true; @@ -522,7 +528,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_message_vibra && + if (moduleConfig.external_notification.alert_message_vibra && !mutedNode && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); isNagging = true; @@ -534,7 +540,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_message_buzzer && + if (moduleConfig.external_notification.alert_message_buzzer && !mutedNode && (!ch.settings.has_module_settings || !ch.settings.module_settings.is_muted)) { LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || From 723d8cac791bea004a8e418c18e5275af91f3ae8 Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 12 Jan 2026 09:41:34 -0500 Subject: [PATCH 42/83] CI: tiny - include mt-ota in firmware zips (#9275) --- .github/workflows/main_matrix.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 4f7cbf194..9e7fe50f6 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -201,6 +201,7 @@ jobs: ./device-*.bat ./littlefs-*.bin ./bleota*bin + ./mt-*-ota.bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 From f4d7dab4ca3dc54c496389cd0000d28876fcf6b3 Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 12 Jan 2026 09:43:09 -0500 Subject: [PATCH 43/83] EXCLUDE_AUDIO on (original) ESP32 (#9276) iram is scarce, give it back! --- variants/esp32/esp32-common.ini | 2 -- variants/esp32/esp32.ini | 3 ++- variants/esp32c3/esp32c3.ini | 5 +++++ variants/esp32s2/esp32s2.ini | 7 ++++++- variants/esp32s3/esp32s3.ini | 5 +++++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index e582b6880..81a49223b 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -65,8 +65,6 @@ lib_deps = https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip - # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master - https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index d1a8a63b0..5999bc098 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -5,4 +5,5 @@ extends = esp32_common custom_esp32_kind = esp32 build_flags = - ${esp32_common.build_flags} \ No newline at end of file + ${esp32_common.build_flags} + -DMESHTASTIC_EXCLUDE_AUDIO=1 diff --git a/variants/esp32c3/esp32c3.ini b/variants/esp32c3/esp32c3.ini index 2d7ae71bc..e5f117ad7 100644 --- a/variants/esp32c3/esp32c3.ini +++ b/variants/esp32c3/esp32c3.ini @@ -4,3 +4,8 @@ custom_esp32_kind = esp32c3 monitor_speed = 115200 monitor_filters = esp32_c3_exception_decoder + +lib_deps = + ${esp32_common.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master + https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip diff --git a/variants/esp32s2/esp32s2.ini b/variants/esp32s2/esp32s2.ini index c806943ee..836e31d8d 100644 --- a/variants/esp32s2/esp32s2.ini +++ b/variants/esp32s2/esp32s2.ini @@ -12,7 +12,12 @@ build_flags = -DHAS_BLUETOOTH=0 -DMESHTASTIC_EXCLUDE_PAXCOUNTER -DMESHTASTIC_EXCLUDE_BLUETOOTH - + +lib_deps = + ${esp32_common.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master + https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip + lib_ignore = ${esp32_common.lib_ignore} NimBLE-Arduino diff --git a/variants/esp32s3/esp32s3.ini b/variants/esp32s3/esp32s3.ini index 5e333f3ce..299415442 100644 --- a/variants/esp32s3/esp32s3.ini +++ b/variants/esp32s3/esp32s3.ini @@ -3,3 +3,8 @@ extends = esp32_common custom_esp32_kind = esp32s3 monitor_speed = 115200 + +lib_deps = + ${esp32_common.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master + https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip From 405c4f33af423d01dedb5eeeaaca5e683fa47ea0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 08:43:27 -0600 Subject: [PATCH 44/83] Upgrade trunk (#9270) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 54a803206..30dec205a 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,7 +9,7 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.75.0 + - renovate@42.78.2 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.37.1 From 986d70db6ad344318dfff44de3b6c39e2c5337c5 Mon Sep 17 00:00:00 2001 From: Martin Emrich <6672718+MartinEmrich@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:52:39 +0100 Subject: [PATCH 45/83] Pioarduino preparation (#9223) * Resolve naming conflict of Syslog class with namespace * do not include libpax headers if pax counter is excluded * clean only top-level sdkconfigs, keep them in the variants directories * Fix code formatting --- .gitignore | 2 +- src/DebugConfiguration.cpp | 5 ++++- src/DebugConfiguration.h | 6 +++++- src/RedirectablePrint.cpp | 2 +- src/mesh/eth/ethClient.cpp | 2 +- src/mesh/wifi/WiFiAPClient.cpp | 2 +- src/modules/esp32/PaxcounterModule.h | 4 ++-- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 06e8c472f..769603202 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,5 @@ arduino-lib-builder* dependencies.lock idf_component.yml CMakeLists.txt -sdkconfig.* +/sdkconfig.* .dummy/* diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index d65c4f1e8..08c7abc04 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -41,7 +41,8 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...) } #if HAS_NETWORKING - +namespace meshtastic +{ Syslog::Syslog(UDP &client) { this->_client = &client; @@ -195,4 +196,6 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess return true; } +}; // namespace meshtastic + #endif diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index 98bbe0f72..eac6260fc 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -162,6 +162,8 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...); #if HAS_NETWORKING +namespace meshtastic +{ class Syslog { private: @@ -195,4 +197,6 @@ class Syslog bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); }; -#endif // HAS_NETWORKING \ No newline at end of file +}; // namespace meshtastic + +#endif // HAS_NETWORKING diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 895dcb147..e15d56912 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -18,7 +18,7 @@ #endif #if HAS_NETWORKING -extern Syslog syslog; +extern meshtastic::Syslog syslog; #endif void RedirectablePrint::rpInit() { diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 2b4f63512..a811ec16c 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -21,7 +21,7 @@ uint32_t ntp_renew = 0; #endif EthernetUDP syslogClient; -Syslog syslog(syslogClient); +meshtastic::Syslog syslog(syslogClient); bool ethStartupComplete = 0; diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index 45944872e..a95dfa58f 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -58,7 +58,7 @@ bool needReconnect = true; // If we create our reconnector, run it once at the bool isReconnecting = false; // If we are currently reconnecting WiFiUDP syslogClient; -Syslog syslog(syslogClient); +meshtastic::Syslog syslog(syslogClient); Periodic *wifiReconnect; diff --git a/src/modules/esp32/PaxcounterModule.h b/src/modules/esp32/PaxcounterModule.h index ebd6e7191..50656e32e 100644 --- a/src/modules/esp32/PaxcounterModule.h +++ b/src/modules/esp32/PaxcounterModule.h @@ -2,7 +2,7 @@ #include "ProtobufModule.h" #include "configuration.h" -#if defined(ARCH_ESP32) +#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_PAXCOUNTER #include "../mesh/generated/meshtastic/paxcount.pb.h" #include "NodeDB.h" #include @@ -35,4 +35,4 @@ class PaxcounterModule : private concurrency::OSThread, public ProtobufModule Date: Mon, 12 Jan 2026 10:53:31 -0500 Subject: [PATCH 46/83] Update RadioLib to v7.5.0 (#9281) --- platformio.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 1260d56b6..caf8957ce 100644 --- a/platformio.ini +++ b/platformio.ini @@ -113,8 +113,7 @@ lib_deps = [radiolib_base] lib_deps = # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib - # jgromes/RadioLib@7.4.0 - https://github.com/jgromes/RadioLib/archive/536c7267362e2c1345be7054ba45e503252975ff.zip + jgromes/RadioLib@7.5.0 [device-ui_base] lib_deps = From 3b6ea95375031d4d7c63b1312a537d6203d7d491 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 12 Jan 2026 10:22:22 -0600 Subject: [PATCH 47/83] Enhance release notes generation by adding dependency update checks and improving new contributor detection --- bin/generate_release_notes.py | 113 +++++++++++++++++++++++++++++----- 1 file changed, 96 insertions(+), 17 deletions(-) diff --git a/bin/generate_release_notes.py b/bin/generate_release_notes.py index 7c9ecb420..d0f1147da 100755 --- a/bin/generate_release_notes.py +++ b/bin/generate_release_notes.py @@ -22,6 +22,17 @@ def get_last_release_tag(): return result.stdout.strip() +def get_tag_date(tag): + """Get the commit date (ISO 8601) of the tag.""" + result = subprocess.run( + ["git", "show", "-s", "--format=%cI", tag], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + def get_merged_prs_since_tag(tag, branch): """Get all merged PRs since the given tag on the specified branch.""" # Get commits since tag on the branch - look for PR numbers in parentheses @@ -94,12 +105,33 @@ def should_exclude_pr(pr_details): if "bump release version" in title or "bump version" in title: return True - # Exclude automated dependency digest updates (chore(deps): update X digest to Y) - if re.match(r"^chore\(deps\):\s*update .+ digest to [a-f0-9]+$", title, re.IGNORECASE): + return False + + +def is_dependency_update(pr_details): + """Check if PR is a dependency/chore update.""" + if not pr_details: + return False + + title = pr_details.get("title", "").lower() + author = pr_details.get("author", {}).get("login", "").lower() + labels = [label.get("name", "").lower() for label in pr_details.get("labels", [])] + + # Check for renovate or dependabot authors + if "renovate" in author or "dependabot" in author: return True - # Exclude generic "Update X digest to Y" patterns - if re.match(r"^update .+ digest to [a-f0-9]+$", title, re.IGNORECASE): + # Check for chore(deps) pattern + if re.match(r"^chore\(deps\):", title): + return True + + # Check for digest update patterns + if re.match(r".*digest to [a-f0-9]+", title, re.IGNORECASE): + return True + + # Check for dependency-related labels + dependency_labels = ["dependencies", "deps", "renovate"] + if any(dep in label for label in labels for dep in dependency_labels): return True return False @@ -166,14 +198,25 @@ def format_pr_line(pr_details): return f"- {title} by @{author} in {url}" -def get_new_contributors(pr_details_list, tag): - """Find contributors who made their first contribution in this release.""" - # Exclude bots from new contributors +def get_new_contributors(pr_details_list, tag, repo="meshtastic/firmware"): + """Find contributors who made their first merged PR before this release. + + GitHub usernames do not necessarily match git commit authors, so we use the + GitHub search API via `gh` to see if the user has any merged PRs before the + tag date. This mirrors how GitHub's "Generate release notes" feature works. + """ + bot_authors = {"github-actions", "renovate", "dependabot", "app/renovate", "app/github-actions", "app/dependabot"} new_contributors = [] seen_authors = set() + try: + tag_date = get_tag_date(tag) + except subprocess.CalledProcessError: + print(f"Warning: Could not determine tag date for {tag}; skipping new contributor detection", file=sys.stderr) + return [] + for pr in pr_details_list: author = pr.get("author", {}).get("login", "") if not author or author in seen_authors: @@ -185,14 +228,41 @@ def get_new_contributors(pr_details_list, tag): seen_authors.add(author) - # Check if this author appears in git history before tag - author_check = subprocess.run( - ["git", "log", f"{tag}", f"--author={author}", "--oneline", "-1"], - capture_output=True, - text=True, - ) - if not author_check.stdout.strip(): - new_contributors.append((author, pr.get("url", ""))) + try: + # Search for merged PRs by this author created before the tag date + search_query = f"is:pr author:{author} repo:{repo} closed:<=\"{tag_date}\"" + search = subprocess.run( + [ + "gh", + "search", + "issues", + "--json", + "number,mergedAt,createdAt", + "--state", + "closed", + "--limit", + "200", + search_query, + ], + capture_output=True, + text=True, + ) + + if search.returncode != 0: + # If gh fails, be conservative and skip adding to new contributors + print(f"Warning: gh search failed for author {author}: {search.stderr.strip()}", file=sys.stderr) + continue + + results = json.loads(search.stdout or "[]") + # If any merged PR exists before or on tag date, not a new contributor + had_prior_pr = any(item.get("mergedAt") for item in results) + + if not had_prior_pr: + new_contributors.append((author, pr.get("url", ""))) + + except Exception as e: + print(f"Warning: Could not check contributor history for {author}: {e}", file=sys.stderr) + continue return new_contributors @@ -224,13 +294,16 @@ def main(): # Get details for all PRs enhancements = [] bug_fixes = [] + dependencies = [] all_pr_details = [] for pr_number in sorted(all_pr_numbers, key=int): details = get_pr_details(pr_number) if details and not should_exclude_pr(details): all_pr_details.append(details) - if is_enhancement(details): + if is_dependency_update(details): + dependencies.append(details) + elif is_enhancement(details): enhancements.append(details) else: bug_fixes.append(details) @@ -250,7 +323,13 @@ def main(): output.append(format_pr_line(pr)) output.append("") - # Find new contributors + if dependencies: + output.append("## ⚙️ Dependencies\n") + for pr in dependencies: + output.append(format_pr_line(pr)) + output.append("") + + # Find new contributors (GitHub-accurate check using merged PRs before tag date) new_contributors = get_new_contributors(all_pr_details, last_tag) if new_contributors: output.append("## New Contributors\n") From 1df26c2c5a9db0c9cc1ec56a74e288579e38093c Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 12 Jan 2026 11:47:35 -0500 Subject: [PATCH 48/83] Renovate: Ignore lovyangfx for elecrow-panel (#9279) --- variants/esp32s3/elecrow_panel/platformio.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index 5c9a4bfaf..2033ccb59 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -45,10 +45,9 @@ lib_deps = ${esp32s3_base.lib_deps} earlephilhower/ESP8266Audio@1.9.9 # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.0.1 - # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality # renovate: datasource=custom.pio depName=TCA9534 packageName=hideakitai/library/TCA9534 hideakitai/TCA9534@0.1.1 + lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality [crowpanel_small_esp32s3_base] ; 2.4, 2.8, 3.5 inch extends = crowpanel_base From 30d6eb01e69fea59575fecd71081a12d9712abb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=9E=97?= <47050377+linser233@users.noreply.github.com> Date: Tue, 13 Jan 2026 06:13:09 +0800 Subject: [PATCH 49/83] add support for uMesh Modules (#9259) * add support for uMesh Modules * Update lora-usb-umesh-1262.yaml * Update lora-usb-umesh-1268.yaml --- bin/config.d/lora-usb-umesh-1262.yaml | 15 +++++++++++++++ bin/config.d/lora-usb-umesh-1268.yaml | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 bin/config.d/lora-usb-umesh-1262.yaml create mode 100644 bin/config.d/lora-usb-umesh-1268.yaml diff --git a/bin/config.d/lora-usb-umesh-1262.yaml b/bin/config.d/lora-usb-umesh-1262.yaml new file mode 100644 index 000000000..6008e63b7 --- /dev/null +++ b/bin/config.d/lora-usb-umesh-1262.yaml @@ -0,0 +1,15 @@ +Lora: + Module: sx1262 + CS: 0 + IRQ: 6 + Reset: 1 + Busy: 4 + RXen: 2 + DIO2_AS_RF_SWITCH: true + spidev: ch341 + USB_PID: 0x5512 + USB_VID: 0x1A86 + DIO3_TCXO_VOLTAGE: true +# USB_Serialnum: 12345678 + SX126X_MAX_POWER: 30 +# Reduce output power to improve EMI diff --git a/bin/config.d/lora-usb-umesh-1268.yaml b/bin/config.d/lora-usb-umesh-1268.yaml new file mode 100644 index 000000000..637472966 --- /dev/null +++ b/bin/config.d/lora-usb-umesh-1268.yaml @@ -0,0 +1,15 @@ +Lora: + Module: sx1268 + CS: 0 + IRQ: 6 + Reset: 1 + Busy: 4 + RXen: 2 + DIO2_AS_RF_SWITCH: true + spidev: ch341 + USB_PID: 0x5512 + USB_VID: 0x1A86 + DIO3_TCXO_VOLTAGE: true +# USB_Serialnum: 12345678 + SX126X_MAX_POWER: 30 +# Reduce output power to improve EMI From daad424806b72bb96c9672edf0b785bd02e8542d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:45:46 -0600 Subject: [PATCH 50/83] Update protobufs (#9291) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/admin.pb.h | 5 ----- src/mesh/generated/meshtastic/mesh.pb.h | 6 ++---- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/protobufs b/protobufs index aa48faf5b..61219de74 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit aa48faf5b5cd5fd1b58503efc6e0a262ae34abcd +Subproject commit 61219de7480ac8ddf27256f405667d2f416ee1bd diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index 047ef2c14..26b4343e9 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -281,8 +281,6 @@ typedef struct _meshtastic_AdminMessage { meshtastic_SharedContact add_contact; /* Initiate or respond to a key verification request */ meshtastic_KeyVerificationAdmin key_verification; - /* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) */ - meshtastic_OTAMode reboot_ota_mode; /* Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. */ int32_t factory_reset_device; /* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) @@ -341,7 +339,6 @@ extern "C" { #define meshtastic_AdminMessage_payload_variant_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_payload_variant_restore_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation -#define meshtastic_AdminMessage_payload_variant_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode #define meshtastic_AdminMessage_OTAEvent_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode @@ -436,7 +433,6 @@ extern "C" { #define meshtastic_AdminMessage_commit_edit_settings_tag 65 #define meshtastic_AdminMessage_add_contact_tag 66 #define meshtastic_AdminMessage_key_verification_tag 67 -#define meshtastic_AdminMessage_reboot_ota_mode_tag 68 #define meshtastic_AdminMessage_factory_reset_device_tag 94 #define meshtastic_AdminMessage_reboot_ota_seconds_tag 95 #define meshtastic_AdminMessage_exit_simulator_tag 96 @@ -497,7 +493,6 @@ X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_ed X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,add_contact,add_contact), 66) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification,key_verification), 67) \ -X(a, STATIC, ONEOF, UENUM, (payload_variant,reboot_ota_mode,reboot_ota_mode), 68) \ X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_device,factory_reset_device), 94) \ X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_ota_seconds,reboot_ota_seconds), 95) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulator), 96) \ diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 58401143c..e0dd9c58b 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -296,6 +296,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_THINKNODE_M6 = 120, /* Elecrow Meshstick 1262 */ meshtastic_HardwareModel_MESHSTICK_1262 = 121, + /* LilyGo T-Beam 1W */ + meshtastic_HardwareModel_TBEAM_1_WATT = 122, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ @@ -1366,10 +1368,6 @@ extern "C" { #define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF #define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1)) -#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE -#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF -#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1)) - #define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET #define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX #define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1)) From f73a944fcb7bbcc2e9428f2f4e16473e7eab7b33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:39:58 -0600 Subject: [PATCH 51/83] Update ESP8266SAM to v1.1.0 (#9271) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32s3/elecrow_panel/platformio.ini | 2 +- variants/esp32s3/tlora-pager/platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index 2033ccb59..e0f6f0760 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -44,7 +44,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio earlephilhower/ESP8266Audio@1.9.9 # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM - earlephilhower/ESP8266SAM@1.0.1 + earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=TCA9534 packageName=hideakitai/library/TCA9534 hideakitai/TCA9534@0.1.1 lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 3a7afb016..08f70f76b 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -33,7 +33,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio earlephilhower/ESP8266Audio@1.9.9 # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM - earlephilhower/ESP8266SAM@1.0.1 + earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library From 3a0f3520d17d9c3c32a21b2b0fe44af3109b6c95 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:40:44 -0500 Subject: [PATCH 52/83] BaseUI: Autosave Messages (#9269) * Autosave Messages * fix * Add logging, code cleanup, and add save on delete. * We already save as part of delete messages, no need to do it again * fix spelling errors * Updating comment --------- Co-authored-by: Jason P --- src/MessageStore.cpp | 90 ++++++++++++++++++++++++++++++- src/MessageStore.h | 5 ++ src/graphics/draw/MenuHandler.cpp | 32 ++--------- src/main.cpp | 6 +++ 4 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index c96645b1c..22da418f5 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -13,6 +13,11 @@ #define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) #endif +// Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc) +#ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC +#define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60) +#endif + // Global message text pool and state static char *g_messagePool = nullptr; static size_t g_poolWritePos = 0; @@ -102,6 +107,60 @@ void MessageStore::addLiveMessage(const StoredMessage &msg) pushWithLimit(liveMessages, msg); } +#if ENABLE_MESSAGE_PERSISTENCE +static bool g_messageStoreHasUnsavedChanges = false; +static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved + +static inline uint32_t autosaveIntervalMs() +{ + uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC; + if (sec < 60) + sec = 60; + return sec * 1000UL; +} + +static inline bool reachedMs(uint32_t now, uint32_t target) +{ + return (int32_t)(now - target) >= 0; +} + +// Mark new messages in RAM that need to be saved later +static inline void markMessageStoreUnsaved() +{ + g_messageStoreHasUnsavedChanges = true; + + if (g_lastAutoSaveMs == 0) { + g_lastAutoSaveMs = millis(); + } +} + +// Called periodically from the main loop in main.cpp +static inline void autosaveTick(MessageStore *store) +{ + if (!store) + return; + + uint32_t now = millis(); + + if (g_lastAutoSaveMs == 0) { + g_lastAutoSaveMs = now; + return; + } + + if (!reachedMs(now, g_lastAutoSaveMs + autosaveIntervalMs())) + return; + + // Autosave interval reached, only save if there are unsaved messages. + if (g_messageStoreHasUnsavedChanges) { + LOG_INFO("Autosaving MessageStore to flash"); + store->saveToFlash(); + } else { + LOG_INFO("Autosave skipped, no changes to save"); + g_lastAutoSaveMs = now; + } +} +#endif + // Add from incoming/outgoing packet const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) { @@ -131,6 +190,11 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa } addLiveMessage(sm); + +#if ENABLE_MESSAGE_PERSISTENCE + markMessageStoreUnsaved(); +#endif + return liveMessages.back(); } @@ -155,6 +219,10 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st sm.ackStatus = AckStatus::NONE; addLiveMessage(sm); + +#if ENABLE_MESSAGE_PERSISTENCE + markMessageStoreUnsaved(); +#endif } #if ENABLE_MESSAGE_PERSISTENCE @@ -239,6 +307,10 @@ void MessageStore::saveToFlash() f.close(); #endif + + // Reset autosave state after any save + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); } void MessageStore::loadFromFlash() @@ -270,6 +342,9 @@ void MessageStore::loadFromFlash() f.close(); #endif + // Loading messages does not trigger an autosave + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); } #else @@ -290,6 +365,11 @@ void MessageStore::clearAllMessages() f.write(&count, 1); // write "0 messages" f.close(); #endif + +#if ENABLE_MESSAGE_PERSISTENCE + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); +#endif } // Internal helper: erase first or last message matching a predicate @@ -421,6 +501,14 @@ uint16_t MessageStore::storeText(const char *src, size_t len) return storeTextInPool(src, len); } +#if ENABLE_MESSAGE_PERSISTENCE +void messageStoreAutosaveTick() +{ + // Called from the main loop to check autosave timing + autosaveTick(&messageStore); +} +#endif + // Global definition MessageStore messageStore("default"); -#endif \ No newline at end of file +#endif diff --git a/src/MessageStore.h b/src/MessageStore.h index 41eb56b66..6203d8ed0 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -125,6 +125,11 @@ class MessageStore std::string filename; // Flash filename for persistence }; +#if ENABLE_MESSAGE_PERSISTENCE +// Called periodically from main loop to trigger time based autosave +void messageStoreAutosaveTick(); +#endif + // Global instance (defined in MessageStore.cpp) extern MessageStore messageStore; diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 7c17c8b92..d374ac0e3 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -449,7 +449,7 @@ void menuHandler::clockMenu() } void menuHandler::messageResponseMenu() { - enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, MuteChannel, Aloud, enumEnd }; + enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd }; static const char *optionsArray[enumEnd]; static int optionsEnumArray[enumEnd]; @@ -479,7 +479,7 @@ void menuHandler::messageResponseMenu() // Delete submenu optionsArray[options] = "Delete"; - optionsEnumArray[options++] = 900; + optionsEnumArray[options++] = DeleteMenu; #ifdef HAS_I2S optionsArray[options] = "Read Aloud"; @@ -520,34 +520,10 @@ void menuHandler::messageResponseMenu() nodeDB->saveToDisk(); } - // Delete submenu - } else if (selected == 900) { + } else if (selected == DeleteMenu) { menuHandler::menuQueue = menuHandler::delete_messages_menu; screen->runNow(); - // Delete oldest FIRST (only change) - } else if (selected == DeleteOldest) { - auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (mode == graphics::MessageRenderer::ThreadMode::ALL) { - // Global oldest - messageStore.deleteOldestMessage(); - } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - // Oldest in current channel - messageStore.deleteOldestMessageInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - // Oldest in current DM - messageStore.deleteOldestMessageWithPeer(peer); - } - - // Delete all messages - } else if (selected == DeleteAll) { - messageStore.clearAllMessages(); - graphics::MessageRenderer::clearThreadRegistries(); - graphics::MessageRenderer::clearMessageCache(); - #ifdef HAS_I2S } else if (selected == Aloud) { const meshtastic_MeshPacket &mp = devicestate.rx_text_message; @@ -716,7 +692,6 @@ void menuHandler::deleteMessagesMenu() } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { messageStore.deleteOldestMessageWithPeer(peer); } - return; } @@ -729,7 +704,6 @@ void menuHandler::deleteMessagesMenu() } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { messageStore.deleteAllMessagesWithPeer(peer); } - return; } }; diff --git a/src/main.cpp b/src/main.cpp index a9ed73bd7..d77767736 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,6 +38,9 @@ #include "target_specific.h" #include #include +#if HAS_SCREEN +#include "MessageStore.h" +#endif #ifdef ELECROW_ThinkNode_M5 PCA9557 io(0x18, &Wire); @@ -1652,6 +1655,9 @@ void loop() if (dispdev) static_cast(dispdev)->sdlLoop(); } +#endif +#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE + messageStoreAutosaveTick(); #endif long delayMsec = mainController.runOrDelay(); From ded4f57cb71ed08392a0ee58d2351fe6086a2ca1 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 Jan 2026 05:47:08 -0600 Subject: [PATCH 53/83] Partition name in manifest script (#9294) * Fix up T-Beam 1W HW_MODEL * Add part_name for bin files * app0 --- .github/workflows/build_firmware.yml | 4 ++-- bin/platformio-custom.py | 11 +++++++++++ src/platform/esp32/architecture.h | 2 ++ variants/esp32s3/t-beam-1w/platformio.ini | 9 +++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 77260cafe..23690766a 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -91,8 +91,8 @@ jobs: if [[ -f "$manifest" ]]; then echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)" # Add OTA entry to files array if not already present - jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" \ - 'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes}] else . end' \ + jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \ + 'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \ "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest" fi done diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index 7481500db..b75c66624 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -60,6 +60,14 @@ def manifest_gather(source, target, env): board_platform = env.BoardConfig().get("platform") board_mcu = env.BoardConfig().get("build.mcu").lower() needs_ota_suffix = board_platform == "nordicnrf52" + + # Mapping of bin files to their target partition names + # Maps the filename pattern to the partition name where it should be flashed + partition_map = { + f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0) + lfsbin: "spiffs", # filesystem image flashed to spiffs + } + check_paths = [ progname, f"{progname}.elf", @@ -85,6 +93,9 @@ def manifest_gather(source, target, env): "md5": f.get_content_hash(), # Returns MD5 hash "bytes": f.get_size() # Returns file size in bytes } + # Add part_name if this file represents a partition that should be flashed + if p in partition_map: + d["part_name"] = partition_map[p] out.append(d) print(d) manifest_write(out, env) diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 085692f96..f34f1fc65 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -195,6 +195,8 @@ #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #elif defined(T_DECK_PRO) #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO +#elif defined(T_BEAM_1W) +#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) diff --git a/variants/esp32s3/t-beam-1w/platformio.ini b/variants/esp32s3/t-beam-1w/platformio.ini index 54ddb6c3e..9abf895db 100644 --- a/variants/esp32s3/t-beam-1w/platformio.ini +++ b/variants/esp32s3/t-beam-1w/platformio.ini @@ -1,5 +1,14 @@ ; LilyGo T-Beam-1W (1 Watt LoRa with external PA) [env:t-beam-1w] +custom_meshtastic_hw_model = 122 +custom_meshtastic_hw_model_slug = TBEAM_1_WATT +custom_meshtastic_architecture = esp32s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Beam 1W +custom_meshtastic_images = tbeam-1w.svg +custom_meshtastic_tags = LilyGo + extends = esp32s3_base board = t-beam-1w board_build.partitions = default_8MB.csv From 6f36f39da95062ce1c310378d0f59bb8e1f177e8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 12 Jan 2026 19:26:39 -0600 Subject: [PATCH 54/83] Fix up T-Beam 1W HW_MODEL --- src/platform/esp32/architecture.h | 2 ++ variants/esp32s3/t-beam-1w/platformio.ini | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 085692f96..f34f1fc65 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -195,6 +195,8 @@ #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #elif defined(T_DECK_PRO) #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO +#elif defined(T_BEAM_1W) +#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) diff --git a/variants/esp32s3/t-beam-1w/platformio.ini b/variants/esp32s3/t-beam-1w/platformio.ini index 54ddb6c3e..9abf895db 100644 --- a/variants/esp32s3/t-beam-1w/platformio.ini +++ b/variants/esp32s3/t-beam-1w/platformio.ini @@ -1,5 +1,14 @@ ; LilyGo T-Beam-1W (1 Watt LoRa with external PA) [env:t-beam-1w] +custom_meshtastic_hw_model = 122 +custom_meshtastic_hw_model_slug = TBEAM_1_WATT +custom_meshtastic_architecture = esp32s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Beam 1W +custom_meshtastic_images = tbeam-1w.svg +custom_meshtastic_tags = LilyGo + extends = esp32s3_base board = t-beam-1w board_build.partitions = default_8MB.csv From 3640e35a8b9731be3ac3e320641817f54bef0a57 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 05:50:40 -0600 Subject: [PATCH 55/83] Upgrade trunk (#9297) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 30dec205a..b5187537c 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,10 +9,10 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.78.2 + - renovate@42.80.1 - prettier@3.7.4 - trufflehog@3.92.4 - - yamllint@1.37.1 + - yamllint@1.38.0 - bandit@1.9.2 - trivy@0.68.2 - taplo@0.10.0 From e99853f660088340d534788949b11878e33cf7b5 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Tue, 13 Jan 2026 06:57:04 -0500 Subject: [PATCH 56/83] SafeFile: use atomic rename-with-overwrite, rather than non-atomic delete-then-rename (#9296) --- src/SafeFile.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/SafeFile.cpp b/src/SafeFile.cpp index 45b96ad07..39436f18e 100644 --- a/src/SafeFile.cpp +++ b/src/SafeFile.cpp @@ -54,7 +54,7 @@ size_t SafeFile::write(const uint8_t *buffer, size_t size) } /** - * Atomically close the file (deleting any old versions) and readback the contents to confirm the hash matches + * Atomically close the file (overwriting any old version) and readback the contents to confirm the hash matches * * @return false for failure */ @@ -73,15 +73,7 @@ bool SafeFile::close() if (!testReadback()) return false; - { // Scope for lock - concurrency::LockGuard g(spiLock); - // brief window of risk here ;-) - if (fullAtomic && FSCom.exists(filename.c_str()) && !FSCom.remove(filename.c_str())) { - LOG_ERROR("Can't remove old pref file"); - return false; - } - } - + // Rename or overwrite (atomic operation) String filenameTmp = filename; filenameTmp += ".tmp"; if (!renameFile(filenameTmp.c_str(), filename.c_str())) { From dae4061b06c881da5dd20ea3e5a959c9b9dd006e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 05:58:12 -0600 Subject: [PATCH 57/83] Update protobufs (#9299) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/config.pb.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/protobufs b/protobufs index 61219de74..547a7d803 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 61219de7480ac8ddf27256f405667d2f416ee1bd +Subproject commit 547a7d8033264996e4b93f993d957195cac9fdfd diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index d4ef5bee4..d93f6fafa 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -66,7 +66,7 @@ typedef enum _meshtastic_Config_DeviceConfig_Role { but should not be given priority over other routers in order to avoid unnecessaraily consuming hops. */ meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, - /* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. + /* Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT. Technical Details: Used for stronger attic/roof nodes to distribute messages more widely from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ From 5610d4809c5c5c51f41a09b5d8b125c70d4869da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:59:09 -0600 Subject: [PATCH 58/83] Update meshtastic/device-ui digest to 5a870c6 (#9301) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 6923c65b9..c1df46746 100644 --- a/platformio.ini +++ b/platformio.ini @@ -119,7 +119,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/12f8cddc1e2908e1988da21e3500c695668e8d92.zip + https://github.com/meshtastic/device-ui/archive/5a870c623a4e9ab7a7abe3d02950536f107d1a31.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 89a83d00faab91550b233719826fca0a16199060 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 06:26:31 -0600 Subject: [PATCH 59/83] Upgrade trunk (#9306) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index b5187537c..49b2ba8e8 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,7 +9,7 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.80.1 + - renovate@42.81.2 - prettier@3.7.4 - trufflehog@3.92.4 - yamllint@1.38.0 From 919f214e8d6f48b5b6300eced4cf9a2988686697 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 Jan 2026 06:33:01 -0600 Subject: [PATCH 60/83] Fix OTA partition name matching (#9302) --- src/platform/esp32/BleOta.cpp | 42 ++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/platform/esp32/BleOta.cpp b/src/platform/esp32/BleOta.cpp index 698336f69..0aa034a1e 100644 --- a/src/platform/esp32/BleOta.cpp +++ b/src/platform/esp32/BleOta.cpp @@ -1,31 +1,53 @@ #include "BleOta.h" #include "Arduino.h" +#include #include +#include -static const String MESHTASTIC_OTA_APP_PROJECT_NAME("Meshtastic-OTA"); +static bool isMeshtasticOtaProject(const esp_app_desc_t &desc) +{ + std::string name(desc.project_name); + return name.find("Meshtastic") != std::string::npos && name.find("OTA") != std::string::npos; +} const esp_partition_t *BleOta::findEspOtaAppPartition() { - const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + esp_err_t ret = ESP_ERR_INVALID_ARG; - if (ret != ESP_OK || MESHTASTIC_OTA_APP_PROJECT_NAME != app_desc.project_name) { - part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); + // Try standard OTA slots first (app0 / app1) + const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); + if (part) { ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); } - if (ret == ESP_OK && MESHTASTIC_OTA_APP_PROJECT_NAME == app_desc.project_name) { - return part; - } else { - return nullptr; + if (!part || ret != ESP_OK || !isMeshtasticOtaProject(app_desc)) { + part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); + if (part) { + ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + } } + + // Fallback: look by partition label "app1" in case table uses custom labels + if ((!part || ret != ESP_OK || !isMeshtasticOtaProject(app_desc))) { + part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, "app1"); + if (part) { + ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); + } + } + + if (part && ret == ESP_OK && isMeshtasticOtaProject(app_desc)) { + return part; + } + return nullptr; } String BleOta::getOtaAppVersion() { const esp_partition_t *part = findEspOtaAppPartition(); + if (!part) { + return String(); + } esp_app_desc_t app_desc; esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); String version; From cdbc8f48d45702433607821164abadd26e7e7fb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 06:40:10 -0600 Subject: [PATCH 61/83] Update protobufs (#9308) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 547a7d803..c8d5047b6 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 547a7d8033264996e4b93f993d957195cac9fdfd +Subproject commit c8d5047b6351b732c0bccfcea6960a532f7ae49a diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index e0dd9c58b..68552ede5 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -298,6 +298,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_MESHSTICK_1262 = 121, /* LilyGo T-Beam 1W */ meshtastic_HardwareModel_TBEAM_1_WATT = 122, + /* LilyGo T5 S3 ePaper Pro (V1 and V2) */ + meshtastic_HardwareModel_T5_S3_EPAPER_PRO = 123, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ From 552df4c88c17410e83a54dd08e83fa62d3a41de6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 Jan 2026 07:06:40 -0600 Subject: [PATCH 62/83] Supress reboot banner in Reboot OTA --- src/graphics/Screen.cpp | 2 +- src/main.cpp | 5 +++-- src/main.h | 1 + src/modules/AdminModule.cpp | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 28f17f962..8bf69b7a0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -825,7 +825,7 @@ int32_t Screen::runOnce() #endif } #endif - if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { + if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0 && !suppressRebootBanner) { showSimpleBanner("Rebooting...", 0); } diff --git a/src/main.cpp b/src/main.cpp index d77767736..7e6488bd8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1541,8 +1541,9 @@ void setup() } #endif -uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) -uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) +uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reboot shortly after the update completes) +uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) +bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for OTA handoff) // If a thread does something that might need for it to be rescheduled ASAP it can set this flag // This will suppress the current delay and instead try to run ASAP. diff --git a/src/main.h b/src/main.h index 7ca14d825..c3528a63d 100644 --- a/src/main.h +++ b/src/main.h @@ -81,6 +81,7 @@ extern uint32_t timeLastPowered; extern uint32_t rebootAtMsec; extern uint32_t shutdownAtMsec; +extern bool suppressRebootBanner; extern uint32_t serialSinceMsec; diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 5eac64a62..4d1ebd931 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -241,6 +241,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #if defined(ARCH_ESP32) #if !MESHTASTIC_EXCLUDE_BLUETOOTH if (!BleOta::getOtaAppVersion().isEmpty()) { + suppressRebootBanner = true; if (screen) screen->startFirmwareUpdateScreen(); BleOta::switchToOtaApp(); @@ -249,6 +250,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #endif #if !MESHTASTIC_EXCLUDE_WIFI if (WiFiOTA::trySwitchToOTA()) { + suppressRebootBanner = true; if (screen) screen->startFirmwareUpdateScreen(); WiFiOTA::saveConfig(&config.network); From d1ae131502a2d53be8a6e803c16da3f72be8a4c9 Mon Sep 17 00:00:00 2001 From: vicliu Date: Thu, 15 Jan 2026 00:00:33 +0800 Subject: [PATCH 63/83] T-Deck Pro: speed up eink force refresh (#9303) --- src/graphics/EInkDisplay2.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 9975527aa..f5418b069 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -9,6 +9,15 @@ #include "GxEPD2Multi.h" #endif +// Limit how often we push a full E-Ink refresh. T-Deck Pro needs faster updates for typing. +#ifndef EINK_FORCE_DISPLAY_THROTTLE_MS +#if defined(T_DECK_PRO) +#define EINK_FORCE_DISPLAY_THROTTLE_MS 200 +#else +#define EINK_FORCE_DISPLAY_THROTTLE_MS 1000 +#endif +#endif + /** * An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation. * @@ -42,7 +51,7 @@ class EInkDisplay : public OLEDDisplay * * @return true if we did draw the screen */ - virtual bool forceDisplay(uint32_t msecLimit = 1000); + virtual bool forceDisplay(uint32_t msecLimit = EINK_FORCE_DISPLAY_THROTTLE_MS); /** * Run any code needed to complete an update, after the physical refresh has completed. From 940b3e236b4aefea72e346d9564519b4130cbe61 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:01:08 +0100 Subject: [PATCH 64/83] fix GPS for T-Watch S3 plus (#9312) * support T-Watch S3 Plus GPS * HAS_GPS * define BUTTON_PIN * swap GPS pins, USB_MODE=1 --- boards/t-watch-s3.json | 2 +- variants/esp32s3/t-watch-s3/variant.h | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/boards/t-watch-s3.json b/boards/t-watch-s3.json index bae4f47b0..f3c0bea8e 100644 --- a/boards/t-watch-s3.json +++ b/boards/t-watch-s3.json @@ -9,7 +9,7 @@ "-DBOARD_HAS_PSRAM", "-DT_WATCH_S3", "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], diff --git a/variants/esp32s3/t-watch-s3/variant.h b/variants/esp32s3/t-watch-s3/variant.h index dfd219391..216dda589 100644 --- a/variants/esp32s3/t-watch-s3/variant.h +++ b/variants/esp32s3/t-watch-s3/variant.h @@ -53,10 +53,13 @@ #define HAS_BMA423 1 #define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor +#define HAS_GPS 1 #define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_BAUDRATE 38400 -#define GPS_RX_PIN 42 -#define GPS_TX_PIN 41 +#define GPS_RX_PIN 41 +#define GPS_TX_PIN 42 + +#define BUTTON_PIN 0 // only for Plus version #define USE_SX1262 #define USE_SX1268 From 5d7d1ae7a5cf1acfac5834715952f193bb52a529 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 14 Jan 2026 11:40:35 -0600 Subject: [PATCH 65/83] Adds Custom battery curve for thinknode m6 (#9313) --- src/Power.cpp | 4 +++- variants/nrf52840/ELECROW-ThinkNode-M6/variant.h | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index e9cde0eb6..b96ca2dce 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -476,7 +476,9 @@ class AnalogBatteryLevel : public HasBatteryLevel return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; } #endif -#ifdef EXT_CHRG_DETECT +#if defined(ELECROW_ThinkNode_M6) + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn(); +#elif EXT_CHRG_DETECT return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; #elif defined(BATTERY_CHARGING_INV) return !digitalRead(BATTERY_CHARGING_INV); diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h index 984f967d8..e46391207 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h @@ -134,12 +134,14 @@ static const uint8_t A0 = PIN_A0; #define BATTERY_SENSE_RESOLUTION_BITS 12 #define BATTERY_SENSE_RESOLUTION 4096.0 #undef AREF_VOLTAGE -#define AREF_VOLTAGE 3.0 -#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define AREF_VOLTAGE 2.4 +#define VBAT_AR_INTERNAL AR_INTERNAL_2_4 #define ADC_MULTIPLIER (1.75F) #define HAS_SOLAR +#define OCV_ARRAY 4080, 3990, 3935, 3880, 3825, 3770, 3715, 3660, 3605, 3550, 3450 + #ifdef __cplusplus } #endif From 5a81403594020428b2c6ef6841ce46a275c5ec68 Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Wed, 14 Jan 2026 20:00:08 +0100 Subject: [PATCH 66/83] Move PMSA003I to separate class and update AQ telemetry (#7190) --- platformio.ini | 2 - src/configuration.h | 3 +- src/detect/ScanI2C.cpp | 2 +- src/detect/ScanI2C.h | 2 +- src/detect/ScanI2CTwoWire.cpp | 2 +- src/detect/reClockI2C.h | 40 +++ src/graphics/draw/MenuHandler.cpp | 16 +- src/main.cpp | 2 +- src/modules/Modules.cpp | 6 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 334 ++++++++++++------ src/modules/Telemetry/AirQualityTelemetry.h | 48 +-- .../Telemetry/EnvironmentTelemetry.cpp | 31 +- src/modules/Telemetry/EnvironmentTelemetry.h | 1 - .../Telemetry/Sensor/AddI2CSensorTemplate.h | 34 ++ .../Telemetry/Sensor/PMSA003ISensor.cpp | 164 +++++++++ src/modules/Telemetry/Sensor/PMSA003ISensor.h | 35 ++ .../Telemetry/Sensor/TelemetrySensor.cpp | 2 +- .../Telemetry/Sensor/TelemetrySensor.h | 6 + variants/esp32/esp32-common.ini | 1 + .../heltec_wireless_bridge/platformio.ini | 5 +- variants/stm32/CDEBYTE_E77-MBL/platformio.ini | 1 + variants/stm32/rak3172/platformio.ini | 1 + 22 files changed, 562 insertions(+), 176 deletions(-) create mode 100644 src/detect/reClockI2C.h create mode 100644 src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h create mode 100644 src/modules/Telemetry/Sensor/PMSA003ISensor.cpp create mode 100644 src/modules/Telemetry/Sensor/PMSA003ISensor.h diff --git a/platformio.ini b/platformio.ini index c1df46746..b72d9b5b1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -142,8 +142,6 @@ lib_deps = adafruit/Adafruit INA260 Library@1.5.3 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 - # renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor - adafruit/Adafruit PM25 AQI Sensor@2.0.0 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 adafruit/Adafruit MPU6050@2.2.6 # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH diff --git a/src/configuration.h b/src/configuration.h index ec1b9acc2..be483b924 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -214,7 +214,7 @@ along with this program. If not, see . #define LPS22HB_ADDR_ALT 0x5D #define SHT31_4x_ADDR 0x44 #define SHT31_4x_ADDR_ALT 0x45 -#define PMSA0031_ADDR 0x12 +#define PMSA003I_ADDR 0x12 #define QMA6100P_ADDR 0x12 #define AHT10_ADDR 0x38 #define RCWL9620_ADDR 0x57 @@ -480,6 +480,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_AUDIO 1 #define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1 #define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1 +#define MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR 1 #define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1 #define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1 #define MESHTASTIC_EXCLUDE_PAXCOUNTER 1 diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 83a455de7..4795d2abc 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -43,7 +43,7 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const ScanI2C::FoundDevice ScanI2C::firstAQI() const { - ScanI2C::DeviceType types[] = {PMSA0031, SCD4X}; + ScanI2C::DeviceType types[] = {PMSA003I, SCD4X}; return firstOfOrNONE(2, types); } diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 3a79d97c5..ceb894304 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -39,7 +39,7 @@ class ScanI2C QMI8658, QMC5883L, HMC5883L, - PMSA0031, + PMSA003I, QMA6100P, MPU6050, LIS3DH, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 2be9212cf..202d73d84 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -442,7 +442,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #ifdef HAS_QMA6100P SCAN_SIMPLE_CASE(QMA6100P_ADDR, QMA6100P, "QMA6100P", (uint8_t)addr.address) #else - SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address) #endif case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2); diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h new file mode 100644 index 000000000..edcd0afb6 --- /dev/null +++ b/src/detect/reClockI2C.h @@ -0,0 +1,40 @@ +#ifdef CAN_RECLOCK_I2C +#include "ScanI2CTwoWire.h" + +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) { + + uint32_t currentClock; + + /* See https://github.com/arduino/Arduino/issues/11457 + Currently, only ESP32 can getClock() + While all cores can setClock() + https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) + we need to reclock I2C and set it back to the previous desired speed. + Only for cases where we can know OR predefine the speed, we can do this. + */ + +#ifdef ARCH_ESP32 + currentClock = i2cBus->getClock(); +#elif defined(ARCH_NRF52) + // TODO add getClock function or return a predefined clock speed per variant? + return 0; +#elif defined(ARCH_RP2040) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#elif defined(ARCH_STM32WL) + // TODO add getClock function or return a predefined clock speed per variant + return 0; +#else + return 0; +#endif + + if (currentClock != desiredClock){ + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + i2cBus->setClock(desiredClock); + } + return currentClock; +} +#endif diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index d374ac0e3..e44798bc0 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -2247,7 +2247,8 @@ void menuHandler::FrameToggles_menu() lora, clock, show_favorites, - show_telemetry, + show_env_telemetry, + show_aq_telemetry, show_power, enumEnd }; @@ -2292,8 +2293,11 @@ void menuHandler::FrameToggles_menu() optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; optionsEnumArray[options++] = show_favorites; - optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry"; - optionsEnumArray[options++] = show_telemetry; + optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Env. Telemetry" : "Show Env. Telemetry"; + optionsEnumArray[options++] = show_env_telemetry; + + optionsArray[options] = moduleConfig.telemetry.air_quality_screen_enabled ? "Hide AQ Telemetry" : "Show AQ Telemetry"; + optionsEnumArray[options++] = show_aq_telemetry; optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power"; optionsEnumArray[options++] = show_power; @@ -2356,10 +2360,14 @@ void menuHandler::FrameToggles_menu() screen->toggleFrameVisibility("show_favorites"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); - } else if (selected == show_telemetry) { + } else if (selected == show_env_telemetry) { moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); + } else if (selected == show_aq_telemetry) { + moduleConfig.telemetry.air_quality_screen_enabled = !moduleConfig.telemetry.air_quality_screen_enabled; + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); } else if (selected == show_power) { moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled; menuHandler::menuQueue = menuHandler::FrameToggles; diff --git a/src/main.cpp b/src/main.cpp index 7e6488bd8..cdaf1ce37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -574,6 +574,7 @@ void setup() Wire.setSCL(I2C_SCL); Wire.begin(); #elif defined(I2C_SDA) && !defined(ARCH_RP2040) + LOG_INFO("Starting Bus with (SDA) %d and (SCL) %d: ", I2C_SDA, I2C_SCL); Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) if (portduino_config.i2cdev != "") { @@ -762,7 +763,6 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PMSA0031, meshtastic_TelemetrySensorType_PMSA003I); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 63392f7e4..e17868baf 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -252,9 +252,9 @@ void setupModules() (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); } -#if __has_include("Adafruit_PM25AQI.h") - if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled && - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) { +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + if (moduleConfig.has_telemetry && + (moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled)) { new AirQualityTelemetryModule(); } #endif diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 21a563b9d..dff23abf1 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,36 +1,64 @@ #include "configuration.h" -#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "AirQualityTelemetry.h" #include "Default.h" +#include "AirQualityTelemetry.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "Router.h" -#include "detect/ScanI2CTwoWire.h" +#include "UnitConversions.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "graphics/ScreenFonts.h" #include "main.h" +#include "sleep.h" #include +#include "Sensor/AddI2CSensorTemplate.h" -#ifndef PMSA003I_WARMUP_MS -// from the PMSA003I datasheet: -// "Stable data should be got at least 30 seconds after the sensor wakeup -// from the sleep mode because of the fan’s performance." -#define PMSA003I_WARMUP_MS 30000 -#endif +// Sensors +#include "Sensor/PMSA003ISensor.h" -int32_t AirQualityTelemetryModule::runOnce() + +void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { + return; + } + LOG_INFO("Air Quality Telemetry adding I2C devices..."); + /* Uncomment the preferences below if you want to use the module without having to configure it from the PythonAPI or WebUI. + Note: this was previously on runOnce, which didnt take effect + as other modules already had already been initialized (screen) */ // moduleConfig.telemetry.air_quality_enabled = 1; + // moduleConfig.telemetry.air_quality_screen_enabled = 1; + // moduleConfig.telemetry.air_quality_interval = 15; - if (!(moduleConfig.telemetry.air_quality_enabled)) { + // order by priority of metrics/values (low top, high bottom) + addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); +} + +int32_t AirQualityTelemetryModule::runOnce() +{ + if (sleepOnNextExecution == true) { + sleepOnNextExecution = false; + uint32_t nightyNightMs = Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs); + LOG_DEBUG("Sleeping for %ims, then awaking to send metrics again.", nightyNightMs); + doDeepSleep(nightyNightMs, true, false); + } + + uint32_t result = UINT32_MAX; + + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || + AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); } @@ -42,82 +70,154 @@ int32_t AirQualityTelemetryModule::runOnce() if (moduleConfig.telemetry.air_quality_enabled) { LOG_INFO("Air quality Telemetry: init"); -#ifdef PMSA003I_ENABLE_PIN - // put the sensor to sleep on startup - pinMode(PMSA003I_ENABLE_PIN, OUTPUT); - digitalWrite(PMSA003I_ENABLE_PIN, LOW); -#endif /* PMSA003I_ENABLE_PIN */ - - if (!aqi.begin_I2C()) { -#ifndef I2C_NO_RESCAN - LOG_WARN("Could not establish i2c connection to AQI sensor. Rescan"); - // rescan for late arriving sensors. AQI Module starts about 10 seconds into the boot so this is plenty. - uint8_t i2caddr_scan[] = {PMSA0031_ADDR}; - uint8_t i2caddr_asize = 1; - auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); -#if defined(I2C_SDA1) - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE1, i2caddr_scan, i2caddr_asize); -#endif - i2cScanner->scanPort(ScanI2C::I2CPort::WIRE, i2caddr_scan, i2caddr_asize); - auto found = i2cScanner->find(ScanI2C::DeviceType::PMSA0031); - if (found.type != ScanI2C::DeviceType::NONE) { - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first = found.address.address; - nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].second = - i2cScanner->fetchI2CBus(found.address); - return setStartDelay(); - } -#endif - return disable(); + // check if we have at least one sensor + if (!sensors.empty()) { + result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - return setStartDelay(); + } - return disable(); + + // it's possible to have this module enabled, only for displaying values on the screen. + // therefore, we should only enable the sensor loop if measurement is also enabled + return result == UINT32_MAX ? disable() : setStartDelay(); } else { // if we somehow got to a second run of this module with measurement disabled, then just wait forever - if (!moduleConfig.telemetry.air_quality_enabled) - return disable(); - - switch (state) { -#ifdef PMSA003I_ENABLE_PIN - case State::IDLE: - // sensor is in standby; fire it up and sleep - LOG_DEBUG("runOnce(): state = idle"); - digitalWrite(PMSA003I_ENABLE_PIN, HIGH); - state = State::ACTIVE; - - return PMSA003I_WARMUP_MS; -#endif /* PMSA003I_ENABLE_PIN */ - case State::ACTIVE: - // sensor is already warmed up; grab telemetry and send it - LOG_DEBUG("runOnce(): state = active"); - - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && - airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && - airTime->isTxAllowedAirUtil()) { - sendTelemetry(); - lastSentToMesh = millis(); - } else if (service->isToPhoneQueueEmpty()) { - // Just send to phone when it's not our time to send to mesh yet - // Only send while queue is empty (phone assumed connected) - sendTelemetry(NODENUM_BROADCAST, true); - } - -#ifdef PMSA003I_ENABLE_PIN - // put sensor back to sleep - digitalWrite(PMSA003I_ENABLE_PIN, LOW); - state = State::IDLE; -#endif /* PMSA003I_ENABLE_PIN */ - - return sendToPhoneIntervalMs; - default: + if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { return disable(); } + + // Wake up the sensors that need it + LOG_INFO("Waking up sensors"); + for (TelemetrySensor *sensor : sensors) { + if (!sensor->isActive()) { + return sensor->wakeUp(); + } + } + + if (((lastSentToMesh == 0) || + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + sendTelemetry(); + lastSentToMesh = millis(); + } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && + (service->isToPhoneQueueEmpty())) { + // Just send to phone when it's not our time to send to mesh yet + // Only send while queue is empty (phone assumed connected) + sendTelemetry(NODENUM_BROADCAST, true); + lastSentToPhone = millis(); + } + + // Send to sleep sensors that consume power + LOG_INFO("Sending sensors to sleep"); + for (TelemetrySensor *sensor : sensors) { + sensor->sleep(); } + + } + return min(sendToPhoneIntervalMs, result); } +bool AirQualityTelemetryModule::wantUIFrame() +{ + return moduleConfig.telemetry.air_quality_screen_enabled; +} + +#if HAS_SCREEN +void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // === Setup display === + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; + + // === Set Title + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Air Quality" : "AQ."; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = graphics::getTextPositions(display)[line++]; + + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // Decode the telemetry message from the latest received packet + const meshtastic_Data &p = lastMeasurementPacket->decoded; + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + const auto &m = telemetry.variant.air_quality_metrics; + + // Check if any telemetry field has valid data + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || m.has_pm25_environmental || + m.has_pm100_environmental; + + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_pm10_standard) + entries.push_back("PM1: " + String(m.pm10_standard) + "ug/m3"); + if (m.has_pm25_standard) + entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); + if (m.has_pm100_standard) + entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === + currentY += rowHeight; + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); + } + + currentY += rowHeight; + } + graphics::drawCommonFooter(display, x, y); +} +#endif + bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { @@ -144,35 +244,21 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - if (!aqi.read(&data)) { - LOG_WARN("Skip send measurements. Could not read AQIn"); - return false; - } - + bool valid = true; + bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; - m->variant.air_quality_metrics.has_pm10_standard = true; - m->variant.air_quality_metrics.pm10_standard = data.pm10_standard; - m->variant.air_quality_metrics.has_pm25_standard = true; - m->variant.air_quality_metrics.pm25_standard = data.pm25_standard; - m->variant.air_quality_metrics.has_pm100_standard = true; - m->variant.air_quality_metrics.pm100_standard = data.pm100_standard; + m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - m->variant.air_quality_metrics.has_pm10_environmental = true; - m->variant.air_quality_metrics.pm10_environmental = data.pm10_env; - m->variant.air_quality_metrics.has_pm25_environmental = true; - m->variant.air_quality_metrics.pm25_environmental = data.pm25_env; - m->variant.air_quality_metrics.has_pm100_environmental = true; - m->variant.air_quality_metrics.pm100_environmental = data.pm100_env; + // TODO - Should we check for sensor state here? + // If a sensor is sleeping, we should know and check to wake it up + for (TelemetrySensor *sensor : sensors) { + LOG_INFO("Reading AQ sensors"); + valid = valid && sensor->getMetrics(m); + hasSensor = true; + } - LOG_INFO("Send: PM1.0(Standard)=%i, PM2.5(Standard)=%i, PM10.0(Standard)=%i", m->variant.air_quality_metrics.pm10_standard, - m->variant.air_quality_metrics.pm25_standard, m->variant.air_quality_metrics.pm100_standard); - - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - m->variant.air_quality_metrics.pm10_environmental, m->variant.air_quality_metrics.pm25_environmental, - m->variant.air_quality_metrics.pm100_environmental); - - return true; + return valid && hasSensor; } meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() @@ -206,7 +292,15 @@ meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) { meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; + m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; + m.time = getTime(); if (getAirQualityTelemetry(&m)) { + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ + pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \ + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, \ + m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, \ + m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); + meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; @@ -221,16 +315,44 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { - LOG_INFO("Send packet to phone"); + LOG_INFO("Sending packet to phone"); service->sendToPhone(p); } else { - LOG_INFO("Send packet to mesh"); + LOG_INFO("Sending packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } } return true; } - return false; } +AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; + + for (TelemetrySensor *sensor : sensors) { + result = sensor->handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } + + return result; +} + #endif \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 0142ee686..af9c4ebc0 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -1,14 +1,23 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #pragma once + +#ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE +#define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0 +#endif + #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "Adafruit_PM25AQI.h" #include "NodeDB.h" #include "ProtobufModule.h" +#include "detect/ScanI2CConsumer.h" +#include +#include -class AirQualityTelemetryModule : private concurrency::OSThread, public ProtobufModule +class AirQualityTelemetryModule : private concurrency::OSThread, + public ScanI2CConsumer, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, @@ -16,22 +25,19 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf public: AirQualityTelemetryModule() - : concurrency::OSThread("AirQualityTelemetry"), + : concurrency::OSThread("AirQualityTelemetry"), ScanI2CConsumer(), ProtobufModule("AirQualityTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { lastMeasurementPacket = nullptr; - setIntervalFromNow(10 * 1000); - aqi = Adafruit_PM25AQI(); nodeStatusObserver.observe(&nodeStatus->onNewStatus); - -#ifdef PMSA003I_ENABLE_PIN - // the PMSA003I sensor uses about 300mW on its own; support powering it off when it's not actively taking - // a reading - state = State::IDLE; -#else - state = State::ACTIVE; -#endif + setIntervalFromNow(10 * 1000); } + virtual bool wantUIFrame() override; +#if !HAS_SCREEN + void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +#else + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif protected: /** Called to handle a particular incoming message @@ -49,19 +55,17 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public Protobuf */ bool sendTelemetry(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false); - private: - enum State { - IDLE = 0, - ACTIVE = 1, - }; + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; + void i2cScanFinished(ScanI2C *i2cScanner); - State state; - Adafruit_PM25AQI aqi; - PM25_AQI_Data data = {0}; + private: bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; + uint32_t lastSentToPhone = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 843d7b8d5..5d70ac308 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -143,34 +143,7 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "graphics/ScreenFonts.h" #include - -#include - -static std::forward_list sensors; - -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) -{ - ScanI2C::FoundDevice dev = i2cScanner->find(type); - if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { - TelemetrySensor *sensor = new T(); -#if WIRE_INTERFACES_COUNT > 1 - TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); - if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { - // This sensor only works on Wire (Wire1 is not supported) - delete sensor; - return; - } -#else - TwoWire *bus = &Wire; -#endif - if (sensor->initDevice(bus, &dev)) { - sensors.push_front(sensor); - return; - } - // destroy sensor - delete sensor; - } -} +#include "Sensor/AddI2CSensorTemplate.h" void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -642,8 +615,6 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, m.variant.environment_metrics.soil_moisture); - sensor_read_error_count = 0; - meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; p->decoded.want_response = false; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index 6e4ce82e7..049ed6b77 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -67,7 +67,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; - uint32_t sensor_read_error_count = 0; }; #endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h new file mode 100644 index 000000000..01aacc674 --- /dev/null +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -0,0 +1,34 @@ +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include +#include "TelemetrySensor.h" +#include "detect/ScanI2C.h" +#include "detect/ScanI2CTwoWire.h" +#include + +static std::forward_list sensors; + +template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +{ + ScanI2C::FoundDevice dev = i2cScanner->find(type); + if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { + TelemetrySensor *sensor = new T(); +#if WIRE_INTERFACES_COUNT > 1 + TwoWire *bus = ScanI2CTwoWire::fetchI2CBus(dev.address); + if (dev.address.port != ScanI2C::I2CPort::WIRE1 && sensor->onlyWire1()) { + // This sensor only works on Wire (Wire1 is not supported) + delete sensor; + return; + } +#else + TwoWire *bus = &Wire; +#endif + if (sensor->initDevice(bus, &dev)) { + sensors.push_front(sensor); + return; + } + // destroy sensor + delete sensor; + } +} +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp new file mode 100644 index 000000000..467659efe --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -0,0 +1,164 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "PMSA003ISensor.h" +#include "TelemetrySensor.h" +#include "../detect/reClockI2C.h" + +#include + +PMSA003ISensor::PMSA003ISensor() + : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") +{ +} + +bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); +#ifdef PMSA003I_ENABLE_PIN + pinMode(PMSA003I_ENABLE_PIN, OUTPUT); +#endif + + _bus = bus; + _address = dev->address.address; + +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); + if (!currentClock){ + LOG_WARN("PMSA003I can't be used at this clock speed"); + return false; + } +#endif + + _bus->beginTransmission(_address); + if (_bus->endTransmission() != 0) { + LOG_WARN("PMSA003I not found on I2C at 0x12"); + return false; + } + +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus); +#endif + + status = 1; + LOG_INFO("PMSA003I Enabled"); + + initI2CSensor(); + return true; +} + +bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if(!isActive()){ + LOG_WARN("PMSA003I is not active"); + return false; + } + +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); +#endif + + _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); + if (_bus->available() < PMSA003I_FRAME_LENGTH) { + LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available()); + return false; + } + +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus); +#endif + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { + buffer[i] = _bus->read(); + } + + if (buffer[0] != 0x42 || buffer[1] != 0x4D) { + LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]); + return false; + } + + auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { + return (data[idx] << 8) | data[idx + 1]; + }; + + computedChecksum = 0; + + for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH - 2; i++) { + computedChecksum += buffer[i]; + } + receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); + + if (computedChecksum != receivedChecksum) { + LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum); + return false; + } + + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = read16(buffer, 4); + + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = read16(buffer, 6); + + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = read16(buffer, 8); + + // TODO - Add admin command to remove environmental metrics to save protobuf space + measurement->variant.air_quality_metrics.has_pm10_environmental = true; + measurement->variant.air_quality_metrics.pm10_environmental = read16(buffer, 10); + + measurement->variant.air_quality_metrics.has_pm25_environmental = true; + measurement->variant.air_quality_metrics.pm25_environmental = read16(buffer, 12); + + measurement->variant.air_quality_metrics.has_pm100_environmental = true; + measurement->variant.air_quality_metrics.pm100_environmental = read16(buffer, 14); + + // TODO - Add admin command to remove PN to save protobuf space + measurement->variant.air_quality_metrics.has_particles_03um = true; + measurement->variant.air_quality_metrics.particles_03um = read16(buffer, 16); + + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = read16(buffer, 18); + + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = read16(buffer, 20); + + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = read16(buffer, 22); + + measurement->variant.air_quality_metrics.has_particles_50um = true; + measurement->variant.air_quality_metrics.particles_50um = read16(buffer, 24); + + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); + + return true; +} + +bool PMSA003ISensor::isActive() +{ + return state == State::ACTIVE; +} + + +void PMSA003ISensor::sleep() +{ +#ifdef PMSA003I_ENABLE_PIN + digitalWrite(PMSA003I_ENABLE_PIN, LOW); + state = State::IDLE; +#endif +} + +uint32_t PMSA003ISensor::wakeUp() +{ +#ifdef PMSA003I_ENABLE_PIN + LOG_INFO("Waking up PMSA003I"); + digitalWrite(PMSA003I_ENABLE_PIN, HIGH); + state = State::ACTIVE; + return PMSA003I_WARMUP_MS; +#endif + // No need to wait for warmup if already active + return 0; +} +#endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h new file mode 100644 index 000000000..47c8a05cc --- /dev/null +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -0,0 +1,35 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" + +#define PMSA003I_I2C_CLOCK_SPEED 100000 +#define PMSA003I_FRAME_LENGTH 32 +#define PMSA003I_WARMUP_MS 30000 + +class PMSA003ISensor : public TelemetrySensor +{ +public: + PMSA003ISensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; + +private: + enum class State { IDLE, ACTIVE }; + State state = State::ACTIVE; + + uint16_t computedChecksum = 0; + uint16_t receivedChecksum = 0; + + uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; + TwoWire * _bus{}; + uint8_t _address{}; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp index d6e7d1fac..f854cb5fe 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.cpp +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "NodeDB.h" diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 3c3e61808..4a325aeed 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -58,6 +58,11 @@ class TelemetrySensor // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + // Functions to sleep / wakeup sensors that support it + virtual void sleep() {}; + virtual uint32_t wakeUp() { return 0; } + // Return active by default, override per sensor + virtual bool isActive() { return true; } #if WIRE_INTERFACES_COUNT > 1 // Set to true if Implementation only works first I2C port (Wire) @@ -65,6 +70,7 @@ class TelemetrySensor #endif virtual int32_t runOnce() { return INT32_MAX; } virtual bool isInitialized() { return initialized; } + // TODO: is this used? virtual bool isRunning() { return status > 0; } virtual bool getMetrics(meshtastic_Telemetry *measurement) = 0; diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index 81a49223b..3ee2b9516 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -49,6 +49,7 @@ build_flags = -DLIBPAX_BLE -DHAS_UDP_MULTICAST=1 ;-DDEBUG_HEAP + -DCAN_RECLOCK_I2C lib_deps = ${arduino_base.lib_deps} diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini index 93c3e3394..6f9de7a84 100644 --- a/variants/esp32/heltec_wireless_bridge/platformio.ini +++ b/variants/esp32/heltec_wireless_bridge/platformio.ini @@ -1,9 +1,9 @@ [env:heltec-wireless-bridge] -;build_type = debug ; to make it possible to step through our jtag debugger +;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base board_level = extra board = heltec_wifi_lora_32 -build_flags = +build_flags = ${esp32_base.build_flags} -I variants/esp32/heltec_wireless_bridge -D HELTEC_WIRELESS_BRIDGE @@ -13,6 +13,7 @@ build_flags = -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -D MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 -D MESHTASTIC_EXCLUDE_GPS=1 diff --git a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini index c5af9a4a4..b4c0c958f 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/platformio.ini +++ b/variants/stm32/CDEBYTE_E77-MBL/platformio.ini @@ -13,6 +13,7 @@ build_flags = -DPIN_SERIAL1_RX=PB7 -DPIN_SERIAL1_TX=PB6 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini index b9a4b8a04..4d96e98f9 100644 --- a/variants/stm32/rak3172/platformio.ini +++ b/variants/stm32/rak3172/platformio.ini @@ -12,6 +12,7 @@ build_flags = -DPIN_WIRE_SDA=PA11 -DPIN_WIRE_SCL=PA12 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 -DMESHTASTIC_EXCLUDE_I2C=1 -DMESHTASTIC_EXCLUDE_GPS=1 From 2d4f1b6bfe93ccff06c0dad6443ea415977cda1e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:47:54 +1100 Subject: [PATCH 67/83] Update Adafruit BMP280 to v3 (#9307) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index b72d9b5b1..4c19136af 100644 --- a/platformio.ini +++ b/platformio.ini @@ -129,7 +129,7 @@ lib_deps = # renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor adafruit/Adafruit Unified Sensor@1.1.15 # renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library - adafruit/Adafruit BMP280 Library@2.6.8 + adafruit/Adafruit BMP280 Library@3.0.0 # renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library adafruit/Adafruit BMP085 Library@1.2.4 # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library From fad315e99d97540fbe1426ad721fcd61ad8012db Mon Sep 17 00:00:00 2001 From: brad112358 Date: Wed, 14 Jan 2026 17:59:24 -0600 Subject: [PATCH 68/83] Fix rotary encoder long press (#9039) --- src/input/RotaryEncoderInterruptBase.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index c315f23d9..80ac08175 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -93,6 +93,8 @@ int32_t RotaryEncoderInterruptBase::runOnce() if (!pressDetected) { this->action = ROTARY_ACTION_NONE; + } else if (now - pressStartTime < LONG_PRESS_DURATION) { + return (20); // keep checking for long/short until time expires } return INT32_MAX; From 6537eeab0302c0b655e76248e41ccc5f5ee1bec3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:00:24 +1100 Subject: [PATCH 69/83] Update pschatzmann_arduino-audio-driver to v0.2.0 (#9272) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32s3/tlora-pager/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 08f70f76b..5973db1d0 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -41,7 +41,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.3 # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver - https://github.com/pschatzmann/arduino-audio-driver/archive/v0.1.3.zip + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.0.zip # TODO renovate https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip # TODO renovate From 64e95554bb40b36eee9a9d5e2b7db2ef82e0e91d Mon Sep 17 00:00:00 2001 From: oscgonfer Date: Thu, 15 Jan 2026 01:00:42 +0100 Subject: [PATCH 70/83] Small fix in register size for SHT4X (#9309) --- src/detect/ScanI2CTwoWire.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 202d73d84..a6579902a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -106,7 +106,7 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation if (i2cBus->available()) i2cBus->read(); } - LOG_DEBUG("Register value: 0x%x", value); + LOG_DEBUG("Register value from 0x%x: 0x%x", registerLocation.i2cAddress.address, value); return value; } @@ -382,11 +382,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2); - if (registerValue == 0x5449) { + if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != 0) { // unique SHT4x serial number (6 bytes inc. CRC) type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else { From a6a80b067f646e01ac182ecfb9251829ec440c75 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Wed, 14 Jan 2026 19:02:09 -0500 Subject: [PATCH 71/83] Recover `long_name`, `short_name` from our own NodeDB entry if device.proto is unreadable (#9248) * Recover long_name, short_name from our own NodeDB entry if device.proto is unreadable * NodeDB::loadFromDisk: restore long/short name with memcpy and explicit null termination --------- Co-authored-by: Ben Meadors --- src/mesh/NodeDB.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 8913e0019..eac34c0e7 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1264,6 +1264,23 @@ void NodeDB::loadFromDisk() if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) { LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version); installDefaultDeviceState(); + + // Attempt recovery of owner fields from our own NodeDB entry if available. + meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum()); + if (us && us->has_user) { + LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x", + us->num); + memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name)); + owner.long_name[sizeof(owner.long_name) - 1] = '\0'; + memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name)); + owner.short_name[sizeof(owner.short_name) - 1] = '\0'; + owner.is_licensed = us->user.is_licensed; + owner.has_is_unmessagable = us->user.has_is_unmessagable; + owner.is_unmessagable = us->user.is_unmessagable; + + // Save the recovered owner to device state on disk + saveToDisk(SEGMENT_DEVICESTATE); + } } else { LOG_INFO("Loaded saved devicestate version %d", devicestate.version); } From c0afe92a7f401b17da2b7a27ce10510d249bf76f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 14 Jan 2026 20:54:31 -0600 Subject: [PATCH 72/83] Meshtastic unified OTA (#9231) * Initial commit of combined BLE and WiFi OTA * Incorporate ota_hash in AdminMessage protobuf * OTA protobuf changes * Trunk fmt --------- Co-authored-by: Jake-B --- protobufs | 2 +- src/detect/reClockI2C.h | 5 +- src/mesh/NodeDB.cpp | 6 +- src/modules/AdminModule.cpp | 30 ++++---- src/modules/Telemetry/AirQualityTelemetry.cpp | 45 ++++++------ src/modules/Telemetry/AirQualityTelemetry.h | 4 +- .../Telemetry/EnvironmentTelemetry.cpp | 2 +- .../Telemetry/Sensor/AddI2CSensorTemplate.h | 2 +- .../Telemetry/Sensor/PMSA003ISensor.cpp | 16 ++--- src/modules/Telemetry/Sensor/PMSA003ISensor.h | 8 +-- .../Telemetry/Sensor/TelemetrySensor.h | 2 +- src/platform/esp32/BleOta.cpp | 68 ------------------- src/platform/esp32/BleOta.h | 20 ------ .../esp32/{WiFiOTA.cpp => MeshtasticOTA.cpp} | 22 +++--- src/platform/esp32/MeshtasticOTA.h | 18 +++++ src/platform/esp32/WiFiOTA.h | 18 ----- src/platform/esp32/main-esp32.cpp | 19 ++---- 17 files changed, 92 insertions(+), 195 deletions(-) delete mode 100644 src/platform/esp32/BleOta.cpp delete mode 100644 src/platform/esp32/BleOta.h rename src/platform/esp32/{WiFiOTA.cpp => MeshtasticOTA.cpp} (77%) create mode 100644 src/platform/esp32/MeshtasticOTA.h delete mode 100644 src/platform/esp32/WiFiOTA.h diff --git a/protobufs b/protobufs index c8d5047b6..4b9f104a1 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit c8d5047b6351b732c0bccfcea6960a532f7ae49a +Subproject commit 4b9f104a18ea43b1b2091ee2b48899fe43ad8a0b diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h index edcd0afb6..689e88d6f 100644 --- a/src/detect/reClockI2C.h +++ b/src/detect/reClockI2C.h @@ -1,7 +1,8 @@ #ifdef CAN_RECLOCK_I2C #include "ScanI2CTwoWire.h" -uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) { +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) +{ uint32_t currentClock; @@ -31,7 +32,7 @@ uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) { return 0; #endif - if (currentClock != desiredClock){ + if (currentClock != desiredClock) { LOG_DEBUG("Changing I2C clock to %u", desiredClock); i2cBus->setClock(desiredClock); } diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 8913e0019..40aa37f2e 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -53,7 +53,7 @@ #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI -#include +#include #endif NodeDB *nodeDB = nullptr; @@ -756,8 +756,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.display.compass_orientation = COMPASS_ORIENTATION; #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::isUpdated()) { - WiFiOTA::recoverConfig(&config.network); + if (MeshtasticOTA::isUpdated()) { + MeshtasticOTA::recoverConfig(&config.network); } #endif diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4d1ebd931..990ca0f46 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -9,11 +9,8 @@ #include "meshUtils.h" #include #include // for better whitespace handling -#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH -#include "BleOta.h" -#endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI -#include "WiFiOTA.h" +#include "MeshtasticOTA.h" #endif #include "Router.h" #include "configuration.h" @@ -236,28 +233,27 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta reboot(r->reboot_seconds); break; } - case meshtastic_AdminMessage_reboot_ota_seconds_tag: { - int32_t s = r->reboot_ota_seconds; + case meshtastic_AdminMessage_ota_request_tag: { #if defined(ARCH_ESP32) -#if !MESHTASTIC_EXCLUDE_BLUETOOTH - if (!BleOta::getOtaAppVersion().isEmpty()) { + if (r->ota_request.ota_hash.size != 32) { suppressRebootBanner = true; - if (screen) - screen->startFirmwareUpdateScreen(); - BleOta::switchToOtaApp(); - LOG_INFO("Rebooting to BLE OTA"); + LOG_INFO("OTA Failed: Invalid `ota_hash` provided"); + break; } -#endif -#if !MESHTASTIC_EXCLUDE_WIFI - if (WiFiOTA::trySwitchToOTA()) { + + meshtastic_OTAMode mode = r->ota_request.reboot_ota_mode; + if (MeshtasticOTA::trySwitchToOTA()) { + LOG_INFO("OTA Requested"); suppressRebootBanner = true; if (screen) screen->startFirmwareUpdateScreen(); - WiFiOTA::saveConfig(&config.network); + MeshtasticOTA::saveConfig(&config.network, mode, r->ota_request.ota_hash.bytes); LOG_INFO("Rebooting to WiFi OTA"); + } else { + LOG_INFO("WIFI OTA Failed"); } #endif -#endif + int s = 1; // Reboot in 1 second, hard coded LOG_INFO("Reboot in %d seconds", s); rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000); break; diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index dff23abf1..01f5da2c6 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -3,26 +3,25 @@ #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "Default.h" #include "AirQualityTelemetry.h" +#include "Default.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "Router.h" +#include "Sensor/AddI2CSensorTemplate.h" #include "UnitConversions.h" +#include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" -#include "graphics/ScreenFonts.h" #include "main.h" #include "sleep.h" #include -#include "Sensor/AddI2CSensorTemplate.h" // Sensors #include "Sensor/PMSA003ISensor.h" - void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.air_quality_enabled && !AIR_QUALITY_TELEMETRY_MODULE_ENABLE) { @@ -57,7 +56,7 @@ int32_t AirQualityTelemetryModule::runOnce() uint32_t result = UINT32_MAX; - if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || + if (!(moduleConfig.telemetry.air_quality_enabled || moduleConfig.telemetry.air_quality_screen_enabled || AIR_QUALITY_TELEMETRY_MODULE_ENABLE)) { // If this module is not enabled, and the user doesn't want the display screen don't waste any OSThread time on it return disable(); @@ -74,7 +73,6 @@ int32_t AirQualityTelemetryModule::runOnce() if (!sensors.empty()) { result = DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; } - } // it's possible to have this module enabled, only for displaying values on the screen. @@ -95,27 +93,26 @@ int32_t AirQualityTelemetryModule::runOnce() } if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); lastSentToMesh = millis(); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && - (service->isToPhoneQueueEmpty())) { + (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet // Only send while queue is empty (phone assumed connected) sendTelemetry(NODENUM_BROADCAST, true); lastSentToPhone = millis(); } - // Send to sleep sensors that consume power - LOG_INFO("Sending sensors to sleep"); - for (TelemetrySensor *sensor : sensors) { - sensor->sleep(); - } - + // Send to sleep sensors that consume power + LOG_INFO("Sending sensors to sleep"); + for (TelemetrySensor *sensor : sensors) { + sensor->sleep(); + } } return min(sendToPhoneIntervalMs, result); } @@ -161,8 +158,8 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta const auto &m = telemetry.variant.air_quality_metrics; // Check if any telemetry field has valid data - bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || m.has_pm25_environmental || - m.has_pm100_environmental; + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || + m.has_pm25_environmental || m.has_pm100_environmental; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); @@ -296,10 +293,10 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) m.time = getTime(); if (getAirQualityTelemetry(&m)) { LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ - pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", \ - m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, \ - m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, \ - m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); + pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", + m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, + m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, + m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; @@ -341,8 +338,8 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) } AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) { AdminMessageHandleResult result = AdminMessageHandleResult::NOT_HANDLED; diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index af9c4ebc0..2b88b74ba 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -16,8 +16,8 @@ #include class AirQualityTelemetryModule : private concurrency::OSThread, - public ScanI2CConsumer, - public ProtobufModule + public ScanI2CConsumer, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 5d70ac308..ec6fe4799 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -141,9 +141,9 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true +#include "Sensor/AddI2CSensorTemplate.h" #include "graphics/ScreenFonts.h" #include -#include "Sensor/AddI2CSensorTemplate.h" void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h index 01aacc674..37d909d71 100644 --- a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -1,10 +1,10 @@ #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR -#include #include "TelemetrySensor.h" #include "detect/ScanI2C.h" #include "detect/ScanI2CTwoWire.h" #include +#include static std::forward_list sensors; diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 467659efe..2225a4d87 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -2,17 +2,14 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +#include "../detect/reClockI2C.h" #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "PMSA003ISensor.h" #include "TelemetrySensor.h" -#include "../detect/reClockI2C.h" #include -PMSA003ISensor::PMSA003ISensor() - : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") -{ -} +PMSA003ISensor::PMSA003ISensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PMSA003I, "PMSA003I") {} bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { @@ -26,7 +23,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); - if (!currentClock){ + if (!currentClock) { LOG_WARN("PMSA003I can't be used at this clock speed"); return false; } @@ -51,7 +48,7 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { - if(!isActive()){ + if (!isActive()) { LOG_WARN("PMSA003I is not active"); return false; } @@ -79,9 +76,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) return false; } - auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { - return (data[idx] << 8) | data[idx + 1]; - }; + auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; computedChecksum = 0; @@ -141,7 +136,6 @@ bool PMSA003ISensor::isActive() return state == State::ACTIVE; } - void PMSA003ISensor::sleep() { #ifdef PMSA003I_ENABLE_PIN diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 47c8a05cc..09b43d620 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -6,12 +6,12 @@ #include "TelemetrySensor.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 -#define PMSA003I_FRAME_LENGTH 32 +#define PMSA003I_FRAME_LENGTH 32 #define PMSA003I_WARMUP_MS 30000 class PMSA003ISensor : public TelemetrySensor { -public: + public: PMSA003ISensor(); virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; @@ -20,7 +20,7 @@ public: virtual void sleep() override; virtual uint32_t wakeUp() override; -private: + private: enum class State { IDLE, ACTIVE }; State state = State::ACTIVE; @@ -28,7 +28,7 @@ private: uint16_t receivedChecksum = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; - TwoWire * _bus{}; + TwoWire *_bus{}; uint8_t _address{}; }; diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index 4a325aeed..af51ddfad 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -59,7 +59,7 @@ class TelemetrySensor // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } // Functions to sleep / wakeup sensors that support it - virtual void sleep() {}; + virtual void sleep(){}; virtual uint32_t wakeUp() { return 0; } // Return active by default, override per sensor virtual bool isActive() { return true; } diff --git a/src/platform/esp32/BleOta.cpp b/src/platform/esp32/BleOta.cpp deleted file mode 100644 index 0aa034a1e..000000000 --- a/src/platform/esp32/BleOta.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "BleOta.h" -#include "Arduino.h" -#include -#include -#include - -static bool isMeshtasticOtaProject(const esp_app_desc_t &desc) -{ - std::string name(desc.project_name); - return name.find("Meshtastic") != std::string::npos && name.find("OTA") != std::string::npos; -} - -const esp_partition_t *BleOta::findEspOtaAppPartition() -{ - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERR_INVALID_ARG; - - // Try standard OTA slots first (app0 / app1) - const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr); - if (part) { - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } - - if (!part || ret != ESP_OK || !isMeshtasticOtaProject(app_desc)) { - part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr); - if (part) { - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } - } - - // Fallback: look by partition label "app1" in case table uses custom labels - if ((!part || ret != ESP_OK || !isMeshtasticOtaProject(app_desc))) { - part = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, "app1"); - if (part) { - ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - } - } - - if (part && ret == ESP_OK && isMeshtasticOtaProject(app_desc)) { - return part; - } - return nullptr; -} - -String BleOta::getOtaAppVersion() -{ - const esp_partition_t *part = findEspOtaAppPartition(); - if (!part) { - return String(); - } - esp_app_desc_t app_desc; - esp_err_t ret = ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_get_partition_description(part, &app_desc)); - String version; - if (ret == ESP_OK) { - version = app_desc.version; - } - return version; -} - -bool BleOta::switchToOtaApp() -{ - bool success = false; - const esp_partition_t *part = findEspOtaAppPartition(); - if (part) { - success = (ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_set_boot_partition(part)) == ESP_OK); - } - return success; -} \ No newline at end of file diff --git a/src/platform/esp32/BleOta.h b/src/platform/esp32/BleOta.h deleted file mode 100644 index f4c510920..000000000 --- a/src/platform/esp32/BleOta.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef BLEOTA_H -#define BLEOTA_H - -#include -#include - -class BleOta -{ - public: - explicit BleOta(){}; - - static String getOtaAppVersion(); - static bool switchToOtaApp(); - - private: - String mUserAgent; - static const esp_partition_t *findEspOtaAppPartition(); -}; - -#endif // BLEOTA_H \ No newline at end of file diff --git a/src/platform/esp32/WiFiOTA.cpp b/src/platform/esp32/MeshtasticOTA.cpp similarity index 77% rename from src/platform/esp32/WiFiOTA.cpp rename to src/platform/esp32/MeshtasticOTA.cpp index 4cf157b4c..b8cb052ef 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/MeshtasticOTA.cpp @@ -1,13 +1,13 @@ -#include "WiFiOTA.h" +#include "MeshtasticOTA.h" #include "configuration.h" #include #include -namespace WiFiOTA +namespace MeshtasticOTA { -static const char *nvsNamespace = "ota-wifi"; -static const char *appProjectName = "OTA-WiFi"; +static const char *nvsNamespace = "MeshtasticOTA"; +static const char *appProjectName = "MeshtasticOTA"; static bool updated = false; @@ -43,12 +43,14 @@ void recoverConfig(meshtastic_Config_NetworkConfig *network) strncpy(network->wifi_psk, psk.c_str(), sizeof(network->wifi_psk)); } -void saveConfig(meshtastic_Config_NetworkConfig *network) +void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash) { LOG_INFO("Saving WiFi settings for upcoming OTA update"); Preferences prefs; prefs.begin(nvsNamespace); + prefs.putUChar("method", method); + prefs.putBytes("ota_hash", ota_hash, 32); prefs.putString("ssid", network->wifi_ssid); prefs.putString("psk", network->wifi_psk); prefs.putBool("updated", false); @@ -62,10 +64,14 @@ const esp_partition_t *getAppPartition() bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) { - if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) + if (esp_ota_get_partition_description(part, app_desc) != ESP_OK) { + LOG_INFO("esp_ota_get_partition_description failed"); return false; - if (strcmp(app_desc->project_name, appProjectName) != 0) + } + if (strcmp(app_desc->project_name, appProjectName) != 0) { + LOG_INFO("app_desc->project_name == 0"); return false; + } return true; } @@ -89,4 +95,4 @@ const char *getVersion() return app_desc.version; } -} // namespace WiFiOTA +} // namespace MeshtasticOTA diff --git a/src/platform/esp32/MeshtasticOTA.h b/src/platform/esp32/MeshtasticOTA.h new file mode 100644 index 000000000..001eba039 --- /dev/null +++ b/src/platform/esp32/MeshtasticOTA.h @@ -0,0 +1,18 @@ +#ifndef MESHTASTICOTA_H +#define MESHTASTICOTA_H + +#include "mesh-pb-constants.h" +#include + +namespace MeshtasticOTA +{ +void initialize(); +bool isUpdated(); + +void recoverConfig(meshtastic_Config_NetworkConfig *network); +void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash); +bool trySwitchToOTA(); +const char *getVersion(); +} // namespace MeshtasticOTA + +#endif // MESHTASTICOTA_H diff --git a/src/platform/esp32/WiFiOTA.h b/src/platform/esp32/WiFiOTA.h deleted file mode 100644 index 5a7ee348a..000000000 --- a/src/platform/esp32/WiFiOTA.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef WIFIOTA_H -#define WIFIOTA_H - -#include "mesh-pb-constants.h" -#include - -namespace WiFiOTA -{ -void initialize(); -bool isUpdated(); - -void recoverConfig(meshtastic_Config_NetworkConfig *network); -void saveConfig(meshtastic_Config_NetworkConfig *network); -bool trySwitchToOTA(); -const char *getVersion(); -} // namespace WiFiOTA - -#endif // WIFIOTA_H diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 760964119..6667acf5c 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -5,11 +5,10 @@ #include "main.h" #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH -#include "BleOta.h" #include "nimble/NimbleBluetooth.h" #endif -#include +#include #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" @@ -144,22 +143,14 @@ void esp32Setup() preferences.putUInt("hwVendor", HW_VENDOR); preferences.end(); LOG_DEBUG("Number of Device Reboots: %d", rebootCounter); -#if !MESHTASTIC_EXCLUDE_BLUETOOTH - String BLEOTA = BleOta::getOtaAppVersion(); - if (BLEOTA.isEmpty()) { - LOG_INFO("No BLE OTA firmware available"); - } else { - LOG_INFO("BLE OTA firmware version %s", BLEOTA.c_str()); - } -#endif #if !MESHTASTIC_EXCLUDE_WIFI - String version = WiFiOTA::getVersion(); + String version = MeshtasticOTA::getVersion(); if (version.isEmpty()) { - LOG_INFO("No WiFi OTA firmware available"); + LOG_INFO("MeshtasticOTA firmware not available"); } else { - LOG_INFO("WiFi OTA firmware version %s", version.c_str()); + LOG_INFO("MeshtasticOTA firmware version %s", version.c_str()); } - WiFiOTA::initialize(); + MeshtasticOTA::initialize(); #endif // enableModemSleep(); From 5f63f91cbc0b91f001b7ba353228aa04613cbaf1 Mon Sep 17 00:00:00 2001 From: Lewis He Date: Thu, 15 Jan 2026 10:54:57 +0800 Subject: [PATCH 73/83] Added I2C scanner a check for the QMC6310N. (#9305) * Added support for the new SSD1306 control panel. * Added QMC6310N inspection to I2C scanner --------- Co-authored-by: Ben Meadors --- src/configuration.h | 5 +++-- src/detect/ScanI2C.h | 3 ++- src/detect/ScanI2CTwoWire.cpp | 9 +++++++-- src/main.cpp | 4 +++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index be483b924..cbadedf3f 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -176,7 +176,8 @@ along with this program. If not, see . #define SSD1306_ADDRESS 0x3D #define USE_SH1106 #else -#define SSD1306_ADDRESS 0x3C +#define SSD1306_ADDRESS_L 0x3C //Addr = 0 +#define SSD1306_ADDRESS_H 0x3D //Addr = 1 #endif #define ST7567_ADDRESS 0x3F @@ -205,7 +206,7 @@ along with this program. If not, see . #define INA_ADDR_WAVESHARE_UPS 0x43 #define INA3221_ADDR 0x42 #define MAX1704X_ADDR 0x36 -#define QMC6310_ADDR 0x1C +#define QMC6310U_ADDR 0x1C #define QMI8658_ADDR 0x6B #define QMC5883L_ADDR 0x0D #define HMC5883L_ADDR 0x1E diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index ceb894304..dffcd8fb6 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -35,7 +35,8 @@ class ScanI2C SHT4X, SHTC3, LPS22HB, - QMC6310, + QMC6310U, + QMC6310N, QMI8658, QMC5883L, HMC5883L, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index a6579902a..7a263cd52 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -63,6 +63,10 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const if (i2cBus->available()) { r = i2cBus->read(); } + if(r == 0x80){ + LOG_INFO("QMC6310N found at address 0x%02X", addr.address); + return ScanI2C::DeviceType::QMC6310N; + } r &= 0x0f; if (r == 0x08 || r == 0x00) { @@ -175,7 +179,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = NONE; if (err == 0) { switch (addr.address) { - case SSD1306_ADDRESS: + case SSD1306_ADDRESS_H: + case SSD1306_ADDRESS_L: type = probeOLED(addr); break; @@ -411,7 +416,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case LPS22HB_ADDR_ALT: SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) - SCAN_SIMPLE_CASE(QMC6310_ADDR, QMC6310, "QMC6310", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address) case QMI8658_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0A), 1); // get ID diff --git a/src/main.cpp b/src/main.cpp index cdaf1ce37..2961f6041 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -759,7 +759,9 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA219, meshtastic_TelemetrySensorType_INA219); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310, meshtastic_TelemetrySensorType_QMC6310); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310U, meshtastic_TelemetrySensorType_QMC6310); + //TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N + // scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); From 233e6acc85102cc84fbfc60e943db7d024d58b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Thu, 15 Jan 2026 04:36:53 +0100 Subject: [PATCH 74/83] Preliminary Thinknode M4 Support (#8754) * Preliminary Thinknode M4 Support * oops * Fix RF switch TX configuration * trunk'd * GPS fix for M4 * Battery handling and LED for M4 * Trunk * Drop debug warnings * Make Red LED notification * Merge cleanup * Make white LEDs flash during charge --------- Co-authored-by: Jonathan Bennett --- boards/ThinkNode-M4.json | 53 +++++++ src/Power.cpp | 134 +++++++++++++++++ src/gps/GPS.cpp | 13 +- src/gps/GPS.h | 5 + src/mesh/NodeDB.cpp | 2 +- src/modules/SerialModule.cpp | 18 ++- src/modules/StatusLEDModule.cpp | 36 +++++ src/modules/StatusLEDModule.h | 6 + src/platform/nrf52/architecture.h | 2 + src/power.h | 2 + .../ELECROW-ThinkNode-M4/platformio.ini | 15 ++ .../nrf52840/ELECROW-ThinkNode-M4/rfswitch.h | 11 ++ .../nrf52840/ELECROW-ThinkNode-M4/variant.cpp | 51 +++++++ .../nrf52840/ELECROW-ThinkNode-M4/variant.h | 142 ++++++++++++++++++ 14 files changed, 475 insertions(+), 15 deletions(-) create mode 100644 boards/ThinkNode-M4.json create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M4/variant.h diff --git a/boards/ThinkNode-M4.json b/boards/ThinkNode-M4.json new file mode 100644 index 000000000..178bfaee9 --- /dev/null +++ b/boards/ThinkNode-M4.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_ELECROW_M4 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "elecrow_thinknode_m4", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M4", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "ELECROW ThinkNode m4", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html", + "vendor": "ELECROW" +} diff --git a/src/Power.cpp b/src/Power.cpp index e9cde0eb6..c7d7c5d8b 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -693,6 +693,8 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; + } else if (serialBatteryInit()) { + found = true; } else if (meshSolarInit()) { found = true; } else if (analogInit()) { @@ -1569,3 +1571,135 @@ bool Power::meshSolarInit() return false; } #endif + +#ifdef HAS_SERIAL_BATTERY_LEVEL +#include + +/** + * SerialBatteryLevel class for pulling battery information from a secondary MCU over serial. + */ +class SerialBatteryLevel : public HasBatteryLevel +{ + + public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() + { + BatterySerial.begin(4800); + + return true; + } + + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return v_percent; } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return voltage * 1000; } + + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override + { + // definitely need to gobble up more bytes at once + if (BatterySerial.available() > 5) { + // LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available()); + while (BatterySerial.available() > 11) { + BatterySerial.read(); // flush old data + } + // LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available()); + int tries = 0; + while (BatterySerial.read() != 0xFE) { + tries++; // wait for start byte + if (tries > 10) { + LOG_WARN("SerialBatteryLevel: no start byte found"); + return 1; + } + } + + Data[1] = BatterySerial.read(); + Data[2] = BatterySerial.read(); + Data[3] = BatterySerial.read(); + Data[4] = BatterySerial.read(); + Data[5] = BatterySerial.read(); + if (Data[5] != 0xFD) { + LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]); + return true; + } + v_percent = Data[1]; + voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000); + voltage *= 2; + // LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]); + return true; + } + // This function runs first, so use it to grab the latest data from the secondary MCU + return true; + } + + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override + { +#if defined(EXT_CHRG_DETECT) + + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + +#endif + return false; + } + + virtual bool isCharging() override + { +#ifdef EXT_CHRG_DETECT + return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value; + +#endif + // by default, we check the battery voltage only + return isVbusIn(); + } + + private: + SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX); + uint8_t Data[6] = {0}; + int v_percent = 0; + float voltage = 0.0; +}; + +SerialBatteryLevel serialBatteryLevel; + +/** + * Init the serial battery level sensor + */ +bool Power::serialBatteryInit() +{ +#ifdef EXT_PWR_DETECT + pinMode(EXT_PWR_DETECT, INPUT); +#endif +#ifdef EXT_CHRG_DETECT + pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode); +#endif + + bool result = serialBatteryLevel.runOnce(); + LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &serialBatteryLevel; + return true; +} + +#else +/** + * If this device has no serial battery level sensor, don't try to use it. + */ +bool Power::serialBatteryInit() +{ + return false; +} +#endif diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index f53ffe5e4..fd121861c 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -896,14 +896,11 @@ void GPS::writePinEN(bool on) void GPS::writePinStandby(bool standby) { #ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones - -// Determine the new value for the pin -// Normally: active HIGH for awake -#ifdef PIN_GPS_STANDBY_INVERTED - bool val = standby; -#else - bool val = !standby; -#endif + bool val; + if (standby) + val = GPS_STANDBY_ACTIVE; + else + val = !GPS_STANDBY_ACTIVE; // Write and log pinMode(PIN_GPS_STANDBY, OUTPUT); diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 59cee7113..fcbf361d5 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -16,6 +16,11 @@ #define GPS_EN_ACTIVE 1 #endif +// Allow defining the polarity of the STANDBY output. default is LOW for standby +#ifndef GPS_STANDBY_ACTIVE +#define GPS_STANDBY_ACTIVE LOW +#endif + static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL; static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index eac34c0e7..c51c184c0 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -823,7 +823,7 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.nag_timeout = 2; #endif #if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \ - defined(ELECROW_ThinkNode_M6) + defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6) // Default to PIN_LED2 for external notification output (LED color depends on device variant) moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.output = PIN_LED2; diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index f6007a565..5699f3be6 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -64,8 +64,9 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; #if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \ - defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || \ - defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) + defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M5) || \ + defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) + SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") { api_type = TYPE_SERIAL; @@ -205,8 +206,9 @@ int32_t SerialModule::runOnce() Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } #elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && \ - !defined(MUZI_BASE) + !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 Serial2.setFIFOSize(RX_BUFFER); @@ -263,7 +265,8 @@ int32_t SerialModule::runOnce() } #if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) + !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); @@ -539,7 +542,10 @@ void SerialModule::processWXSerial() { #if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ - !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) + !defined(ELECROW_ThinkNode_M3) && \ + !defined(ELECROW_ThinkNode_M4) && \ + !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) + static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. static double dir_sum_sin = 0; diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index 8738c16ca..33aa58127 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -13,6 +13,8 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus); + if (inputBroker) + inputObserver.observe(inputBroker); } int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) @@ -60,6 +62,12 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) return 0; }; +int StatusLEDModule::handleInputEvent(const InputEvent *event) +{ + lastUserbuttonTime = millis(); + return 0; +} + int32_t StatusLEDModule::runOnce() { my_interval = 1000; @@ -103,6 +111,21 @@ int32_t StatusLEDModule::runOnce() PAIRING_LED_state = LED_STATE_ON; } + bool chargeIndicatorLED1 = LED_STATE_OFF; + bool chargeIndicatorLED2 = LED_STATE_OFF; + bool chargeIndicatorLED3 = LED_STATE_OFF; + bool chargeIndicatorLED4 = LED_STATE_OFF; + if (lastUserbuttonTime + 10 * 1000 > millis() || CHARGE_LED_state == LED_STATE_ON) { + // should this be off at very low percentages? + chargeIndicatorLED1 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 25) + chargeIndicatorLED2 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 50) + chargeIndicatorLED3 = LED_STATE_ON; + if (powerStatus && powerStatus->getBatteryChargePercent() >= 75) + chargeIndicatorLED4 = LED_STATE_ON; + } + #ifdef LED_CHARGE digitalWrite(LED_CHARGE, CHARGE_LED_state); #endif @@ -111,5 +134,18 @@ int32_t StatusLEDModule::runOnce() digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif +#ifdef Battery_LED_1 + digitalWrite(Battery_LED_1, chargeIndicatorLED1); +#endif +#ifdef Battery_LED_2 + digitalWrite(Battery_LED_2, chargeIndicatorLED2); +#endif +#ifdef Battery_LED_3 + digitalWrite(Battery_LED_3, chargeIndicatorLED3); +#endif +#ifdef Battery_LED_4 + digitalWrite(Battery_LED_4, chargeIndicatorLED4); +#endif + return (my_interval); } diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index d90ff718c..98020cb32 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -5,6 +5,7 @@ #include "PowerStatus.h" #include "concurrency/OSThread.h" #include "configuration.h" +#include "input/InputBroker.h" #include #include @@ -17,6 +18,8 @@ class StatusLEDModule : private concurrency::OSThread int handleStatusUpdate(const meshtastic::Status *); + int handleInputEvent(const InputEvent *arg); + protected: unsigned int my_interval = 1000; // interval in millisconds virtual int32_t runOnce() override; @@ -25,12 +28,15 @@ class StatusLEDModule : private concurrency::OSThread CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); CallbackObserver powerStatusObserver = CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); + CallbackObserver inputObserver = + CallbackObserver(this, &StatusLEDModule::handleInputEvent); private: bool CHARGE_LED_state = LED_STATE_OFF; bool PAIRING_LED_state = LED_STATE_OFF; uint32_t PAIRING_LED_starttime = 0; + uint32_t lastUserbuttonTime = 0; uint32_t POWER_LED_starttime = 0; bool doing_fast_blink = false; diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index afe96963d..7734c0020 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -74,6 +74,8 @@ #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M3 #elif defined(ELECROW_ThinkNode_M6) #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6 +#elif defined(ELECROW_ThinkNode_M4) +#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M4 #elif defined(NANO_G2_ULTRA) #define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA #elif defined(CANARYONE) diff --git a/src/power.h b/src/power.h index c826d98b4..5f887c36b 100644 --- a/src/power.h +++ b/src/power.h @@ -121,6 +121,8 @@ class Power : private concurrency::OSThread bool lipoChargerInit(); /// Setup a meshSolar battery sensor bool meshSolarInit(); + /// Setup a serial battery sensor + bool serialBatteryInit(); private: void shutdown(); diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini new file mode 100644 index 000000000..9a2b3a467 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini @@ -0,0 +1,15 @@ +; ThinkNode M4 - Powerbank nrf52840/LR1110 by Elecrow +[env:thinknode_m4] +extends = nrf52840_base +board = ThinkNode-M4 +board_check = true +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/ELECROW-ThinkNode-M4 + -DELECROW_ThinkNode_M4 + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M4> +lib_deps = + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h b/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h new file mode 100644 index 000000000..e5fe182c4 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/rfswitch.h @@ -0,0 +1,11 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, +}; diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp new file mode 100644 index 000000000..af9bed998 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp @@ -0,0 +1,51 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + pinMode(LED_PAIRING, OUTPUT); + ledOff(LED_PAIRING); + + pinMode(Battery_LED_1, OUTPUT); + ledOff(Battery_LED_1); + pinMode(Battery_LED_2, OUTPUT); + ledOff(Battery_LED_2); + + pinMode(Battery_LED_3, OUTPUT); + ledOff(Battery_LED_3); + + pinMode(Battery_LED_4, OUTPUT); + ledOff(Battery_LED_4); +} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h new file mode 100644 index 000000000..faca5b075 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h @@ -0,0 +1,142 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_ELECROW_THINKNODE_M4_ +#define _VARIANT_ELECROW_THINKNODE_M4_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define LED_BUILTIN -1 +#define LED_BLUE -1 +#define PIN_LED2 (32 + 9) +#define LED_PAIRING (13) + +#define Battery_LED_1 (15) +#define Battery_LED_2 (17) +#define Battery_LED_3 (32 + 2) +#define Battery_LED_4 (32 + 4) + +#define LED_STATE_ON 1 + +// Button +#define PIN_BUTTON1 (4) + +// Battery ADC +#define PIN_A0 (2) +#define BATTERY_PIN PIN_A0 +#define BATTERY_SENSE_SAMPLES 30 +#define ADC_RESOLUTION 14 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#define ADC_MULTIPLIER (2.00F) +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 + +#define HAS_SERIAL_BATTERY_LEVEL 1 +#define SERIAL_BATTERY_RX 30 +#define SERIAL_BATTERY_TX 5 + +static const uint8_t A0 = PIN_A0; + +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +// I2C +#define WIRE_INTERFACES_COUNT 1 +#define PIN_WIRE_SDA (23) +#define PIN_WIRE_SCL (25) + +// actually the LORA Radio +#define PIN_POWER_EN (11) + +// charger status +#define EXT_CHRG_DETECT (32 + 6) +#define EXT_CHRG_DETECT_VALUE HIGH + +// SPI +#define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (7) +#define PIN_SPI_SCK (6) + +#define LORA_RESET (32 + 8) +#define LORA_DIO1 (12) +#define LORA_DIO2 (26) +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS (27) + +#define USE_LR1110 +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +// Peripherals on I2C bus. Active Low +#define VEXT_ENABLE (32) +#define VEXT_ON_VALUE LOW + +// GPS L76K +#define HAS_GPS 1 +#define GPS_L76K +#define GPS_BAUDRATE 9600 +#define PIN_GPS_EN (32 + 11) +#define GPS_EN_ACTIVE LOW +#define PIN_GPS_RESET (3) +#define GPS_RESET_MODE HIGH +#define PIN_GPS_STANDBY (28) +#define GPS_STANDBY_ACTIVE HIGH +#define GPS_TX_PIN (32 + 12) +#define GPS_RX_PIN (32 + 14) +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +#ifdef __cplusplus +} +#endif + +#endif From 6ee52ca7fa78285d588816aeafdb6b5a90ca0a65 Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 14 Jan 2026 23:22:55 -0600 Subject: [PATCH 75/83] Node Actions Menu Overhaul (#9287) * Start overhaul and clean up of the Node Actions menu * Wired up commands - still a lot of work and testing * Remove old favorites menu * Remove addFavoritesMenu * CoPilot to the rescue, wired up some function in both directions * Clean up CoPilot actions * Cross out Mute or Ignored in lists, add Save to NodeDB on changes * Improve strikethrough for columns * Correct menu wording and adjust vertical divider on Node List * Code cleanup * Testing unveiled some issues - fixed with these changes --- src/graphics/draw/MenuHandler.cpp | 200 ++++++++++++++++++++----- src/graphics/draw/MenuHandler.h | 7 +- src/graphics/draw/NodeListRenderer.cpp | 34 +++++ 3 files changed, 205 insertions(+), 36 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index d374ac0e3..13e7d0dd2 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -59,6 +59,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp } // namespace menuHandler::screenMenus menuHandler::menuQueue = menu_none; +uint32_t menuHandler::pickedNodeNum = 0; bool test_enabled = false; uint8_t test_count = 0; @@ -1213,20 +1214,13 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; + enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; - optionsArray[options] = "Add Favorite"; - optionsEnumArray[options++] = Favorite; - optionsArray[options] = "Trace Route"; - optionsEnumArray[options++] = TraceRoute; - - if (currentResolution != ScreenResolution::UltraLow) { - optionsArray[options] = "Key Verification"; - optionsEnumArray[options++] = Verify; - } + optionsArray[options] = "Node Actions / Settings"; + optionsEnumArray[options++] = NodePicker; if (currentResolution != ScreenResolution::UltraLow) { optionsArray[options] = "Show Long/Short Name"; @@ -1241,18 +1235,12 @@ void menuHandler::nodeListMenu() bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Favorite) { - menuQueue = add_favorite; - screen->runNow(); - } else if (selected == Verify) { - menuQueue = key_verification_init; + if (selected == NodePicker) { + menuQueue = NodePicker_menu; screen->runNow(); } else if (selected == Reset) { menuQueue = reset_node_db_menu; screen->runNow(); - } else if (selected == TraceRoute) { - menuQueue = trace_route_menu; - screen->runNow(); } else if (selected == NodeNameLength) { menuHandler::menuQueue = menuHandler::node_name_length_menu; screen->runNow(); @@ -1261,6 +1249,159 @@ void menuHandler::nodeListMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::NodePicker() +{ + const char *NODE_PICKER_TITLE; + if (currentResolution == ScreenResolution::UltraLow) { + NODE_PICKER_TITLE = "Pick Node"; + } else { + NODE_PICKER_TITLE = "Pick A Node"; + } + screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { + LOG_INFO("Nodenum: %u", nodenum); + // Store the selection so the Manage Node menu knows which node to operate on + menuHandler::pickedNodeNum = nodenum; + // Keep UI favorite context in sync (used elsewhere for some node-based actions) + graphics::UIRenderer::currentFavoriteNodeNum = nodenum; + menuQueue = Manage_Node_menu; + screen->runNow(); + }); +} + +void menuHandler::ManageNodeMenu() +{ + // If we don't have a node selected yet, go fast exit + auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!node) { + return; + } + enum optionsNumbers { Back, Favorite, Mute, TraceRoute, KeyVerification, Ignore, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + if (node->is_favorite) { + optionsArray[options] = "Unfavorite"; + } else { + optionsArray[options] = "Favorite"; + } + optionsEnumArray[options++] = Favorite; + + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; + if (isMuted) { + optionsArray[options] = "Unmute Notifications"; + } else { + optionsArray[options] = "Mute Notifications"; + } + optionsEnumArray[options++] = Mute; + + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + + optionsArray[options] = "Key Verification"; + optionsEnumArray[options++] = KeyVerification; + + if (node->is_ignored) { + optionsArray[options] = "Unignore Node"; + } else { + optionsArray[options] = "Ignore Node"; + } + optionsEnumArray[options++] = Ignore; + + BannerOverlayOptions bannerOptions; + + std::string title = ""; + if (node->has_user && node->user.long_name && node->user.long_name[0]) { + title += sanitizeString(node->user.long_name).substr(0, 15); + } else { + char buf[20]; + snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num); + title += buf; + } + bannerOptions.message = title.c_str(); + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuQueue = node_base_menu; + screen->runNow(); + return; + } + + if (selected == Favorite) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + if (n->is_favorite) { + LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum); + nodeDB->set_favorite(false, menuHandler::pickedNodeNum); + } else { + LOG_INFO("Adding node %08X to favorites", menuHandler::pickedNodeNum); + nodeDB->set_favorite(true, menuHandler::pickedNodeNum); + } + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + + if (selected == Mute) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + + if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) { + n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK; + LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum); + } else { + n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK; + LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum); + } + nodeDB->notifyObservers(true); + nodeDB->saveToDisk(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + + if (selected == TraceRoute) { + LOG_INFO("Starting traceroute to %08X", menuHandler::pickedNodeNum); + if (traceRouteModule) { + traceRouteModule->startTraceRoute(menuHandler::pickedNodeNum); + } + return; + } + + if (selected == KeyVerification) { + LOG_INFO("Initiating key verification with %08X", menuHandler::pickedNodeNum); + if (keyVerificationModule) { + keyVerificationModule->sendInitialRequest(menuHandler::pickedNodeNum); + } + return; + } + + if (selected == Ignore) { + auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + if (!n) { + return; + } + + if (n->is_ignored) { + n->is_ignored = false; + LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum); + } else { + n->is_ignored = true; + LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum); + } + nodeDB->notifyObservers(true); + nodeDB->saveToDisk(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + return; + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::nodeNameLengthMenu() { static const NodeNameOption nodeNameOptions[] = { @@ -1289,6 +1430,7 @@ void menuHandler::nodeNameLengthMenu() } config.display.use_long_node_name = option.value; + saveUIConfig(); LOG_INFO("Setting names to %s", option.value ? "long" : "short"); }); @@ -1958,21 +2100,6 @@ void menuHandler::shutdownMenu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::addFavoriteMenu() -{ - const char *NODE_PICKER_TITLE; - if (currentResolution == ScreenResolution::UltraLow) { - NODE_PICKER_TITLE = "Node Favorite"; - } else { - NODE_PICKER_TITLE = "Node To Favorite"; - } - screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { - LOG_WARN("Nodenum: %u", nodenum); - nodeDB->set_favorite(true, nodenum); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - }); -} - void menuHandler::removeFavoriteMenu() { @@ -2484,8 +2611,11 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case shutdown_menu: shutdownMenu(); break; - case add_favorite: - addFavoriteMenu(); + case NodePicker_menu: + NodePicker(); + break; + case Manage_Node_menu: + ManageNodeMenu(); break; case remove_favorite: removeFavoriteMenu(); diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 445513e25..121b6dfc9 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -33,7 +33,8 @@ class menuHandler brightness_picker, reboot_menu, shutdown_menu, - add_favorite, + NodePicker_menu, + Manage_Node_menu, remove_favorite, test_menu, number_test, @@ -55,6 +56,7 @@ class menuHandler DisplayUnits }; static screenMenus menuQueue; + static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); @@ -90,6 +92,8 @@ class menuHandler static void BrightnessPickerMenu(); static void rebootMenu(); static void shutdownMenu(); + static void NodePicker(); + static void ManageNodeMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); static void traceRouteMenu(); @@ -149,6 +153,7 @@ using GPSToggleOption = MenuOption; using GPSFormatOption = MenuOption; using NodeNameOption = MenuOption; using PositionMenuOption = MenuOption; +using ManageNodeOption = MenuOption; using ClockFaceOption = MenuOption; } // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index e10d8c40a..9d6780130 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -176,6 +176,7 @@ int calculateMaxScroll(int totalEntries, int visibleRows) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { + x = (currentResolution == ScreenResolution::High) ? x - 2 : (currentResolution == ScreenResolution::Low) ? x - 1 : x; for (int y = yStart; y <= yEnd; y += 2) { display->setPixel(x, y); } @@ -205,9 +206,11 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); + int nameMaxWidth = columnWidth - 25; int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char timeStr[10]; uint32_t seconds = sinceLastSeen(node); @@ -234,6 +237,13 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } int rightEdge = x + columnWidth - timeOffset; if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time @@ -253,6 +263,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int barsXOffset = columnWidth - barsOffset; const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -265,6 +276,13 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } // Draw signal strength bars int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; @@ -298,6 +316,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -358,6 +377,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } if (strlen(distStr) > 0) { int offset = (currentResolution == ScreenResolution::High) @@ -392,6 +418,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); const char *nodeName = getSafeNodeName(display, node, columnWidth); + bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -403,6 +430,13 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } + if (node->is_ignored || isMuted) { + if (currentResolution == ScreenResolution::High) { + display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); + } else { + display->drawLine(x + 4, y + 6, (isLeftCol ? 0 : x - 3) + nameMaxWidth - 4, y + 6); + } + } } void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, From 360579926c49cd59e56bf151eb51e4491cdeab70 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 15 Jan 2026 06:19:18 -0600 Subject: [PATCH 76/83] Trunk fmt --- src/configuration.h | 4 ++-- src/detect/ScanI2CTwoWire.cpp | 5 +++-- src/main.cpp | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index cbadedf3f..178e86fb9 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -176,8 +176,8 @@ along with this program. If not, see . #define SSD1306_ADDRESS 0x3D #define USE_SH1106 #else -#define SSD1306_ADDRESS_L 0x3C //Addr = 0 -#define SSD1306_ADDRESS_H 0x3D //Addr = 1 +#define SSD1306_ADDRESS_L 0x3C // Addr = 0 +#define SSD1306_ADDRESS_H 0x3D // Addr = 1 #endif #define ST7567_ADDRESS 0x3F diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 7a263cd52..c6ef34846 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -63,7 +63,7 @@ ScanI2C::DeviceType ScanI2CTwoWire::probeOLED(ScanI2C::DeviceAddress addr) const if (i2cBus->available()) { r = i2cBus->read(); } - if(r == 0x80){ + if (r == 0x80) { LOG_INFO("QMC6310N found at address 0x%02X", addr.address); return ScanI2C::DeviceType::QMC6310N; } @@ -390,7 +390,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != 0) { // unique SHT4x serial number (6 bytes inc. CRC) + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != + 0) { // unique SHT4x serial number (6 bytes inc. CRC) type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else { diff --git a/src/main.cpp b/src/main.cpp index 2961f6041..88282c837 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -760,8 +760,8 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA3221, meshtastic_TelemetrySensorType_INA3221); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX17048, meshtastic_TelemetrySensorType_MAX17048); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310U, meshtastic_TelemetrySensorType_QMC6310); - //TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N - // scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N); + // TODO: Types need to be added meshtastic_TelemetrySensorType_QMC6310N + // scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC6310N, meshtastic_TelemetrySensorType_QMC6310N); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L); From e8fbdb4d846b172b022257c7d9c567afe4294454 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:21:03 -0600 Subject: [PATCH 77/83] Upgrade trunk (#9323) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 49b2ba8e8..12e6696c0 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,8 +9,8 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.81.2 - - prettier@3.7.4 + - renovate@42.81.8 + - prettier@3.8.0 - trufflehog@3.92.4 - yamllint@1.38.0 - bandit@1.9.2 From 82735ca04ea350d454bf8e8217a61094fa4f725f Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 15 Jan 2026 07:23:40 -0500 Subject: [PATCH 78/83] ICM20948 IMU sleep (#9324) --- src/motion/ICM20948Sensor.cpp | 2 -- src/motion/ICM20948Sensor.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 9455eafe0..ecada2085 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -47,7 +47,6 @@ int32_t ICM20948Sensor::runOnce() int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN -#if defined(MUZI_BASE) // temporarily gated to single device due to feature freeze if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { if (!isAsleep) { LOG_DEBUG("sleeping IMU"); @@ -60,7 +59,6 @@ int32_t ICM20948Sensor::runOnce() sensor->sleep(false); isAsleep = false; } -#endif float magX = 0, magY = 0, magZ = 0; if (sensor->dataReady()) { diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index a9b7b69d0..091cb9a1e 100755 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -82,8 +82,8 @@ class ICM20948Sensor : public MotionSensor private: ICM20948Singleton *sensor = nullptr; bool showingScreen = false; -#ifdef MUZI_BASE bool isAsleep = false; +#ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; #else From 7e4e77211335187395241508157a897d983a48fc Mon Sep 17 00:00:00 2001 From: Austin Date: Thu, 15 Jan 2026 07:24:10 -0500 Subject: [PATCH 79/83] Add EByte EoRa-Hub (#9169) --- boards/CDEBYTE_EoRa-Hub.json | 38 ++++++++++++++ .../esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h | 28 +++++++++++ .../esp32s3/CDEBYTE_EoRa-Hub/platformio.ini | 8 +++ variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h | 19 +++++++ variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h | 50 +++++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 boards/CDEBYTE_EoRa-Hub.json create mode 100644 variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h create mode 100644 variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini create mode 100644 variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h create mode 100644 variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h diff --git a/boards/CDEBYTE_EoRa-Hub.json b/boards/CDEBYTE_EoRa-Hub.json new file mode 100644 index 000000000..66e2cae95 --- /dev/null +++ b/boards/CDEBYTE_EoRa-Hub.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "CDEBYTE_EoRa-Hub", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.cdebyte.com/products/EoRa-HUB-900TB", + "vendor": "CDEBYTE" +} diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h new file mode 100644 index 000000000..46415d30f --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/pins_arduino.h @@ -0,0 +1,28 @@ +// Need this file for ESP32-S3 +// No need to modify this file, changes to pins imported from variant.h +// Most is similar to https://github.com/espressif/arduino-esp32/blob/master/variants/esp32s3/pins_arduino.h + +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Serial +static const uint8_t TX = UART_TX; +static const uint8_t RX = UART_RX; + +// Default SPI will be mapped to Radio +static const uint8_t SS = LORA_CS; +static const uint8_t SCK = LORA_SCK; +static const uint8_t MOSI = LORA_MOSI; +static const uint8_t MISO = LORA_MISO; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SCL = I2C_SCL; +static const uint8_t SDA = I2C_SDA; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini b/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini new file mode 100644 index 000000000..42c311a69 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/platformio.ini @@ -0,0 +1,8 @@ +[env:CDEBYTE_EoRa-Hub] +extends = esp32s3_base +board = CDEBYTE_EoRa-Hub +board_level = extra +build_flags = + ${esp32s3_base.build_flags} + -D PRIVATE_HW + -I variants/esp32s3/CDEBYTE_EoRa-Hub diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h new file mode 100644 index 000000000..1448b1d74 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/rfswitch.h @@ -0,0 +1,19 @@ +#include "RadioLib.h" + +// This is rewritten to match the requirements of the E80-900M2213S +// The E80 does not conform to the reference Semtech switches(!) and therefore needs a custom matrix. +// See footnote #3 in "https://www.cdebyte.com/products/E80-900M2213S/2#Pin" +// RF Switch Matrix SubG RFO_HP_LF / RFO_LP_LF / RFI_[NP]_LF0 +// DIO5 -> RFSW0_V1 +// DIO6 -> RFSW1_V2 +// DIO7 -> not connected on E80 module - note that GNSS and Wifi scanning are not possible. + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 DIO7 + {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h new file mode 100644 index 000000000..1591f6395 --- /dev/null +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h @@ -0,0 +1,50 @@ +// EByte EoRA-Hub +// Uses E80 (LR1121) LoRa module + +#define LED_PIN 35 + +// Button - user interface +#define BUTTON_PIN 0 // BOOT button + +#define BATTERY_PIN 1 +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define ADC_MULTIPLIER 103.0 // Calibrated value +#define ADC_ATTENUATION ADC_ATTEN_DB_0 +#define ADC_CTRL 37 +#define ADC_CTRL_ENABLED LOW + +// Display - OLED connected via I2C by the default hardware configuration +#define HAS_SCREEN 1 +#define USE_SSD1306 +#define I2C_SCL 17 +#define I2C_SDA 18 + +// UART - The 1mm JST SH connector closest to the USB-C port +#define UART_TX 43 +#define UART_RX 44 + +// Peripheral I2C - The 1mm JST SH connector furthest from the USB-C port which follows Adafruit connection standard. There are no +// pull-up resistors on these lines, the downstream device needs to include them. TODO: test, currently untested +#define I2C_SCL1 21 +#define I2C_SDA1 10 + +// Radio +#define USE_LR1121 + +#define LORA_SCK 9 +#define LORA_MOSI 10 +#define LORA_MISO 11 +#define LORA_RESET 12 +#define LORA_CS 8 +#define LORA_DIO9 13 + +// LR1121 +#define LR1121_IRQ_PIN 14 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO9 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 1.8 +#define LR11X0_DIO_AS_RF_SWITCH From b4157bd9bb73edf24c42aead2b5d25f5bd29dad6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 15 Jan 2026 06:48:41 -0600 Subject: [PATCH 80/83] Heltec V4 TFT metadata (#9325) * Upgrade trunk (#9323) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> * ICM20948 IMU sleep (#9324) * Add v4-tft metadata --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- src/motion/ICM20948Sensor.cpp | 2 -- src/motion/ICM20948Sensor.h | 2 +- variants/esp32s3/heltec_v4/platformio.ini | 11 +++++++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 49b2ba8e8..12e6696c0 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,8 +9,8 @@ plugins: lint: enabled: - checkov@3.2.497 - - renovate@42.81.2 - - prettier@3.7.4 + - renovate@42.81.8 + - prettier@3.8.0 - trufflehog@3.92.4 - yamllint@1.38.0 - bandit@1.9.2 diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index 9455eafe0..ecada2085 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -47,7 +47,6 @@ int32_t ICM20948Sensor::runOnce() int32_t ICM20948Sensor::runOnce() { #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN -#if defined(MUZI_BASE) // temporarily gated to single device due to feature freeze if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) { if (!isAsleep) { LOG_DEBUG("sleeping IMU"); @@ -60,7 +59,6 @@ int32_t ICM20948Sensor::runOnce() sensor->sleep(false); isAsleep = false; } -#endif float magX = 0, magY = 0, magZ = 0; if (sensor->dataReady()) { diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h index a9b7b69d0..091cb9a1e 100755 --- a/src/motion/ICM20948Sensor.h +++ b/src/motion/ICM20948Sensor.h @@ -82,8 +82,8 @@ class ICM20948Sensor : public MotionSensor private: ICM20948Singleton *sensor = nullptr; bool showingScreen = false; -#ifdef MUZI_BASE bool isAsleep = false; +#ifdef MUZI_BASE float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000, lowestZ = 98.000000; #else diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 6582335af..4495a409f 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -34,6 +34,17 @@ build_flags = -D I2C_SCL1=3 [env:heltec-v4-tft] +custom_meshtastic_hw_model = 110 +custom_meshtastic_hw_model_slug = HELTEC_V4 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec V4 TFT +custom_meshtastic_images = heltec_v4.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = heltec_v4_base build_flags = ${heltec_v4_base.build_flags} ;-Os From 3911d5fe15f415ef351a7d5856a44f1813c1a299 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 15 Jan 2026 07:54:33 -0600 Subject: [PATCH 81/83] Fix build with high / low i2c address for OLED --- src/configuration.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/configuration.h b/src/configuration.h index 178e86fb9..e15e6aa18 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -174,6 +174,8 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- #if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #define SSD1306_ADDRESS 0x3D +#define SSD1306_ADDRESS_H SSD1306_ADDRESS +#define SSD1306_ADDRESS_L 0x3C // Alternate low address for scanners #define USE_SH1106 #else #define SSD1306_ADDRESS_L 0x3C // Addr = 0 From c8f0295a9cf71bd1d08dd13869ddeb5c50fe5502 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 15 Jan 2026 08:25:38 -0600 Subject: [PATCH 82/83] Cleanup --- src/configuration.h | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index e15e6aa18..eb258651c 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -172,14 +172,12 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- -#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) -#define SSD1306_ADDRESS 0x3D -#define SSD1306_ADDRESS_H SSD1306_ADDRESS -#define SSD1306_ADDRESS_L 0x3C // Alternate low address for scanners -#define USE_SH1106 -#else #define SSD1306_ADDRESS_L 0x3C // Addr = 0 #define SSD1306_ADDRESS_H 0x3D // Addr = 1 + +#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) +#define SSD1306_ADDRESS SSD1306_ADDRESS_H +#define USE_SH1106 #endif #define ST7567_ADDRESS 0x3F From bbabd8ffe04ef57aae34a88a6cfbce878c382de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 22 Sep 2025 09:28:25 +0200 Subject: [PATCH 83/83] ThinkNode G3, ETH support WIP --- src/DebugConfiguration.h | 9 ++++- src/main.cpp | 2 +- src/mesh/InterfacesTemplates.cpp | 2 +- src/mesh/api/ethServerAPI.cpp | 2 +- src/mesh/api/ethServerAPI.h | 2 +- src/mesh/wifi/WiFiAPClient.cpp | 8 ++--- src/modules/AdminModule.cpp | 2 +- src/mqtt/MQTT.h | 2 +- .../ELECROW-ThinkNode-G3/pins_arduino.h | 26 ++++++++++++++ .../ELECROW-ThinkNode-G3/platformio.ini | 22 ++++++++++++ .../esp32s3/ELECROW-ThinkNode-G3/variant.h | 36 +++++++++++++++++++ 11 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 variants/esp32s3/ELECROW-ThinkNode-G3/pins_arduino.h create mode 100644 variants/esp32s3/ELECROW-ThinkNode-G3/platformio.ini create mode 100644 variants/esp32s3/ELECROW-ThinkNode-G3/variant.h diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index eac6260fc..a78dde78e 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -13,6 +13,11 @@ extern MemGet memGet; #define LED_STATE_ON 1 #endif +// WIFI LED +#ifndef WIFI_STATE_ON +#define WIFI_STATE_ON 1 +#endif + // ----------------------------------------------------------------------------- // DEBUG // ----------------------------------------------------------------------------- @@ -147,7 +152,9 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...); // Default Bluetooth PIN #define defaultBLEPin 123456 -#if HAS_ETHERNET && !defined(USE_WS5500) +#if HAS_ETHERNET && defined(USE_CH390D) +#include +#elif HAS_ETHERNET && !defined(USE_WS5500) #include #endif // HAS_ETHERNET diff --git a/src/main.cpp b/src/main.cpp index 88282c837..69678237d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -337,7 +337,7 @@ void setup() #ifdef WIFI_LED pinMode(WIFI_LED, OUTPUT); - digitalWrite(WIFI_LED, LOW); + digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON); #endif #ifdef BLE_LED diff --git a/src/mesh/InterfacesTemplates.cpp b/src/mesh/InterfacesTemplates.cpp index 57abbf0ee..c246141dc 100644 --- a/src/mesh/InterfacesTemplates.cpp +++ b/src/mesh/InterfacesTemplates.cpp @@ -25,7 +25,7 @@ template class LR11x0Interface; template class SX126xInterface; #endif -#if HAS_ETHERNET && !defined(USE_WS5500) +#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D) #include "api/ethServerAPI.h" template class ServerAPI; template class APIServerPort; diff --git a/src/mesh/api/ethServerAPI.cpp b/src/mesh/api/ethServerAPI.cpp index 10ff06df2..892718e04 100644 --- a/src/mesh/api/ethServerAPI.cpp +++ b/src/mesh/api/ethServerAPI.cpp @@ -1,7 +1,7 @@ #include "configuration.h" #include -#if HAS_ETHERNET && !defined(USE_WS5500) +#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D) #include "ethServerAPI.h" diff --git a/src/mesh/api/ethServerAPI.h b/src/mesh/api/ethServerAPI.h index c616c87be..5667dea15 100644 --- a/src/mesh/api/ethServerAPI.h +++ b/src/mesh/api/ethServerAPI.h @@ -1,7 +1,7 @@ #pragma once #include "ServerAPI.h" -#ifndef USE_WS5500 +#if !defined(USE_WS5500) && !defined(USE_CH390D) #include /** diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index a95dfa58f..7bb5220e5 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -384,13 +384,13 @@ static void WiFiEvent(WiFiEvent_t event) #endif } #ifdef WIFI_LED - digitalWrite(WIFI_LED, HIGH); + digitalWrite(WIFI_LED, LOW ^ WIFI_STATE_ON); #endif break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: LOG_INFO("Disconnected from WiFi access point"); #ifdef WIFI_LED - digitalWrite(WIFI_LED, LOW); + digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON); #endif if (!isReconnecting) { WiFi.disconnect(false, true); @@ -442,13 +442,13 @@ static void WiFiEvent(WiFiEvent_t event) case ARDUINO_EVENT_WIFI_AP_START: LOG_INFO("WiFi access point started"); #ifdef WIFI_LED - digitalWrite(WIFI_LED, HIGH); + digitalWrite(WIFI_LED, LOW ^ WIFI_STATE_ON); #endif break; case ARDUINO_EVENT_WIFI_AP_STOP: LOG_INFO("WiFi access point stopped"); #ifdef WIFI_LED - digitalWrite(WIFI_LED, LOW); + digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON); #endif break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 990ca0f46..75a6ebff8 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -1238,7 +1238,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r } #endif -#if HAS_ETHERNET && !defined(USE_WS5500) +#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D) conn.has_ethernet = true; conn.ethernet.has_status = true; if (Ethernet.linkStatus() == LinkON) { diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h index 7d5715602..b1b143f04 100644 --- a/src/mqtt/MQTT.h +++ b/src/mqtt/MQTT.h @@ -15,7 +15,7 @@ #include #endif #endif -#if HAS_ETHERNET && !defined(USE_WS5500) +#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D) #include #endif diff --git a/variants/esp32s3/ELECROW-ThinkNode-G3/pins_arduino.h b/variants/esp32s3/ELECROW-ThinkNode-G3/pins_arduino.h new file mode 100644 index 000000000..359355772 --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-G3/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 17; +static const uint8_t SCL = 18; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 39; +static const uint8_t MOSI = 40; +static const uint8_t MISO = 41; +static const uint8_t SCK = 42; + +// #define SPI_MOSI (11) +// #define SPI_SCK (10) +// #define SPI_MISO (9) +// #define SPI_CS (12) + +// #define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/esp32s3/ELECROW-ThinkNode-G3/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-G3/platformio.ini new file mode 100644 index 000000000..6271b99ac --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-G3/platformio.ini @@ -0,0 +1,22 @@ +[env:thinknode_g3] +extends = esp32s3_base +board = ESP32-S3-WROOM-1-N4 +board_build.psram_type = opi + +build_flags = + ${esp32s3_base.build_flags} + -D ELECROW_ThinkNode_G3 + -D HAS_UDP_MULTICAST=1 + -D BOARD_HAS_PSRAM + -D PRIVATE_HW +# -D CONFIG_ETH_ENABLED=1 +# -D CONFIG_ETH_USE_ESP32_EMAC=1 + -I variants/esp32s3/ELECROW-ThinkNode-G3 + -mfix-esp32-psram-cache-issue + +lib_ignore = + Ethernet + +lib_deps = + ${esp32s3_base.lib_deps} +# file://../ESP32-CH390/ESP32-CH390-1.0.1.tar.gz diff --git a/variants/esp32s3/ELECROW-ThinkNode-G3/variant.h b/variants/esp32s3/ELECROW-ThinkNode-G3/variant.h new file mode 100644 index 000000000..4ec352d23 --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-G3/variant.h @@ -0,0 +1,36 @@ +#define HAS_GPS 0 +#define HAS_WIRE 0 +#define I2C_NO_RESCAN + +#define WIFI_LED 5 +#define WIFI_STATE_ON 0 + +#define LED_PIN 6 // The blue LORA LED +#define LED_STATE_ON 0 +#define BUTTON_PIN 4 // the external user button of the device, BOOT and RESET are not accessible without opening it up. + +#define USE_SX1262 +#define LORA_SCK 42 +#define LORA_MISO 41 +#define LORA_MOSI 40 +#define LORA_CS 39 +#define LORA_RESET 21 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 15 +#define SX126X_BUSY 47 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define PIN_POWER_EN 45 + +// #define HAS_ETHERNET 1 +// #define USE_CH390D 1 // this driver uses the same stack as the ESP32 Wifi driver + +// #define ETH_MISO_PIN 47 +// #define ETH_MOSI_PIN 21 +// #define ETH_SCLK_PIN 48 +// #define ETH_CS_PIN 45 +// #define ETH_INT_PIN 14 +// #define ETH_RST_PIN -1 +// #define ETH_ADDR 1 \ No newline at end of file