diff --git a/arch/esp32/AsyncElegantOTA/LICENSE b/arch/esp32/AsyncElegantOTA/LICENSE new file mode 100644 index 00000000..c85f54d9 --- /dev/null +++ b/arch/esp32/AsyncElegantOTA/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Ayush Sharma + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/arch/esp32/AsyncElegantOTA/keywords.txt b/arch/esp32/AsyncElegantOTA/keywords.txt new file mode 100644 index 00000000..67de2a43 --- /dev/null +++ b/arch/esp32/AsyncElegantOTA/keywords.txt @@ -0,0 +1,2 @@ +AsyncElegantOTA KEYWORD1 +begin KEYWORD2 \ No newline at end of file diff --git a/arch/esp32/AsyncElegantOTA/library.properties b/arch/esp32/AsyncElegantOTA/library.properties new file mode 100644 index 00000000..4e934398 --- /dev/null +++ b/arch/esp32/AsyncElegantOTA/library.properties @@ -0,0 +1,9 @@ +name=AsyncElegantOTA +version=2.2.8 +author=Ayush Sharma +category=Communication +maintainer=Ayush Sharma +sentence=Perform OTAs for ESP8266 & ESP32 Asynchronously. +paragraph=A User Interface Library which provides interactive elements for your Over the Air Updates on ESP8266/ESP32. +url=https://github.com/ayushsharma82/AsyncElegantOTA +architectures=esp8266,esp32 diff --git a/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.cpp b/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.cpp new file mode 100644 index 00000000..2366906f --- /dev/null +++ b/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.cpp @@ -0,0 +1,129 @@ +#include + +AsyncElegantOtaClass AsyncElegantOTA; + +void AsyncElegantOtaClass::setID(const char* id){ + _id = id; +} + +void AsyncElegantOtaClass::begin(AsyncWebServer *server, const char* username, const char* password){ + _server = server; + + if(strlen(username) > 0){ + _authRequired = true; + _username = username; + _password = password; + }else{ + _authRequired = false; + _username = ""; + _password = ""; + } + + _server->on("/update/identity", HTTP_GET, [&](AsyncWebServerRequest *request){ + if(_authRequired){ + if(!request->authenticate(_username.c_str(), _password.c_str())){ + return request->requestAuthentication(); + } + } + #if defined(ESP8266) + request->send(200, "application/json", "{\"id\": \""+_id+"\", \"hardware\": \"ESP8266\"}"); + #elif defined(ESP32) + request->send(200, "application/json", "{\"id\": \""+_id+"\", \"hardware\": \"ESP32\"}"); + #endif + }); + + _server->on("/update", HTTP_GET, [&](AsyncWebServerRequest *request){ + if(_authRequired){ + if(!request->authenticate(_username.c_str(), _password.c_str())){ + return request->requestAuthentication(); + } + } + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", ELEGANT_HTML, ELEGANT_HTML_SIZE); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + _server->on("/update", HTTP_POST, [&](AsyncWebServerRequest *request) { + if(_authRequired){ + if(!request->authenticate(_username.c_str(), _password.c_str())){ + return request->requestAuthentication(); + } + } + // the request handler is triggered after the upload has finished... + // create the response, add header, and send response + AsyncWebServerResponse *response = request->beginResponse((Update.hasError())?500:200, "text/plain", (Update.hasError())?"FAIL":"OK"); + response->addHeader("Connection", "close"); + response->addHeader("Access-Control-Allow-Origin", "*"); + request->send(response); + restart(); + }, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + //Upload handler chunks in data + if(_authRequired){ + if(!request->authenticate(_username.c_str(), _password.c_str())){ + return request->requestAuthentication(); + } + } + + if (!index) { + if(!request->hasParam("MD5", true)) { + return request->send(400, "text/plain", "MD5 parameter missing"); + } + + if(!Update.setMD5(request->getParam("MD5", true)->value().c_str())) { + return request->send(400, "text/plain", "MD5 parameter invalid"); + } + + #if defined(ESP8266) + int cmd = (filename == "filesystem") ? U_FS : U_FLASH; + Update.runAsync(true); + size_t fsSize = ((size_t) &_FS_end - (size_t) &_FS_start); + uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + if (!Update.begin((cmd == U_FS)?fsSize:maxSketchSpace, cmd)){ // Start with max available size + #elif defined(ESP32) + int cmd = (filename == "filesystem") ? U_SPIFFS : U_FLASH; + if (!Update.begin(UPDATE_SIZE_UNKNOWN, cmd)) { // Start with max available size + #endif + Update.printError(Serial); + return request->send(400, "text/plain", "OTA could not begin"); + } + } + + // Write chunked data to the free sketch space + if(len){ + if (Update.write(data, len) != len) { + return request->send(400, "text/plain", "OTA could not begin"); + } + } + + if (final) { // if the final flag is set then this is the last frame of data + if (!Update.end(true)) { //true to set the size to the current progress + Update.printError(Serial); + return request->send(400, "text/plain", "Could not end OTA"); + } + }else{ + return; + } + }); +} + +// deprecated, keeping for backward compatibility +void AsyncElegantOtaClass::loop() { +} + +void AsyncElegantOtaClass::restart() { + yield(); + delay(1000); + yield(); + ESP.restart(); +} + +String AsyncElegantOtaClass::getID(){ + String id = ""; + #if defined(ESP8266) + id = String(ESP.getChipId()); + #elif defined(ESP32) + id = String((uint32_t)ESP.getEfuseMac(), HEX); + #endif + id.toUpperCase(); + return id; +} diff --git a/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.h b/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.h new file mode 100644 index 00000000..219d1102 --- /dev/null +++ b/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.h @@ -0,0 +1,50 @@ +#ifndef AsyncElegantOTA_h +#define AsyncElegantOTA_h + +#warning AsyncElegantOTA library is deprecated, Please consider moving to newer ElegantOTA library which now comes with an Async Mode. Learn More: https://docs.elegantota.pro/async-mode/ + +#include "Arduino.h" +#include "stdlib_noniso.h" + +#if defined(ESP8266) + #include "ESP8266WiFi.h" + #include "ESPAsyncTCP.h" + #include "flash_hal.h" + #include "FS.h" +#elif defined(ESP32) + #include "WiFi.h" + #include "AsyncTCP.h" + #include "Update.h" +#endif + +#include "Hash.h" +#include "ESPAsyncWebServer.h" +#include "FS.h" + +#include "elegantWebpage.h" + + +class AsyncElegantOtaClass{ + + public: + void + setID(const char* id), + begin(AsyncWebServer *server, const char* username = "", const char* password = ""), + loop(), + restart(); + + private: + AsyncWebServer *_server; + + String getID(); + + String _id = getID(); + String _username = ""; + String _password = ""; + bool _authRequired = false; + +}; + +extern AsyncElegantOtaClass AsyncElegantOTA; + +#endif diff --git a/arch/esp32/AsyncElegantOTA/src/Hash.h b/arch/esp32/AsyncElegantOTA/src/Hash.h new file mode 100644 index 00000000..bb4599f7 --- /dev/null +++ b/arch/esp32/AsyncElegantOTA/src/Hash.h @@ -0,0 +1,38 @@ +/** + * @file Hash.h + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the esp8266 core for Arduino environment. + * + * 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 HASH_H_ +#define HASH_H_ + +//#define DEBUG_SHA1 + +void sha1(const uint8_t* data, uint32_t size, uint8_t hash[20]); +void sha1(const char* data, uint32_t size, uint8_t hash[20]); +void sha1(const String& data, uint8_t hash[20]); + +String sha1(const uint8_t* data, uint32_t size); +String sha1(const char* data, uint32_t size); +String sha1(const String& data); + +#endif /* HASH_H_ */ \ No newline at end of file diff --git a/arch/esp32/AsyncElegantOTA/src/elegantWebpage.h b/arch/esp32/AsyncElegantOTA/src/elegantWebpage.h new file mode 100644 index 00000000..e963d845 --- /dev/null +++ b/arch/esp32/AsyncElegantOTA/src/elegantWebpage.h @@ -0,0 +1,1799 @@ +#ifndef ElegantOTAWebpage_h +#define ElegantOTAWebpage_h + +const uint32_t ELEGANT_HTML_SIZE = 53715; +const uint8_t ELEGANT_HTML[] PROGMEM = { +31,139,8,0,0,0,0,0,2,3,148,58,233,122,26,185,178,255,243,20,141,206,249,56,221,99,185,1,219,113, +98,136,236,155,197,158,53,147,57,217,102,33,190,254,68,119,1,74,26,137,145,4,14,99,243,238,183,164,110,209, +128,177,103,238,44,109,45,165,82,237,85,146,120,244,172,241,234,205,203,247,191,255,114,30,141,237,164,56,125,244, +204,253,137,10,46,71,12,228,233,35,236,3,207,79,31,69,248,207,179,9,88,30,101,99,174,13,88,54,179,195, +253,167,235,19,99,107,167,251,240,231,76,204,217,111,251,31,158,239,191,84,147,41,183,98,80,64,148,41,105,65, +90,70,190,63,103,144,143,128,172,175,147,124,2,108,46,224,122,170,180,173,65,175,69,110,199,44,135,185,200,96, +223,119,168,144,194,10,94,236,155,140,23,192,58,1,75,33,228,151,72,67,193,4,46,142,198,26,134,172,53,228, +115,215,75,241,115,26,61,179,194,22,112,122,94,192,136,75,251,230,253,243,103,45,63,82,173,55,153,22,83,27, +229,220,242,125,79,12,121,241,250,229,254,175,2,9,181,36,226,102,33,179,200,232,140,17,199,161,233,182,90,89, +46,63,155,116,48,91,76,128,103,106,56,4,72,51,53,105,117,210,118,218,110,93,251,117,233,84,171,60,157,8, +153,126,54,164,68,45,114,70,142,255,251,237,199,233,187,207,213,72,14,229,214,66,73,70,222,205,166,94,0,19, +136,144,139,23,179,133,107,241,168,196,223,168,86,76,192,24,62,66,10,127,87,179,40,227,50,226,197,53,95,152, +200,132,197,139,232,90,233,47,209,96,17,33,121,66,142,118,33,201,84,161,52,35,255,186,184,120,218,57,188,168, +6,167,202,136,146,16,45,70,99,91,141,126,189,154,112,61,18,56,122,112,84,13,45,214,135,78,159,181,74,22, +208,112,90,206,82,156,197,12,84,190,136,140,93,20,72,167,154,131,30,22,234,186,27,141,69,158,131,236,5,165, +73,21,22,186,110,169,7,171,149,28,157,254,10,255,209,16,25,165,181,99,194,70,181,218,162,92,129,145,255,177, +37,143,40,225,41,232,2,57,22,118,172,16,240,7,62,231,239,74,93,130,228,104,119,121,26,253,82,0,55,80, +245,35,97,35,171,188,137,9,57,131,20,137,247,91,86,36,181,54,105,122,150,139,121,132,74,227,211,41,178,137, +157,45,115,129,33,232,211,120,56,147,153,147,91,108,147,155,57,215,17,176,155,101,47,12,70,58,150,201,141,24, +198,208,151,151,137,6,59,211,50,114,237,20,190,58,125,153,158,91,162,152,27,98,55,162,43,105,209,109,116,104, +53,217,189,89,46,123,213,34,235,22,161,217,23,177,10,107,169,162,117,91,39,216,41,88,163,93,143,45,117,58, +97,150,234,52,99,128,223,156,213,164,82,160,72,151,78,149,107,38,183,183,111,6,159,33,179,41,178,36,36,252, +226,229,106,23,30,236,6,228,108,2,218,73,175,139,200,209,178,187,114,153,44,17,159,102,235,172,147,153,44,87, +231,164,193,152,93,76,65,13,163,119,139,201,64,21,205,102,249,55,181,234,157,213,104,147,239,249,168,217,188,111, +203,187,176,20,229,90,204,160,75,94,171,124,86,0,89,38,244,190,197,228,234,10,76,5,22,150,53,218,37,189, +118,131,127,175,149,78,19,154,205,216,50,141,28,36,244,105,19,146,32,237,30,206,30,185,89,162,252,86,132,173, +152,178,205,166,251,47,173,183,170,87,57,109,74,86,81,151,105,224,22,98,57,43,138,196,225,67,145,197,242,62, +218,37,37,56,194,103,133,37,219,50,47,217,176,200,246,129,167,200,120,193,144,198,138,160,100,168,116,236,13,41, +18,18,187,168,107,196,167,232,138,95,228,118,101,70,112,185,76,7,66,230,158,46,170,146,36,88,152,116,66,146, +236,174,61,111,113,123,182,130,88,195,186,34,254,114,217,221,49,191,50,99,71,26,80,194,9,133,132,130,219,81, +109,170,37,44,169,164,132,62,110,149,227,51,29,115,243,230,90,6,121,121,87,240,11,28,142,41,35,45,66,53, +74,216,176,118,178,76,226,155,118,119,195,216,117,114,99,131,91,160,182,201,227,227,252,9,193,165,164,221,6,32, +119,96,189,26,29,220,224,248,224,128,36,84,49,25,147,53,139,196,33,225,220,92,244,213,37,35,127,17,90,35, +39,253,210,96,162,191,46,157,205,148,75,98,225,247,58,60,62,126,96,175,78,214,30,144,164,87,163,218,134,68, +19,114,90,161,115,37,242,168,141,200,215,12,207,96,12,204,198,49,66,101,46,220,181,187,213,204,14,93,148,162, +67,193,245,60,104,231,14,168,190,3,75,117,128,62,184,11,77,229,14,120,42,195,138,195,93,43,168,218,185,6, +135,151,203,7,40,199,72,92,44,16,20,243,15,58,136,180,6,193,157,96,31,63,62,122,64,176,39,199,252,8, +5,27,252,166,246,100,137,206,47,89,191,111,83,129,123,19,114,121,153,80,153,22,10,9,50,56,85,107,34,12, +38,101,184,118,72,143,78,78,128,36,105,101,246,61,133,250,123,114,220,233,60,61,124,74,16,23,122,237,141,81, +51,157,193,107,62,117,17,221,140,121,174,174,209,129,0,123,203,196,19,253,100,248,0,209,195,236,152,59,203,115, +237,131,163,78,134,123,13,189,209,173,34,35,229,108,21,156,2,71,215,66,226,54,205,102,248,91,121,209,8,236, +154,239,252,204,39,96,206,238,159,138,203,213,73,183,127,73,205,70,60,176,122,17,148,161,176,191,204,184,51,186, +218,105,121,106,10,145,65,140,106,169,237,56,29,174,225,168,65,155,205,149,171,252,234,247,115,254,34,42,191,78, +206,12,126,186,202,27,124,226,117,124,156,61,36,174,167,135,124,16,196,149,119,224,9,182,133,119,244,236,56,195, +54,95,19,169,113,237,172,125,4,216,206,60,76,231,240,49,182,11,215,110,103,67,135,103,198,118,138,231,85,85, +175,41,221,3,100,75,158,205,186,119,114,138,101,220,57,41,48,19,3,218,1,226,77,214,196,54,115,112,65,112, +200,192,18,87,100,126,44,248,178,136,27,42,29,214,241,141,186,144,237,68,224,105,251,167,34,104,31,158,4,17, +100,89,231,96,35,174,52,208,238,27,42,142,239,122,216,147,6,219,157,157,4,162,20,115,146,248,200,125,227,138, +128,29,139,151,203,36,229,203,196,25,119,231,132,115,178,37,155,7,3,91,35,182,145,144,198,114,153,57,59,70, +113,216,177,86,215,209,123,180,235,115,173,21,242,241,189,204,148,214,206,94,200,94,172,207,244,30,137,72,151,144, +100,143,8,57,71,247,116,24,145,207,85,202,113,100,12,224,161,192,144,183,143,143,55,36,35,113,76,101,62,180, +16,186,106,98,253,233,71,28,99,46,72,255,19,198,60,83,36,244,215,115,245,54,95,85,134,176,9,242,35,76, +36,149,141,184,15,126,187,248,201,158,252,211,108,37,44,104,110,149,246,86,208,232,244,156,13,58,64,206,218,212, +176,27,9,95,119,232,240,38,87,18,3,84,131,239,237,225,118,229,216,58,20,98,106,163,107,27,151,246,118,196, +231,177,48,75,250,92,107,190,72,135,90,77,98,67,215,109,172,100,252,96,153,4,243,207,208,252,119,27,69,105, +18,128,102,42,42,183,64,14,28,245,122,157,21,233,82,176,220,73,203,131,252,105,207,5,50,104,99,185,78,75, +40,81,74,73,231,217,3,146,62,60,58,24,110,88,78,43,22,211,49,34,191,21,83,149,227,135,231,73,250,141, +75,87,112,13,131,47,194,182,68,106,193,184,13,29,242,252,105,251,31,27,81,200,246,119,45,135,188,228,238,44, +228,34,69,52,1,59,86,185,59,56,146,61,187,101,52,121,254,128,209,132,80,161,214,12,72,248,164,147,183,219, +62,114,170,152,152,41,100,2,204,58,199,187,98,186,56,101,143,59,183,183,13,185,22,91,66,17,137,185,68,51, +119,76,150,198,234,89,102,149,118,218,11,34,239,243,93,74,28,42,213,237,32,11,157,6,99,208,183,151,241,11, +165,10,224,50,73,113,102,233,243,194,193,193,241,241,3,33,241,224,241,42,137,194,9,182,67,86,104,103,71,33, +43,184,170,44,100,133,195,199,188,19,178,194,201,32,207,177,93,108,25,166,51,242,212,88,53,157,66,206,44,245, +93,13,102,86,88,6,75,58,99,247,69,57,58,163,195,146,178,156,78,233,152,78,232,156,14,232,136,46,24,143, +253,244,217,65,183,227,143,9,195,4,17,247,160,48,224,212,63,101,46,19,210,29,161,100,122,215,32,222,115,61, +2,27,194,136,176,229,25,130,120,180,42,158,38,9,138,84,199,99,12,1,19,38,98,155,22,32,71,118,156,244, +38,167,227,222,120,111,47,113,230,198,102,103,139,88,198,35,102,251,227,203,164,223,190,164,163,126,231,50,233,46, +98,63,64,231,205,230,124,61,84,23,33,113,205,87,7,9,184,142,138,184,209,73,150,57,155,134,132,190,28,176, +60,117,94,217,187,30,139,2,226,6,238,48,40,39,243,36,73,157,99,150,219,103,113,78,23,116,148,250,83,15, +157,37,244,78,145,131,20,252,127,136,232,205,188,190,118,24,108,0,106,83,235,19,236,193,97,246,80,130,229,199, +39,29,111,77,175,185,29,167,19,254,149,138,170,41,100,239,190,64,86,70,45,183,233,202,216,159,181,207,84,172, +247,128,182,147,174,136,53,133,106,115,120,242,144,155,242,167,71,193,148,125,45,228,235,65,111,168,157,206,65,48, +230,99,128,193,170,196,129,186,196,129,167,39,135,161,196,57,57,202,56,73,30,38,216,89,43,173,12,21,77,218, +122,187,162,115,108,142,10,53,224,5,29,96,19,165,239,207,201,51,54,63,147,221,193,153,236,79,46,111,111,77, +60,161,55,203,164,27,151,93,108,214,135,56,58,243,71,213,161,59,166,66,82,154,55,244,135,151,212,166,82,149, +198,251,45,216,51,180,80,21,59,127,161,57,27,55,155,227,210,22,146,110,206,102,14,88,179,34,158,159,13,187, +147,189,120,112,70,82,210,37,255,194,42,96,136,72,16,121,6,121,66,27,186,217,44,227,38,70,142,220,111,20, +156,166,174,254,243,36,220,3,245,178,120,74,243,100,25,35,79,99,62,185,189,205,155,205,220,55,147,102,83,224, +28,113,109,226,171,57,238,8,67,193,88,127,244,240,165,249,3,90,203,214,180,246,228,105,85,147,41,23,5,51, +110,99,82,250,31,161,100,37,33,146,248,202,242,254,226,252,246,118,151,29,227,198,34,41,233,217,85,35,146,153, +129,200,29,127,50,75,234,187,169,13,11,181,253,14,106,139,16,42,177,121,120,233,244,218,144,73,48,89,215,197, +116,92,71,161,90,140,3,171,120,137,4,57,139,165,51,68,153,150,103,31,131,78,50,141,227,187,4,147,214,55, +255,138,74,152,15,111,127,98,36,8,36,44,124,171,148,117,196,36,97,194,82,18,125,211,34,201,114,117,93,209, +215,151,97,146,175,192,250,226,50,73,63,43,33,99,242,73,34,116,13,186,54,184,226,95,213,87,28,142,137,120, +38,193,100,124,10,49,200,76,229,72,217,247,238,6,91,73,144,54,254,225,221,155,159,211,242,0,41,134,11,92, +136,255,160,33,146,213,33,111,138,51,142,23,119,69,218,197,110,33,202,138,180,245,217,40,217,219,184,51,239,13, +184,129,227,35,186,98,27,144,169,90,46,171,97,29,152,222,229,170,117,94,13,113,5,86,39,195,221,181,217,182, +54,214,130,19,208,58,60,65,255,224,242,140,252,207,4,114,193,107,90,220,40,146,115,67,146,53,234,150,36,233, +234,101,18,68,238,10,100,72,197,122,60,241,151,3,119,15,222,214,223,186,245,251,254,10,202,250,147,119,56,90, +223,44,157,173,201,213,149,150,96,237,158,120,230,201,47,189,165,39,48,81,85,165,172,27,70,157,99,146,234,57, +84,13,198,17,177,114,197,4,186,234,50,160,48,136,194,60,179,97,189,9,235,51,20,222,202,192,250,6,73,144, +205,166,234,103,136,14,93,33,214,136,43,115,194,112,31,70,54,212,194,101,30,213,162,112,0,73,215,131,233,4, +69,48,157,25,87,81,58,135,4,239,148,143,135,237,191,113,202,16,49,66,4,87,107,229,139,8,101,90,136,242, +60,63,206,125,148,39,65,227,132,102,236,45,140,206,191,78,215,130,109,193,50,228,138,206,152,216,113,214,35,45, +222,26,144,6,43,202,28,92,221,86,116,221,249,110,88,240,145,233,146,1,89,58,127,163,67,132,113,175,34,13, +102,122,241,12,227,15,6,69,25,223,217,205,208,237,162,207,50,21,59,13,185,195,112,56,229,84,14,238,124,199, +166,126,35,42,195,228,234,94,11,37,191,113,26,44,247,194,211,64,76,252,18,34,100,148,37,103,60,212,22,93, +93,59,16,217,131,61,247,149,203,132,222,204,164,225,195,112,3,124,112,124,112,252,143,148,16,14,134,84,149,117, +224,48,212,196,161,62,230,107,199,109,195,196,195,245,113,237,170,216,116,124,171,116,216,67,59,5,228,7,80,63, +205,166,142,157,244,110,208,152,134,98,52,219,184,106,191,231,144,181,44,99,253,160,13,127,99,86,27,177,247,81, +235,155,198,163,232,155,232,227,12,240,81,42,154,31,164,199,105,167,227,70,208,88,163,131,118,231,104,31,63,39, +209,249,156,203,8,223,151,220,204,91,240,239,39,121,228,174,247,53,238,14,209,235,239,223,71,63,137,12,164,129, +20,65,90,143,124,20,9,41,107,168,1,254,130,24,211,254,70,162,89,209,191,82,50,6,120,231,179,174,181,30, +146,183,33,27,56,223,108,122,239,222,128,20,53,100,163,189,137,132,175,77,117,54,167,76,61,181,35,42,97,202, +145,179,201,0,244,214,160,241,79,18,91,131,131,242,44,178,62,90,239,147,173,241,17,104,223,249,150,176,116,194, +43,216,157,219,238,224,216,181,16,103,53,202,213,197,89,185,204,95,52,23,193,25,106,34,134,59,86,148,174,116, +223,138,188,54,214,169,75,88,23,133,226,182,190,161,168,115,196,41,107,55,155,190,246,29,22,74,233,24,18,68, +136,22,45,204,133,123,163,133,13,172,211,154,14,175,224,221,117,132,77,209,184,228,125,115,254,148,94,163,28,111, +201,23,33,207,8,233,150,119,15,194,248,191,8,114,123,59,243,251,217,149,60,29,223,103,219,201,156,250,60,116, +144,116,87,156,214,59,77,118,138,164,78,151,194,252,204,127,70,254,207,108,23,234,85,243,178,180,10,25,72,239, +122,18,162,210,213,208,211,66,216,152,80,31,109,218,61,245,76,134,52,165,48,77,233,190,187,222,112,217,108,37, +249,179,29,229,159,238,59,6,127,82,215,160,95,114,3,113,130,143,48,187,193,46,151,203,121,76,76,161,44,205, +66,109,227,235,90,31,251,6,12,39,191,192,130,106,24,82,7,228,63,251,38,83,83,160,194,144,53,151,30,213, +247,157,21,193,171,66,50,21,50,135,175,111,134,49,148,175,94,167,251,157,80,71,150,252,102,16,107,218,73,150, +222,246,23,236,111,94,122,234,61,175,55,30,136,214,159,128,86,32,95,107,109,237,122,132,219,241,210,81,198,124, +192,26,49,204,74,204,253,174,207,44,206,87,84,94,177,214,126,252,233,58,105,141,232,23,246,117,87,73,139,188, +105,152,22,28,153,187,162,241,206,39,45,56,115,126,253,97,58,13,106,234,18,76,177,62,199,254,116,47,82,87, +57,62,183,113,59,217,92,186,103,171,171,246,142,95,255,154,181,62,189,136,251,207,247,255,184,116,52,190,252,91, +26,95,83,178,255,239,14,73,54,237,6,81,213,226,254,92,18,191,234,235,90,92,171,247,151,96,172,65,116,103, +242,180,115,182,235,157,166,91,63,243,212,237,100,117,229,150,94,149,136,88,176,38,170,107,165,190,219,144,162,45, +95,47,33,241,138,121,195,46,42,176,218,128,60,192,217,187,238,231,154,151,231,37,10,96,112,123,219,174,110,18, +195,86,251,64,37,115,55,1,101,212,208,73,117,67,161,247,247,19,233,237,160,175,247,160,54,143,154,176,247,91, +94,94,30,107,173,91,179,110,80,182,94,241,115,108,107,120,192,98,151,106,244,122,93,23,167,122,111,207,35,104, +54,223,163,172,92,171,14,186,53,154,243,144,236,189,12,190,223,186,106,170,147,223,146,254,197,118,153,193,178,150, +204,191,107,71,94,123,81,108,4,33,185,68,134,226,201,42,103,110,104,119,189,23,128,176,78,115,111,9,171,168, +89,191,119,66,178,186,165,85,108,59,42,83,177,53,84,226,86,152,62,66,160,8,242,112,36,85,77,23,197,97, +14,122,177,110,217,53,179,37,35,78,94,203,196,163,219,168,32,95,113,139,217,9,182,135,234,237,70,96,223,139, +9,196,62,143,213,61,79,215,237,237,214,45,52,15,193,5,99,165,113,12,153,141,1,88,169,140,215,108,152,21, +27,252,14,27,27,76,32,11,129,143,112,57,93,172,105,180,54,130,87,155,214,183,195,144,144,248,128,48,169,143, +242,101,99,191,83,99,250,165,142,154,200,225,221,247,87,112,17,17,147,16,13,158,237,138,208,218,185,67,144,188, +96,196,255,64,200,128,70,6,247,53,184,146,17,114,66,63,176,62,89,75,55,36,23,26,16,247,28,176,61,20, +133,5,77,46,233,239,8,52,0,228,6,94,186,144,237,230,50,215,112,8,170,137,215,106,230,215,79,240,239,250, +248,135,105,94,46,152,249,70,61,241,10,140,213,106,225,182,44,91,126,142,187,189,43,184,28,214,123,160,181,194, +80,56,69,1,248,126,201,201,47,26,134,128,122,64,34,223,178,27,53,117,82,121,13,122,4,104,236,184,112,36, +192,116,119,229,119,35,10,228,215,189,254,78,181,202,103,94,154,239,197,212,13,228,48,183,74,21,198,79,130,70, +90,39,206,44,93,215,211,240,29,151,121,1,186,235,16,209,107,174,229,198,128,24,73,133,4,86,239,82,198,189, +211,162,225,189,84,249,61,132,8,243,22,60,43,249,123,62,234,126,191,214,127,110,173,246,3,31,228,23,169,174, +101,133,18,135,208,5,16,216,95,57,77,57,82,118,78,125,249,243,75,193,173,163,182,154,235,254,69,39,51,99, +63,24,255,84,136,203,252,143,229,220,233,229,170,16,67,200,22,89,1,223,41,245,197,116,127,95,210,23,172,197, +247,255,194,28,245,105,214,110,191,120,226,190,47,219,251,238,207,171,99,255,125,234,59,23,199,254,235,59,135,79, +94,249,239,5,118,58,23,23,23,159,102,7,184,104,223,255,121,229,190,135,23,190,115,212,118,223,39,109,215,233, +60,117,96,47,219,190,115,113,142,157,195,118,187,131,157,87,79,28,130,139,19,63,115,241,234,229,133,251,94,184, +14,98,126,213,170,99,226,127,107,127,136,237,30,33,137,207,194,78,190,62,19,7,15,57,60,102,204,37,148,147, +199,238,111,237,78,223,58,199,44,175,62,238,253,93,85,248,113,146,166,235,191,245,105,72,122,173,133,13,39,192, +173,19,225,178,76,121,223,185,100,85,21,242,49,233,255,47,217,123,81,157,170,247,72,250,239,171,79,159,242,203, +245,74,237,215,234,29,169,241,157,127,129,194,94,224,109,85,125,166,228,110,105,100,183,35,11,172,71,22,143,208, +86,49,165,103,49,69,186,144,117,185,172,223,157,60,169,31,233,111,140,92,93,249,204,124,117,69,132,196,132,247, +7,219,241,83,177,240,203,5,250,195,238,217,95,127,59,151,115,161,149,116,214,137,89,167,177,49,144,78,43,171, +164,127,178,31,154,205,221,115,155,149,14,253,145,253,17,126,38,145,74,62,23,35,247,88,154,206,12,232,231,35, +183,108,19,218,90,246,99,179,217,154,24,1,183,86,139,28,33,90,165,56,127,76,40,248,201,31,87,117,47,113, +96,209,73,218,38,201,105,155,234,237,89,200,71,208,242,83,210,178,120,115,14,189,92,43,225,46,119,220,118,171, +39,69,158,87,239,138,202,172,182,197,35,40,118,221,33,233,207,132,170,18,83,43,27,107,53,129,79,173,79,249, +94,77,159,155,152,142,185,180,106,242,217,212,195,126,231,137,79,48,173,161,208,48,84,95,63,181,98,92,153,180, +146,132,10,235,126,107,114,237,166,41,183,46,47,160,202,255,72,66,86,55,110,186,183,219,188,141,165,100,202,141, +17,115,184,251,99,1,110,253,235,107,66,43,217,243,60,63,159,131,180,63,9,99,65,130,142,137,163,111,127,181, +222,7,60,99,67,42,252,194,171,162,39,179,117,101,115,247,114,225,99,179,25,127,100,13,84,113,3,13,98,167, +73,249,59,200,190,187,121,207,192,24,130,21,87,21,238,157,72,215,39,82,144,243,244,227,135,243,171,243,159,63, +162,96,62,46,105,97,215,140,231,234,202,205,189,58,255,248,254,205,155,159,222,93,125,251,211,155,23,207,127,186, +250,238,205,155,31,175,174,214,78,239,182,174,191,118,158,112,81,71,146,187,156,24,185,187,231,74,75,245,185,53, +78,74,231,31,90,154,91,246,224,143,45,113,171,178,153,220,195,249,91,24,22,144,89,15,89,181,83,140,253,63, +194,194,96,220,184,15,59,148,11,240,111,114,246,110,83,167,43,46,109,28,30,72,193,254,31,99,95,194,29,183, +141,252,249,85,70,253,188,27,114,13,107,236,188,155,109,76,191,196,113,102,146,141,115,223,94,143,30,221,13,89, +140,187,65,13,136,150,227,72,218,207,254,175,31,10,71,241,144,198,57,212,32,136,179,0,20,234,230,18,3,150, +240,195,132,219,43,43,57,145,233,196,166,192,183,42,81,7,155,102,84,71,20,196,238,26,149,165,27,168,117,165, +244,253,3,164,170,183,21,51,195,151,94,63,87,23,30,186,82,63,171,222,145,98,202,63,124,168,184,173,227,235, +129,68,201,183,235,195,100,140,63,28,95,207,135,137,210,44,33,246,212,221,168,138,51,135,254,202,76,107,189,169, +114,61,53,171,178,51,151,198,238,228,248,14,172,174,195,114,229,36,6,243,153,185,12,13,77,91,176,189,39,33, +136,158,201,110,115,167,201,166,107,93,248,150,199,170,176,79,107,243,212,173,77,96,91,204,171,83,166,192,42,116, +146,58,215,128,108,0,233,149,39,48,149,51,241,218,135,123,214,39,104,136,42,130,101,122,227,43,46,212,95,86, +178,200,149,127,121,149,89,184,39,175,88,152,224,167,90,119,152,193,118,170,85,67,132,190,111,223,36,173,61,104, +85,109,56,189,37,142,111,231,140,213,142,159,189,249,147,198,205,105,179,63,232,158,147,118,208,140,98,98,173,222, +134,130,29,63,158,219,103,156,33,10,33,247,155,64,49,14,147,220,31,32,85,249,98,39,115,193,62,104,240,41, +72,112,78,38,155,83,35,237,36,255,139,200,209,140,218,33,82,141,94,141,178,92,251,142,144,56,167,187,225,7, +79,184,102,43,50,160,113,163,166,140,3,122,78,153,164,254,194,21,42,138,61,219,83,167,59,145,241,13,186,78, +143,129,248,251,188,133,113,199,123,61,136,188,23,198,183,114,52,224,252,144,255,45,100,17,23,253,126,103,156,6, +183,250,142,110,149,176,20,205,68,26,14,171,163,119,158,151,9,220,217,2,238,95,6,203,237,162,185,117,103,134, +234,189,56,4,212,49,31,122,90,59,121,244,242,125,194,10,171,213,138,75,25,80,96,180,217,132,230,13,171,238, +149,145,64,123,172,140,224,179,207,188,96,192,185,122,21,33,50,254,41,210,214,114,6,222,134,202,178,235,202,99, +43,43,31,118,177,138,144,113,144,159,150,116,58,184,202,135,241,41,143,173,140,178,189,229,231,217,238,162,44,185, +134,69,244,128,157,239,233,143,50,101,243,248,156,164,92,236,92,143,191,35,24,248,146,86,70,156,15,95,210,202, +136,19,226,75,90,25,113,70,124,73,43,35,182,148,47,105,101,196,246,4,232,1,173,175,60,75,26,196,74,191, +152,226,253,175,124,173,158,121,98,62,129,133,160,138,239,47,193,249,93,116,231,30,28,165,77,41,150,92,34,209, +59,60,59,112,241,131,89,189,90,63,11,134,7,207,91,34,79,22,188,65,190,194,173,180,254,103,245,194,43,63, +83,145,57,216,69,205,229,105,81,2,101,73,2,229,224,25,146,223,211,3,235,73,85,167,141,100,198,29,180,82, +72,17,61,210,191,38,202,35,26,132,123,54,8,231,201,53,72,230,41,53,189,118,235,215,4,132,183,107,228,167, +9,34,59,238,156,143,107,126,159,174,237,30,194,139,254,117,32,147,88,114,211,83,183,184,131,226,45,66,123,173, +99,137,102,24,229,31,254,30,67,6,2,72,173,126,192,57,41,135,228,155,176,207,41,147,245,34,159,248,249,245, +25,216,166,136,194,209,115,56,15,135,248,124,117,120,6,209,0,221,79,224,193,86,12,138,149,98,37,228,84,12, +181,169,126,221,252,232,169,28,13,164,249,154,19,234,15,95,115,83,114,154,56,140,77,200,125,215,238,223,210,147, +56,215,104,129,237,247,50,159,35,121,193,208,174,81,174,112,83,150,70,7,16,199,149,182,79,251,181,77,26,233, +78,59,172,48,6,223,41,3,107,134,91,33,244,243,194,94,3,212,248,54,232,54,38,150,179,239,125,22,245,188, +19,48,8,74,16,78,203,210,159,248,141,211,233,5,169,63,168,185,173,175,168,112,181,172,74,169,179,73,121,55, +60,255,211,27,59,192,189,143,199,65,205,116,3,233,22,169,178,99,121,170,71,13,69,25,142,254,139,139,3,170, +73,200,118,191,240,229,162,78,48,136,43,218,254,55,27,108,212,100,169,100,75,82,73,24,9,182,167,242,218,224, +246,6,24,5,180,104,68,109,57,57,24,191,30,48,211,155,155,143,81,105,114,248,160,250,7,67,27,77,18,246, +250,132,246,61,1,223,213,235,15,118,147,186,95,151,27,49,195,176,25,138,6,59,161,90,65,190,85,93,36,239, +232,88,237,233,113,143,199,146,53,145,161,210,18,124,230,233,7,16,191,85,131,236,48,66,194,78,122,52,90,107, +82,118,152,147,160,186,179,39,225,41,2,166,218,110,182,69,193,210,56,109,84,6,132,161,83,158,15,60,237,80, +185,73,255,242,197,168,123,182,135,200,170,138,42,207,100,188,201,156,174,74,57,176,122,207,170,34,163,158,0,187, +177,9,18,36,236,216,164,156,154,104,142,74,211,88,61,237,168,14,79,59,109,240,4,227,180,83,73,3,77,243, +78,27,115,227,26,187,169,104,67,218,104,123,104,208,175,29,163,55,87,55,85,106,93,92,208,15,194,180,239,155, +180,156,79,205,214,157,225,32,151,193,149,81,201,227,114,115,195,106,47,90,255,157,217,27,111,194,228,20,138,140, +70,86,75,201,176,151,226,155,76,26,58,194,60,182,208,237,238,169,13,178,28,163,89,68,12,186,147,135,82,82, +31,180,227,110,63,145,252,4,80,164,94,18,35,153,169,212,124,65,174,244,133,207,82,124,53,106,85,162,227,89, +235,247,48,37,180,93,217,127,34,108,133,111,189,254,254,116,81,128,91,16,250,231,101,41,79,132,131,85,234,39, +178,20,122,231,55,19,222,153,32,209,140,213,0,170,165,25,182,79,187,52,168,22,42,93,221,145,125,82,198,203, +39,193,218,165,178,188,6,189,54,248,193,138,187,122,131,19,73,39,238,88,217,26,127,123,250,75,131,3,162,108, +194,49,115,148,170,23,148,76,63,229,51,24,223,185,205,4,247,88,189,36,131,48,27,195,71,222,97,155,27,213, +47,150,242,27,47,74,249,162,250,75,99,235,111,27,179,241,11,94,130,84,224,190,110,153,144,193,31,116,126,111, +215,162,164,175,169,187,70,204,254,55,121,77,106,140,196,23,59,187,102,186,133,55,166,161,237,81,166,225,54,223, +3,211,55,226,138,250,94,158,166,104,201,62,215,181,60,130,165,137,201,146,60,44,39,173,87,180,5,187,75,143, +247,105,190,253,120,188,253,132,54,141,54,50,165,234,230,199,170,87,6,48,254,150,73,255,114,22,166,75,158,119, +65,51,50,220,60,17,112,247,13,23,162,195,246,219,50,9,251,109,16,169,252,230,161,99,254,233,238,34,15,87, +36,39,211,159,134,98,223,122,150,26,206,56,241,172,100,236,152,155,98,172,84,43,147,178,76,206,42,7,239,14, +120,20,241,51,160,82,76,7,179,213,32,174,7,192,202,83,251,201,90,176,39,130,74,13,218,208,15,12,176,78, +198,123,161,5,138,109,53,29,206,90,161,164,110,55,109,218,56,195,100,227,80,198,134,212,46,67,22,120,247,183, +152,55,28,210,7,77,9,246,198,8,201,206,98,2,72,129,217,58,122,179,91,6,204,124,58,247,24,48,240,204, +128,178,121,63,168,220,255,21,201,166,245,79,62,52,241,221,88,26,50,151,146,98,7,72,30,245,159,163,163,19, +231,3,88,59,206,139,194,20,0,122,118,209,57,82,43,88,237,230,12,76,31,104,90,53,55,183,234,65,225,232, +183,129,135,0,188,175,145,31,20,92,68,81,224,130,252,27,108,203,209,112,54,228,196,170,58,180,8,187,70,212, +29,98,93,96,199,77,223,112,19,253,237,58,84,127,188,78,75,210,138,203,241,95,147,73,242,2,201,89,230,204, +229,137,150,241,176,197,142,19,22,59,48,24,8,22,59,215,112,128,106,144,198,96,228,92,228,14,117,105,107,58, +236,73,203,51,105,107,58,229,92,191,187,85,109,221,112,154,230,192,179,18,115,249,69,200,5,252,105,86,171,134, +53,51,245,196,46,65,154,186,72,236,42,189,97,241,82,95,195,126,162,177,138,101,121,141,29,17,120,63,75,2, +111,17,157,135,147,108,226,53,75,11,68,187,10,229,213,191,226,47,13,218,224,136,159,158,193,14,26,165,79,13, +88,137,221,16,208,2,119,144,178,168,66,77,79,135,238,207,142,218,170,199,76,84,202,159,242,82,185,17,126,141, +237,231,152,152,143,187,23,205,116,236,43,63,84,93,93,158,77,141,187,183,35,110,135,243,139,221,96,198,208,132, +239,44,153,7,127,231,215,45,37,96,226,138,246,77,232,68,217,44,226,110,11,200,126,245,163,147,62,63,9,46, +181,13,82,5,107,247,174,234,49,241,132,92,176,98,40,128,29,239,234,84,160,19,5,58,46,208,234,175,170,46, +23,104,69,129,150,11,12,154,26,163,209,163,6,253,69,118,44,50,136,53,254,125,118,45,193,251,10,126,132,239, +42,167,124,48,128,69,198,160,189,73,62,89,170,63,197,108,66,231,3,76,190,232,183,35,36,139,129,228,224,0, +117,221,66,147,148,14,196,10,48,32,222,173,165,159,103,89,49,185,69,171,44,5,203,141,86,219,167,143,105,77, +158,110,25,73,195,186,91,184,198,233,150,244,74,250,203,64,127,40,159,216,182,31,252,154,228,9,84,84,17,17, +216,214,138,30,246,75,203,243,101,217,209,128,155,145,195,77,103,230,52,102,101,106,11,236,244,131,184,197,25,201, +144,61,9,93,42,121,72,75,175,9,244,197,47,5,132,127,120,67,185,27,145,110,238,56,155,171,207,197,21,254, +31,95,25,6,205,198,102,142,206,138,37,164,2,50,126,131,84,36,37,157,223,191,255,223,240,127,114,121,50,110, +131,222,175,16,26,134,252,65,96,159,150,75,252,95,47,47,17,238,64,243,72,4,35,228,77,161,158,167,180,86, +220,141,104,9,14,7,155,199,205,163,39,249,198,102,230,196,76,153,19,106,7,197,65,21,251,123,109,88,140,73, +171,72,106,5,182,62,162,186,5,235,165,27,73,219,211,7,44,30,79,123,219,150,149,26,217,127,4,219,159,137, +31,66,47,157,16,138,75,109,48,51,198,153,226,181,176,138,135,130,22,218,56,104,161,191,180,134,126,148,157,152, +155,252,237,130,236,36,86,240,42,178,105,42,183,36,102,160,246,222,95,67,7,34,22,215,153,153,208,132,39,172, +157,48,190,147,134,118,10,71,177,203,2,154,75,194,18,156,113,17,236,74,118,65,216,192,35,156,219,13,50,112, +209,25,185,40,87,36,246,56,116,131,249,123,144,188,214,193,37,71,149,134,112,52,197,84,77,156,106,159,15,94, +87,166,97,141,56,120,223,159,74,195,23,233,50,63,126,19,33,12,173,48,87,22,189,189,109,217,206,186,231,94, +81,102,197,146,152,81,19,52,230,62,117,45,76,207,229,104,88,115,124,115,35,84,161,229,48,82,147,3,225,188, +232,126,233,215,241,153,251,128,116,48,172,136,81,109,208,140,12,129,99,216,34,93,238,147,189,33,182,136,243,88, +219,54,24,150,185,194,188,100,72,167,64,63,150,58,183,181,41,28,71,210,183,85,1,15,46,42,108,227,58,5, +165,109,76,71,124,118,52,154,51,160,113,164,145,95,5,67,55,35,117,42,71,19,172,177,43,154,165,130,193,197, +192,6,113,61,181,245,188,190,13,51,123,156,105,52,239,239,0,212,139,163,111,209,224,55,204,188,195,118,144,26, +152,230,214,37,18,196,172,198,179,226,46,252,10,115,155,190,151,250,241,206,44,107,174,105,236,95,28,130,59,147, +103,104,200,140,90,242,169,242,5,166,62,10,101,35,64,176,55,138,118,121,17,226,156,27,13,147,46,8,46,103, +51,8,32,188,52,58,5,19,136,244,252,143,68,223,124,221,239,210,69,87,157,27,98,229,119,89,206,81,93,26, +117,13,163,163,118,235,141,195,221,17,236,127,212,120,149,168,95,170,248,240,73,253,191,62,166,46,152,31,44,237, +197,101,18,70,243,102,34,64,30,34,123,42,181,18,88,207,112,250,18,238,152,31,103,163,86,214,252,73,208,216, +18,190,226,93,224,130,23,75,192,5,91,3,233,33,122,166,209,86,148,113,66,251,231,190,29,90,11,165,88,204, +154,32,33,237,111,163,249,195,129,129,124,238,203,81,186,50,40,243,26,115,59,16,168,15,81,227,95,137,163,253, +90,78,92,89,53,51,73,101,134,178,58,233,33,252,12,194,238,44,229,254,220,245,127,25,27,242,102,114,118,0, +43,137,239,146,240,218,75,201,89,23,46,145,96,227,80,101,114,109,29,108,3,232,25,71,151,240,119,145,90,9, +195,99,26,50,75,229,24,192,215,118,46,60,179,119,212,178,84,15,21,163,13,214,27,163,255,92,210,75,173,254, +55,78,170,48,48,95,123,109,54,197,172,188,241,209,4,120,245,255,231,5,221,172,32,100,74,39,211,130,137,90, +210,118,84,94,93,195,159,140,126,123,216,60,58,181,229,43,176,177,42,154,253,16,87,58,50,69,127,111,102,182, +232,60,13,95,164,247,10,220,231,185,29,214,115,210,195,22,202,193,84,150,47,134,92,15,219,249,234,17,142,71, +186,29,214,133,191,43,58,212,133,251,159,26,195,157,207,237,249,89,59,197,186,29,195,210,94,170,61,222,165,27, +188,135,109,66,164,123,213,30,110,215,161,247,45,51,39,123,226,8,182,240,224,51,248,57,215,111,76,181,173,21, +161,101,218,141,149,173,142,245,166,162,7,52,15,186,152,75,107,2,214,158,218,164,33,87,231,167,128,176,120,215, +82,22,96,79,93,157,159,70,176,83,73,183,144,77,41,94,13,78,185,246,64,109,54,240,100,58,82,123,199,48, +167,189,10,173,30,235,186,140,218,212,182,66,46,122,77,35,238,83,251,152,135,232,89,122,110,24,233,60,221,174, +199,135,141,37,70,44,242,58,5,161,68,243,23,79,250,58,169,28,7,230,161,132,35,22,29,176,101,171,101,245, +166,106,49,11,181,173,111,45,36,58,45,64,135,145,55,125,53,68,160,118,148,58,64,84,188,35,88,183,154,234, +227,69,246,240,108,184,206,160,168,150,106,99,73,160,62,140,66,11,38,227,108,52,189,174,240,199,69,200,114,98, +129,35,120,250,52,33,5,117,120,235,189,163,198,165,44,134,198,70,171,223,163,247,180,79,247,128,123,23,47,119, +226,164,72,80,244,214,84,173,226,29,117,242,152,202,135,231,129,159,159,8,46,72,216,24,20,162,146,154,10,29, +153,58,243,68,133,27,205,238,13,170,187,185,137,42,9,60,82,63,235,88,216,78,11,219,113,97,139,194,113,12, +39,130,134,255,42,160,242,123,72,158,153,48,6,96,206,157,77,84,253,81,120,23,23,159,104,48,191,36,44,127, +97,4,157,11,204,186,121,25,76,53,94,53,51,125,241,31,40,218,48,255,86,26,120,102,102,238,109,244,23,22, +23,72,182,149,176,129,16,219,253,143,209,149,20,151,10,214,81,152,253,130,120,185,101,5,129,173,218,122,209,247, +176,197,165,171,143,217,30,74,237,245,17,103,109,42,227,220,180,177,200,63,30,7,78,154,198,209,170,202,80,155, +8,164,116,182,122,232,200,26,130,242,94,62,198,1,166,212,30,231,24,77,105,2,203,62,76,235,33,222,134,20, +246,124,48,38,192,61,127,12,167,34,130,251,8,233,67,51,160,199,208,200,102,218,4,156,159,78,52,20,178,92, +175,162,119,168,130,206,209,243,98,37,238,180,169,186,138,149,101,100,52,202,240,110,97,13,67,41,130,15,110,200, +144,7,188,199,143,176,65,190,218,83,89,114,14,230,73,62,164,156,85,26,50,250,205,91,227,40,252,142,140,148, +174,73,118,30,82,214,53,17,147,24,70,124,220,221,175,82,161,81,75,54,249,27,209,244,39,166,18,173,179,228, +17,123,21,237,67,128,241,164,86,99,181,210,92,14,15,209,101,80,157,185,236,58,162,88,246,33,250,252,164,176, +230,254,191,186,38,46,233,183,252,88,191,229,23,93,22,35,126,131,223,34,208,129,208,115,117,217,107,31,140,50, +100,154,106,200,124,249,16,198,53,20,112,146,125,173,124,196,118,186,134,148,84,139,76,136,175,216,64,101,160,236, +200,214,223,2,149,14,64,21,89,142,131,187,148,170,38,241,18,210,69,160,67,15,139,75,151,21,224,166,110,182, +89,244,90,98,68,21,11,16,33,242,240,196,226,100,135,72,46,43,180,18,14,104,157,69,151,254,14,251,143,32, +76,108,117,23,238,54,0,16,22,11,124,13,228,4,209,36,61,93,136,17,157,202,76,213,37,227,46,214,233,119, +176,179,42,25,52,184,54,121,93,183,161,66,93,185,8,9,26,121,73,19,34,170,107,62,27,157,80,86,15,177, +150,218,6,9,60,213,192,15,10,175,87,222,28,96,105,111,0,193,14,167,113,179,149,248,96,171,186,108,151,118, +115,67,53,154,109,106,159,64,41,239,49,71,38,79,251,87,209,71,234,107,83,231,137,34,59,171,233,132,133,141, +196,191,2,217,18,41,63,182,102,35,28,247,55,140,142,113,179,176,171,49,41,30,110,178,172,154,28,183,132,48, +97,97,181,57,161,86,31,12,209,69,163,83,131,102,81,32,149,197,106,249,196,19,88,120,244,236,187,191,204,46, +95,133,50,51,174,44,204,15,88,3,60,192,22,35,52,67,227,134,176,134,30,136,107,248,58,212,200,28,82,222, +73,129,210,234,177,157,124,13,98,139,56,172,7,56,99,136,93,129,16,24,192,155,95,192,214,96,171,240,186,142, +108,26,235,200,36,180,77,29,126,122,90,203,158,224,171,255,66,165,189,80,43,223,109,240,51,158,166,238,73,210, +15,89,111,4,206,74,181,49,3,147,90,169,33,62,149,89,173,84,87,171,94,152,2,141,72,36,59,179,72,158, +25,234,108,18,97,55,38,232,235,198,33,252,64,158,193,29,158,247,51,5,32,174,126,255,170,9,4,129,2,193, +201,98,220,100,20,244,68,60,209,146,19,160,197,197,190,97,170,160,145,129,110,9,75,253,73,171,121,143,205,80, +8,166,172,238,179,28,2,207,111,133,117,77,68,53,115,23,61,25,222,87,152,165,140,25,95,182,132,198,222,155, +91,119,73,173,132,12,43,236,132,107,108,198,108,1,137,117,18,137,117,64,98,108,172,104,88,35,98,235,34,231, +231,168,10,247,183,29,26,165,150,188,108,201,62,124,34,27,2,111,14,204,190,243,88,129,24,41,58,69,58,36, +252,14,115,202,40,253,159,191,174,16,214,115,31,98,144,85,117,138,66,118,228,200,99,142,145,145,169,142,209,246, +199,165,169,202,74,124,136,48,242,153,7,168,156,74,59,1,83,59,5,19,225,81,64,40,65,11,202,204,1,211, +204,68,164,3,127,135,201,80,195,153,208,1,91,225,132,145,79,60,46,2,113,177,1,232,131,224,175,191,251,129, +16,245,0,211,211,110,67,77,145,236,13,119,79,176,205,251,177,250,177,194,3,172,153,112,135,119,161,63,88,21, +164,6,98,85,202,140,106,165,96,110,132,236,52,198,118,195,69,153,106,136,110,131,85,185,5,212,53,74,147,210, +146,45,66,138,171,169,196,214,80,140,161,149,68,252,36,111,208,97,165,60,179,45,127,149,154,159,143,247,254,236, +236,194,0,99,20,124,160,9,119,158,48,139,153,75,205,201,26,40,57,78,190,52,52,93,151,151,32,224,230,147, +209,235,13,141,160,39,168,53,29,82,157,130,198,101,243,172,178,53,186,153,177,5,191,141,122,11,218,101,108,96, +250,73,18,251,169,90,57,172,205,215,40,192,64,47,8,176,101,189,225,118,79,252,120,84,153,173,66,120,253,248, +240,26,20,44,173,190,188,169,125,34,26,98,34,104,139,214,29,76,252,190,63,21,62,155,33,246,12,81,233,158, +14,194,1,25,3,109,189,242,0,206,186,137,77,224,5,167,144,203,94,88,250,45,84,107,123,98,58,219,40,68, +219,50,51,10,35,189,125,74,86,176,121,210,142,254,168,62,9,161,105,132,189,165,38,241,131,246,214,199,151,171, +168,113,94,61,164,210,35,9,32,170,195,169,112,61,49,5,104,171,129,79,229,2,103,247,189,68,126,209,54,122, +8,246,234,63,58,99,48,155,89,94,56,113,54,40,55,83,131,216,6,102,99,155,138,179,245,104,195,158,114,221, +239,131,167,243,231,22,103,134,105,70,110,217,133,252,111,113,15,176,144,136,13,145,191,51,149,133,1,24,87,38, +226,248,161,7,83,174,108,45,44,131,204,196,160,135,42,121,84,130,64,7,85,12,98,224,6,94,6,49,112,113, +90,84,158,59,74,223,99,134,41,181,232,107,91,248,76,160,38,160,110,25,98,62,33,108,206,255,103,196,237,204, +69,89,229,34,106,254,231,76,157,82,178,174,139,159,64,16,140,176,147,139,242,201,121,196,9,211,140,66,73,27, +156,150,35,253,228,245,195,54,193,159,77,64,94,72,213,141,160,102,172,48,114,136,38,39,93,144,60,172,145,214, +253,166,196,250,130,234,156,76,43,238,218,55,191,20,196,202,1,35,174,51,189,231,74,127,204,250,248,5,214,199, +131,245,25,195,188,171,55,212,106,199,230,88,29,206,67,162,14,42,144,232,252,0,213,25,140,190,1,160,87,26, +249,73,78,195,166,25,129,58,212,150,202,8,107,140,48,214,123,92,117,245,199,210,238,99,57,6,58,90,143,14, +188,40,245,240,9,49,144,115,176,252,58,194,191,243,134,252,198,60,244,141,23,86,4,84,33,216,197,247,250,83, +163,232,215,234,3,126,6,125,129,159,189,126,16,114,189,254,44,252,254,71,63,192,79,167,63,195,207,65,127,31, +114,207,245,183,225,247,173,254,41,252,190,214,191,133,223,43,18,11,224,151,132,219,225,247,168,127,9,249,111,244, +191,194,239,78,255,28,126,47,245,175,70,168,250,35,169,15,177,43,195,37,8,215,32,23,220,235,54,137,228,214, +193,102,225,236,8,63,220,77,53,76,24,228,190,86,3,205,201,117,111,160,156,213,116,189,81,145,94,245,186,47, +185,140,193,17,120,109,127,122,6,187,175,142,84,163,136,164,118,114,92,23,223,51,175,144,102,201,222,204,17,205, +142,60,185,122,126,218,71,39,217,33,34,79,135,236,36,51,192,208,33,76,216,199,103,213,215,252,58,92,231,11, +30,83,219,120,211,223,220,128,5,58,21,100,131,74,175,52,49,183,22,56,59,231,44,59,84,69,225,234,74,180, +81,62,99,113,111,60,179,89,215,101,200,108,92,174,32,110,30,97,93,205,136,52,13,81,86,80,51,10,72,207, +59,40,117,105,94,180,64,3,123,22,109,24,105,111,231,246,121,241,96,31,93,53,168,152,167,206,75,4,168,25, +39,193,87,158,240,95,42,157,40,193,146,235,30,238,50,205,221,221,198,14,38,253,74,43,143,180,159,187,188,159, +181,79,219,88,109,193,31,238,245,32,101,199,251,130,253,143,192,152,251,122,251,242,248,74,147,197,207,81,237,21, +33,187,132,210,251,202,242,93,79,115,241,174,218,170,244,168,240,6,45,150,55,252,200,155,254,28,132,48,14,154, +85,91,213,42,144,73,106,71,99,224,187,80,104,239,207,105,206,128,34,200,249,177,172,63,219,137,152,106,135,25, +199,67,160,134,88,124,12,236,93,93,208,223,165,126,97,40,3,2,6,117,33,40,242,203,76,145,31,8,65,30, +158,166,231,245,129,208,246,5,197,169,213,212,215,37,253,206,186,139,67,185,16,64,167,162,51,58,242,173,12,9, +38,151,216,169,78,248,183,89,101,162,240,166,170,88,196,115,115,19,19,65,133,17,94,106,46,131,189,145,251,244, +238,142,80,67,48,8,99,113,252,45,225,219,47,77,17,127,243,114,24,146,57,33,34,219,194,119,10,230,206,146, +16,153,204,115,193,128,196,64,42,16,203,69,181,203,91,99,46,63,217,119,87,133,192,90,27,240,188,230,50,104, +139,131,123,68,38,71,173,94,104,87,247,152,212,55,180,225,72,214,17,66,186,84,102,19,124,20,35,49,205,129, +145,83,147,114,6,185,83,51,119,97,180,218,204,251,90,234,127,253,133,171,172,114,197,60,75,185,130,96,177,192, +25,25,215,183,170,11,190,177,205,92,141,90,220,41,221,98,39,129,127,123,193,1,107,104,177,229,35,200,161,111, +93,229,74,64,155,186,86,83,240,226,230,23,117,54,95,58,152,167,63,64,181,32,222,85,49,178,205,108,104,203, +115,30,173,38,6,52,237,111,243,153,171,12,154,110,136,224,136,109,7,68,172,156,27,11,198,156,140,240,232,248, +76,176,54,147,149,89,201,158,111,175,93,70,221,108,243,89,156,215,42,175,247,209,212,19,229,151,173,254,35,195, +128,90,22,219,182,219,161,230,17,247,167,126,231,128,184,196,55,113,138,149,216,123,188,74,99,90,51,37,119,171, +254,196,192,84,79,64,61,16,95,7,237,198,224,170,130,51,77,194,98,103,16,132,121,212,132,33,99,41,113,154, +70,216,238,83,79,140,134,207,185,47,174,191,131,130,207,162,42,126,233,129,99,59,124,99,149,108,170,125,77,224, +109,183,9,80,151,241,228,175,13,16,247,37,205,145,159,245,37,153,10,184,42,14,237,162,96,120,232,84,105,86, +131,58,36,175,224,213,213,209,60,202,171,254,136,56,11,128,235,97,117,177,89,209,195,69,96,21,140,90,244,59, +118,234,250,153,239,93,227,85,62,16,205,185,202,7,162,217,41,18,234,54,131,74,135,2,188,253,49,227,188,195, +173,192,144,189,147,103,244,154,118,92,14,164,28,66,3,49,130,253,217,210,2,80,119,252,212,24,136,198,227,110, +236,236,190,179,230,199,40,76,40,114,17,27,152,228,120,151,104,27,19,202,77,249,48,109,167,57,181,2,132,230, +30,207,167,152,115,37,25,151,206,141,149,144,190,232,155,147,170,57,250,127,56,39,53,116,41,232,183,75,142,60, +204,129,4,135,206,30,66,241,155,27,146,42,16,145,200,74,98,106,208,48,99,210,186,170,131,148,164,147,55,123, +59,2,225,248,99,75,62,252,40,19,126,132,192,49,182,60,145,19,13,163,150,60,111,123,66,227,156,8,216,143, +196,9,65,234,181,82,118,94,192,32,58,11,149,232,236,229,17,17,101,77,22,6,152,34,12,168,131,158,55,214, +8,109,173,179,158,219,162,104,100,242,161,218,0,96,134,92,24,4,193,235,118,251,118,221,67,87,56,185,219,219, +40,214,105,179,88,103,168,27,24,249,5,123,7,180,164,95,14,34,14,120,221,132,188,129,101,19,78,147,26,212, +233,143,69,20,150,136,166,18,7,192,160,155,25,248,211,145,162,191,232,162,215,22,242,0,229,178,107,74,87,181, +252,98,239,136,160,23,237,137,45,20,179,165,42,29,226,29,250,27,141,131,50,230,8,162,200,81,129,46,76,205, +104,164,96,37,47,75,178,92,72,209,25,92,79,198,108,239,8,170,106,89,31,144,228,127,78,18,194,250,58,106, +123,26,148,162,211,151,173,14,97,237,162,105,130,27,11,122,202,214,13,30,183,236,167,246,21,50,234,5,119,14, +3,94,9,122,210,43,156,107,236,31,78,157,218,1,34,167,73,60,49,246,142,251,254,116,20,154,140,50,55,17, +145,125,127,186,16,110,12,181,0,213,9,230,242,117,67,99,195,69,14,13,87,95,109,53,4,139,66,170,152,14, +61,152,18,83,231,62,204,114,91,22,52,173,83,158,222,153,96,225,65,25,134,51,50,186,155,171,213,27,222,191, +125,133,245,219,185,10,151,142,226,101,189,116,244,3,71,14,172,161,8,182,27,119,73,164,198,44,120,192,21,77, +214,116,111,236,55,69,93,1,93,154,112,142,82,142,206,55,90,46,113,30,152,164,47,31,247,41,111,178,227,225, +211,97,221,150,176,231,165,0,52,168,4,176,168,61,175,44,37,45,204,75,32,18,134,24,232,42,200,128,226,123, +76,107,139,17,75,68,117,25,208,229,150,134,19,36,146,84,234,202,228,7,133,252,32,182,76,249,252,32,108,18, +93,20,18,132,189,162,89,74,54,150,201,33,111,61,213,194,43,155,183,26,161,18,121,163,168,94,67,166,145,200, +178,181,23,172,172,73,50,185,103,113,254,129,71,30,243,139,142,50,36,55,22,143,183,228,198,10,14,129,45,141, +242,19,49,248,135,214,133,13,41,99,73,12,152,53,190,65,139,191,122,16,16,235,10,86,219,93,66,183,142,69, +136,88,252,88,40,222,205,40,152,65,240,85,202,147,21,216,116,210,169,43,199,192,44,134,145,1,250,16,212,8, +190,97,20,61,233,65,178,244,156,199,104,138,6,165,28,83,72,86,74,64,158,105,240,148,97,89,139,211,70,46, +100,186,208,85,79,73,185,148,235,30,27,127,198,206,247,76,36,136,92,133,66,57,81,242,225,59,148,118,73,207, +17,73,29,36,45,218,70,213,254,88,70,107,38,11,185,104,250,202,21,86,53,181,98,226,182,205,118,250,17,192, +73,122,54,119,5,151,74,68,54,105,131,161,141,154,24,187,129,216,208,64,22,244,70,72,128,196,169,123,227,164, +48,174,146,159,231,36,59,43,40,60,249,129,99,153,205,63,169,250,170,230,222,147,194,63,156,212,122,99,10,45, +46,37,121,239,221,140,227,197,240,10,195,59,142,252,227,85,39,98,180,92,99,177,26,163,226,121,108,92,161,32, +109,32,41,251,91,201,233,190,139,51,99,170,155,109,234,113,53,166,52,40,201,124,125,138,60,190,69,125,178,104, +223,149,50,57,43,218,179,94,57,148,117,220,38,217,182,152,32,227,136,234,35,126,206,228,6,186,206,153,80,17, +82,142,194,184,246,125,187,35,80,98,100,229,105,50,54,145,27,123,60,17,93,102,107,152,152,161,65,55,14,32, +222,246,140,6,143,124,88,193,60,217,106,5,218,179,17,81,76,171,185,40,141,236,25,129,155,235,200,194,220,227, +38,223,78,221,228,91,196,238,122,64,133,182,49,152,42,45,46,164,20,169,32,213,138,97,236,247,148,27,12,172, +179,49,124,29,7,92,167,34,199,105,145,99,29,39,19,248,85,253,237,216,112,169,44,144,166,109,237,224,206,58, +108,74,199,205,57,236,152,96,209,116,49,170,73,123,100,188,41,168,215,248,8,32,134,90,161,218,65,251,106,167, +46,242,110,221,86,7,148,189,164,159,141,21,59,134,50,15,236,245,128,210,13,189,47,220,2,42,136,199,82,76, +245,148,159,54,169,28,14,230,194,111,48,35,46,87,54,77,217,51,165,108,204,192,252,193,200,30,232,108,238,219, +247,155,92,146,102,213,236,181,112,67,144,91,32,238,153,201,124,172,56,65,165,203,8,158,39,0,143,138,221,220, +220,80,68,213,56,76,207,237,51,147,125,71,127,199,197,254,46,42,228,162,217,210,10,253,131,109,253,68,229,254, +55,163,201,55,165,5,129,224,254,116,119,89,245,140,141,122,132,133,45,106,220,171,238,154,27,148,102,174,136,182, +127,33,195,241,51,101,20,65,20,209,152,92,93,76,201,165,237,108,162,98,2,179,52,44,154,212,209,91,50,125, +65,48,220,231,40,68,16,153,17,53,179,75,28,54,128,127,48,66,20,86,178,17,67,30,24,51,140,95,190,144, +47,207,207,39,111,159,141,120,193,67,86,119,139,47,117,100,143,207,101,219,154,53,158,57,46,69,238,193,74,122, +144,135,171,28,134,160,189,122,7,49,10,51,31,95,57,245,194,169,103,193,129,148,222,77,21,230,63,184,34,185, +250,251,191,3,186,251,251,122,68,138,244,242,227,212,197,78,8,36,197,250,190,117,239,199,198,42,253,211,142,93, +181,25,124,164,173,75,122,204,202,198,21,100,227,135,209,163,48,137,195,55,126,82,44,93,16,205,211,133,125,44, +63,169,61,153,193,214,44,125,110,141,231,48,90,5,1,93,149,214,194,201,15,31,167,46,72,188,172,141,138,123, +1,165,221,184,83,52,115,119,159,16,213,77,237,170,242,14,191,119,63,187,37,152,151,139,230,14,51,199,56,45, +214,34,215,197,156,47,57,103,59,1,114,30,220,232,27,100,133,31,150,5,25,23,185,117,178,81,28,249,197,12, +228,23,3,98,66,183,28,52,32,216,65,66,40,143,20,249,13,167,184,60,131,122,50,9,119,54,5,164,57,116, +126,233,243,232,137,164,29,143,28,170,251,28,15,129,190,138,240,73,8,43,178,46,240,249,164,74,128,71,215,4, +172,143,66,253,228,63,2,59,163,191,173,62,122,232,31,126,180,250,8,238,39,128,78,154,90,247,180,77,46,40, +48,96,13,58,32,35,188,44,205,45,127,29,97,74,240,127,34,14,217,55,233,248,163,152,87,2,185,211,179,145, +6,174,110,193,14,154,167,204,248,42,81,55,166,72,52,175,25,248,66,242,155,94,5,196,193,245,106,167,115,26, +116,78,34,12,115,120,84,159,222,106,167,40,237,250,158,82,27,199,169,198,43,95,234,208,249,12,69,204,57,4, +82,192,182,33,10,137,113,153,167,236,108,8,245,110,114,6,199,75,248,34,101,227,138,146,210,121,60,75,105,121, +201,249,212,208,221,37,179,133,53,106,128,213,136,25,98,163,150,59,143,160,178,0,130,217,131,251,137,252,68,248, +64,2,60,253,83,6,45,79,40,163,251,13,68,72,65,31,114,118,22,130,128,52,50,131,27,162,14,130,61,73, +87,213,202,50,130,58,35,185,240,217,25,211,96,220,20,229,135,223,252,206,133,23,161,59,177,68,33,201,99,208, +101,177,226,176,208,70,202,66,91,60,145,49,51,56,162,42,151,163,204,174,203,98,225,122,79,233,18,75,118,220, +94,36,130,23,219,98,132,182,176,72,20,167,134,86,102,250,93,128,122,121,61,31,151,155,57,110,206,19,194,25, +102,94,148,124,2,231,27,252,230,230,77,101,202,198,196,117,119,199,4,189,105,221,174,127,71,51,136,172,73,121, +55,204,157,251,196,75,98,21,100,101,122,3,38,43,71,239,26,63,167,72,98,143,30,205,246,51,204,113,202,238, +73,203,170,120,155,48,192,10,203,65,211,96,228,141,4,239,160,240,59,217,93,69,2,88,165,100,226,97,153,56, +148,33,13,221,200,228,55,225,35,222,76,70,9,224,50,215,77,119,242,44,143,56,209,52,86,249,149,136,90,141, +44,136,125,58,135,152,37,87,12,209,220,110,131,94,192,132,171,83,61,87,215,220,68,35,107,10,156,0,5,233, +68,29,154,122,22,223,161,160,41,6,49,141,3,194,96,107,123,9,148,185,6,208,143,53,128,194,36,218,101,123, +149,236,61,177,32,253,24,208,190,204,32,75,171,147,10,33,142,218,100,171,78,34,109,54,52,63,25,74,22,149, +24,87,12,38,70,84,174,13,137,90,237,209,78,39,119,249,84,128,118,115,179,13,2,108,81,66,74,112,116,159, +246,3,39,207,4,20,70,91,163,175,213,221,157,232,142,94,178,76,44,77,63,9,197,240,162,88,197,88,228,152, +121,108,142,250,154,29,105,242,237,123,204,209,55,212,185,184,211,56,15,206,46,193,132,96,71,12,195,238,233,121, +58,138,187,196,48,208,183,132,95,238,96,97,48,237,135,108,24,47,131,49,5,62,156,107,16,173,59,70,32,89, +138,6,66,119,172,197,136,153,110,57,220,203,7,220,243,78,91,21,104,110,171,14,28,38,82,10,60,59,213,39, +65,40,6,49,230,240,107,97,88,238,170,124,103,39,95,203,116,73,131,122,42,215,167,248,66,208,220,129,239,65, +17,222,240,223,197,91,54,116,22,155,97,147,234,165,146,177,192,122,220,123,249,104,96,201,227,51,90,58,88,143, +173,225,4,145,32,245,117,24,107,121,19,2,180,225,40,202,207,195,140,98,42,150,169,85,184,13,150,166,246,88, +157,240,220,96,253,122,247,0,31,127,216,0,63,187,107,128,59,179,60,196,240,214,196,0,40,220,124,217,55,8, +26,105,53,25,110,114,156,17,166,80,199,28,146,27,115,72,76,93,130,61,226,15,228,177,2,100,194,198,210,89, +99,194,56,74,168,200,48,181,86,8,87,194,95,4,10,241,141,127,10,127,127,11,222,79,223,7,164,248,105,248, +251,29,166,95,12,70,225,211,77,89,159,167,97,232,159,114,234,113,169,29,170,242,119,80,28,229,255,226,52,157, +37,68,177,127,23,62,80,1,192,71,98,245,103,167,227,39,26,196,23,118,214,63,187,69,77,25,149,70,27,244, +241,16,26,198,63,38,193,26,194,76,171,85,248,89,213,65,178,65,6,173,7,104,232,169,255,185,32,142,219,194, +231,212,132,249,164,43,82,240,176,254,52,124,244,21,96,241,88,209,172,135,222,73,25,203,248,155,103,221,238,145, +161,63,183,117,205,96,251,206,61,205,128,162,7,88,238,18,228,94,126,231,64,5,243,109,132,165,225,20,245,98, +52,218,32,48,210,62,72,4,176,59,22,202,131,96,157,60,221,45,53,148,30,214,88,21,245,159,160,192,250,221, +225,235,206,123,90,242,239,79,227,247,139,200,188,124,239,79,121,249,207,247,8,49,45,102,252,187,100,27,198,228, +140,121,244,72,138,94,168,75,196,59,93,219,66,180,135,187,202,142,175,94,59,191,122,109,249,230,147,60,9,95, +162,235,9,86,160,109,152,248,10,97,64,23,10,254,151,32,43,163,179,171,128,55,40,19,55,188,176,9,27,205, +149,195,62,48,162,10,16,15,104,35,164,208,194,167,142,139,138,189,254,232,73,34,244,254,241,29,77,252,115,24, +17,80,51,255,64,91,53,17,127,107,42,153,62,157,72,46,49,208,83,50,230,252,60,79,106,253,61,200,163,239, +195,110,186,48,213,175,249,219,133,30,172,184,177,11,95,70,72,33,181,233,238,81,61,176,126,129,63,94,8,186, +53,73,191,57,219,110,170,24,126,219,92,18,133,96,67,66,133,44,124,64,39,100,33,193,89,251,246,175,247,33, +11,9,206,130,0,47,100,33,193,89,188,83,181,141,137,186,41,61,148,134,75,123,165,153,244,241,129,237,107,237, +84,250,56,199,195,135,222,242,67,89,56,110,176,35,99,213,210,78,14,35,14,211,127,126,32,122,240,51,241,140, +151,95,236,134,24,118,164,20,153,229,226,195,191,206,12,3,65,88,175,86,106,217,27,24,5,161,10,247,52,25, +211,84,242,241,23,40,183,75,6,22,83,190,126,94,215,170,4,64,47,51,72,158,103,169,108,133,192,52,226,99, +136,200,155,177,74,235,215,158,151,50,49,58,188,13,130,134,204,107,209,109,250,104,227,40,10,12,88,172,188,38, +49,10,210,219,118,157,52,100,31,161,106,148,95,240,238,97,57,198,24,70,144,106,148,0,87,121,177,89,77,204, +87,72,92,87,120,186,31,47,177,38,149,176,75,87,163,73,242,247,77,202,60,199,135,113,188,104,33,26,139,129, +135,213,56,31,141,80,254,104,23,164,195,37,182,66,169,239,227,55,94,24,148,245,120,72,114,220,203,172,42,154, +155,176,121,62,225,69,83,74,64,156,180,52,1,154,24,141,65,124,52,38,126,221,133,49,139,24,239,90,110,227, +113,75,211,237,236,38,25,41,148,142,114,101,60,226,200,136,194,163,166,38,13,13,249,26,159,128,232,56,19,10, +148,109,93,78,43,244,30,249,184,115,126,184,185,26,32,93,122,154,0,30,47,245,56,160,145,64,4,114,1,248, +184,0,85,195,83,172,28,174,155,27,142,1,148,55,165,92,19,46,130,74,227,207,17,148,243,128,51,196,8,137, +79,79,60,92,202,171,133,88,74,233,229,71,201,14,234,67,142,13,163,254,187,59,185,157,64,196,96,148,75,144, +230,209,23,88,40,9,245,39,104,101,249,83,67,31,186,151,229,38,206,97,196,199,205,38,57,198,189,107,22,103, +183,36,128,121,147,166,94,174,42,85,80,219,135,143,110,114,140,214,162,127,128,130,45,53,156,213,31,16,232,255, +185,26,240,87,4,214,181,37,60,179,189,251,243,53,160,209,97,200,173,168,16,181,48,194,102,229,181,246,119,186, +96,40,167,156,116,34,139,85,11,100,232,82,155,41,157,214,134,89,82,10,47,128,81,198,167,90,153,20,197,152, +212,122,244,162,60,227,21,120,240,205,128,230,155,231,30,116,3,155,176,179,220,195,228,80,199,20,220,3,85,75, +6,94,98,48,100,252,16,19,39,28,252,249,128,114,49,75,204,160,29,139,75,23,184,104,246,107,205,236,60,198, +208,47,113,244,152,59,34,152,22,153,30,201,53,88,42,48,117,183,36,81,75,31,131,19,164,119,191,123,246,222, +82,190,134,137,14,59,92,40,14,33,75,44,41,70,185,226,1,192,171,125,226,166,104,130,155,98,12,70,42,12, +51,237,146,88,157,205,128,144,195,48,93,166,35,182,54,132,208,108,216,166,249,200,215,25,204,134,19,61,63,9, +68,111,103,176,35,40,85,37,47,173,109,241,123,142,231,164,79,55,82,167,193,10,174,31,67,162,252,14,0,160, +30,191,35,0,229,217,99,180,97,242,248,66,196,152,58,222,90,193,152,138,200,142,126,137,188,72,47,179,193,13, +26,174,234,85,173,174,111,199,1,49,249,219,249,116,36,113,97,208,225,19,7,238,56,217,55,103,105,3,254,146, +78,194,114,212,147,173,23,223,57,235,121,241,82,196,38,112,194,203,235,209,109,186,166,195,185,6,120,104,33,0, +41,157,229,138,180,69,159,171,231,106,15,179,201,62,238,152,115,188,232,213,200,228,23,121,50,254,193,73,24,204, +82,127,110,83,69,60,98,55,59,88,66,52,151,176,164,72,120,227,121,221,164,247,14,127,55,180,100,225,243,40, +32,231,104,246,165,14,94,227,8,167,154,14,127,105,184,245,157,24,198,140,49,12,181,228,151,194,17,148,251,114, +6,122,194,39,139,249,73,111,150,53,124,134,47,34,96,138,116,129,201,15,192,81,126,249,50,133,225,171,76,218, +37,46,143,44,229,204,62,36,32,170,94,196,205,51,147,238,45,125,232,123,49,178,62,189,216,60,111,190,73,113, +107,75,211,135,216,244,221,161,185,231,90,85,97,223,185,16,111,231,10,45,58,133,120,59,209,115,43,229,200,41, +93,45,120,149,177,173,3,91,55,187,211,168,117,92,178,236,117,193,130,24,179,13,50,196,128,163,43,110,76,196, +88,20,216,140,191,249,185,120,197,49,86,131,55,201,253,165,2,192,111,239,250,194,168,252,116,219,234,1,99,30, +83,171,15,41,28,145,180,155,152,57,98,247,255,229,199,121,28,119,70,63,152,100,47,125,221,96,106,139,112,20, +225,142,175,108,9,8,92,12,177,65,43,38,77,83,159,144,133,8,28,12,83,240,28,164,20,56,211,164,216,194, +61,239,245,132,50,187,64,74,118,202,78,104,200,92,91,82,147,253,140,148,156,159,144,94,168,150,82,12,73,43, +37,117,239,237,76,199,217,217,187,84,227,160,47,224,74,171,223,144,250,95,25,14,65,204,252,56,4,84,210,55, +100,243,46,221,106,249,224,33,144,251,159,148,123,186,45,129,96,105,229,0,67,44,248,216,126,83,27,100,12,102, +127,78,169,31,29,238,189,183,225,239,69,248,251,45,253,85,227,79,140,211,150,49,120,213,91,252,253,193,164,98, +229,211,227,212,71,193,2,208,121,153,228,171,54,202,151,103,237,221,29,52,203,244,219,30,114,78,37,90,190,157, +152,51,175,93,82,145,152,152,80,110,84,64,219,180,135,102,54,75,84,183,104,27,122,233,232,54,215,34,244,194, +247,205,205,20,48,125,182,24,199,203,212,15,44,250,233,21,62,68,104,162,54,78,250,231,152,187,253,115,204,52, +71,70,130,148,27,40,1,134,213,0,195,145,142,116,130,44,202,113,14,160,22,147,223,148,226,142,16,50,161,187, +241,11,237,34,180,206,208,203,186,199,23,52,124,180,122,141,69,84,207,226,82,177,5,157,154,148,161,18,112,190, +98,239,188,226,94,240,146,115,137,70,167,13,49,255,220,203,89,153,153,114,165,7,30,189,105,247,38,53,63,38, +65,92,13,130,2,179,161,31,244,152,104,61,101,40,67,227,229,210,199,101,222,102,198,129,207,231,72,240,249,21, +191,4,26,90,60,183,92,137,22,105,79,163,250,118,127,124,67,15,212,237,242,11,216,70,1,226,226,243,55,53, +62,1,32,48,122,36,75,199,54,47,197,117,41,126,147,48,180,127,135,23,224,105,236,117,147,83,41,172,39,130, +42,220,241,77,101,47,173,216,96,183,53,18,232,136,67,251,130,225,193,95,140,24,65,68,76,66,110,9,249,172, +230,237,61,139,237,109,9,243,37,115,130,39,235,180,139,198,172,157,14,248,108,61,54,9,65,205,192,201,156,193, +67,13,160,231,20,214,29,176,238,201,130,42,1,24,233,117,10,63,204,62,129,110,228,35,56,230,112,22,246,69, +14,235,83,144,250,4,93,57,233,33,32,138,73,44,166,91,122,131,25,27,66,245,237,232,0,73,80,181,47,87, +225,72,174,104,239,150,98,137,11,253,3,241,110,74,182,224,37,127,224,55,9,134,46,38,40,135,87,205,241,47, +61,99,87,59,252,189,235,163,69,45,172,198,28,253,9,209,235,233,76,149,254,196,105,198,71,128,208,225,24,129, +164,146,121,36,233,141,87,237,248,24,235,16,170,164,45,200,29,11,5,32,73,123,201,37,108,183,76,111,90,59, +166,101,206,50,45,35,172,40,23,155,75,48,156,182,120,62,110,145,163,71,202,48,149,161,181,37,24,130,60,134, +58,163,0,245,127,120,251,242,238,182,109,165,239,175,242,152,39,231,30,242,122,226,52,207,125,86,42,172,78,154, +165,73,155,165,77,220,213,55,175,15,45,81,18,18,154,80,73,202,75,98,125,247,119,126,24,128,0,36,218,237, +187,254,97,11,203,96,33,150,153,193,96,48,19,57,159,74,253,155,43,108,72,216,93,183,22,142,204,122,44,90, +187,76,123,39,89,140,222,7,219,113,229,18,25,37,131,87,27,91,209,232,129,8,53,91,191,53,173,243,91,211, +50,130,12,235,62,169,140,155,42,51,243,64,25,227,121,219,109,100,51,51,66,1,230,138,228,73,64,174,221,87, +128,4,6,165,142,155,187,77,85,5,86,170,128,39,243,49,99,42,226,47,176,79,19,74,178,29,240,131,3,32, +88,227,212,163,234,98,247,27,111,98,222,67,206,123,212,136,107,94,156,249,157,222,197,228,118,63,68,192,46,131, +53,9,30,128,114,95,23,218,184,207,172,204,83,183,103,120,148,10,238,83,199,154,61,254,192,17,250,182,57,208, +198,231,162,225,25,96,241,16,191,252,46,118,196,232,128,127,123,78,253,112,103,185,228,182,248,115,153,1,253,212, +100,116,38,63,239,91,243,243,166,149,68,252,136,20,228,37,43,5,90,63,50,239,170,229,179,171,181,88,195,253, +64,159,155,66,44,144,39,120,246,126,191,172,177,186,200,233,111,65,140,103,118,86,206,150,19,102,245,102,94,229, +47,27,170,174,134,32,187,170,204,93,197,111,96,217,14,50,59,203,48,230,59,34,86,153,129,81,105,131,243,248, +13,242,57,60,227,143,107,112,147,212,99,146,124,117,25,143,174,143,81,239,171,202,182,100,53,137,242,113,245,56, +252,115,71,184,196,126,94,66,209,190,54,107,104,204,11,200,177,176,231,226,224,151,162,154,48,58,127,189,166,131, +168,166,45,9,155,56,222,99,171,204,226,30,42,81,85,24,69,127,106,129,11,246,205,63,68,126,188,30,27,169, +136,22,154,170,152,83,181,31,76,37,135,109,151,81,0,4,224,128,5,55,232,151,162,38,99,161,150,24,231,228, +120,137,120,228,153,173,147,250,102,133,27,255,186,144,193,183,143,92,24,75,33,58,109,13,170,144,247,247,173,177, +136,154,228,172,31,97,130,120,136,159,27,176,9,236,175,76,211,49,155,21,200,217,79,230,77,80,211,38,163,90, +88,153,77,150,229,41,32,139,202,39,201,228,240,58,229,251,121,167,129,108,222,211,190,108,250,212,229,201,230,157, +81,205,79,208,168,150,34,130,28,240,108,46,54,11,129,3,155,99,104,129,240,196,0,38,31,227,239,241,86,250, +222,1,229,159,155,64,6,247,244,207,37,2,239,110,63,225,59,207,45,137,56,131,221,244,170,46,190,92,150,109, +147,175,123,18,250,144,31,147,121,241,110,167,62,255,185,39,169,229,157,40,200,240,102,237,161,59,58,156,237,195, +3,253,240,174,17,27,200,186,221,128,16,127,140,247,131,136,59,35,212,229,16,255,232,134,190,141,78,238,16,153, +145,178,178,161,98,34,88,244,132,163,201,62,91,194,99,158,145,225,223,233,181,249,255,196,252,55,180,122,251,84, +112,226,232,160,114,78,36,10,97,179,108,198,69,73,34,150,72,103,253,246,47,22,236,186,214,218,183,73,232,46, +41,142,211,121,12,34,71,190,236,246,142,230,104,240,118,85,214,114,70,244,13,26,233,71,254,93,197,197,185,95, +220,127,185,114,255,215,163,255,56,122,248,48,17,63,175,77,113,145,138,137,68,18,171,137,25,61,71,154,88,49, +32,212,84,242,240,147,140,46,117,21,12,93,3,235,47,219,202,64,255,212,220,226,89,211,90,74,16,5,149,231, +141,33,196,201,217,166,239,69,2,8,75,234,82,91,53,183,64,137,52,130,88,207,185,140,49,102,159,124,166,233, +144,203,59,223,4,197,96,226,90,155,28,118,207,105,58,111,52,249,128,220,149,81,224,164,121,91,46,151,38,212, +173,171,186,54,53,115,223,223,25,96,121,140,64,179,146,251,77,60,113,160,86,235,186,84,102,24,239,235,166,134, +250,243,55,241,103,122,13,38,172,92,238,208,162,172,59,97,188,166,54,156,239,246,2,185,208,9,106,196,155,106, +210,183,60,60,91,250,209,116,130,79,105,250,114,193,43,188,227,213,94,53,100,158,110,81,185,233,245,66,207,54, +157,9,113,167,174,201,142,10,97,153,243,230,37,52,210,234,186,227,29,205,19,216,86,228,240,191,253,117,240,54, +202,3,231,35,110,252,17,175,90,154,171,14,253,156,83,213,200,47,116,193,26,205,243,168,192,39,210,74,205,231, +220,53,240,88,125,213,158,171,6,137,170,225,149,72,170,59,47,215,164,250,234,220,40,201,82,173,245,154,206,185, +9,181,230,65,151,70,27,189,106,171,5,255,240,202,81,159,43,14,116,171,114,94,145,111,129,131,151,45,215,195, +53,192,195,9,31,75,52,99,31,213,19,175,192,57,38,130,3,127,108,84,91,205,57,128,245,204,1,180,135,159, +170,60,175,121,69,210,240,77,157,110,101,242,251,182,108,186,26,213,99,196,121,1,112,38,246,39,108,123,26,15, +111,116,161,140,177,102,158,230,111,121,127,172,250,126,157,63,120,112,121,121,121,116,249,143,35,221,46,31,60,252, +239,255,254,239,7,87,181,106,62,37,244,98,236,208,155,228,145,67,151,127,199,82,23,120,36,59,135,85,244,239, +76,194,127,25,61,52,191,64,100,234,32,255,131,6,3,194,240,44,71,63,143,150,177,10,211,240,232,142,86,2, +130,242,107,116,182,1,125,194,233,1,220,173,187,172,74,155,125,122,153,101,77,49,146,60,232,186,187,167,247,198, +198,194,239,77,42,49,226,114,67,173,144,143,59,101,88,88,154,136,224,43,146,184,183,153,253,29,39,90,249,214, +19,32,31,170,246,204,30,252,30,109,183,47,1,112,254,7,103,197,165,195,88,38,248,44,215,131,45,133,233,9, +66,65,51,31,114,27,8,21,226,226,237,173,141,126,131,198,142,69,115,244,61,54,47,38,37,208,140,139,75,244, +211,106,218,179,242,40,235,119,230,61,174,29,67,88,46,222,223,113,218,65,107,185,188,47,71,112,252,184,19,53, +222,235,72,45,143,218,34,73,110,179,57,205,95,81,160,3,144,135,96,129,26,44,140,243,32,254,14,11,238,114, +70,252,91,101,35,182,227,43,29,248,67,74,226,211,113,111,238,119,48,203,248,179,21,241,111,27,200,218,12,176, +46,190,176,157,140,124,100,131,241,147,218,175,30,112,94,66,188,33,87,249,248,22,252,175,7,240,127,111,254,189, +126,197,187,162,209,140,56,25,244,188,166,51,61,191,38,240,2,180,98,76,65,216,121,116,94,241,226,20,202,214, +171,158,255,179,14,18,104,22,149,45,175,17,196,59,166,27,140,227,52,35,51,83,14,63,15,105,245,175,180,250, +7,173,254,141,86,255,78,171,255,160,213,178,213,155,53,53,229,5,117,98,59,146,241,228,5,205,25,105,214,52, +239,137,57,175,89,41,196,209,232,55,84,180,86,179,30,191,171,150,212,249,146,59,195,31,165,26,210,53,173,105, +205,233,155,154,74,58,227,83,212,89,75,103,115,197,127,154,56,56,99,236,73,51,236,52,179,173,230,139,134,170, +115,82,244,233,108,206,21,180,159,232,15,106,215,212,246,212,246,51,106,55,103,215,196,200,142,117,107,137,81,111, +93,51,97,43,27,194,201,136,169,87,183,57,227,191,53,65,255,150,54,196,131,79,151,220,130,33,228,229,102,174, +52,1,91,227,12,247,137,12,241,228,150,206,24,43,138,237,119,50,62,140,168,211,155,118,198,61,42,155,139,146, +91,154,181,106,221,243,152,219,0,115,136,48,245,70,238,219,103,186,198,159,12,150,197,187,102,46,122,51,55,253, +156,163,220,34,9,3,96,62,177,86,29,134,175,170,231,204,119,26,90,67,194,116,212,229,25,215,94,87,75,200, +172,206,65,107,192,126,72,213,150,15,209,155,30,144,194,135,56,172,31,176,43,115,158,124,5,162,168,202,90,47, +185,146,102,99,254,129,66,241,208,156,243,128,94,147,165,207,84,137,17,11,2,57,210,151,228,76,115,211,89,173, +103,159,254,216,104,14,170,5,15,9,127,18,86,11,175,110,109,150,30,47,88,42,27,117,14,216,153,106,103,134, +135,82,235,53,47,80,154,109,90,38,64,32,172,160,204,221,140,27,225,44,94,160,98,184,155,22,220,246,253,69, +201,3,28,25,178,161,37,45,235,235,245,138,184,214,101,133,133,92,153,201,175,90,254,233,120,81,171,174,99,140, +112,95,128,208,18,254,113,133,76,47,117,125,189,212,242,107,202,181,168,175,187,84,160,112,157,49,108,97,198,199, +252,51,37,123,179,102,32,252,187,80,213,101,98,84,82,148,30,37,53,22,15,226,39,160,51,165,14,96,76,230, +20,131,194,212,16,219,216,160,173,169,4,157,213,111,115,44,28,247,216,237,149,32,80,145,248,150,244,79,40,56, +170,144,238,82,30,34,165,135,209,31,253,74,95,50,215,203,155,63,117,134,21,58,125,210,15,50,95,68,156,50, +207,174,186,186,76,124,96,145,210,11,156,216,12,28,100,72,83,83,188,136,165,183,133,211,151,127,113,252,250,213, +79,205,39,102,91,26,91,215,205,205,29,160,22,38,151,58,31,236,151,126,96,5,85,161,183,70,81,222,48,11, +206,76,159,184,8,48,14,224,46,117,107,56,159,150,103,184,98,36,131,9,174,105,211,214,73,48,156,27,55,156, +251,212,132,211,119,198,229,143,77,213,94,191,55,155,73,183,193,184,48,29,187,101,236,18,198,134,201,152,25,228, +133,14,37,108,127,54,242,246,44,96,116,3,166,109,158,86,150,117,168,130,87,76,129,75,224,48,249,200,241,153, +198,244,126,213,63,230,68,197,120,166,74,19,151,147,144,15,102,145,24,120,173,35,234,61,222,205,55,239,211,22, +83,22,153,8,88,133,139,255,54,167,149,145,74,195,29,5,172,209,134,8,254,66,123,195,224,98,85,243,27,121, +164,16,219,15,63,179,95,208,91,205,60,115,21,25,201,58,151,3,68,185,134,26,200,62,196,117,216,53,103,73, +7,31,224,65,46,99,16,72,5,222,51,211,204,11,42,184,144,140,97,122,177,69,230,243,79,135,142,96,37,63, +17,244,91,132,55,113,14,32,158,199,138,146,68,246,193,171,1,119,44,218,170,250,92,165,95,162,137,202,23,154, +118,102,46,95,107,138,39,37,95,105,138,70,61,63,215,20,14,48,35,43,10,198,50,63,211,20,12,92,190,212, +228,7,40,191,214,20,140,69,126,169,201,126,118,126,165,9,54,65,252,151,230,167,38,229,61,120,18,99,240,56, +255,164,183,25,189,214,133,253,138,29,203,173,79,52,38,201,73,233,227,60,107,255,145,15,84,126,55,112,132,57, +48,46,213,27,84,142,226,183,217,61,117,64,219,0,155,63,137,54,172,111,192,217,251,144,156,38,48,228,170,198, +44,167,222,220,24,211,180,84,22,141,188,50,159,84,211,29,43,112,184,59,153,46,205,47,44,204,227,23,118,31, +185,243,8,58,11,130,185,239,194,203,230,185,110,71,107,193,255,1,103,171,236,17,59,107,67,146,200,247,92,221, +44,171,151,128,18,45,141,143,122,48,3,154,16,95,60,193,153,193,123,93,156,88,101,134,196,63,109,227,32,15, +191,4,100,73,112,192,14,104,18,120,79,124,27,163,17,177,221,111,229,155,252,89,102,47,32,46,54,234,2,51, +45,72,244,49,49,146,100,14,73,156,161,45,22,100,102,253,177,212,15,123,115,61,70,0,210,1,54,249,55,171, +86,186,102,190,53,219,181,246,130,106,163,4,152,184,137,83,196,222,77,116,135,163,221,203,61,17,255,8,54,70, +151,61,29,182,14,114,10,156,245,122,215,59,68,218,193,8,118,107,60,104,144,50,48,213,93,48,131,245,15,177, +250,89,139,221,82,254,81,225,29,145,142,52,135,160,80,234,156,51,193,91,121,243,168,104,39,135,135,77,38,46, +193,48,224,132,10,176,152,112,47,233,77,17,150,193,237,143,14,244,11,76,125,198,117,228,185,49,5,214,81,205, +97,156,121,223,218,171,69,121,238,244,222,249,52,229,230,170,140,211,185,129,247,154,175,92,96,242,196,90,58,157, +121,144,54,211,233,140,23,157,133,225,14,57,112,89,156,65,94,72,173,3,12,106,215,104,237,16,41,103,197,236, +142,172,93,111,145,49,32,190,35,110,96,97,43,233,254,253,192,116,51,219,41,76,251,129,116,7,25,69,21,218, +69,157,251,163,95,29,144,6,208,110,241,29,88,199,148,39,234,200,122,120,68,101,204,113,90,35,159,6,67,152, +69,209,137,125,181,142,125,111,137,109,240,140,176,192,223,105,221,191,52,40,185,56,40,233,96,229,106,241,62,81, +68,248,176,8,76,52,210,92,22,43,247,106,158,77,165,13,76,100,55,173,247,200,57,146,105,158,229,59,57,233, +156,59,79,167,41,255,187,22,75,197,48,244,180,225,62,94,153,1,53,247,106,166,98,116,37,87,145,207,74,215, +100,189,67,211,133,218,237,150,13,128,67,226,52,14,29,234,92,198,215,133,165,29,9,65,210,101,230,174,35,245, +152,217,114,254,142,224,166,194,22,41,196,51,44,231,73,4,154,14,0,76,197,215,202,120,69,142,185,62,223,31, +22,82,102,82,207,134,126,50,157,9,250,47,37,190,56,60,119,4,186,202,132,83,38,219,88,116,13,189,225,85, +52,10,54,154,58,152,85,192,184,142,116,218,152,252,184,76,49,81,118,54,49,209,89,110,40,102,160,241,18,242, +86,241,96,27,47,118,206,90,206,200,168,192,49,99,209,221,42,66,3,94,226,108,139,16,17,81,71,70,56,169, +208,152,53,9,36,238,147,203,35,71,129,60,66,81,153,79,101,204,150,126,212,48,186,109,187,221,57,147,60,225, +84,4,44,160,195,161,226,119,213,208,243,105,184,155,57,46,226,233,58,230,53,165,88,94,71,172,35,6,47,228, +30,111,117,161,83,101,140,118,38,177,23,157,224,139,154,108,109,108,59,147,237,178,179,195,201,181,53,246,237,78, +231,61,196,198,125,144,2,245,184,151,120,87,40,236,229,165,127,184,63,182,162,113,150,188,109,226,188,93,237,93, +5,134,43,81,231,136,191,176,148,78,69,31,106,211,240,181,152,56,120,45,13,189,52,147,245,194,202,63,2,136, +152,11,74,1,66,158,114,219,164,29,89,173,167,14,81,219,173,205,45,120,215,26,89,86,31,69,204,167,29,193, +42,112,91,217,14,171,187,205,80,188,117,156,158,184,137,13,140,145,14,110,58,56,231,182,122,201,75,131,183,40, +253,182,197,55,129,165,112,213,218,168,247,2,241,191,211,76,112,122,8,108,198,42,153,20,176,7,218,174,52,113, +117,100,141,254,236,40,101,191,10,249,129,38,196,169,78,171,28,29,107,60,166,68,47,45,31,136,175,2,2,17, +46,161,116,201,33,179,48,36,50,181,103,80,63,61,177,101,98,225,33,124,106,200,76,188,74,125,122,172,123,244, +90,62,219,126,112,37,252,144,83,149,23,109,18,97,172,240,49,102,253,78,83,120,99,163,87,248,182,124,206,137, +60,146,17,145,121,50,48,130,218,92,111,13,92,233,224,33,177,40,45,209,183,125,60,20,99,18,0,159,86,158, +147,56,44,154,188,42,22,233,64,28,192,6,142,83,38,228,56,51,78,18,117,152,242,137,216,186,148,209,241,237, +250,177,113,105,60,46,166,227,19,212,21,236,46,87,187,5,203,166,173,1,203,43,231,169,208,162,147,208,237,180, +93,75,42,240,127,4,167,243,48,117,66,176,179,112,94,84,222,135,244,69,81,65,55,224,172,168,224,26,101,89, +180,62,235,186,104,145,117,201,63,203,15,116,197,236,140,221,98,243,71,197,57,191,24,123,84,44,179,38,189,200, +166,168,227,240,112,254,33,111,210,179,108,138,170,238,223,63,255,144,243,201,226,130,174,121,202,222,226,151,251,211, +194,152,166,3,70,245,28,88,125,200,0,120,70,151,0,196,175,1,92,102,228,234,65,7,56,176,20,192,11,11, +120,225,1,175,70,80,255,133,224,216,240,176,159,158,153,129,242,61,136,43,62,179,93,61,115,93,29,175,216,84, +34,213,7,93,244,223,146,54,194,24,178,98,32,175,40,154,19,90,156,21,58,189,54,78,171,167,221,137,9,124, +200,223,167,215,36,249,212,164,179,108,186,78,209,174,237,185,108,243,21,87,183,41,140,63,125,238,224,198,118,112, +227,59,136,44,123,230,28,237,236,38,232,108,150,143,182,144,249,190,79,230,95,159,79,211,69,209,164,60,225,240, +16,54,5,81,203,37,98,202,0,77,45,80,140,150,164,184,198,213,215,124,36,124,45,159,25,42,40,122,222,99, +80,65,43,90,230,13,26,24,180,115,220,31,56,1,65,39,216,41,230,24,90,14,220,153,10,182,244,91,91,157, +89,199,217,23,251,150,215,112,98,85,204,141,87,3,55,94,13,22,114,43,97,170,240,31,173,221,114,10,205,244, +206,25,211,155,60,157,190,145,45,38,236,68,53,82,26,111,60,204,102,52,245,87,131,47,60,238,18,154,11,162, +149,61,93,247,246,116,45,208,79,106,198,40,120,233,45,209,183,134,77,205,254,162,139,27,79,3,231,180,182,135, +86,198,34,107,51,42,243,98,237,208,8,34,243,193,121,15,39,128,25,114,254,70,2,124,14,244,48,68,48,98, +82,215,101,90,89,54,79,44,38,149,242,178,59,64,102,243,204,165,157,204,29,50,219,239,128,64,12,205,111,155, +180,18,134,103,170,211,21,160,96,178,151,95,140,2,195,124,76,23,188,212,206,169,229,105,207,77,70,170,67,166, +42,150,81,49,48,92,159,124,226,95,172,91,46,199,168,206,99,179,150,215,43,154,152,190,78,87,156,179,26,114, +144,252,39,149,230,146,143,85,103,2,163,112,246,59,232,246,177,215,93,31,15,126,168,77,249,56,96,69,85,234, +236,97,187,43,105,23,26,59,60,84,178,248,238,224,85,193,187,121,154,98,177,68,138,84,145,77,30,67,68,111, +4,28,114,245,76,225,221,180,132,13,251,66,188,106,67,1,253,155,157,19,29,117,34,39,162,153,93,136,84,239, +44,38,99,13,107,246,183,191,205,176,18,201,238,77,82,105,32,73,194,151,239,108,198,1,45,220,178,251,152,241, +150,149,138,61,102,78,134,179,241,147,161,188,90,37,36,142,108,175,44,56,28,98,42,134,106,97,201,219,185,86, +3,250,129,117,6,195,203,131,113,239,82,36,74,171,174,193,193,183,106,208,131,166,106,113,109,34,85,149,224,30, +131,196,193,206,150,217,205,222,132,25,204,56,131,205,84,109,215,155,22,73,182,95,29,25,44,195,141,211,130,181, +41,222,240,50,172,121,247,89,121,3,23,127,104,207,86,108,30,54,18,124,163,196,134,223,168,198,13,243,121,168, +34,60,162,112,163,233,108,161,5,6,183,86,184,56,159,101,168,224,56,93,49,4,231,50,245,49,3,102,27,59, +88,243,4,159,88,95,182,31,140,185,15,31,205,182,210,148,44,229,96,75,165,146,98,227,78,246,229,117,18,99, +93,45,239,131,170,178,189,52,102,98,22,44,208,178,78,164,178,108,134,158,193,251,237,34,11,176,36,211,98,145, +145,29,27,15,106,168,100,110,233,15,87,37,196,70,80,136,252,235,108,97,6,156,155,54,161,179,226,107,128,252, +146,151,131,23,249,63,199,26,116,87,26,97,178,117,235,33,155,251,141,180,51,172,184,199,232,166,184,120,157,244, +197,6,210,45,135,153,65,123,206,99,1,214,202,244,219,124,26,99,178,211,186,42,89,134,117,38,244,122,135,237, +97,88,57,128,73,113,59,88,23,254,169,217,89,1,220,110,89,187,11,79,178,151,188,208,150,99,135,130,101,112, +40,88,126,224,34,232,139,97,42,132,216,210,153,175,228,154,43,185,30,57,86,94,251,99,229,181,57,86,114,45, +128,191,42,46,246,16,21,170,191,58,18,199,71,153,171,248,148,159,218,156,62,186,226,3,88,231,106,61,229,173, +96,18,78,78,63,56,246,152,5,36,168,153,107,181,199,185,173,161,35,175,83,220,137,125,69,95,9,242,23,215, +39,175,48,232,219,104,62,102,153,224,40,46,213,15,16,104,254,153,191,249,120,169,229,158,3,161,209,27,139,151, +88,90,31,117,116,99,129,52,156,83,156,44,201,189,128,128,207,226,106,55,137,91,254,44,240,129,53,191,240,202, +67,206,141,5,68,242,60,148,188,133,36,208,21,79,245,126,3,212,123,115,129,51,64,84,123,16,149,135,168,33, +33,222,96,87,153,99,158,108,254,166,232,196,3,214,12,63,13,147,229,35,70,197,63,27,75,35,141,245,174,110, +146,30,183,75,78,40,219,37,61,215,169,246,55,18,144,180,50,196,28,215,62,230,199,35,99,88,46,20,179,18, +34,39,208,144,115,73,105,188,253,24,43,43,203,4,101,234,161,12,22,141,67,146,50,74,129,133,236,93,103,186, +117,104,147,143,219,170,241,89,137,171,86,154,220,78,212,244,170,74,43,151,158,240,230,205,23,105,6,84,186,25, +220,125,8,196,64,233,19,74,111,109,116,179,211,232,198,52,26,141,131,111,60,99,98,164,178,97,10,186,12,35, +127,115,195,197,58,83,108,211,200,224,244,212,83,41,168,227,222,159,40,43,60,221,93,65,99,224,98,107,119,120, +132,54,9,207,250,65,247,113,88,199,74,192,61,132,90,168,202,248,14,11,98,197,61,141,151,81,63,224,52,255, +1,254,182,48,111,112,225,84,5,46,156,252,2,76,168,49,207,110,40,176,179,30,58,152,143,47,107,219,242,242, +141,125,163,131,66,135,201,81,114,24,185,238,15,123,197,79,235,142,62,106,213,164,12,21,108,167,231,122,223,253, +73,47,107,204,252,88,231,1,202,60,211,102,12,110,240,92,47,240,251,22,127,6,233,83,240,81,172,86,231,250, +199,193,193,72,163,197,39,63,241,29,222,107,77,207,116,112,51,247,155,190,219,13,38,58,148,30,104,227,204,76, +108,63,196,15,151,84,179,170,90,213,63,182,23,87,198,95,99,160,124,128,82,76,61,195,20,231,183,94,110,60, +44,54,175,139,176,148,24,35,225,172,56,201,44,12,243,206,136,23,178,53,74,140,54,99,72,121,44,183,201,50, +254,43,139,13,172,154,119,69,109,126,96,145,150,53,140,153,233,48,130,139,161,190,180,103,197,20,35,55,220,8, +98,97,192,90,66,22,220,249,181,179,249,25,213,89,147,162,110,180,255,162,129,99,238,217,46,53,230,123,148,111, +27,250,5,153,89,254,27,255,240,96,236,1,33,51,96,208,223,121,165,6,119,173,181,167,114,243,141,131,201,127, +52,186,210,63,55,144,86,239,51,3,21,204,119,20,187,170,211,238,245,219,179,215,223,60,123,138,200,208,208,84, +84,174,243,138,246,244,11,90,249,2,110,108,47,235,155,198,230,191,184,163,51,126,44,42,134,116,117,4,89,242, +61,238,203,66,255,245,58,56,178,152,202,179,145,47,245,220,83,223,227,69,25,255,75,142,159,253,122,252,248,221, +179,199,209,55,114,250,218,115,245,110,44,18,107,210,24,70,172,85,181,94,57,225,96,248,250,2,218,179,122,253, +210,25,67,0,227,93,46,75,65,186,228,186,244,236,34,120,11,239,110,137,141,115,69,99,227,237,182,108,114,13, +227,117,204,200,224,203,246,253,209,179,3,191,89,118,0,161,128,224,127,27,109,102,217,89,202,157,146,194,171,176, +131,148,151,99,160,14,140,173,139,148,153,13,115,118,201,171,149,255,239,1,149,22,40,27,238,210,126,197,248,208, +12,210,73,127,93,99,10,84,221,196,30,151,186,130,245,128,59,232,7,207,184,32,118,34,192,249,120,118,97,0, +25,98,87,55,73,56,121,56,191,11,1,139,206,14,198,11,77,191,248,241,248,214,142,7,66,172,15,174,139,228, +244,180,77,232,87,19,152,37,126,132,126,119,218,94,252,123,242,179,254,144,13,122,224,253,52,97,245,244,102,201, +27,192,185,168,52,239,5,189,187,127,83,128,144,8,163,206,25,201,43,160,127,49,201,91,212,247,43,215,103,216, +114,169,40,44,138,60,114,25,81,113,41,22,104,88,199,247,249,197,11,189,231,92,69,167,146,169,254,204,185,138, +98,194,162,184,58,13,26,98,7,238,15,93,148,112,208,156,194,123,180,188,58,228,224,9,75,226,30,21,255,254, +143,128,116,127,63,80,43,51,96,127,104,247,0,243,69,139,69,53,169,10,197,6,193,90,238,64,232,161,77,70, +215,217,214,17,173,255,77,219,194,198,131,73,97,2,234,237,222,126,93,232,40,254,168,248,10,113,41,43,238,172, +158,90,69,48,254,154,185,13,14,226,59,249,120,177,189,19,56,52,217,190,208,251,155,13,159,82,246,211,47,80, +200,221,180,85,46,26,130,120,219,213,108,243,16,225,244,202,127,118,202,114,132,23,58,27,219,220,0,26,62,255, +230,38,70,90,149,114,23,4,1,61,212,77,72,12,17,245,27,85,82,184,53,177,58,22,198,39,47,180,221,201, +191,131,10,195,17,78,75,154,190,215,212,43,250,78,135,108,52,67,90,61,82,81,40,87,212,168,97,139,84,74, +182,8,66,1,202,208,35,93,245,130,5,223,97,159,232,186,13,18,106,123,214,21,59,5,209,113,208,246,253,84, +207,225,51,102,8,104,248,30,176,144,241,25,144,69,38,39,130,155,27,209,127,74,146,44,56,39,136,44,75,14, +9,73,160,55,7,220,206,19,147,12,226,15,147,96,160,189,180,136,27,246,145,29,183,165,224,121,51,140,173,106, +54,213,68,188,199,206,6,121,204,192,140,151,145,118,71,8,2,143,124,96,220,163,151,94,201,15,239,222,126,251, +238,217,251,247,160,57,165,35,75,112,31,115,106,192,10,53,17,95,215,134,167,72,146,92,110,108,57,50,81,42, +45,169,206,140,145,1,129,173,179,193,236,121,244,157,104,72,235,212,87,15,196,29,139,135,90,85,180,234,79,180, +86,169,85,190,76,145,60,98,221,229,175,147,67,117,152,60,122,96,130,166,167,27,174,39,16,29,217,35,126,25, +36,101,241,16,69,89,22,124,19,131,135,23,217,97,150,255,92,117,96,39,8,172,178,232,174,5,12,242,54,148, +121,42,21,234,158,57,199,244,80,19,231,145,76,222,254,112,252,242,237,155,144,71,128,63,3,101,213,201,58,9, +132,134,4,85,64,98,33,147,71,7,188,254,174,104,32,184,113,132,252,45,236,150,83,37,2,191,239,248,204,202, +215,221,69,117,91,16,177,73,120,241,218,157,46,134,91,87,172,228,230,72,212,157,189,72,177,205,32,214,6,71, +100,178,121,241,156,187,204,214,196,82,0,84,54,56,244,8,105,226,236,216,99,12,109,49,6,66,91,170,85,113, +149,166,251,38,94,96,132,170,120,48,73,167,7,39,255,35,253,240,247,127,102,217,131,37,247,249,65,158,30,29, +102,15,28,237,114,86,8,218,108,220,142,6,104,134,255,112,129,109,178,137,119,123,132,157,122,130,171,66,219,115, +88,221,120,232,34,25,140,45,80,197,255,2,77,49,229,251,184,80,131,39,217,160,67,94,228,60,61,78,163,184, +185,252,12,180,197,212,93,207,146,222,220,246,20,169,86,59,238,47,231,42,58,12,243,208,97,146,108,130,246,175, +207,70,132,198,186,208,119,232,205,240,110,119,79,201,218,130,63,92,98,89,6,163,68,13,40,147,36,247,59,201, +206,212,139,109,87,21,202,201,239,84,84,157,138,203,5,94,201,140,224,86,209,74,177,167,181,251,247,31,208,57, +7,254,217,253,253,64,113,79,91,238,99,127,239,1,93,40,207,29,120,86,126,165,156,209,137,204,78,13,248,191, +225,229,46,192,134,235,174,115,11,219,142,195,62,1,251,137,203,107,195,217,51,180,185,162,73,134,62,36,129,96, +182,41,150,74,54,199,158,179,238,63,113,98,224,154,62,105,196,82,146,244,46,76,220,110,233,76,177,150,236,47, +213,217,39,213,39,196,174,83,63,67,179,158,229,209,180,220,219,61,16,173,170,98,253,103,56,88,234,167,190,248, +196,133,40,145,231,49,160,29,120,58,15,2,184,86,131,121,164,65,124,94,249,183,150,95,65,39,242,167,245,218, +233,68,30,186,71,148,15,157,198,192,153,26,241,62,127,166,24,167,30,86,6,137,196,173,52,219,104,155,93,135, +107,218,29,53,212,206,81,163,13,55,23,47,35,73,113,97,181,151,171,36,215,29,49,98,65,65,4,78,27,142, +55,240,201,80,171,207,213,28,105,140,191,109,121,195,133,44,138,250,230,102,67,115,32,1,233,158,173,28,185,19, +73,217,173,161,208,233,220,241,38,83,240,33,208,133,156,79,228,154,98,174,228,130,199,140,182,177,203,186,200,154, +116,205,78,225,224,155,90,165,51,234,176,0,125,246,58,43,11,100,19,46,34,22,28,112,96,214,167,79,9,50, +95,90,246,252,210,227,223,107,139,127,17,218,210,149,217,90,135,15,252,208,159,122,214,205,60,45,28,176,186,220, +31,201,57,13,252,106,22,200,16,254,69,100,8,149,197,177,87,106,12,33,123,77,237,160,22,107,214,156,103,63, +223,79,13,149,164,32,122,226,230,151,35,199,57,30,116,30,26,8,167,38,109,216,167,195,10,105,70,61,189,31, +63,7,166,237,97,149,185,239,11,61,102,254,127,30,4,225,100,226,113,136,51,40,76,149,157,117,115,227,242,247, +62,236,127,125,228,168,41,134,17,179,152,219,143,101,147,125,205,252,107,91,120,116,216,16,62,119,130,36,25,24, +106,167,227,99,12,9,204,173,253,12,199,252,149,242,36,27,172,167,188,139,12,105,159,103,14,6,231,62,162,216, +214,117,32,34,21,189,86,169,179,48,150,48,150,203,8,137,61,104,184,192,143,209,83,148,201,242,240,132,243,122, +23,171,186,119,208,48,43,208,202,51,104,126,108,124,223,68,19,50,63,199,122,39,253,126,175,109,214,99,195,187, +237,102,11,71,151,80,93,149,97,38,162,46,21,117,70,233,168,83,178,124,157,65,182,171,211,176,45,79,84,241, +187,145,89,209,71,85,36,94,130,146,208,123,142,203,3,74,19,125,187,147,253,56,138,87,144,204,31,199,37,222, +132,81,0,76,158,224,21,137,12,161,127,123,167,155,168,26,255,142,204,3,92,26,154,182,3,150,162,71,66,237, +142,119,250,117,185,147,250,140,27,207,104,191,97,215,187,187,219,141,161,210,227,161,217,199,241,183,94,6,137,174, +81,225,114,158,241,32,79,109,173,48,146,80,117,30,236,57,222,174,222,153,121,132,123,143,84,32,178,220,123,50, +206,199,236,251,165,89,120,5,167,144,245,76,5,171,212,68,113,219,18,94,180,197,156,255,136,32,141,15,191,35, +201,98,49,209,111,255,202,160,208,212,106,191,66,133,123,239,0,115,207,54,53,82,27,239,205,229,104,43,12,79, +22,209,6,142,172,84,44,168,122,46,0,164,139,198,190,112,65,64,6,202,188,118,130,45,44,227,64,208,176,5, +122,56,150,164,153,53,135,164,113,163,168,166,143,85,254,70,209,172,128,3,243,96,212,198,101,172,29,76,24,181, +41,222,129,197,226,90,47,129,98,78,233,240,112,246,53,68,255,53,38,231,22,79,212,179,71,22,32,35,117,200, +220,145,33,109,251,141,25,196,243,3,136,241,89,106,6,10,47,179,111,88,200,158,165,116,115,47,11,232,243,243, +152,221,119,139,154,59,245,68,204,232,9,179,145,246,24,179,180,57,121,203,39,235,167,240,161,157,124,48,200,126, +48,220,6,131,1,106,128,216,180,178,226,71,128,202,226,39,149,66,185,151,58,128,31,223,85,225,76,32,238,174, +176,70,133,120,151,194,163,203,74,34,208,224,149,57,42,191,54,235,140,131,156,83,114,142,26,44,114,0,226,189, +154,214,22,226,61,32,106,134,152,57,8,232,27,194,74,1,108,71,25,137,6,241,41,227,235,175,184,206,122,250, +81,229,239,85,110,196,153,11,120,104,149,214,92,229,249,204,5,152,253,18,61,11,129,96,31,78,114,70,176,131, +228,206,6,201,96,208,244,11,150,100,222,82,111,119,238,134,134,229,152,47,136,117,43,142,221,92,230,161,251,240, +159,236,28,58,197,120,105,126,80,124,130,90,252,32,227,29,206,69,238,227,66,225,108,197,9,235,144,187,8,204, +28,254,134,35,201,33,255,239,141,230,114,164,150,143,212,1,240,97,245,143,191,91,145,173,183,154,194,202,101,3, +177,79,40,33,220,57,6,21,188,139,81,11,152,104,232,255,14,250,28,204,180,6,177,163,25,14,150,117,45,46, +37,125,122,154,185,211,226,43,119,142,12,223,102,76,68,124,168,224,175,206,84,46,4,24,149,67,112,214,122,85, +24,171,151,169,192,10,240,42,85,130,42,106,14,120,154,109,152,250,144,88,155,245,181,75,162,105,206,137,144,20, +149,182,212,122,136,187,98,43,151,18,149,187,224,84,177,222,252,12,85,210,153,171,156,150,128,95,112,72,50,174, +135,94,185,65,161,203,161,236,99,83,47,93,13,77,208,169,43,109,179,62,185,172,160,252,43,78,155,219,13,71, +175,89,231,159,158,240,63,107,211,202,114,115,79,88,165,219,29,196,95,23,79,156,124,151,33,93,178,153,138,143, +197,193,235,192,175,25,139,106,227,151,97,102,78,62,178,183,121,108,107,158,132,43,25,251,247,197,71,214,12,156, +206,243,154,222,34,184,154,174,120,3,60,70,112,61,93,243,190,56,70,240,242,230,230,130,222,32,52,102,38,243, +106,122,149,159,209,51,100,159,178,231,20,122,137,208,167,155,155,107,250,204,178,167,89,250,42,155,190,146,145,203, +95,101,19,121,21,121,175,48,60,96,41,172,206,211,226,91,149,190,201,232,121,225,23,75,241,67,132,143,239,241, +194,100,98,213,210,227,140,204,239,219,140,225,253,2,157,166,12,97,50,222,103,244,242,111,127,123,137,51,124,254, +140,141,209,113,128,130,106,177,255,112,102,181,203,182,91,233,75,30,21,222,205,94,203,34,221,115,57,208,6,154, +80,212,136,141,192,83,171,18,25,134,79,122,163,105,61,129,249,31,8,16,157,32,17,81,222,106,195,6,218,141, +167,25,189,129,130,86,75,207,177,227,233,152,249,98,244,27,95,253,217,126,148,249,125,203,31,167,226,129,145,236, +112,44,152,59,248,108,135,234,41,135,127,84,233,231,108,26,208,185,231,244,57,203,159,2,162,227,246,50,131,99, +40,24,15,177,78,83,133,157,226,174,220,220,112,101,207,211,72,131,244,155,113,116,18,238,248,32,22,163,19,151, +254,151,208,137,220,151,99,201,4,216,195,105,104,166,130,113,34,60,118,7,106,17,16,65,45,46,26,160,22,73, +136,81,139,219,230,175,144,69,107,7,68,43,183,201,37,227,194,101,248,45,14,116,50,7,213,21,136,101,184,227, +175,163,77,112,137,77,176,206,232,202,108,154,101,54,93,74,93,249,210,109,154,211,194,125,224,222,246,8,22,40, +79,93,24,11,214,105,26,167,199,107,214,62,8,188,118,251,108,35,251,12,250,130,116,26,238,51,134,48,25,12, +124,193,76,186,217,103,41,22,203,138,209,7,98,158,80,248,221,118,54,61,75,63,101,249,167,52,124,129,196,253, +62,141,86,237,65,180,8,227,111,74,199,123,207,165,70,211,97,208,58,115,159,214,103,52,135,2,116,43,223,247, +217,118,223,252,46,70,119,20,178,227,190,125,182,131,114,41,59,234,42,218,81,167,116,53,236,168,83,183,163,214, +140,67,57,229,148,27,189,185,225,98,167,177,172,130,43,241,38,209,68,112,31,91,25,63,80,221,155,242,77,100, +229,226,91,119,214,110,34,235,46,86,202,7,69,68,255,250,174,202,166,12,190,251,196,112,138,183,55,121,101,158, +212,158,122,121,132,132,178,175,3,175,187,47,236,222,62,8,76,136,200,196,48,19,81,9,231,251,11,14,82,78, +66,245,66,145,57,201,218,176,240,231,254,80,228,43,11,102,121,250,141,242,175,139,182,249,23,190,174,103,161,233, +143,184,206,167,70,209,76,209,165,162,95,212,7,250,85,21,63,43,199,92,253,164,51,250,93,21,252,48,253,139, +125,121,158,191,210,100,31,164,231,191,170,109,54,129,210,135,19,165,238,179,238,98,64,5,221,146,59,248,125,156, +223,223,114,137,51,233,205,117,13,183,5,183,13,26,15,127,173,130,64,182,181,104,236,59,22,218,57,69,190,224, +243,221,53,178,109,27,115,45,246,91,229,73,25,207,183,165,9,23,86,217,10,186,127,237,173,154,125,223,169,61, +5,70,105,195,200,162,254,80,18,9,221,52,187,138,113,76,4,255,41,158,63,188,191,128,170,196,78,78,156,89, +168,161,131,198,214,64,111,240,103,6,132,30,93,64,21,149,87,117,35,31,22,119,123,188,85,198,6,223,94,185, +153,86,251,18,20,183,45,51,186,27,82,36,26,205,45,112,110,22,145,143,153,79,221,20,49,161,193,118,164,221, +161,202,71,46,33,118,39,38,251,178,55,138,19,57,220,6,99,73,186,248,43,35,43,234,237,236,140,247,188,26, +119,198,123,112,143,99,205,73,101,56,126,175,14,232,12,241,76,173,43,162,184,134,72,210,128,165,168,205,228,87, +254,58,209,41,201,50,2,101,0,155,65,154,187,227,22,175,29,58,144,245,64,82,129,47,151,129,249,222,133,200, +235,197,141,159,153,61,232,54,163,200,141,237,142,108,96,232,70,240,129,66,193,111,110,98,124,229,222,121,15,42, +130,124,238,156,121,11,244,238,170,162,123,52,155,116,124,93,193,117,148,62,23,66,118,157,169,226,105,218,240,12, +164,101,6,49,47,149,71,206,232,165,168,196,164,62,161,80,254,150,233,94,106,138,4,102,154,33,142,74,251,1, +248,37,132,44,92,5,139,73,118,83,139,142,49,1,127,203,94,250,253,135,33,1,232,203,200,12,10,252,81,85, +237,117,48,162,225,226,168,104,71,72,84,149,1,249,16,109,1,70,68,198,196,187,196,114,123,101,236,139,180,166, +136,211,168,241,55,223,188,73,60,80,115,11,16,62,115,164,228,67,210,165,203,240,168,48,232,39,178,61,139,56, +238,2,27,26,5,38,216,37,34,191,82,189,228,84,100,94,218,24,238,84,117,6,11,74,70,27,180,160,130,145, +56,24,183,177,99,47,48,247,120,203,105,159,163,244,173,151,169,66,229,202,210,58,1,112,77,142,46,231,73,91, +112,93,246,30,85,23,173,109,179,221,109,147,20,48,6,163,140,86,45,225,119,238,169,234,96,171,182,72,26,238, +1,144,143,187,213,156,75,6,238,131,118,146,112,190,208,211,180,245,116,20,12,53,147,229,54,34,17,59,165,10, +5,212,146,239,166,54,83,149,75,219,163,214,147,198,182,173,71,43,114,212,231,109,112,192,10,97,225,0,168,219, +7,96,162,246,186,222,76,255,172,243,163,99,6,100,247,205,221,5,221,184,222,242,233,99,181,186,209,224,225,104, +226,105,15,244,181,177,189,255,74,23,51,174,166,227,245,99,168,81,254,157,34,124,114,94,150,76,146,74,235,46, +192,218,252,23,225,0,107,189,106,184,248,37,62,182,12,97,20,118,96,32,195,46,236,133,36,46,197,159,109,34, +152,99,189,15,21,167,133,119,30,251,176,113,122,208,221,145,164,91,97,119,26,116,199,160,252,68,132,88,100,211, +69,143,254,67,64,140,234,50,176,208,139,93,28,171,165,123,166,23,167,214,88,33,221,121,95,152,114,29,167,109, +160,222,149,69,186,24,155,50,86,97,9,29,153,58,250,99,110,190,3,215,87,230,137,163,49,163,228,118,252,158, +251,171,216,29,134,230,18,159,82,197,202,42,154,85,197,71,92,43,45,202,225,246,242,193,63,231,247,189,23,137, +123,222,212,97,185,28,8,82,159,70,142,38,190,136,119,137,125,165,125,223,229,109,232,99,177,12,76,124,20,253, +160,235,177,24,57,129,75,131,33,149,88,239,210,174,232,145,113,21,202,62,228,81,89,57,234,253,72,216,203,171, +22,135,28,58,31,131,73,176,95,4,43,98,179,108,233,194,109,155,232,246,75,190,125,86,134,238,54,156,3,136, +113,71,84,212,142,57,130,0,62,19,183,47,237,145,104,87,164,171,50,243,254,75,97,161,197,251,228,3,187,91, +77,36,69,27,211,5,40,143,145,245,166,233,135,233,210,22,47,242,74,212,34,176,80,62,203,186,38,150,131,51, +239,2,155,35,75,66,59,127,177,201,105,112,27,115,63,17,47,206,112,131,199,87,137,201,68,97,240,173,175,8, +19,158,42,255,196,117,90,30,38,51,9,38,121,121,168,48,242,121,151,26,184,108,138,91,56,217,126,146,226,125, +195,148,217,212,164,112,25,249,53,255,39,242,250,209,234,2,221,220,216,128,57,116,7,11,167,216,200,80,224,234, +32,112,4,65,27,12,130,188,244,81,241,75,48,220,58,237,37,9,211,123,94,226,240,161,34,194,145,209,6,175, +52,132,208,28,240,162,84,180,201,56,196,43,10,191,252,224,116,140,29,24,75,149,174,133,111,130,221,19,174,205, +238,118,112,186,168,232,126,194,60,240,125,101,164,161,141,155,179,104,34,205,75,213,10,111,186,189,156,40,62,200, +85,1,236,67,248,5,100,164,49,171,228,156,146,26,105,160,91,4,162,216,121,159,155,148,246,128,40,248,75,213, +176,196,234,137,123,148,31,212,63,199,45,23,119,97,70,137,151,102,243,142,201,72,18,99,153,54,50,108,135,189, +216,42,236,112,207,53,22,61,88,209,237,240,76,10,186,78,37,134,5,139,202,162,241,115,29,19,1,16,188,108, +98,213,220,207,74,217,58,232,238,178,44,44,234,58,43,73,196,108,70,136,61,238,184,133,156,27,57,97,85,38, +97,196,127,117,224,32,232,177,177,204,10,210,108,216,199,211,211,180,119,203,16,216,106,13,195,93,150,197,148,228, +194,38,235,52,35,235,29,211,57,62,29,195,40,14,197,75,191,4,161,5,219,223,46,30,164,38,176,32,156,80, +59,238,36,88,138,155,167,13,66,162,36,197,27,73,208,99,248,10,143,7,72,197,144,5,39,149,126,235,241,153, +137,15,72,122,56,43,57,181,174,25,147,161,206,32,172,153,33,43,28,144,135,2,51,65,226,95,29,12,56,97, +22,227,132,132,217,156,90,117,224,243,149,53,207,151,81,123,50,19,97,219,140,24,222,162,133,217,56,90,40,205, +49,107,178,69,147,126,4,135,119,159,230,106,113,225,29,210,46,92,151,231,69,115,178,248,48,153,239,237,200,146, +108,26,31,75,138,57,196,40,184,88,253,70,111,140,44,240,73,173,120,145,191,227,49,79,209,207,185,233,231,180, +150,174,207,179,124,227,66,91,51,142,152,253,2,103,15,12,7,57,151,74,34,206,154,23,27,183,236,7,8,53, +152,29,29,119,143,20,207,170,91,192,110,127,56,255,135,129,254,11,99,115,228,38,147,126,208,43,7,4,222,124, +191,230,244,20,78,114,196,60,136,136,99,156,182,210,117,153,145,143,93,70,177,43,142,201,94,105,171,69,173,47, +253,73,12,38,199,143,244,98,193,135,250,23,149,90,174,122,234,111,215,202,149,81,54,35,17,73,253,169,41,172, +18,223,196,8,74,225,28,252,104,184,16,231,235,126,167,24,226,83,252,244,185,59,103,152,227,111,247,165,59,143, +21,36,202,102,188,206,10,127,122,198,210,177,247,44,56,123,30,24,5,127,78,56,120,48,180,235,248,38,209,54, +192,21,176,209,127,135,204,216,206,231,94,75,125,22,52,134,233,21,41,56,148,38,160,87,204,63,100,157,154,231, +95,48,31,251,34,78,80,246,39,42,50,189,45,3,111,193,29,178,142,18,39,131,107,53,24,102,49,15,238,179, +201,184,82,198,88,234,248,132,125,66,207,229,37,239,169,157,148,118,244,112,68,130,89,170,216,104,28,78,114,78, +151,195,171,249,58,80,59,128,14,148,162,207,225,233,13,47,209,35,209,210,181,8,24,176,106,220,64,227,163,130, +104,154,145,141,11,109,66,126,20,79,67,75,117,82,157,213,220,172,46,127,96,12,208,223,129,1,66,123,116,225, +185,98,192,31,212,22,81,109,212,24,227,85,139,254,126,107,126,204,249,183,215,107,142,242,127,76,47,191,204,225, +19,97,184,55,156,11,102,101,187,34,59,67,5,59,66,237,237,136,100,112,189,146,38,135,205,97,178,190,162,228, +80,227,55,75,72,141,110,151,175,186,68,84,223,78,25,199,122,245,170,252,162,36,31,251,22,174,0,242,101,185, +157,176,95,35,241,126,117,4,111,46,63,117,230,217,98,241,83,67,62,131,175,140,43,248,172,170,230,112,194,171, +244,104,22,212,1,139,31,130,98,24,106,134,199,230,98,26,55,171,138,50,46,24,219,109,47,102,154,142,83,206, +119,135,180,192,12,64,87,102,65,94,236,160,234,148,243,66,151,81,158,164,243,37,196,239,42,127,22,230,58,135, +202,227,142,136,122,156,35,127,159,110,180,215,33,164,103,173,188,29,3,220,150,126,191,85,202,249,142,105,239,69, +175,117,205,123,177,238,241,119,84,157,67,71,6,98,171,132,251,32,226,79,170,78,146,50,97,203,80,205,54,11, +188,194,67,88,253,63,153,187,174,245,198,117,28,252,42,10,183,68,58,166,187,227,196,242,106,124,122,239,229,202, +147,111,62,154,146,19,109,108,49,43,217,41,39,227,125,159,125,141,125,178,197,207,34,75,110,51,115,183,167,36, +12,5,54,144,4,1,144,0,174,102,130,129,144,176,158,156,15,216,97,109,11,87,218,162,138,192,99,113,53,96, +56,198,145,238,244,71,250,181,14,170,25,206,134,148,94,32,221,233,15,145,94,35,221,157,37,128,159,107,24,217, +237,81,58,214,249,50,150,148,190,143,68,107,161,164,185,129,188,165,63,104,152,165,29,43,95,82,134,36,118,52, +223,102,61,80,22,97,85,34,114,196,140,210,63,80,66,220,208,198,23,89,150,44,248,13,101,125,110,117,115,156, +124,111,240,71,72,208,79,17,83,25,2,17,61,227,121,118,226,244,243,111,118,77,3,31,65,39,126,122,204,182, +17,218,74,27,204,71,242,151,225,248,69,164,185,190,34,226,119,219,42,14,70,226,39,254,14,96,223,215,192,222, +56,107,135,13,255,161,246,65,239,248,149,29,146,79,79,50,137,255,53,107,72,170,69,131,181,219,172,1,151,74, +5,156,49,208,49,188,164,163,227,118,63,178,177,113,105,220,53,47,54,182,177,136,157,185,74,30,24,191,163,219, +47,83,122,92,191,141,174,252,72,206,208,136,113,170,12,194,175,188,179,168,70,179,249,114,219,114,96,223,19,217, +213,139,53,155,249,207,1,127,166,3,170,214,195,10,10,105,201,217,153,164,106,37,252,232,101,245,193,148,241,235, +252,59,52,177,9,73,237,123,211,162,189,187,3,168,243,28,12,145,231,51,186,23,54,110,194,103,58,34,34,108, +45,122,220,252,238,210,195,205,165,65,112,244,61,207,162,133,159,86,145,206,83,222,13,130,240,76,236,29,252,111, +223,110,177,112,230,176,176,45,9,203,172,150,49,235,248,77,135,89,33,78,170,240,127,192,152,158,160,110,193,133, +142,206,103,193,164,218,247,117,237,144,171,64,5,211,167,218,36,172,107,71,156,97,166,205,234,170,9,0,21,50, +1,132,224,230,35,244,179,232,7,46,14,92,84,89,68,48,254,61,137,29,230,217,69,242,132,33,20,17,42,10, +111,185,222,125,225,114,3,242,16,119,58,39,200,67,133,56,8,164,251,131,222,92,19,138,180,220,175,18,215,19, +133,139,102,7,95,221,218,17,214,195,213,120,49,161,62,46,220,243,190,22,211,47,53,137,175,108,100,48,254,13, +97,237,3,183,149,58,228,151,223,254,34,190,73,94,183,253,215,49,25,113,5,92,71,181,4,220,171,232,114,16, +212,33,63,187,205,213,178,2,155,209,103,21,1,184,62,90,69,207,48,213,134,155,78,31,30,163,33,120,32,108, +227,109,185,204,103,25,9,203,55,98,165,114,248,155,47,72,105,121,3,165,134,126,18,143,10,47,68,247,68,133, +243,139,24,72,82,26,97,243,43,80,68,141,200,217,176,215,211,136,76,137,144,175,146,28,245,215,26,222,33,92, +238,113,242,170,100,231,166,226,154,36,191,41,251,248,227,178,60,101,168,41,74,92,111,208,179,203,228,234,68,207, +174,250,98,230,122,54,154,205,123,174,103,87,189,11,225,166,56,158,95,94,212,209,49,57,20,235,48,77,138,93, +166,20,66,112,233,141,212,79,176,76,156,124,133,199,181,206,79,243,43,25,168,214,28,2,47,205,153,36,34,101, +98,55,151,204,159,30,199,108,150,156,26,199,176,27,159,194,220,153,137,110,104,100,77,194,223,138,86,204,163,135, +7,61,95,228,185,162,10,72,23,113,190,242,104,55,120,172,225,60,20,195,214,65,20,158,240,202,243,157,237,116, +74,138,254,126,167,176,62,188,130,234,144,43,54,46,123,56,188,24,92,178,192,218,68,25,132,15,71,243,190,67, +248,101,28,119,52,194,153,105,221,251,198,205,39,33,77,225,188,36,140,41,240,60,148,255,37,245,24,183,187,190, +129,229,182,204,142,198,164,48,135,191,121,118,42,120,161,97,194,114,112,92,75,214,97,103,3,214,97,239,37,2, +79,34,105,201,15,108,170,76,97,205,133,234,98,14,13,234,85,105,165,54,177,209,44,237,217,16,171,44,33,125, +40,145,165,85,4,221,136,10,184,45,219,40,3,159,113,91,100,101,161,187,27,19,68,215,108,146,157,213,84,33, +90,192,253,160,119,117,114,15,131,70,213,22,45,202,12,68,124,170,140,97,116,84,133,209,73,35,198,12,189,170, +86,85,99,205,172,204,101,182,132,207,254,100,65,41,248,125,83,124,145,209,217,155,35,216,150,223,193,216,38,251, +124,132,155,188,40,210,225,153,82,167,253,129,63,70,83,41,200,191,77,234,65,196,189,19,131,0,77,113,131,184, +148,151,118,129,149,187,91,128,152,173,51,56,109,70,175,10,77,187,205,253,119,185,202,199,206,72,162,32,242,2, +213,232,220,47,184,224,47,134,181,94,235,225,64,213,109,38,79,185,176,175,252,240,238,67,37,8,166,68,107,193, +244,62,249,144,105,171,66,58,225,95,51,249,133,34,225,6,17,12,117,148,12,74,104,127,117,187,108,208,196,126, +6,203,237,146,168,9,220,218,224,106,208,57,222,19,75,1,149,225,98,59,179,67,52,187,184,79,36,145,189,119, +117,152,10,2,19,213,168,83,110,255,56,76,35,184,134,75,251,185,182,188,39,180,5,19,160,55,215,157,29,245, +59,167,87,110,173,23,103,118,61,130,96,84,56,221,223,116,144,49,68,60,63,184,134,45,109,48,80,126,96,55, +227,96,52,74,222,65,229,220,71,207,14,187,244,0,6,38,53,3,111,174,136,220,171,242,209,58,172,89,75,7, +34,83,117,13,116,194,7,115,65,191,186,215,92,210,175,222,53,95,208,175,62,180,115,47,105,12,203,165,144,68, +83,125,69,89,112,45,39,132,210,134,192,251,65,220,135,139,205,56,35,148,77,240,3,247,74,171,194,69,144,14, +115,147,194,23,93,149,224,250,123,72,145,166,55,91,251,243,77,222,202,141,25,47,34,100,48,171,235,172,171,205, +45,236,173,123,117,165,34,218,75,230,20,140,217,89,201,53,59,53,23,228,244,131,0,159,127,241,233,31,95,253, +253,239,246,215,153,178,231,17,120,89,123,30,61,172,147,166,22,224,155,11,133,184,135,158,20,89,166,86,222,44, +241,8,241,49,120,77,225,101,42,107,206,168,96,65,223,147,236,33,205,85,166,181,107,30,137,217,222,139,103,116, +84,161,119,14,157,240,185,183,65,161,103,66,152,71,90,0,146,152,239,60,179,161,189,149,162,47,113,42,197,42, +161,74,181,184,157,55,181,214,25,203,161,86,51,179,23,237,152,82,1,126,202,47,85,122,136,37,88,177,36,166, +150,173,232,93,124,250,108,229,116,159,1,136,5,52,213,32,59,160,27,214,200,230,172,91,53,158,9,96,52,107, +62,199,17,131,12,213,212,248,40,242,102,138,203,130,195,88,183,44,26,104,65,123,89,164,137,55,29,54,71,215, +175,103,86,31,87,126,110,149,252,91,61,86,75,48,222,13,225,129,59,119,18,117,169,47,10,166,186,246,66,204, +172,113,183,131,151,120,176,83,246,124,119,237,155,101,47,246,150,125,17,137,169,50,11,189,104,165,241,245,88,34, +136,82,209,108,242,220,41,183,55,201,196,119,141,113,180,18,132,194,249,39,52,213,230,181,106,165,182,9,199,146, +3,61,49,245,85,244,220,84,98,241,15,105,119,134,45,183,160,114,54,107,186,184,246,203,155,146,116,42,209,169, +154,15,139,101,253,222,1,142,244,87,174,158,196,13,43,215,206,236,121,70,195,202,245,176,140,178,61,211,157,105, +52,198,174,188,233,127,86,239,13,70,97,179,104,32,126,94,38,3,93,14,3,62,90,192,32,109,86,45,20,140, +235,245,191,170,151,166,165,91,255,30,213,191,7,155,210,42,182,134,246,131,189,16,7,219,55,72,48,52,7,41, +14,44,132,93,75,125,68,13,189,15,165,242,254,152,45,62,168,65,133,1,53,111,35,35,253,114,178,45,97,59, +91,115,90,98,162,188,151,213,207,106,209,19,142,69,19,52,109,76,89,35,110,156,255,59,98,231,100,169,159,82, +146,93,159,7,118,38,233,231,194,9,31,235,113,86,121,4,93,147,91,51,237,117,230,222,93,135,73,154,250,44, +194,165,67,17,209,64,17,136,225,217,24,52,26,247,179,92,233,64,54,249,126,110,199,250,91,201,80,142,138,61, +86,1,80,162,66,51,78,245,198,97,45,1,94,92,25,59,32,251,19,111,246,163,210,66,56,107,233,35,6,25, +38,133,172,242,180,209,217,229,95,22,33,227,4,172,174,237,112,14,189,145,113,47,187,203,90,99,53,217,238,28, +180,197,194,22,34,162,179,114,111,1,236,131,28,231,207,241,117,198,168,106,218,172,59,209,93,42,215,143,57,158, +111,233,1,97,218,236,139,161,223,110,147,100,21,84,255,0,0,124,123,71,55,212,190,170,152,129,167,209,177,104, +137,10,156,207,170,226,97,104,44,168,183,80,149,87,17,142,60,2,116,60,216,78,112,196,148,235,239,97,61,216, +97,90,125,165,248,184,227,17,178,208,10,113,51,17,90,8,41,113,175,215,229,190,201,190,134,100,88,34,115,132, +233,255,38,222,3,137,33,142,196,1,87,38,182,50,161,181,253,209,95,60,83,239,31,191,146,175,161,134,178,173, +20,208,80,48,239,163,54,227,59,128,232,0,130,209,3,30,167,84,8,237,88,106,212,156,237,127,22,42,27,35, +226,241,112,192,89,99,182,82,194,95,103,73,33,197,125,226,39,153,84,49,53,243,205,103,78,211,236,127,251,219, +79,63,90,49,43,157,63,19,162,233,31,211,106,192,223,61,131,185,153,60,251,178,166,234,175,168,62,51,181,79, +227,250,12,28,155,115,68,160,211,172,123,60,28,156,82,114,200,97,201,48,95,116,228,192,49,204,189,190,4,243, +44,78,170,80,169,170,74,24,143,136,50,160,100,86,190,179,182,132,154,217,248,158,210,43,154,216,254,179,40,15, +236,120,23,175,214,38,176,147,156,174,27,13,237,100,179,216,62,31,42,157,198,143,9,110,76,0,128,245,233,18, +123,109,156,119,193,135,247,26,209,13,75,23,70,248,246,246,109,103,108,171,160,214,154,93,186,76,170,8,160,105, +38,23,107,90,253,161,240,113,139,111,239,168,241,87,23,204,58,198,127,10,85,98,56,234,2,85,214,6,53,205, +142,40,46,92,127,200,50,85,249,200,225,163,78,231,146,2,109,247,72,153,48,232,140,70,221,32,132,84,117,209, +237,95,132,135,36,165,151,205,142,170,251,168,160,226,184,96,35,125,34,11,245,14,71,189,227,163,144,131,126,103, +43,46,75,232,95,252,119,84,143,16,173,116,10,224,87,180,141,20,75,82,206,11,36,127,159,57,157,33,67,192, +71,115,166,190,216,188,144,245,91,195,214,128,153,119,137,217,132,221,175,115,56,74,188,89,168,153,88,48,78,2, +237,115,142,155,228,144,253,247,63,94,175,211,235,120,159,39,89,90,120,63,83,45,119,130,46,195,61,255,207,133, +74,115,37,239,90,249,58,96,155,0,19,53,140,47,223,33,219,24,177,96,76,131,76,122,67,12,24,169,161,156, +155,148,148,98,104,82,226,114,4,20,108,21,62,35,97,209,211,194,73,99,55,195,172,147,152,205,112,236,157,73, +235,175,181,115,159,107,91,252,34,89,204,91,111,200,253,92,25,232,138,234,66,204,114,198,107,1,247,25,108,6, +40,59,201,217,134,79,115,227,192,103,15,100,177,94,102,133,183,92,53,251,128,162,234,151,62,45,226,163,208,205, +110,207,187,111,74,125,13,154,196,76,11,250,16,74,136,82,77,78,22,66,11,181,146,182,177,110,160,169,255,155, +196,71,171,181,39,117,47,218,127,110,104,31,219,205,69,156,216,9,119,111,158,54,168,196,53,143,71,214,235,251, +242,15,243,10,141,240,245,211,239,159,104,49,106,114,12,242,76,195,252,182,150,80,88,31,129,154,152,30,134,229, +16,241,16,141,169,149,176,62,83,223,115,196,71,49,68,144,248,89,44,155,35,157,88,198,205,225,123,149,197,197, +109,83,199,90,247,238,87,77,221,104,207,66,235,184,236,7,225,115,26,147,242,116,50,205,16,124,220,91,230,101, +49,59,162,151,237,157,104,56,181,83,0,228,83,141,214,247,116,200,30,154,54,199,106,238,140,133,35,109,250,60, +41,204,54,69,6,219,92,115,59,147,248,51,100,186,121,198,237,180,166,249,242,81,228,137,134,68,116,245,122,150, +171,187,204,164,94,90,151,140,84,159,188,77,228,93,18,99,253,252,203,183,205,111,33,105,79,171,12,64,184,250, +11,171,98,153,229,151,43,149,110,104,37,99,240,7,241,149,74,69,139,205,60,208,242,153,247,165,45,229,49,90, +187,252,67,49,189,248,255,199,244,34,41,232,214,52,89,190,15,174,29,236,123,97,219,1,127,40,190,93,57,96, +28,56,127,239,189,208,223,65,53,9,93,102,128,140,239,23,213,64,30,190,154,36,171,35,211,20,19,82,38,247, +171,144,65,246,224,248,209,186,249,147,213,134,238,200,6,17,21,26,229,255,216,187,14,230,104,109,32,250,87,20, +210,19,144,181,234,242,180,244,222,147,41,233,185,54,231,155,15,151,225,59,183,148,255,158,183,128,174,216,184,59, +61,3,39,172,135,244,180,160,93,173,88,138,191,231,229,226,192,241,180,27,108,174,27,55,248,0,30,103,240,16, +124,85,216,231,122,30,254,175,255,204,179,250,110,217,238,47,236,110,145,27,213,252,52,247,252,160,128,14,46,166, +211,26,58,149,207,194,233,98,186,220,131,231,213,71,103,69,185,215,62,168,149,115,39,139,217,233,27,135,103,187, +133,18,74,104,139,21,88,118,213,36,169,40,207,246,235,3,72,187,183,92,30,237,238,236,156,158,158,202,83,35, +15,155,249,142,86,74,237,180,242,20,93,153,51,152,201,147,98,168,40,165,148,118,186,221,191,117,199,49,95,59, +9,120,232,195,39,179,238,205,9,112,117,217,170,149,25,100,144,0,221,87,175,118,115,166,106,142,107,158,52,204, +78,102,136,137,77,51,39,219,226,154,22,87,235,197,152,159,225,41,74,62,188,162,60,111,211,254,92,104,187,62, +19,248,187,87,234,201,162,153,128,121,139,227,83,24,87,229,178,20,207,90,253,198,235,201,23,229,33,66,111,139, +37,115,74,131,105,11,154,32,141,237,121,183,109,176,81,153,245,8,211,195,53,39,40,63,38,47,131,143,201,218, +80,6,25,41,120,252,156,120,147,130,36,235,66,136,12,91,50,201,144,10,2,40,32,231,131,7,106,146,38,128, +64,163,164,168,13,69,3,180,39,51,160,136,210,69,175,144,47,99,38,227,178,94,133,24,147,6,216,113,185,22, +69,125,229,156,41,83,207,165,157,248,136,104,5,67,76,223,226,76,172,100,244,206,90,235,75,8,164,130,85,214, +36,1,84,59,175,172,107,81,8,150,130,21,73,70,155,180,138,190,101,8,46,161,136,23,31,121,105,86,176,89, +193,111,58,153,116,138,81,135,200,176,73,41,26,235,132,147,49,146,209,164,93,73,154,143,207,49,234,165,182,104, +218,88,6,59,54,48,64,74,165,200,59,135,178,212,179,69,17,248,48,44,80,3,52,179,1,245,14,82,146,9, +160,200,108,56,104,37,173,214,41,105,8,97,165,39,159,8,18,15,119,211,55,69,201,122,241,25,122,181,74,250, +162,98,160,207,243,64,199,207,236,94,52,122,118,34,98,127,92,161,222,45,141,24,186,221,143,166,24,150,68,63, +217,89,57,176,113,83,108,111,143,151,203,195,75,173,142,151,7,2,191,234,168,89,236,143,154,243,110,124,237,70, +190,26,79,108,236,46,187,7,119,126,187,52,198,100,2,12,106,180,26,84,111,30,17,178,149,145,223,28,113,200, +15,143,56,3,99,66,23,45,134,247,99,51,255,104,116,142,200,184,190,169,216,168,65,12,189,26,35,34,62,60, +32,12,91,126,185,122,174,14,78,247,112,201,143,208,37,37,72,99,125,121,211,202,135,141,185,235,251,254,187,228, +111,30,214,124,163,183,53,240,36,136,222,99,115,174,141,244,166,178,210,248,17,9,18,138,127,21,73,103,145,232, +88,87,78,96,135,228,242,156,40,94,42,169,146,36,55,81,66,42,39,56,141,128,64,102,94,239,40,120,177,16, +112,180,206,242,126,227,55,171,116,53,46,114,139,142,187,70,179,27,100,132,53,109,178,121,91,73,109,196,6,66, +6,2,91,250,8,164,104,218,188,183,85,94,85,26,174,52,187,204,236,253,223,64,87,136,98,216,143,182,247,240, +254,171,94,244,127,47,250,214,59,54,184,116,7,47,58,36,215,23,64,48,111,195,164,47,93,34,102,38,106,165, +13,107,105,215,194,70,144,115,145,91,145,87,111,34,40,113,77,11,228,135,154,208,185,137,27,61,194,189,156,65, +241,42,7,20,94,90,95,37,35,204,56,236,21,134,237,38,79,177,111,244,25,53,31,78,119,153,249,167,120,140, +255,61,198,255,30,227,46,83,153,250,162,98,226,162,184,57,135,85,252,179,117,19,215,187,136,16,236,221,65,49, +239,175,123,90,73,35,200,72,107,214,61,196,202,230,164,119,175,183,221,199,73,139,99,191,150,20,5,37,198,248, +215,195,142,181,107,27,194,95,177,10,216,104,223,37,29,108,193,42,72,250,80,87,160,10,149,52,126,171,89,114, +50,110,41,18,114,140,214,86,106,43,195,30,234,108,42,158,177,80,117,47,141,193,154,49,106,161,16,177,102,72, +87,146,168,150,42,241,118,194,22,227,144,233,12,199,0,115,79,129,17,80,164,118,68,160,21,156,172,141,5,120, +172,101,112,149,221,144,22,172,6,38,41,117,64,105,235,94,79,82,147,64,146,165,235,78,150,1,196,112,70,13, +240,209,54,148,184,92,66,125,210,130,147,30,6,17,89,233,227,70,147,50,180,93,51,96,61,95,176,226,179,249, +228,229,126,113,217,103,86,129,84,190,219,118,124,116,251,32,49,95,215,220,109,230,182,81,49,207,225,66,158,195, +217,110,226,214,176,126,223,138,112,60,106,58,58,252,1,166,107,11,86,11,14,74,241,171,24,203,197,17,187,95, +56,222,108,87,75,126,134,110,206,241,185,87,139,231,193,178,101,158,185,202,197,82,191,93,29,221,226,51,82,100, +207,221,123,237,92,247,101,174,204,125,181,14,166,131,231,78,241,120,180,92,210,3,67,240,57,74,126,187,123,13, +215,137,55,48,219,30,152,245,116,129,87,209,166,85,195,243,203,217,20,115,140,203,231,9,239,203,44,38,51,254, +0,1,223,230,221,80,246,10,138,94,222,129,188,203,173,189,198,181,77,237,141,154,41,71,87,209,96,94,74,66, +152,20,31,105,122,248,29,159,155,186,246,242,9,92,236,207,47,143,253,81,177,255,25,213,24,251,209,228,124,116, +176,252,244,171,215,161,201,205,100,183,213,212,93,28,233,124,198,14,236,213,179,253,58,223,194,254,157,72,42,88, +82,22,7,131,15,180,151,8,176,53,28,145,48,16,39,9,170,40,192,77,19,23,9,160,83,35,16,146,167,223, +143,250,167,234,63,1,221,221,164,155,102,143,3,239,86,232,70,14,23,35,156,126,190,41,212,178,147,167,73,187, +237,111,3,159,235,164,159,106,139,90,89,30,7,154,55,237,222,122,95,251,148,59,181,217,130,111,153,4,112,215, +244,136,4,126,205,212,149,174,52,190,203,140,63,139,65,204,76,133,154,69,129,149,131,104,73,250,24,111,137,111, +179,231,227,125,45,252,159,253,105,247,146,233,81,103,237,199,44,193,71,159,194,210,33,48,181,249,88,178,188,243, +169,251,71,7,57,156,186,220,217,107,17,76,251,19,89,168,138,91,146,244,29,248,89,85,246,189,232,63,22,208, +120,192,105,145,54,112,78,23,16,181,213,247,136,64,167,190,151,104,55,223,49,116,138,66,77,48,217,240,40,244, +152,33,141,112,143,254,45,57,7,100,184,76,85,177,123,215,167,77,91,21,252,167,118,119,179,44,252,46,27,252, +94,154,213,19,54,76,157,17,193,141,205,12,249,39,82,139,149,105,15,250,45,186,151,219,30,114,160,186,124,32, +234,112,43,86,190,233,38,225,30,244,221,169,191,111,137,254,151,216,221,39,179,161,229,120,12,126,185,229,47,119, +240,178,248,146,28,21,112,74,207,191,153,214,51,158,191,29,255,187,99,166,75,135,184,28,19,196,149,24,153,146, +125,117,218,60,234,156,195,30,82,213,67,60,86,78,172,235,60,214,85,193,52,79,206,46,124,15,36,134,207,73, +56,53,236,128,160,227,230,33,211,221,247,61,106,12,85,228,205,34,173,41,38,19,107,245,194,48,155,50,220,184, +92,29,22,174,42,143,217,216,231,6,112,11,188,61,111,96,11,7,52,14,207,137,207,243,243,55,81,222,87,148, +47,186,42,143,47,146,240,119,85,114,11,57,173,196,33,236,114,8,72,135,70,222,122,11,205,15,27,170,142,14, +205,63,9,205,99,43,85,55,49,167,113,168,122,172,216,139,230,231,41,75,171,145,25,111,185,226,243,196,158,112, +95,52,35,63,121,70,182,200,82,181,250,170,137,149,175,38,63,173,207,213,200,177,4,95,12,88,229,195,123,94, +240,159,46,151,22,33,238,130,102,197,13,178,89,20,54,36,97,35,112,136,226,112,100,173,103,193,179,94,55,52, +135,78,67,3,185,122,200,16,78,60,97,26,176,255,249,172,154,246,70,113,38,248,151,100,32,239,51,215,196,216, +25,50,145,120,25,32,30,124,75,240,60,172,241,100,114,240,238,98,233,215,111,85,73,222,36,151,61,65,235,163, +63,170,171,187,133,179,60,99,255,114,178,219,211,167,108,244,250,63,99,47,211,94,187,64,222,119,188,87,175,77, +0,70,224,34,236,32,114,235,37,27,156,203,31,231,211,217,173,105,203,194,199,135,10,186,66,5,159,154,192,179, +19,245,32,7,250,63,39,187,197,207,227,157,195,191,7,102,176,225,102,174,195,247,179,61,98,173,132,61,221,105, +32,3,171,181,49,136,189,148,31,173,49,88,51,252,183,252,247,188,7,28,142,241,95,122,129,139,195,30,124,242, +135,25,123,109,194,71,62,42,150,24,135,236,87,56,75,249,97,198,63,120,66,12,154,119,12,214,194,96,6,127, +188,236,120,202,195,217,122,98,141,253,87,123,73,252,242,194,87,126,247,194,247,48,111,121,15,251,13,109,231,145, +127,134,88,40,78,98,52,120,218,222,224,62,191,19,207,153,148,159,75,91,130,47,224,119,242,71,246,165,31,118, +225,135,31,200,215,22,188,150,156,244,120,225,75,190,144,147,69,91,110,176,254,244,191,106,99,110,176,175,124,216, +157,98,207,196,21,113,68,248,201,111,200,153,252,10,183,137,79,194,220,43,230,196,167,24,115,175,152,223,125,195, +186,228,125,153,240,15,117,171,220,44,184,43,187,240,245,221,135,31,226,111,206,60,184,178,65,157,88,216,70,44, +17,243,139,238,92,49,254,77,123,150,177,101,146,149,147,97,170,214,119,179,108,139,55,102,145,79,49,30,214,22, +238,108,223,132,67,6,92,127,72,134,111,168,73,47,174,20,240,173,128,255,209,62,184,237,132,93,99,192,71,226, +69,217,144,171,49,71,3,226,21,70,31,121,3,249,48,199,220,53,216,47,110,180,238,151,43,150,121,196,18,120, +48,182,136,145,248,66,14,75,54,246,2,156,10,213,14,214,225,95,172,147,110,196,89,96,248,149,56,85,208,55, +202,79,196,149,63,151,91,112,243,187,236,10,23,249,177,81,173,168,222,119,239,156,84,254,244,173,200,9,19,239, +54,177,38,62,247,2,250,11,63,220,12,252,196,43,124,121,119,137,177,40,151,180,159,184,89,49,118,226,20,84, +179,234,3,15,153,250,215,110,130,253,125,166,90,219,77,226,218,65,252,122,186,246,178,127,255,221,215,1,250,127, +177,175,153,195,140,187,253,36,125,63,143,183,204,175,179,33,230,203,33,175,131,167,14,96,163,126,48,166,120,27, +226,205,254,160,26,85,158,90,228,161,188,174,143,236,77,249,24,40,11,39,201,186,103,44,122,132,250,132,193,121, +238,1,227,129,53,38,238,226,76,150,106,71,50,185,128,51,226,157,112,246,137,175,226,135,248,154,75,206,132,43, +49,36,47,180,46,222,170,191,30,66,148,197,137,160,56,32,55,161,9,226,0,253,213,30,251,58,226,43,185,102, +63,212,76,15,157,167,232,231,61,102,135,47,86,7,242,220,179,215,77,156,55,23,96,93,224,14,253,212,186,230, +5,248,198,222,232,58,198,23,229,103,241,199,157,233,115,173,218,116,204,131,127,97,158,35,183,223,128,77,22,229, +69,50,247,187,178,207,200,9,249,32,126,246,6,88,45,200,129,176,23,182,59,198,30,231,2,250,24,108,87,127, +255,127,122,251,242,152,141,31,103,253,105,8,67,81,223,55,151,253,113,245,135,45,135,236,113,87,229,251,110,252, +179,190,223,30,89,119,182,123,122,29,160,123,223,157,140,43,123,188,117,190,251,97,119,19,246,120,55,189,116,230, +248,178,219,98,238,255,42,190,129,231,118,174,86,152,217,139,235,70,131,249,238,235,110,196,253,77,156,253,255,181, +79,110,160,198,28,114,80,119,19,223,4,89,205,24,209,59,129,239,130,90,204,81,123,55,110,30,113,198,174,190, +181,122,243,124,25,239,183,192,240,110,126,185,223,6,190,9,241,206,225,187,140,113,113,6,1,7,107,112,63,27, +66,143,220,211,135,145,245,4,236,6,225,2,253,196,12,92,26,249,46,41,196,149,174,81,143,112,177,215,198,190, +51,55,168,35,213,231,37,246,16,35,29,113,14,158,16,95,156,111,206,179,14,39,238,95,196,233,110,163,60,217, +89,124,242,88,187,1,166,70,185,67,13,167,185,121,137,181,61,104,134,160,135,21,207,229,38,104,30,202,47,227, +107,172,193,23,245,110,23,251,222,42,174,245,88,219,206,248,166,26,168,224,63,108,93,103,25,239,183,250,191,214, +76,145,234,119,5,251,140,127,37,46,207,39,214,130,177,175,136,209,179,118,105,87,49,228,105,254,200,47,245,150, +76,253,210,19,91,234,114,29,238,82,223,154,247,78,208,63,138,151,208,21,156,230,215,200,122,206,56,183,97,35, +135,30,143,53,214,37,125,194,60,184,13,194,61,52,185,48,125,207,155,242,15,219,193,81,199,111,189,201,10,235, +101,123,161,15,192,18,254,143,140,39,103,188,200,89,38,238,39,127,135,160,56,152,215,115,204,171,81,94,145,67, +245,139,148,87,245,157,56,15,6,229,137,231,81,143,75,156,29,145,27,92,251,196,141,242,19,55,152,211,179,114, +170,89,190,40,167,194,203,47,242,165,37,70,59,246,232,10,119,38,205,132,90,28,232,189,238,118,90,91,113,45, +241,149,249,36,30,178,169,188,41,159,250,87,62,165,71,181,213,175,196,51,190,111,129,53,116,16,7,63,4,203, +220,69,155,234,211,102,97,79,248,135,182,243,254,110,84,199,226,248,191,226,176,13,189,104,60,96,199,41,56,138, +95,239,213,147,173,57,57,57,128,197,68,27,98,188,148,105,30,255,239,123,239,85,177,72,152,243,234,252,224,34, +33,9,113,213,48,214,247,115,193,230,102,28,208,90,163,215,61,56,39,244,21,60,127,68,229,65,95,130,177,136, +245,176,115,191,110,207,75,28,67,209,196,220,231,76,232,254,225,205,71,104,243,41,204,61,83,184,78,106,163,222, +124,67,191,51,254,179,134,176,208,15,41,118,252,253,253,132,117,127,196,197,209,47,122,98,99,158,62,140,204,231, +147,242,185,121,198,128,245,188,198,205,154,69,70,219,145,73,177,55,43,244,183,213,52,59,209,223,206,112,3,167, +183,133,62,223,99,54,201,47,255,182,173,95,27,161,78,11,220,196,48,101,40,107,215,44,82,227,78,181,100,91, +95,196,87,67,196,174,25,163,144,117,33,195,156,37,176,135,234,94,53,168,68,104,170,242,5,70,141,219,91,185, +14,225,20,30,14,166,28,6,74,244,144,129,78,212,104,236,72,58,49,83,188,147,241,81,193,142,225,108,56,13, +146,215,222,69,18,121,38,15,21,93,101,138,62,2,145,188,16,112,31,246,222,60,74,67,210,52,74,148,26,1, +58,233,2,61,142,165,209,88,63,116,75,202,156,84,26,119,199,30,173,166,218,244,50,138,83,211,28,147,211,76, +127,155,204,138,136,190,145,164,218,28,93,101,145,223,68,133,35,29,105,125,226,216,40,222,201,7,179,21,202,162, +32,255,170,162,192,232,35,107,182,107,36,215,233,20,30,170,165,173,180,220,209,236,120,102,108,235,41,22,65,221, +90,161,40,77,153,109,202,232,99,215,169,159,82,142,189,226,65,45,80,62,5,142,38,69,152,138,134,136,22,33, +99,212,27,216,28,139,99,170,176,62,177,140,235,100,198,201,105,130,17,31,9,33,89,86,203,244,206,244,174,156, +109,43,172,133,18,249,174,64,180,107,249,154,250,228,218,110,115,109,174,2,109,95,20,252,123,129,144,217,244,104, +42,171,44,84,187,189,206,209,195,113,66,79,115,202,67,95,13,255,213,26,170,8,215,157,182,173,188,223,180,168, +16,92,73,173,93,237,106,73,186,67,213,106,93,243,72,25,152,97,224,245,162,77,111,215,119,71,237,82,24,135, +198,112,82,77,231,180,72,77,240,85,19,112,122,240,244,72,105,185,53,227,27,213,198,221,198,5,98,110,159,130, +38,17,183,127,99,106,151,77,251,109,189,152,12,27,218,219,71,202,113,63,159,78,183,127,102,71,255,218,82,156, +125,120,71,161,221,206,131,110,97,190,239,62,157,13,83,61,127,102,88,238,117,57,92,9,220,215,232,245,193,166, +84,200,134,124,16,123,95,189,121,54,120,64,59,151,163,99,60,21,147,232,236,36,158,77,120,163,101,4,185,84, +37,106,125,212,27,249,148,166,43,172,142,28,63,219,164,245,29,212,125,76,195,242,227,174,40,100,205,75,58,136, +165,47,37,170,75,121,135,242,192,113,181,70,19,63,100,39,228,70,37,178,71,113,131,229,187,178,101,188,59,20, +49,239,206,27,86,133,158,122,72,138,124,140,201,113,207,60,201,8,176,96,137,77,212,43,25,223,80,23,252,104, +214,235,62,72,145,57,112,152,94,8,125,57,11,253,145,64,20,84,18,1,63,31,53,222,213,132,78,50,143,211, +29,150,25,106,229,178,219,147,249,174,118,195,183,253,255,20,150,159,85,107,227,84,245,253,167,198,129,172,195,70, +24,241,195,102,111,10,218,224,202,99,70,101,81,136,136,38,212,195,43,42,232,95,223,125,251,101,219,110,150,218, +171,223,188,26,0,180,96,5,250,66,227,73,132,34,206,138,252,204,118,205,66,122,219,213,161,154,201,44,138,14, +250,135,205,32,16,193,213,151,151,151,63,142,62,91,46,127,88,94,143,2,235,188,194,36,37,177,16,75,188,244, +21,54,38,72,144,26,137,135,184,244,205,17,67,208,14,52,17,33,209,173,50,135,7,174,193,38,124,0,161,112, +37,152,94,218,214,112,48,212,127,31,200,213,83,148,224,182,105,249,65,28,69,76,159,224,165,106,111,63,169,229, +10,10,87,105,137,87,203,141,105,105,8,134,46,96,150,211,222,217,106,219,85,3,72,138,107,22,119,49,210,236, +159,166,188,92,135,240,124,21,28,13,201,223,69,192,131,167,26,169,24,224,129,6,115,213,88,171,177,158,146,253, +246,241,45,110,64,99,35,137,193,17,73,146,70,218,76,124,68,59,168,71,80,209,145,70,22,144,18,9,230,210, +54,85,101,51,14,126,198,222,59,110,255,146,246,167,70,178,128,59,61,205,92,20,229,119,145,88,71,185,17,96, +24,13,6,244,243,107,11,195,169,16,185,102,239,98,77,206,11,137,6,178,230,123,170,168,245,218,215,129,105,163, +71,67,188,12,107,9,2,108,189,114,46,161,100,117,143,222,233,234,59,103,100,148,106,238,37,145,3,169,95,214, +125,103,118,86,12,22,179,230,165,194,218,72,34,96,10,73,139,49,219,230,105,35,71,81,130,44,152,122,92,221, +177,173,62,32,102,115,189,236,154,51,153,216,41,175,199,40,224,11,217,156,50,78,19,255,111,30,33,177,119,32, +179,114,223,118,58,221,44,193,143,0,253,134,36,174,196,166,173,54,40,7,101,156,124,246,58,9,239,128,212,75, +154,27,55,180,42,119,247,125,164,247,100,216,10,226,118,142,242,58,226,81,204,61,241,112,239,238,149,55,6,182, +202,75,49,228,59,173,93,56,119,21,73,59,55,110,139,203,177,86,252,11,201,75,227,222,122,73,17,159,175,27, +81,67,220,13,82,241,148,246,163,197,248,154,178,216,194,211,146,34,21,69,222,32,92,68,126,229,148,251,192,140, +86,140,167,139,48,239,205,255,173,104,125,104,45,212,193,11,160,104,243,19,242,127,98,18,25,109,47,38,114,1, +147,118,32,206,207,207,161,132,61,47,192,51,195,205,205,63,254,254,217,205,179,103,203,155,79,126,248,254,242,179, +127,93,222,220,32,22,85,12,196,147,76,180,242,136,122,168,174,38,42,38,244,76,213,208,143,21,39,229,108,222, +121,128,60,85,35,15,14,141,212,212,75,147,68,228,44,65,25,106,46,154,133,55,84,252,243,105,211,212,85,181, +103,196,3,142,58,93,85,47,151,85,133,120,152,138,241,156,169,162,215,38,108,11,231,81,107,188,239,165,253,97, +141,200,105,129,235,132,109,236,185,253,50,44,215,203,117,13,36,227,157,145,4,186,69,190,16,165,113,79,246,9, +253,242,155,247,131,162,88,92,93,219,197,167,224,112,129,87,249,181,25,99,91,35,17,76,90,110,123,97,169,181, +210,25,168,188,239,177,255,34,30,153,230,34,247,198,248,115,145,89,113,225,92,17,230,208,176,30,113,206,92,117, +84,251,75,181,193,185,111,8,79,245,58,52,153,220,237,20,15,236,189,81,96,65,68,213,56,221,49,142,171,182, +78,28,110,181,121,134,189,236,62,199,49,109,144,143,97,240,39,24,185,129,214,20,202,159,165,139,25,217,228,228, +40,206,29,1,234,40,62,37,105,160,131,207,244,9,100,75,89,144,167,32,30,192,98,243,141,124,77,14,185,31, +223,216,32,73,43,133,32,138,227,213,184,112,63,189,23,210,54,71,141,71,19,137,58,85,156,72,126,41,192,42, +154,205,142,60,61,35,222,200,80,9,199,239,38,53,13,246,170,173,116,76,164,228,32,140,255,218,50,222,7,11, +133,19,140,123,89,171,214,132,143,48,172,27,72,146,18,249,56,90,165,63,83,237,65,194,153,1,60,169,61,213, +140,167,158,233,27,138,143,229,137,165,93,158,100,81,102,104,151,154,29,133,50,100,211,185,210,166,129,29,150,188, +16,131,157,205,71,241,128,150,184,234,221,60,104,201,58,122,225,235,194,109,150,196,59,222,133,5,226,221,161,203, +241,158,121,34,142,0,23,47,171,198,250,242,32,115,93,118,202,253,12,49,1,131,57,148,58,193,27,151,204,201, +160,201,32,45,98,91,140,179,9,3,249,65,203,4,105,150,67,245,159,223,170,255,222,149,247,235,106,243,191,186, +1,19,58,28,65,125,45,78,120,101,177,125,1,27,6,255,74,20,245,34,163,157,159,28,136,14,47,167,101,144, +245,237,91,21,82,72,50,75,48,8,216,129,168,16,134,53,36,89,206,201,25,241,35,144,19,24,62,6,99,167, +227,130,175,68,51,46,12,59,174,186,232,116,198,13,191,21,101,232,114,145,16,156,33,148,114,161,194,91,230,102, +34,248,206,18,140,225,47,196,189,45,58,115,40,186,23,23,25,219,136,251,171,12,101,228,136,117,94,233,25,240, +150,111,8,224,124,181,185,22,183,240,230,81,131,146,110,199,145,237,246,243,106,111,252,30,175,78,35,236,129,63, +35,134,55,217,105,45,183,60,116,116,174,199,72,27,95,99,151,116,252,17,59,87,159,71,111,223,230,23,162,92, +180,139,32,48,20,182,36,84,162,33,230,220,39,213,74,126,212,34,51,93,157,207,102,147,179,99,104,151,139,217, +241,52,62,131,92,135,136,206,42,225,2,211,126,106,56,192,216,57,164,154,64,217,233,197,236,100,122,52,133,226, +117,26,42,45,81,9,134,181,35,224,156,231,135,19,150,164,79,40,199,97,168,158,208,153,206,207,227,136,29,162, +158,230,152,245,101,244,57,157,38,81,40,155,231,84,38,5,34,70,131,124,86,68,63,71,252,51,86,149,167,217, +236,119,240,185,246,238,11,91,70,110,56,90,31,213,197,7,128,160,128,9,173,15,132,230,166,25,127,195,110,89, +97,11,108,33,42,167,6,226,20,55,170,228,154,98,204,3,52,150,184,175,184,218,236,221,225,215,11,42,51,169, +89,136,65,185,136,18,169,173,113,150,14,88,35,116,17,189,105,151,92,56,248,163,115,96,114,148,226,10,247,227, +7,49,188,38,240,154,194,235,8,94,51,120,29,195,235,4,94,167,240,58,131,87,10,175,12,94,57,188,86,240, +34,33,64,112,189,191,1,173,123,30,89,145,228,181,134,143,24,249,71,45,146,188,254,207,216,125,168,54,143,4, +1,0,126,27,97,35,12,211,11,103,231,69,14,153,26,64,20,11,68,7,225,103,191,217,40,46,119,127,46,44, +77,236,72,195,248,115,102,54,40,192,102,174,11,79,127,173,227,165,118,190,101,187,223,134,249,56,126,214,115,39, +11,8,183,100,219,160,54,165,195,122,62,251,182,126,124,124,144,30,199,91,197,230,177,130,195,109,187,175,195,210, +82,112,58,113,164,154,104,88,187,125,57,204,213,95,180,205,45,167,218,108,173,224,50,86,116,88,183,251,60,220, +90,14,77,163,129,33,104,96,182,219,151,195,82,57,190,45,149,131,85,103,174,224,109,172,232,48,111,247,101,88, +91,14,79,39,4,17,37,101,134,118,191,62,249,249,76,180,221,90,82,21,90,42,248,95,143,84,146,155,96,68, +122,167,71,167,17,9,0,2,132,186,65,86,117,196,153,24,89,176,87,228,211,73,212,65,51,184,215,19,211,136, +238,0,172,130,214,9,202,250,108,169,33,40,130,222,11,194,234,4,33,48,238,197,32,182,50,9,2,130,70,189, +30,164,2,5,136,1,91,80,39,8,171,21,4,88,16,1,187,61,173,19,20,8,128,40,161,27,245,213,12,108, +202,202,148,191,162,42,107,25,238,59,170,74,153,122,154,34,60,73,186,147,252,157,180,108,183,225,190,60,250,7, +44,21,208,152,158,166,220,73,252,78,186,109,235,112,223,73,88,83,36,236,232,142,252,18,201,46,138,119,209,186, +205,195,125,253,30,112,118,142,116,6,122,121,96,247,208,207,30,157,78,14,168,26,150,216,233,65,152,70,142,210, +64,112,47,167,202,152,129,120,48,107,47,71,90,39,168,178,135,68,47,39,167,81,45,68,76,56,122,57,173,123, +0,51,128,45,161,19,196,149,19,206,198,105,216,235,105,243,141,198,202,168,128,189,32,108,149,164,64,129,98,222, +73,162,233,164,40,192,30,210,233,241,182,247,176,50,133,56,247,130,176,234,96,146,25,184,179,252,42,186,46,215, +71,187,177,135,106,60,37,178,75,226,77,114,173,199,247,47,236,68,64,164,94,124,126,66,16,119,9,190,73,174, +149,243,24,27,12,78,96,80,163,151,195,118,135,189,57,174,149,244,248,241,23,28,82,213,94,4,254,34,228,15, +2,44,115,123,60,9,12,122,20,210,182,25,167,72,102,237,83,120,43,161,146,110,220,105,128,202,128,20,103,48, +129,46,5,79,163,5,146,39,186,244,40,170,2,107,40,59,17,117,33,170,111,157,72,9,211,179,11,97,211,232, +6,148,24,217,35,200,233,84,86,54,145,240,30,64,107,86,33,12,212,96,237,18,160,78,163,50,184,148,2,186, +8,85,34,83,153,195,20,127,49,44,215,195,109,171,241,254,254,94,49,195,24,66,228,201,176,157,97,79,70,177, +15,235,86,195,253,152,85,36,139,68,193,55,9,236,18,122,74,202,126,152,183,154,239,231,46,39,104,172,146,160, +47,141,238,26,127,106,202,127,88,182,26,240,239,113,85,23,22,80,125,121,112,31,111,252,17,180,255,214,119,0, +9,85,199,62,210,190,157,166,100,152,129,245,146,246,174,87,84,226,94,15,182,29,69,37,137,60,179,87,20,5, +10,103,70,102,205,78,16,182,141,14,12,209,69,122,61,86,28,53,192,12,14,232,22,241,52,34,67,162,162,73, +55,233,171,25,84,137,193,161,87,132,149,131,4,132,192,158,189,166,106,6,199,240,112,210,236,37,213,100,179,176, +132,170,226,255,129,218,91,207,101,29,219,229,107,133,211,229,86,43,220,87,52,93,150,90,209,190,226,233,50,143, +237,178,189,255,15,228,183,35,58,31,7,142,238,231,172,154,212,9,171,23,57,174,127,127,150,171,254,200,242,254, +142,254,121,28,15,255,14,140,120,60,159,227,207,48,85,24,237,207,56,87,156,228,31,238,174,132,187,109,92,87, +255,149,68,243,38,79,106,96,215,217,186,200,85,125,50,157,173,231,100,233,105,50,111,115,125,122,100,153,178,217, +200,148,159,22,167,185,142,254,251,5,72,137,166,21,58,147,206,221,239,76,91,75,20,23,16,4,64,136,2,249, +121,122,165,208,128,81,126,58,73,67,54,194,138,135,216,248,72,53,78,215,135,35,217,162,186,57,26,109,107,134, +235,102,154,239,23,16,17,120,227,122,233,103,136,174,192,33,189,84,188,62,130,206,33,250,160,104,209,94,190,134, +142,78,62,134,38,245,85,67,38,145,248,38,72,136,206,23,72,168,91,130,160,207,227,229,56,87,235,41,12,173, +53,45,74,121,50,127,17,180,159,121,144,174,73,224,193,176,7,143,254,63,2,197,153,148,26,60,240,184,98,204, +125,107,176,144,213,46,251,254,248,205,155,35,5,68,217,228,58,56,124,181,126,2,236,237,201,137,71,84,17,213, +220,83,53,35,39,117,213,35,76,168,249,24,6,175,158,37,16,6,161,14,104,113,145,229,13,50,134,219,125,54, +240,220,238,170,7,175,42,239,63,158,211,26,231,34,204,114,246,94,20,110,72,111,230,152,23,34,51,237,64,166, +221,147,58,145,137,14,114,250,61,33,132,189,154,152,114,61,112,225,122,224,196,223,114,224,82,53,112,161,12,44, +106,143,27,221,191,73,6,173,28,158,79,171,248,191,113,81,188,58,149,137,180,210,242,71,198,83,180,199,19,185, +255,111,53,136,121,51,136,25,16,24,158,228,106,70,164,189,57,238,103,68,42,90,67,54,44,222,190,125,245,44, +219,63,222,35,31,164,185,165,27,29,252,178,174,49,210,98,97,88,140,134,245,170,251,5,117,63,39,187,96,224, +124,232,229,97,107,120,220,243,225,167,146,214,73,58,159,202,159,241,191,145,58,214,91,45,217,21,193,99,71,171, +22,132,2,99,2,47,27,171,85,166,80,168,120,13,51,178,68,30,219,216,18,164,212,51,88,36,20,139,248,48, +107,153,229,76,119,139,13,184,159,26,120,203,198,138,111,29,128,22,103,233,252,157,42,218,124,255,149,223,189,90, +13,23,158,137,164,188,185,32,218,206,218,29,223,21,236,76,118,107,159,25,55,154,42,65,167,225,186,15,91,0, +235,3,134,252,51,107,129,108,32,124,209,29,75,46,25,97,95,155,243,133,193,91,67,16,68,231,128,100,224,208, +171,195,225,26,177,213,198,217,101,112,232,145,252,106,98,183,243,73,61,1,19,153,127,214,68,53,100,140,122,210, +44,213,59,39,19,92,147,234,29,134,227,232,120,140,27,206,95,140,49,122,234,245,228,53,45,235,188,140,240,76, +41,58,73,62,114,57,157,81,159,36,169,227,161,92,217,190,79,184,180,144,125,178,87,120,251,245,21,35,195,226, +22,202,1,119,89,253,155,201,95,205,109,233,165,171,252,89,5,214,207,209,134,220,221,223,27,55,15,195,197,140, +239,65,186,215,172,5,53,215,187,47,72,253,139,55,189,129,58,100,54,252,234,22,251,12,122,158,175,238,185,234, +85,181,189,37,43,180,109,99,230,3,98,177,33,18,144,4,12,179,69,244,133,44,210,221,166,160,41,84,207,18, +159,9,124,230,1,30,193,59,104,43,25,146,228,166,65,217,73,128,183,21,80,158,55,221,22,110,78,6,176,149, +70,212,64,2,148,93,202,111,238,1,151,81,19,48,51,122,165,66,129,108,223,94,168,124,253,248,7,46,194,236, +206,77,164,54,80,122,101,169,67,101,218,168,73,97,81,146,74,236,7,5,168,187,68,105,224,90,13,106,47,39, +88,103,110,158,152,115,95,170,231,62,13,112,57,3,225,26,133,108,110,140,217,21,153,43,216,90,192,214,177,54, +103,244,105,241,70,53,100,34,191,97,10,149,157,178,187,69,98,139,91,212,234,6,42,9,71,27,193,105,100,179, +32,50,248,129,93,144,95,84,22,116,62,53,152,10,15,217,102,199,100,178,25,102,248,144,81,142,179,49,96,65, +15,214,13,125,155,15,99,97,236,148,21,87,116,216,203,67,18,86,212,184,111,176,87,53,239,155,180,0,209,224, +175,201,81,170,137,102,109,179,145,252,65,35,133,173,163,133,180,217,27,157,213,194,105,244,25,211,232,199,210,25, +25,6,156,26,146,175,177,254,12,81,53,83,100,187,70,138,110,119,163,222,102,164,45,159,231,26,84,74,38,79, +254,30,134,166,175,21,214,190,86,184,246,181,12,50,10,124,128,194,23,146,211,21,42,175,35,36,167,75,160,183, +181,193,98,17,136,167,57,94,233,218,201,18,141,227,197,141,52,195,241,42,164,227,149,66,33,29,47,14,45,202, +168,247,116,105,15,188,81,207,12,75,36,161,22,205,84,43,48,18,167,156,34,136,54,188,143,133,43,60,95,80, +105,195,176,154,227,103,234,78,43,219,118,219,105,120,254,193,196,180,76,202,39,128,130,176,27,200,160,155,246,173, +109,20,141,233,163,49,125,220,102,250,82,87,88,252,127,139,193,147,239,2,124,208,154,28,26,71,69,169,142,50, +128,182,247,3,170,200,194,128,127,140,125,68,207,248,175,111,20,205,158,125,147,129,180,48,235,175,106,47,237,164, +217,109,167,138,100,177,89,216,117,192,156,241,82,33,233,143,93,117,129,116,219,27,123,220,134,234,122,202,186,30, +146,110,171,253,221,136,17,180,55,101,154,81,139,105,181,150,49,77,164,197,108,182,202,208,48,88,13,68,104,245, +248,183,25,12,5,255,69,40,133,191,139,23,252,50,126,29,82,4,149,1,15,26,25,81,26,9,93,163,171,125, +216,224,5,19,98,65,131,23,28,191,124,169,241,130,39,61,153,103,17,228,8,85,29,222,32,190,6,204,108,195, +65,150,110,32,129,148,137,34,66,15,128,249,163,49,53,58,214,131,2,128,35,87,225,177,7,50,166,70,134,130, +219,81,33,223,11,10,122,13,11,78,27,93,50,22,49,190,100,25,208,17,122,251,14,222,255,127,201,105,151,154, +177,166,85,81,253,161,106,107,41,181,102,1,227,96,73,18,10,83,252,197,145,129,59,252,69,113,233,11,187,237, +191,83,34,180,4,74,4,86,65,106,235,216,88,231,242,8,154,170,178,135,20,77,117,46,51,118,243,54,32,0, +90,146,86,36,125,50,188,37,108,62,216,66,77,130,119,183,143,17,82,82,6,111,80,96,53,190,157,14,157,169, +170,90,192,179,66,134,183,165,228,226,248,28,152,64,115,24,49,127,6,26,229,82,65,210,190,96,108,252,187,152, +129,144,26,50,198,13,25,147,242,25,177,99,214,200,39,158,102,76,233,145,1,193,153,4,145,28,162,18,127,107, +50,32,110,2,175,212,143,215,196,191,169,219,237,96,23,144,233,248,107,132,225,35,172,189,221,188,91,138,60,140, +25,36,235,148,117,212,30,76,214,169,34,189,150,251,74,126,65,249,176,192,49,83,16,145,163,252,248,117,48,18, +163,16,187,12,28,121,254,27,74,67,170,111,64,70,238,102,94,13,100,19,196,114,133,69,215,160,171,37,4,14, +199,161,25,3,213,64,12,220,104,176,75,128,57,18,217,199,149,161,221,126,227,192,97,26,36,3,5,81,228,167, +205,158,27,95,39,133,46,37,84,158,219,160,70,155,241,182,141,143,101,3,213,179,244,22,173,40,178,69,89,211, +166,15,4,183,44,239,165,113,162,192,205,86,240,41,235,198,193,99,216,135,21,96,184,231,139,199,2,116,9,89, +148,228,201,144,33,41,79,236,228,72,7,232,42,52,205,110,108,11,44,211,161,184,162,171,218,68,83,211,92,6, +104,170,250,169,203,164,218,134,244,219,196,113,114,66,192,197,94,85,216,43,252,94,188,53,164,118,232,24,177,105, +14,56,155,248,46,152,192,243,15,13,199,47,99,188,183,129,145,98,50,65,223,161,113,96,205,136,24,131,227,16, +69,84,120,68,204,26,247,182,42,159,14,104,124,12,191,70,111,206,164,36,234,30,193,146,110,159,81,12,236,77, +226,58,1,24,107,174,19,99,72,139,141,153,34,50,208,231,19,3,125,190,52,102,150,56,112,222,58,132,36,248, +70,98,7,174,209,124,97,22,52,64,220,48,15,74,180,247,63,125,254,240,241,242,250,18,11,45,55,225,8,199, +182,142,77,246,103,251,49,206,5,19,4,109,119,232,186,130,233,70,190,162,75,97,204,204,29,187,74,189,186,81, +146,18,212,96,29,129,87,212,155,22,254,155,11,140,228,239,42,62,105,207,69,237,198,97,21,78,24,22,116,222, +196,117,120,156,73,149,135,44,112,190,132,203,144,72,112,124,71,207,228,10,40,170,59,225,104,185,194,187,160,62, +194,55,218,0,125,98,30,96,190,44,106,140,93,134,84,6,50,84,188,88,19,214,32,67,65,161,182,75,121,96, +116,76,195,70,253,92,235,221,70,79,1,211,43,184,53,123,32,183,177,6,186,216,36,37,32,155,189,61,185,244, +35,49,49,254,167,193,210,157,21,243,68,66,48,120,245,174,88,134,67,113,139,22,106,74,46,202,93,205,199,34, +208,248,138,53,254,85,167,227,105,0,250,197,104,24,98,100,242,72,227,222,187,94,213,207,135,115,57,233,105,177, +85,164,55,208,87,247,247,143,68,137,54,155,1,7,238,18,43,151,160,189,160,182,12,46,129,82,212,176,101,212, +2,129,206,6,216,32,232,248,78,54,200,124,50,214,42,168,147,64,159,159,26,199,126,120,196,94,234,89,142,77, +52,174,47,195,47,232,26,169,251,112,60,214,250,113,44,103,188,200,152,21,165,126,208,100,74,250,97,196,202,198, +6,154,211,196,192,40,95,208,117,200,94,83,253,179,96,209,109,32,169,181,125,129,57,166,34,132,233,47,255,251, +249,234,244,231,211,143,239,63,191,191,254,233,227,233,245,229,199,43,212,159,210,4,53,71,5,114,110,104,211,2, +106,136,178,47,120,137,50,201,4,118,148,174,111,183,188,128,84,253,173,211,108,9,11,248,10,159,189,85,74,28, +133,82,137,195,13,156,193,57,188,107,239,98,45,144,251,139,189,189,107,175,174,251,90,122,130,115,156,230,228,254, +241,38,249,18,101,165,95,239,32,107,118,141,141,253,135,27,244,141,45,191,153,118,247,251,50,251,244,219,178,223, +61,53,123,245,132,124,36,86,95,2,182,239,24,248,225,87,114,75,76,80,24,51,241,105,112,57,92,98,104,254, +101,27,57,30,57,116,137,50,12,215,1,177,230,244,254,254,157,187,240,224,34,112,228,235,130,131,2,140,25,186, +245,160,225,50,53,49,241,2,157,132,155,128,187,23,202,217,36,98,232,181,98,182,251,96,211,61,22,190,145,251, +235,176,68,76,94,203,141,135,163,50,163,192,243,65,232,222,192,204,243,45,193,214,55,72,42,130,180,225,243,37, +220,98,197,57,94,125,129,221,158,124,7,139,9,130,118,248,101,20,224,19,146,216,96,138,100,227,159,46,57,64, +187,242,214,189,34,85,191,182,200,215,169,241,194,88,201,186,118,113,11,24,177,6,201,186,198,192,120,247,18,219, +188,70,173,32,231,230,26,22,180,195,234,44,88,41,249,245,223,185,83,15,72,168,253,175,131,107,188,27,123,160, +56,67,143,238,188,10,62,203,53,169,115,18,176,51,143,24,186,123,181,183,39,111,47,239,239,19,172,252,28,206, +134,231,35,175,175,48,46,221,85,141,216,203,64,114,204,71,178,165,47,58,241,231,247,247,87,21,156,233,55,141, +51,50,31,244,210,245,36,191,88,251,188,60,16,205,123,149,161,86,182,61,130,28,65,116,69,72,214,120,135,190, +167,213,31,219,82,151,147,219,37,39,232,191,20,141,255,193,219,150,9,187,207,243,29,66,59,14,197,78,42,69, +168,5,188,47,55,245,124,3,28,182,21,238,122,231,229,174,125,243,14,237,86,57,128,21,141,132,165,80,85,121, +184,204,39,221,207,87,228,126,62,205,124,71,61,105,142,83,237,60,42,243,173,246,79,153,164,218,58,21,74,64, +196,126,72,146,83,12,82,242,20,33,4,238,246,164,239,45,87,53,51,98,11,241,253,41,155,177,180,207,93,51, +215,220,26,218,76,111,190,77,42,136,249,36,74,118,214,107,32,64,234,167,246,255,27,191,178,111,81,109,65,200, +160,11,218,159,40,157,123,212,213,86,138,205,235,106,246,69,110,66,206,183,10,34,55,94,247,216,145,99,133,69, +236,129,80,187,209,179,80,160,251,225,62,238,191,58,245,86,41,103,191,150,80,61,155,203,157,54,36,175,222,103, +103,223,221,223,207,246,133,167,251,235,202,237,46,64,115,239,118,118,145,32,63,73,44,196,160,93,135,230,70,44, +83,184,123,64,210,80,153,109,217,80,101,43,226,204,113,20,254,158,250,16,81,207,191,187,255,180,54,222,159,186, +207,145,56,27,63,243,33,69,130,104,23,43,11,130,68,110,144,137,112,64,45,239,117,108,64,226,236,239,238,50, +143,80,196,121,87,164,217,60,76,248,159,172,195,173,205,66,55,99,232,199,70,204,77,193,233,58,222,38,120,119, +5,57,86,68,27,142,105,123,91,132,215,23,167,215,239,255,235,167,192,185,112,32,193,219,15,151,103,255,139,39, +133,34,176,235,7,199,224,43,39,118,188,8,31,67,235,60,60,142,105,183,53,11,132,220,167,196,212,103,235,97, +209,229,240,159,207,159,237,238,92,145,212,101,18,165,118,103,217,235,226,33,228,59,247,59,231,239,175,119,206,120, +196,68,206,240,110,202,139,89,57,70,175,122,254,124,193,229,25,42,139,80,28,62,207,85,73,4,121,37,79,119, +21,167,162,232,196,225,156,39,119,126,30,138,188,147,179,140,199,253,206,60,239,200,163,125,115,228,80,39,156,124, +41,243,194,199,3,19,190,239,119,212,129,32,246,167,213,56,157,220,173,230,56,163,112,225,247,170,144,128,33,18, +6,97,206,39,12,226,52,45,88,6,51,117,250,135,8,151,144,51,217,251,85,253,182,224,143,147,52,186,169,102, +7,138,42,170,220,63,100,243,126,93,31,30,194,206,230,59,189,42,230,211,40,92,80,65,144,59,56,25,144,51, +223,174,36,91,53,148,142,211,175,84,23,157,226,162,94,49,100,82,127,75,114,125,68,125,175,159,46,89,22,39, +233,173,191,228,57,173,197,85,225,138,192,23,166,242,140,230,78,68,7,210,251,242,12,107,245,62,181,201,152,9, +139,210,44,36,26,59,249,13,95,248,202,232,229,85,232,135,242,117,3,66,127,70,13,172,210,178,72,184,168,177, +79,136,101,147,9,29,197,160,56,160,14,250,86,146,90,141,129,150,106,197,84,61,186,85,100,114,49,195,241,42, +250,102,26,218,13,100,112,69,83,40,220,140,39,176,64,14,229,225,124,177,49,214,87,63,239,156,167,34,133,43, +54,77,217,206,111,239,213,221,199,116,140,138,167,174,207,153,72,82,120,135,150,141,179,12,230,152,132,61,141,88, +127,61,54,7,108,94,77,98,97,210,202,11,212,169,168,202,145,224,196,24,197,87,40,56,38,137,199,189,94,149, +151,216,163,114,97,228,122,121,242,125,95,50,67,143,193,34,85,39,132,251,25,75,164,99,208,223,4,27,241,233, +160,104,42,66,181,173,144,246,34,157,251,29,60,112,30,41,163,186,139,116,129,183,116,23,150,19,158,194,18,229, +48,213,146,194,133,108,77,9,140,204,128,188,46,220,33,137,67,150,38,249,200,211,89,233,53,86,139,70,197,231, +83,108,44,67,46,235,33,34,18,150,83,89,222,167,45,243,222,74,203,207,140,79,38,76,84,10,33,2,8,242, +79,110,66,151,8,130,144,75,28,116,32,161,65,49,10,205,49,210,131,171,57,164,83,12,46,233,52,173,117,102, +67,171,182,20,55,79,85,187,43,41,172,26,34,68,245,99,40,97,20,229,7,154,17,168,27,228,238,156,227,93, +93,152,44,199,142,122,162,82,70,90,217,232,61,63,196,250,34,230,171,71,213,70,62,223,239,224,73,67,157,56, +141,202,188,195,133,96,25,152,205,217,31,107,2,108,207,85,197,15,31,60,28,160,254,2,149,139,212,157,76,8, +75,38,216,96,157,201,239,105,238,25,153,18,58,0,100,98,51,35,170,148,52,23,246,84,105,29,244,200,52,66, +36,247,150,99,75,95,149,182,43,123,170,219,163,133,132,130,117,72,201,180,206,55,39,179,88,69,118,171,46,104, +97,210,131,31,150,69,90,15,132,60,29,6,105,108,134,86,98,119,142,254,80,47,53,233,117,213,162,156,143,89, +70,131,164,42,83,3,129,61,226,162,83,75,206,150,140,104,5,55,51,174,148,112,155,132,231,40,87,209,204,42, +105,212,97,57,164,253,198,156,166,113,76,139,253,29,4,188,218,40,110,180,169,18,58,17,213,144,24,244,109,207, +107,88,117,27,21,82,121,214,101,36,192,167,58,12,167,174,125,187,146,144,138,107,77,174,234,163,122,96,206,68, +217,154,214,242,114,78,144,0,58,53,225,121,33,209,33,154,158,43,42,176,83,203,208,46,53,213,80,217,163,17, +96,41,124,90,176,13,35,87,61,3,63,140,105,158,246,213,217,26,22,201,208,130,253,48,169,34,211,240,135,132, +201,152,242,17,43,96,195,194,33,156,208,122,118,13,23,157,25,166,38,244,228,225,44,44,29,16,99,158,246,191, +139,227,184,86,199,239,142,198,199,71,39,7,125,211,196,210,80,224,48,41,216,87,80,63,157,146,195,15,4,92, +118,30,70,87,50,229,103,44,161,103,202,122,146,132,95,89,178,100,164,127,59,23,172,164,25,86,59,79,235,174, +116,95,101,108,174,61,137,206,215,122,46,232,75,179,171,142,249,32,54,208,145,36,115,204,127,198,166,124,204,19, +94,252,153,189,43,221,110,28,215,209,175,146,187,119,78,219,46,173,94,162,217,151,127,51,191,239,190,40,150,156, +248,92,57,241,88,78,151,211,62,126,140,249,55,79,55,79,50,132,8,65,252,68,138,74,220,169,154,237,86,157, +36,20,0,2,32,9,146,224,254,170,156,13,86,58,89,46,203,205,6,138,55,235,185,24,186,224,242,187,166,245, +131,204,215,215,181,220,181,15,205,40,101,110,14,15,247,249,55,139,104,18,38,209,36,74,211,201,44,186,205,222, +78,122,201,103,226,192,112,128,229,138,71,195,122,135,233,92,101,189,165,234,11,165,185,105,165,242,166,75,58,150, +69,27,99,113,159,23,155,141,114,255,38,143,209,228,49,158,60,38,147,199,116,242,56,103,188,203,213,73,131,160, +103,41,17,183,228,83,118,6,168,243,111,65,228,13,4,151,153,18,48,123,140,212,79,172,126,18,245,163,82,166, +164,32,95,77,135,174,168,202,19,5,141,148,122,6,52,156,205,53,92,41,28,3,60,209,112,149,138,4,224,154, +15,37,45,53,225,26,58,167,4,247,12,232,178,23,151,90,149,13,51,200,85,7,95,79,94,206,94,183,83,25, +241,77,89,60,148,117,230,71,95,242,251,251,195,111,143,219,99,85,254,158,251,68,201,192,32,37,91,40,212,87, +89,100,235,151,67,173,202,226,177,172,246,110,43,84,14,231,185,87,34,169,116,20,218,172,26,253,51,179,142,198, +65,188,72,162,182,154,110,54,80,133,22,138,154,216,78,232,174,174,86,57,234,181,94,106,205,240,162,17,80,231, +203,213,125,220,171,246,206,100,213,207,116,229,164,138,80,196,243,133,232,201,72,173,110,112,105,218,205,127,123,81, +163,151,86,124,85,110,142,156,26,230,80,228,69,89,38,108,105,26,31,116,252,18,162,212,101,217,49,187,217,223, +85,121,173,26,49,90,20,57,163,217,6,151,231,106,242,82,157,153,131,254,126,174,110,244,239,23,194,77,94,154, +207,23,250,228,216,218,92,154,87,168,58,129,138,190,218,18,89,181,61,27,21,65,155,167,138,74,93,8,123,73, +197,182,94,171,249,30,26,173,41,204,13,32,167,205,189,150,250,181,68,197,19,162,149,235,173,242,86,218,152,164, +166,29,179,162,113,243,52,175,246,143,249,165,168,110,138,35,84,184,69,16,52,208,66,82,146,152,41,81,181,177, +121,18,199,85,252,217,187,205,173,44,55,193,70,204,35,73,211,96,30,100,174,190,146,165,206,224,113,158,158,14, +100,41,173,68,182,139,196,4,29,72,47,13,67,118,252,186,15,24,46,55,246,93,69,192,40,181,170,108,79,5, +68,130,30,142,227,99,36,126,4,28,162,196,209,253,60,137,7,229,124,206,15,79,74,247,158,148,123,85,72,131, +81,154,139,8,33,66,185,76,231,189,8,247,199,65,199,41,35,255,221,2,246,191,251,201,102,247,29,43,51,103, +162,203,84,32,155,218,198,108,255,188,165,171,159,157,22,96,245,229,98,103,75,135,233,29,122,126,88,103,137,81, +163,160,182,12,106,55,217,101,215,151,78,59,91,210,206,227,145,119,178,238,186,228,43,171,174,39,58,133,77,144, +18,166,67,118,247,79,224,236,43,114,193,56,194,244,250,216,94,149,90,240,75,173,10,91,143,106,59,131,114,65, +107,27,104,1,122,35,43,253,160,105,111,128,70,151,65,54,38,253,85,252,174,78,208,132,130,236,107,185,235,195, +180,237,248,86,203,84,1,93,214,213,240,99,87,78,51,212,97,224,200,241,145,105,184,89,164,208,68,141,242,111, +47,172,228,113,5,75,179,49,232,120,76,13,9,70,175,107,128,181,12,85,107,105,88,93,104,190,240,245,219,246, +235,247,103,174,234,69,185,201,95,170,99,214,190,61,57,75,51,174,253,211,146,174,150,173,59,229,233,199,215,70, +59,179,26,148,67,30,92,120,22,216,46,200,104,179,40,71,179,221,22,208,149,38,194,93,37,27,37,139,37,138, +96,143,221,47,226,250,242,66,118,158,46,9,85,138,54,121,30,111,50,15,143,119,84,190,52,152,132,203,104,50, +95,64,221,27,165,28,16,57,177,192,118,73,198,65,94,38,65,63,77,69,30,197,107,95,154,172,146,100,184,179, +36,243,85,30,175,250,34,22,203,50,78,188,34,62,166,36,135,187,125,84,168,88,165,129,2,14,114,120,71,41, +70,113,52,89,206,39,1,20,226,24,161,83,220,4,129,142,242,43,202,52,178,210,178,222,36,133,47,45,80,122, +2,117,149,221,58,73,86,22,251,251,52,137,189,236,63,168,228,104,130,195,208,6,150,50,64,35,3,129,254,19, +242,194,100,51,240,206,9,228,220,7,152,53,119,0,70,187,59,227,128,176,243,195,196,217,134,129,91,172,126,27, +234,61,152,209,87,70,244,8,34,199,58,242,28,34,55,110,32,206,194,101,221,244,109,71,151,235,165,44,70,205, +150,214,176,32,232,13,9,130,126,92,78,170,102,193,41,115,210,84,15,76,19,1,69,115,207,182,191,72,239,90, +195,50,159,85,150,188,236,123,180,77,34,96,72,59,139,58,8,39,131,166,88,165,63,13,113,236,229,118,108,57, +125,182,238,108,24,8,180,171,165,174,221,201,98,18,37,75,245,179,154,204,210,219,174,71,95,165,125,174,60,143, +201,139,124,119,63,254,207,255,248,247,31,95,26,60,49,220,75,217,182,173,15,251,255,52,41,41,168,93,221,130, +55,85,121,50,81,6,56,35,50,10,76,201,55,188,163,95,25,126,26,98,111,40,8,45,30,145,222,133,196,132, +131,170,61,163,249,239,12,190,250,44,238,54,219,67,59,137,160,151,159,186,73,133,91,108,30,116,137,181,35,34, +105,120,212,108,64,15,99,201,32,190,166,160,219,33,73,194,2,204,102,170,235,165,197,182,139,143,18,108,213,137, +143,83,115,64,140,74,69,175,23,149,113,131,197,38,1,202,70,249,189,50,138,130,202,198,144,209,133,160,233,16, +243,178,236,170,111,80,244,237,225,247,54,187,161,41,244,238,227,50,227,135,160,207,214,130,170,160,116,127,98,213, +51,85,197,210,116,50,159,79,84,197,186,117,142,169,117,1,233,197,44,163,255,105,171,27,61,4,252,141,249,24, +245,109,134,141,105,191,101,111,202,46,85,204,186,133,178,56,10,140,70,38,200,122,75,171,198,48,91,70,217,46, +191,190,91,78,206,239,235,231,234,229,200,51,233,194,174,172,170,237,190,222,118,131,202,110,101,180,9,209,106,201, +55,83,165,219,164,145,113,155,141,225,29,99,120,78,5,12,105,133,13,65,179,143,32,126,63,145,155,63,140,121, +247,135,50,107,77,62,14,58,171,210,117,132,135,119,2,108,170,8,91,149,244,15,227,57,59,157,69,254,172,101, +2,145,110,140,0,25,98,140,251,122,86,160,27,80,38,227,191,220,248,177,166,108,205,74,144,54,68,50,107,175, +210,90,157,137,138,64,58,123,241,3,146,157,217,135,36,144,153,62,109,70,148,241,233,194,41,135,108,104,186,31, +213,202,122,178,1,138,134,205,126,132,96,88,184,55,39,152,102,36,43,80,160,215,152,4,111,235,67,101,239,182, +8,157,39,186,84,70,115,101,150,248,139,67,208,110,13,124,249,193,20,152,27,31,100,166,235,252,80,156,253,115, +173,184,236,225,234,23,174,233,247,50,179,95,123,62,108,169,3,105,231,226,0,87,108,15,122,239,24,239,148,144, +222,207,192,172,85,19,191,123,202,156,80,157,198,155,230,247,148,214,140,39,38,64,239,84,3,144,222,181,38,235, +49,224,223,203,162,141,197,213,240,112,28,2,16,139,178,0,123,70,65,90,186,37,108,196,41,8,193,153,212,95, +192,163,121,130,95,68,81,173,23,57,72,99,250,105,64,31,248,105,111,244,190,41,112,221,192,102,44,143,20,176, +14,230,85,110,243,70,151,17,217,3,222,37,225,158,71,80,184,84,244,97,246,141,35,4,158,68,206,44,0,140, +191,96,176,153,31,232,135,70,137,45,77,212,234,125,211,96,104,47,136,127,73,201,239,126,117,26,62,149,15,141, +135,120,23,104,5,235,199,3,13,212,3,199,16,145,63,72,71,123,203,158,189,222,193,73,10,122,106,131,89,141, +22,165,199,140,250,124,141,138,228,183,0,175,5,186,51,39,132,204,9,47,186,120,170,173,98,194,69,196,81,113, +93,211,206,38,35,166,89,148,189,149,228,192,246,97,221,17,173,1,26,230,174,109,226,50,150,177,248,113,120,74, +227,142,193,137,240,187,0,43,9,245,151,98,199,52,163,226,75,2,119,167,65,227,106,164,35,125,106,227,71,112, +63,230,193,106,35,211,178,125,73,226,113,223,216,220,163,164,214,139,190,204,72,29,61,92,179,74,0,151,248,181, +37,200,78,196,9,239,54,100,26,104,232,53,202,154,195,234,109,88,97,217,122,157,28,234,184,181,76,9,147,94, +164,62,68,230,21,93,123,210,13,39,119,156,145,236,169,54,220,4,33,145,244,238,212,15,93,12,150,111,238,29, +152,202,229,182,220,175,215,241,186,116,54,235,184,105,68,242,241,186,197,96,220,235,57,190,54,108,55,11,127,89, +6,126,155,74,93,46,155,6,246,53,150,102,91,43,130,185,113,80,66,92,132,230,115,218,28,40,121,124,174,10, +153,240,102,123,196,72,84,7,222,72,75,221,210,59,121,191,51,194,155,200,102,154,229,85,179,245,54,159,107,166, +237,109,46,60,109,235,223,64,13,203,252,108,74,218,61,238,216,233,157,193,180,169,247,247,178,91,223,64,3,172, +159,14,15,82,60,57,75,224,244,81,77,93,96,102,163,179,134,27,27,219,109,133,121,221,174,241,221,244,153,77, +102,91,65,126,59,32,137,215,97,27,70,122,193,201,197,134,81,67,76,120,17,144,83,195,71,13,174,109,235,175, +111,197,113,203,232,135,110,226,241,152,14,183,245,189,222,9,242,226,191,183,89,98,45,184,25,40,79,251,252,169, +128,61,224,64,53,211,127,222,93,171,67,173,25,212,113,155,239,123,107,185,150,117,99,215,121,230,250,219,221,11, +205,141,80,37,5,48,113,255,189,89,211,236,82,29,102,117,163,159,140,180,56,10,230,108,239,56,68,94,141,47, +216,49,212,203,39,154,197,109,127,162,229,230,229,160,206,125,54,111,147,55,110,204,39,117,174,232,219,211,174,202, +232,242,74,186,52,237,229,184,89,78,126,22,255,163,130,255,44,10,20,70,61,150,250,187,95,60,30,143,251,187, +79,159,62,127,254,60,251,28,207,158,15,15,159,232,137,39,138,252,187,95,40,178,239,182,229,231,127,120,62,41, +194,64,125,209,79,162,126,82,133,139,255,89,241,218,231,199,71,245,189,217,170,119,195,136,62,158,207,23,225,114, +213,68,45,20,228,95,35,138,245,47,20,47,122,76,190,223,81,32,109,62,99,245,249,187,95,124,210,108,72,156, +10,253,248,86,157,164,158,30,74,85,135,143,55,141,111,223,150,152,30,0,126,226,82,76,237,141,146,161,209,142, +109,215,207,122,41,103,34,159,154,202,181,140,1,17,184,205,162,239,126,92,3,117,134,65,172,108,177,214,22,225, +90,44,24,31,157,252,250,27,28,154,0,10,198,37,36,83,6,93,209,176,254,103,250,230,74,229,164,98,87,26, +214,154,67,93,223,60,73,39,0,176,5,58,228,11,197,99,84,228,246,40,17,87,140,230,36,17,135,245,109,22, +163,195,16,25,21,71,122,84,188,83,157,25,182,42,88,179,180,221,232,48,115,176,77,1,149,187,105,146,97,170, +8,16,173,168,6,157,215,149,90,183,56,208,53,48,124,91,226,173,180,113,251,83,171,235,148,194,214,10,148,101, +44,186,152,21,173,173,143,56,166,223,118,5,2,10,250,72,64,99,139,230,191,171,79,177,210,215,124,150,133,63, +133,76,228,79,163,77,229,153,134,240,171,54,160,204,144,120,67,160,199,255,120,211,54,98,203,62,254,50,174,123, +147,74,189,18,20,167,217,44,63,0,74,233,13,14,70,164,161,119,243,174,30,128,55,2,145,183,229,186,8,111, +156,85,121,187,253,89,139,46,208,67,85,229,134,39,201,180,0,179,43,113,214,66,158,219,242,87,66,139,198,49, +59,56,156,16,231,129,156,55,182,8,188,51,7,142,213,83,11,220,173,118,156,184,90,195,89,27,107,123,91,147, +13,176,43,68,64,178,233,231,199,109,86,174,246,39,217,98,128,219,69,98,133,49,6,84,211,185,250,126,143,19, +112,120,62,210,12,100,146,22,229,195,109,54,4,215,154,205,135,250,5,242,7,84,43,162,58,66,21,231,3,154, +60,31,87,40,1,48,59,59,219,162,193,108,75,123,217,22,250,178,77,70,72,146,126,171,10,88,211,200,23,127, +199,225,79,137,197,205,145,182,185,145,54,143,234,227,203,223,236,250,121,209,80,254,210,162,160,251,70,142,13,160, +135,154,8,238,129,188,21,8,235,103,146,26,35,172,217,170,215,172,176,235,219,238,64,156,123,244,184,42,195,193, +31,116,54,110,238,236,15,174,237,46,137,171,191,199,251,250,236,32,142,197,253,99,216,120,148,132,94,3,10,119, +172,110,233,161,69,210,51,93,236,70,70,12,100,177,89,110,86,23,238,63,9,186,191,106,127,28,112,184,49,63, +166,202,244,185,146,160,208,119,79,33,121,22,77,96,38,200,62,21,52,166,221,172,249,237,89,96,129,153,209,55, +114,243,172,188,192,108,9,114,51,38,48,109,4,79,94,92,177,175,160,109,65,126,230,81,158,150,19,101,209,113, +80,45,239,246,214,65,157,199,162,249,115,244,218,200,95,108,55,238,64,222,248,183,229,250,115,232,138,200,86,62, +253,48,22,31,181,175,216,99,211,158,13,198,222,172,241,71,243,103,202,181,145,191,222,126,104,208,193,90,44,243, +100,11,227,253,233,31,165,234,78,76,118,179,95,195,109,79,183,24,3,68,184,184,243,97,59,248,113,253,72,22, +75,96,230,23,224,3,203,45,22,66,98,99,207,180,218,20,155,188,45,61,92,128,113,107,194,185,139,250,32,118, +88,43,68,219,186,125,249,179,116,152,42,94,69,194,212,8,212,189,2,133,224,161,124,221,108,242,205,2,243,21, +215,164,80,3,119,174,34,206,169,13,32,45,157,190,244,169,54,107,2,3,148,176,198,251,30,188,246,228,92,4, +144,54,16,231,33,98,129,94,10,123,96,227,41,46,59,137,111,153,101,28,167,198,228,251,201,253,153,49,30,5, +179,230,237,244,35,206,185,235,240,229,21,57,233,155,143,246,211,250,115,17,137,253,121,56,30,1,115,112,156,122, +116,190,252,203,85,206,107,10,1,167,105,222,158,103,24,241,58,227,64,175,210,220,240,161,146,251,252,249,233,86, +9,249,46,175,182,197,249,135,50,248,210,205,98,102,119,5,239,81,111,124,83,131,160,187,163,39,38,127,0,74, +127,226,132,10,173,125,181,40,207,188,242,234,2,41,61,205,43,186,68,167,48,110,47,128,13,42,135,82,229,205, +83,245,250,123,139,153,12,188,81,107,211,194,80,117,183,9,189,93,39,172,118,40,17,167,7,92,11,62,195,212, +103,247,174,138,199,231,195,246,123,53,248,205,171,179,189,219,16,9,184,254,92,63,245,48,126,200,147,101,122,54, +30,93,102,107,82,70,125,31,218,13,159,114,180,4,79,215,18,228,109,183,11,225,70,113,225,63,123,56,108,139, +233,169,58,27,91,17,163,213,124,127,178,104,170,7,131,102,181,112,145,236,10,131,100,153,186,72,234,157,65,50, +15,93,36,167,218,32,73,180,46,84,5,105,149,69,7,118,5,7,234,29,7,78,85,27,168,97,171,202,143,182, +59,186,83,57,127,58,146,24,58,217,81,127,161,82,237,13,165,18,235,32,180,134,137,26,244,119,250,144,239,171, +178,174,161,144,3,140,22,56,99,252,13,3,207,254,35,228,24,87,229,71,99,114,111,59,100,96,220,74,72,86, +214,178,242,207,246,100,252,7,55,182,142,91,40,228,77,163,108,56,49,191,34,248,138,225,43,129,175,20,190,230, +240,181,128,175,37,124,173,224,43,12,240,51,196,79,84,135,178,199,206,150,64,178,69,111,19,147,208,133,121,156, +177,66,146,20,6,173,194,217,92,255,91,180,168,128,81,203,120,22,243,63,70,173,24,179,72,25,176,100,192,124, +222,231,178,96,76,186,236,51,153,183,152,86,151,148,1,137,165,74,194,152,216,210,36,102,76,212,106,34,73,180, +52,105,83,106,41,50,154,151,129,156,122,135,47,49,56,243,14,1,177,219,233,238,164,57,67,213,50,208,90,240, +4,105,165,82,2,233,97,136,211,223,237,202,98,155,127,99,54,164,203,96,127,186,61,55,241,78,218,164,117,40, +146,80,44,161,68,66,169,132,230,18,90,72,104,41,161,149,132,194,160,11,134,93,80,196,92,103,162,39,167,149, +158,188,134,122,242,218,234,201,97,174,39,159,197,158,124,70,123,114,216,237,201,103,186,39,159,245,158,28,6,124, +242,217,240,201,109,198,144,229,166,29,62,110,139,82,33,6,251,39,238,200,112,83,148,65,96,27,216,106,222,217, +87,245,64,246,197,161,72,66,177,132,18,9,165,18,154,75,104,33,161,165,132,86,18,10,131,46,24,118,65,17, +115,141,125,49,11,180,47,134,186,237,139,177,110,251,98,44,218,23,195,208,190,144,157,219,190,24,137,246,197,48, +180,47,228,230,182,47,70,162,125,49,204,109,95,140,180,237,75,112,110,251,82,8,191,125,25,248,113,251,90,38, +157,125,237,10,178,47,14,69,18,138,37,148,72,40,149,208,92,66,11,9,45,37,180,146,80,24,116,193,176,11, +138,152,107,236,139,89,160,125,49,212,109,95,140,117,219,23,99,209,190,24,134,246,133,236,220,246,197,72,180,47, +134,161,125,33,55,183,125,49,18,237,139,97,110,251,98,164,109,95,130,115,219,151,66,248,237,203,192,143,219,215, +60,232,236,171,222,145,125,113,40,146,80,44,161,68,66,169,132,230,18,90,72,104,41,161,149,132,194,160,11,134, +93,80,196,92,99,95,204,2,237,139,161,110,251,98,172,219,190,24,139,246,197,48,180,47,100,231,182,47,70,162, +125,49,12,237,11,185,185,237,139,145,104,95,12,115,219,23,35,109,251,18,156,219,190,20,194,111,95,6,126,220, +190,18,211,255,170,201,190,56,20,73,40,150,80,34,161,84,66,115,9,45,36,180,148,208,74,66,97,208,5,195, +46,40,98,174,244,191,106,167,255,85,123,253,175,218,235,127,213,96,95,12,243,249,95,181,207,255,170,193,190,24, +230,243,191,106,159,255,85,131,125,49,204,231,127,213,30,255,171,30,242,191,234,17,255,171,246,216,87,91,128,234, +125,189,230,153,211,250,134,47,219,59,7,63,59,15,110,248,11,112,191,31,128,47,199,231,225,136,33,189,53,101, +199,100,184,210,231,127,138,30,118,190,212,21,101,118,161,230,77,73,165,238,102,38,255,209,8,189,163,76,36,57, +177,164,170,48,12,253,12,131,1,86,1,41,253,191,73,217,25,29,16,148,219,102,97,67,167,97,160,57,144,241, +218,35,2,225,30,69,190,73,182,23,175,255,236,194,60,90,17,145,166,233,110,24,103,236,38,216,204,81,9,32, +4,53,0,12,138,20,243,82,113,193,184,14,85,54,162,199,195,33,127,197,179,143,168,2,211,128,116,134,128,224, +188,184,159,175,19,136,209,147,185,190,47,130,98,197,98,155,87,69,12,109,80,168,80,128,84,6,129,216,77,68, +255,49,206,96,90,11,122,190,0,142,157,131,88,161,65,169,4,1,161,252,138,2,196,64,153,124,213,61,139,229, +29,0,120,238,211,150,140,91,11,16,8,242,249,98,219,126,188,158,10,241,106,189,74,238,89,5,190,103,158,113, +124,197,188,173,2,147,161,10,12,68,21,202,121,158,6,65,47,158,157,243,247,101,152,107,21,244,162,26,174,240, +88,10,192,70,0,0,129,112,125,49,44,198,177,68,43,254,241,101,118,255,224,185,90,186,147,79,132,195,151,254, +35,157,54,35,251,85,141,142,10,239,148,125,208,117,204,222,40,137,108,61,119,53,15,114,246,188,31,48,28,199, +125,147,240,96,140,245,244,145,142,173,226,217,27,2,239,84,145,180,96,10,19,236,251,231,103,90,143,97,176,44, +12,48,56,99,48,127,74,132,231,151,163,21,131,225,18,133,191,41,78,183,26,38,106,25,32,162,32,55,133,81, +236,178,20,174,235,101,9,236,92,60,234,16,189,104,2,228,216,228,103,94,185,175,86,36,208,231,71,110,172,42, +166,228,159,77,212,95,242,203,134,156,52,133,230,135,234,206,244,87,63,134,116,199,32,173,157,141,231,67,120,92, +163,73,204,219,110,193,5,84,183,121,54,232,61,66,216,112,229,133,103,82,151,229,228,117,189,173,245,157,206,194, +245,218,243,131,112,57,142,231,36,97,177,165,23,12,15,147,54,48,85,108,142,104,61,142,3,145,64,253,219,230, +46,78,222,147,255,251,246,110,57,166,112,33,173,29,254,208,45,59,238,249,100,200,237,216,131,26,184,229,57,224, +237,212,126,167,108,54,79,61,78,25,99,37,193,198,101,99,253,7,127,194,77,188,73,185,116,8,103,95,209,116, +113,103,74,239,169,157,139,175,40,224,182,56,164,228,5,109,124,80,200,125,181,94,119,91,208,224,81,138,177,227, +43,163,89,251,43,199,185,101,68,141,91,209,89,148,192,251,88,131,47,120,166,134,85,107,135,71,86,181,238,154, +22,243,108,241,108,233,189,33,22,43,15,243,238,221,177,152,63,169,238,187,161,100,252,205,44,173,111,200,206,115, +218,205,179,217,62,109,85,50,223,70,5,103,220,60,143,216,80,142,9,196,253,254,157,185,19,216,70,190,253,36, +14,137,242,172,120,43,246,12,241,148,173,235,160,123,40,249,217,254,165,25,107,163,104,200,98,92,52,156,253,76, +165,71,95,61,5,151,150,130,75,251,48,83,115,37,248,102,123,98,126,124,67,248,243,241,209,149,57,199,188,233, +117,54,74,141,99,35,229,220,4,239,40,216,89,22,19,232,34,97,10,10,3,201,126,218,90,148,125,121,1,210, +181,249,120,182,114,22,233,84,34,202,162,35,106,62,145,66,245,78,235,63,191,118,36,108,187,12,239,104,187,50, +4,12,115,209,87,54,40,81,104,50,58,157,77,165,25,223,194,66,89,84,158,132,21,204,148,193,205,128,173,99, +130,208,230,163,185,214,174,102,208,15,191,93,116,175,122,181,190,84,0,210,163,214,219,205,107,219,200,49,248,50, +219,77,3,121,142,207,204,169,221,61,33,224,250,54,196,87,26,47,89,133,216,3,99,37,223,16,125,100,52,219, +54,34,79,195,156,51,15,207,87,159,194,217,160,180,105,120,54,175,109,232,229,65,136,44,153,6,243,33,4,109, +109,138,131,80,176,222,54,201,145,73,88,67,155,224,228,151,146,141,240,127,29,75,72,230,149,62,141,36,143,18, +59,143,162,62,235,196,206,163,8,181,79,236,60,138,48,143,108,146,35,147,176,150,54,193,201,47,37,27,225,255, +58,150,144,204,39,125,175,172,79,156,63,64,220,119,24,103,77,218,87,29,129,86,28,209,135,14,205,186,35,254, +216,225,109,243,222,159,60,204,51,31,219,87,175,218,217,176,68,101,106,224,49,97,102,116,88,180,68,204,144,16, +117,182,73,14,29,9,24,61,102,76,8,25,99,83,156,70,4,101,99,34,94,71,147,147,249,53,152,70,184,199, +19,51,171,195,162,73,98,102,69,152,6,155,228,208,145,128,245,99,102,117,52,110,3,63,141,8,202,198,68,188, +142,38,39,243,106,80,187,223,172,212,158,38,161,245,115,158,246,145,123,61,196,213,46,143,113,125,47,125,51,138, +84,6,28,1,24,201,253,188,137,213,16,70,115,207,106,226,25,196,4,205,91,161,235,188,46,251,239,211,11,130, +9,95,246,123,55,161,32,90,141,242,253,246,168,68,125,111,81,118,24,38,213,23,138,195,235,164,73,16,48,242, +254,185,42,0,181,16,84,195,101,173,145,250,93,84,13,105,211,164,154,194,18,223,1,46,119,140,107,223,157,56, +187,159,163,104,211,160,190,38,189,8,253,121,4,215,97,100,137,220,99,79,160,54,89,135,50,255,179,120,101,143, +175,251,199,242,73,191,150,64,190,17,2,224,227,179,178,28,29,251,174,249,61,37,128,134,146,112,3,72,78,253, +195,179,172,118,218,239,123,240,181,243,50,153,226,60,31,192,39,175,121,227,63,188,79,18,222,102,239,33,197,10, +17,45,210,238,129,86,163,148,244,140,2,248,250,219,170,228,171,177,220,60,168,101,136,161,101,136,35,74,221,142, +192,70,135,24,233,131,244,121,81,78,155,202,49,229,7,104,52,168,42,243,239,74,6,157,189,207,136,164,3,239, +123,164,181,201,29,216,26,139,86,255,69,219,115,40,55,142,35,251,43,18,234,149,14,168,233,85,217,227,137,212, +226,116,147,115,206,167,82,185,104,10,178,56,150,73,45,8,57,156,204,127,127,221,0,8,130,150,252,242,187,96, +130,8,157,19,154,218,218,189,250,111,192,216,84,128,25,170,11,98,178,146,170,6,246,240,94,54,103,201,124,93, +100,4,149,27,80,160,197,230,44,213,189,66,182,179,98,195,214,149,234,85,70,231,153,97,35,90,198,125,242,195, +209,111,149,25,140,162,165,41,205,229,74,65,33,53,253,19,44,31,206,139,143,186,68,199,52,151,80,74,214,192, +97,82,74,218,86,206,123,95,46,79,209,195,198,238,145,108,106,200,101,57,204,145,252,212,148,250,234,138,253,227, +31,205,11,131,20,151,210,234,178,200,94,197,235,157,25,6,21,110,50,229,23,163,81,23,95,211,99,187,37,122, +103,163,134,136,94,230,152,132,194,177,89,74,53,24,168,150,7,188,173,87,104,0,25,81,121,58,86,201,41,228, +13,159,153,86,212,143,40,219,205,2,137,43,212,121,239,11,47,174,174,38,83,49,210,138,62,165,246,242,225,33, +246,47,203,19,37,223,34,54,13,169,128,188,14,20,44,27,49,27,236,245,187,19,27,130,150,48,23,146,24,160, +241,36,136,47,93,46,57,109,172,235,44,53,217,130,23,162,187,221,44,116,121,238,118,23,117,93,155,33,185,162, +204,172,126,214,146,85,235,106,165,40,43,124,49,169,54,12,230,209,212,175,92,45,103,12,102,146,169,11,149,173, +13,138,137,193,74,178,172,60,93,45,149,81,184,182,144,155,186,21,219,41,23,155,150,133,179,206,219,17,189,17, +210,99,58,114,60,201,167,173,237,52,36,247,204,2,3,156,37,237,178,17,232,177,50,31,27,81,126,152,195,185, +188,28,12,46,249,37,255,192,81,150,66,140,206,7,131,243,190,148,122,48,40,156,48,206,33,23,131,1,63,150, +231,194,66,186,144,71,173,50,228,105,52,238,170,236,88,180,140,28,146,57,79,88,129,225,144,65,35,66,230,136, +100,211,33,230,138,103,41,202,154,7,14,20,42,106,162,34,150,76,135,167,70,213,92,129,65,77,9,209,202,229, +132,182,134,55,197,53,148,144,67,234,12,175,146,104,7,19,61,5,3,165,24,229,115,238,105,65,134,171,33,241, +224,182,101,248,134,10,134,181,204,134,103,233,114,173,26,27,91,15,6,172,180,92,70,126,181,14,162,90,3,59, +60,76,207,211,220,48,49,70,41,159,230,149,26,106,85,149,203,51,197,215,67,191,38,134,6,99,60,231,49,111, +138,123,225,24,75,108,45,160,187,220,16,26,214,69,178,5,127,23,92,79,191,196,99,60,139,193,198,18,221,1, +189,78,185,21,129,176,38,166,91,77,150,220,144,23,71,239,1,12,57,165,39,42,34,2,229,79,244,227,41,160, +33,193,246,251,181,212,99,237,104,46,113,37,65,80,117,172,92,89,182,90,125,219,141,146,235,70,33,1,13,42, +89,108,80,161,5,234,101,38,44,59,150,160,103,244,229,139,179,23,170,112,81,171,151,87,189,116,137,22,58,187, +236,233,117,81,144,15,138,145,63,184,34,16,141,48,240,189,244,128,242,6,221,35,142,212,15,79,149,89,148,51, +89,130,38,17,201,156,10,131,165,226,251,142,186,20,227,241,76,45,213,113,106,20,1,14,166,247,142,167,160,45, +174,202,226,169,16,195,66,80,7,36,47,90,19,171,234,26,215,156,53,224,134,6,155,208,195,74,21,6,223,15, +221,147,80,143,212,18,51,68,76,114,216,31,132,177,110,132,33,87,224,14,233,33,245,109,40,184,61,187,200,212, +202,105,201,41,155,224,121,183,140,129,13,6,184,126,164,215,43,195,155,85,112,39,70,133,156,141,156,203,184,8, +235,24,244,65,149,96,100,206,175,28,57,136,186,44,212,120,149,204,33,163,243,187,36,176,177,22,155,216,117,160, +237,137,59,85,215,158,203,0,20,163,18,113,21,20,226,215,189,90,50,199,82,221,26,209,59,110,90,19,50,33, +245,77,26,0,211,198,14,28,185,173,26,101,177,94,46,225,6,33,71,128,66,68,67,202,2,85,93,137,73,5, +142,140,93,224,132,183,129,197,232,6,158,200,162,191,34,231,222,170,191,46,84,175,65,222,155,149,170,234,21,165, +233,97,64,166,111,9,189,180,247,55,123,248,111,61,7,139,5,207,91,212,46,11,47,121,1,45,249,141,74,187, +46,224,212,231,15,222,64,86,105,117,117,93,94,11,107,24,185,91,13,89,122,156,59,27,224,122,98,40,118,173, +151,230,125,122,138,161,62,119,161,10,193,144,237,75,99,31,111,203,44,228,137,126,100,144,177,124,105,95,144,174, +216,38,67,36,121,194,255,107,2,13,194,116,132,245,114,39,209,180,232,249,168,191,11,124,107,96,79,184,113,230, +165,36,213,25,72,124,98,38,123,211,122,180,143,21,78,207,32,213,106,104,171,10,92,145,102,178,63,21,112,187, +93,153,231,5,38,145,75,183,118,123,10,106,104,219,232,238,253,0,247,218,224,136,112,159,21,70,231,170,26,174, +214,213,130,171,8,255,239,22,191,25,186,170,130,230,175,174,176,66,80,86,145,50,148,59,196,133,81,61,101,85, +23,239,150,170,5,248,133,0,94,67,43,39,13,111,76,151,165,97,245,20,76,200,224,79,44,145,158,84,20,162, +50,188,191,23,81,248,129,0,230,243,134,78,141,156,229,214,237,116,48,49,151,74,141,179,195,29,101,172,51,140, +102,187,161,109,253,188,122,159,190,71,79,92,170,226,216,44,68,83,101,254,177,15,169,12,184,53,166,42,23,169, +111,221,42,255,12,155,201,231,61,78,40,69,32,163,73,156,147,114,10,46,0,201,254,62,232,209,181,117,21,22, +247,64,215,205,106,234,108,56,173,107,31,211,232,53,121,20,197,162,71,72,76,39,222,41,23,235,250,123,205,153, +222,89,84,90,93,160,130,138,202,232,117,134,214,41,143,224,168,243,126,6,71,147,106,42,207,134,190,33,79,14, +37,219,156,247,188,17,34,57,123,181,53,29,36,212,90,207,78,185,15,6,38,198,234,121,237,247,21,89,47,238, +59,187,186,218,129,19,23,184,138,9,187,186,82,195,2,159,66,212,72,15,222,217,78,118,85,121,190,150,172,58, +213,234,120,231,44,55,112,36,18,110,176,190,178,2,59,60,68,1,85,214,175,174,174,184,33,201,236,160,75,8, +48,55,214,174,23,184,72,212,165,84,220,239,32,111,227,75,185,196,212,53,28,242,147,248,118,18,189,76,210,27, +139,114,4,254,40,190,77,201,19,66,71,51,237,9,123,99,130,210,41,37,183,177,234,132,103,97,58,220,124,118, +106,21,61,106,156,39,185,53,68,190,171,52,12,135,109,56,54,206,158,195,129,154,212,115,72,130,184,232,10,144, +225,68,126,51,91,23,225,70,184,189,133,77,92,20,237,5,96,83,70,130,56,81,151,213,14,35,156,76,71,24, +86,56,189,104,171,77,161,92,200,211,129,115,170,126,207,148,174,20,23,176,195,207,149,247,241,38,231,227,249,114, +197,93,185,231,32,94,243,230,34,118,245,198,15,99,7,175,193,11,170,146,31,224,75,100,63,155,200,51,146,47, +96,67,95,18,243,132,56,109,80,92,33,197,114,15,236,152,68,237,199,174,186,163,145,47,240,148,157,14,212,184, +151,56,243,184,169,110,18,164,41,151,7,183,146,69,19,161,127,11,232,27,209,21,44,133,108,230,203,205,108,145, +234,71,134,239,137,112,171,161,101,208,248,238,195,236,45,44,69,151,121,70,5,47,78,218,101,188,82,73,133,6, +83,81,239,37,210,123,196,194,158,173,6,140,188,70,23,166,71,232,102,171,110,253,225,210,150,175,97,85,92,73, +184,20,131,202,168,97,171,158,221,41,121,162,195,3,50,150,24,109,137,137,239,54,214,219,60,124,119,33,12,245, +130,147,171,105,202,19,220,249,31,21,33,253,126,89,55,50,206,175,243,236,173,242,143,253,81,254,119,137,189,184, +63,114,127,115,216,18,78,62,133,74,166,215,132,227,210,174,148,184,224,18,113,99,197,37,103,170,152,185,235,76, +179,246,167,12,86,215,220,109,189,90,83,96,77,45,194,4,44,163,233,182,16,113,176,178,193,96,217,53,224,63, +211,80,199,180,200,219,57,192,156,63,186,182,63,0,141,79,196,179,117,115,153,201,254,187,184,220,81,58,212,95, +110,95,253,80,14,189,202,164,70,157,170,194,244,206,115,212,215,218,244,236,241,30,86,122,158,2,246,63,32,24, +255,3,238,90,212,105,230,137,77,240,174,155,53,175,157,230,181,47,85,182,52,175,109,85,84,110,107,49,184,101, +217,85,212,96,208,18,95,198,100,250,236,81,142,108,187,184,174,115,180,91,102,199,54,185,99,210,110,46,96,246, +29,1,229,1,43,181,233,254,148,121,4,14,15,231,54,252,136,145,51,89,44,232,35,251,76,54,109,13,228,252, +199,64,234,35,82,62,230,55,133,44,26,118,208,80,213,78,43,13,104,197,83,81,67,243,210,21,119,247,206,110, +226,128,97,162,128,225,121,14,91,98,198,195,228,184,37,199,158,77,194,109,56,236,224,33,240,200,16,110,237,222, +110,64,118,231,98,254,156,115,134,134,99,128,136,50,70,169,182,251,148,128,69,13,40,139,188,90,68,156,182,102, +165,110,54,43,229,204,10,133,178,211,254,38,202,21,219,145,164,137,140,198,192,187,18,215,145,94,65,135,203,136, +128,223,92,19,137,53,144,19,253,191,145,104,116,32,207,167,111,125,99,150,40,162,230,93,137,111,164,117,162,178, +110,60,183,174,183,226,66,190,164,100,186,244,145,32,53,24,32,86,134,161,149,53,89,214,182,107,35,238,92,255, +218,67,236,230,227,77,115,113,76,232,130,3,237,181,54,209,224,175,177,73,81,67,232,234,68,150,226,85,239,124, +196,137,213,212,60,124,48,16,35,215,176,62,110,74,166,207,107,180,217,83,37,11,223,159,198,132,21,74,62,166, +25,176,237,189,61,217,211,76,240,66,212,244,241,225,104,54,187,233,227,3,74,229,193,237,187,41,198,194,128,95, +118,119,66,25,119,208,123,229,88,113,170,52,41,139,107,186,207,38,10,223,60,101,105,19,122,76,219,24,25,57, +53,156,149,249,172,183,215,151,50,199,112,198,243,230,202,39,32,173,29,141,243,219,255,17,141,7,233,17,19,80, +210,120,47,155,211,56,111,105,135,148,198,217,222,29,133,227,170,41,236,103,10,77,94,53,95,72,240,54,60,151, +197,184,218,194,129,86,149,147,6,149,76,185,162,212,2,57,25,123,41,34,166,43,191,185,253,64,96,59,118,199, +202,48,44,166,232,75,73,229,135,46,10,197,13,134,71,246,247,254,165,118,125,133,106,189,34,33,43,202,217,94, +66,182,184,180,135,201,48,168,5,238,107,83,1,198,138,70,205,14,182,68,211,253,82,20,228,148,170,135,7,76, +12,155,43,70,184,50,57,201,221,207,238,31,52,146,187,155,221,203,26,201,205,238,56,201,209,248,96,254,128,230, +179,155,202,253,221,118,18,180,181,148,234,22,235,53,248,89,168,220,162,186,153,106,46,119,59,206,249,62,9,21, +169,64,16,75,232,239,91,241,87,147,229,84,102,150,247,244,206,236,224,191,200,249,237,3,117,191,177,144,89,250, +224,78,195,231,108,239,222,189,96,33,119,14,246,28,159,193,162,50,26,223,121,104,231,151,52,158,207,142,230,56, +94,187,179,7,15,113,60,183,242,218,63,184,139,227,25,141,213,131,35,26,175,44,156,123,251,51,28,47,34,107, +60,181,178,62,218,35,248,103,22,102,118,143,230,143,34,43,61,142,116,112,25,233,230,220,226,157,223,39,248,23, +150,175,59,251,180,231,208,90,254,221,251,68,219,137,221,127,103,255,1,142,223,218,249,123,25,205,191,195,177,115, +36,1,79,44,156,125,43,147,223,118,126,127,159,230,191,208,248,158,82,68,219,7,75,195,189,135,52,255,200,210, +121,255,62,141,191,58,222,237,254,247,246,236,158,34,218,158,209,248,232,222,109,154,127,101,229,112,247,128,104,248, +151,163,231,30,209,240,111,145,61,61,181,184,30,206,233,236,71,123,246,254,109,164,167,185,151,192,115,249,136,51, +247,161,29,119,124,147,204,125,167,100,240,75,178,96,46,12,62,203,103,156,153,242,163,206,79,115,147,159,17,228, +199,242,41,117,4,224,19,62,209,9,141,210,207,209,213,190,9,120,225,157,127,242,107,10,47,101,57,116,16,225, +135,204,57,123,253,229,195,123,6,172,178,87,213,124,126,137,112,190,203,183,195,57,252,148,239,240,239,63,229,33, +254,125,45,159,224,223,191,228,7,206,42,123,182,194,109,111,232,181,92,253,209,206,24,99,119,24,237,190,73,251, +149,63,48,16,231,56,73,160,149,105,97,208,14,183,55,222,161,237,142,243,19,130,87,24,36,246,147,163,29,74, +35,251,5,86,105,248,7,249,104,158,148,192,103,79,236,191,220,60,55,178,26,12,214,156,111,251,232,253,190,188, +228,63,249,166,6,150,50,216,160,112,146,237,77,63,109,254,113,59,92,15,224,126,45,134,24,136,5,253,21,98, +188,59,14,127,231,47,64,137,81,49,24,248,78,226,11,12,86,240,211,111,2,156,55,24,223,95,12,6,63,105, +35,20,162,78,126,66,106,226,56,17,74,128,191,38,102,138,148,190,68,198,194,213,254,49,215,224,62,185,126,3, +147,30,39,6,102,170,202,116,238,238,119,170,22,80,97,119,71,15,163,89,219,253,165,75,168,92,142,183,59,29, +94,252,172,109,104,213,201,141,141,39,156,136,190,73,191,172,33,51,91,17,206,72,203,31,110,126,227,120,94,224, +41,31,131,142,108,246,8,204,80,207,2,230,252,47,20,195,24,105,86,197,250,84,105,250,253,243,152,83,251,234, +57,85,239,147,231,211,73,65,95,14,252,8,111,253,200,142,188,36,57,180,7,146,99,190,7,184,66,95,0,253, +217,171,171,159,244,132,99,12,160,27,156,135,0,128,210,151,113,159,222,112,251,79,63,170,97,121,93,13,129,114, +45,207,184,66,237,201,115,164,152,186,125,89,106,248,194,224,75,224,229,35,47,160,243,161,22,173,175,63,247,95, +206,53,40,164,39,51,78,70,104,17,194,18,84,195,186,139,50,136,219,85,2,116,209,31,95,34,17,201,210,112, +122,226,142,26,230,102,71,87,232,8,143,19,99,90,190,142,218,19,170,33,175,111,223,157,102,72,224,74,16,117, +164,33,219,172,232,115,204,207,248,238,15,217,241,95,118,224,230,156,42,112,64,34,84,83,156,39,121,205,118,155, +237,25,17,26,116,109,171,86,50,121,15,180,192,195,132,184,8,237,232,239,40,158,34,16,90,118,246,105,139,89, +59,205,161,97,151,145,145,88,69,150,117,13,171,93,2,249,39,39,58,72,32,147,105,171,35,213,253,210,75,136, +140,69,244,213,62,181,235,164,25,82,15,249,204,98,23,100,107,224,160,17,131,26,191,73,28,150,162,131,69,119, +177,244,27,52,202,10,253,133,29,23,14,21,249,184,197,86,212,182,167,128,60,190,108,113,134,43,127,47,118,187, +237,250,201,133,240,240,93,166,23,117,221,152,240,29,38,172,164,215,116,215,111,238,28,131,65,40,55,195,18,22, +172,99,215,166,228,241,156,72,220,86,80,242,189,45,228,59,82,105,45,203,127,164,120,3,184,39,50,28,55,68, +29,90,179,227,193,142,156,47,231,198,155,157,245,84,20,102,184,42,163,7,149,6,239,221,198,134,74,106,37,206, +243,227,181,243,247,254,30,80,43,81,83,97,100,220,239,29,224,139,13,149,192,154,86,43,131,29,193,255,147,197, +38,134,24,60,73,236,120,6,152,239,126,124,177,89,146,237,252,49,0,34,177,172,59,211,192,252,39,231,6,222, +225,35,51,240,22,31,51,3,23,248,192,244,40,87,6,78,240,177,48,240,10,31,187,97,61,243,176,0,121,228, +63,61,221,81,204,102,219,236,238,76,83,129,157,232,108,77,50,185,186,250,130,82,99,43,95,217,191,170,158,5, +191,97,128,164,111,214,69,149,206,9,112,45,200,250,248,230,120,89,30,165,75,194,68,221,125,122,98,13,146,169, +89,210,207,160,90,164,167,248,172,97,227,44,45,121,137,56,62,114,140,134,70,116,197,245,47,231,60,4,208,164, +154,104,254,6,212,102,138,225,17,28,28,39,219,174,229,244,230,191,114,161,217,80,84,9,183,112,42,251,125,52, +126,201,67,100,115,243,82,131,50,182,221,74,110,139,157,243,231,93,248,182,11,86,25,28,109,57,143,185,197,90, +199,241,201,208,227,87,6,76,192,143,240,205,20,35,118,165,156,157,196,202,160,106,100,207,173,229,116,21,191,190, +182,95,215,177,72,152,203,166,204,73,102,135,164,43,148,144,251,230,146,172,13,116,47,105,73,118,109,38,87,21, +230,7,178,143,232,199,110,79,189,61,160,24,102,230,191,136,28,145,118,129,208,85,189,74,86,215,97,59,27,168, +146,197,127,13,112,183,6,67,215,224,54,79,35,182,221,96,119,120,12,29,58,37,229,145,28,127,56,107,57,69, +185,98,184,140,161,187,5,131,230,17,46,142,19,106,202,77,89,95,254,224,19,67,137,139,109,106,251,182,73,19, +83,71,239,161,192,33,218,70,17,87,84,18,111,241,116,106,144,252,80,37,239,190,147,67,41,17,37,228,114,223, +255,18,230,122,4,254,123,46,74,151,5,218,96,155,223,186,53,245,63,188,81,192,87,156,114,112,136,211,212,133, +244,86,236,5,51,179,27,184,146,219,173,191,102,166,173,236,10,251,69,50,254,124,97,253,11,8,100,235,103,84, +67,150,147,125,114,165,31,195,116,181,90,94,114,146,33,148,36,252,250,223,75,187,210,182,184,113,108,253,125,126, +5,229,167,31,174,124,81,42,85,132,84,18,211,74,101,95,38,59,208,43,195,228,113,217,50,184,49,118,181,151, +108,116,221,223,126,207,171,197,86,97,67,232,153,47,148,36,203,178,44,233,236,239,49,224,84,135,123,36,138,255, +208,76,107,143,227,71,219,226,239,18,178,109,136,159,146,141,113,64,12,94,145,68,56,187,55,13,134,180,132,55, +97,125,50,142,100,154,241,92,151,147,172,40,202,65,203,185,61,6,38,158,45,182,106,127,62,9,88,125,127,50, +207,131,210,7,203,161,39,221,185,23,255,109,59,184,181,119,83,148,19,25,194,150,13,29,187,182,178,246,113,107, +7,223,221,105,237,224,40,78,208,167,113,236,197,68,140,70,228,189,9,215,78,101,234,196,62,173,119,218,59,210, +251,112,142,16,164,203,47,232,85,152,91,237,29,72,3,38,243,184,26,20,135,178,148,97,230,28,206,132,206,166, +121,202,0,143,141,244,214,87,172,29,72,105,73,3,231,165,94,139,41,97,158,172,236,44,136,178,221,150,12,242, +151,249,189,104,106,219,161,196,75,4,53,255,155,119,107,78,109,110,86,164,79,202,97,55,207,145,157,103,250,215, +95,195,43,12,238,224,92,225,237,149,181,183,31,188,213,167,3,21,207,174,123,160,134,221,131,46,75,202,181,156, +230,82,120,142,195,71,203,92,144,229,150,240,142,97,51,143,211,227,188,40,229,227,176,146,166,57,85,205,234,255, +120,103,105,110,91,207,84,107,92,212,15,51,123,127,165,154,154,60,141,138,216,118,107,84,155,78,177,52,77,202, +240,6,189,200,157,201,117,156,137,45,41,164,142,139,39,28,118,32,242,74,156,175,92,191,88,109,119,177,190,204, +41,102,156,139,149,35,105,43,98,155,187,18,108,141,70,179,6,216,225,17,181,242,12,228,197,36,247,30,62,126, +252,116,127,255,221,222,62,66,60,114,220,86,137,24,113,125,226,207,37,41,174,65,196,19,85,159,162,62,61,50, +122,236,174,243,32,26,176,36,174,90,184,39,15,83,202,168,49,55,51,106,163,180,231,154,113,7,55,166,171,221, +108,14,143,220,212,90,161,174,154,22,173,252,160,6,11,157,114,139,219,225,13,79,64,200,106,225,239,93,199,97, +167,1,186,202,133,52,157,193,61,84,57,46,170,200,217,139,204,113,57,53,14,75,75,68,214,65,183,168,30,35, +96,190,188,212,103,121,120,164,16,7,42,146,101,202,204,55,190,249,20,8,53,86,136,144,133,44,245,125,94,140, +68,15,113,175,96,135,133,239,7,177,50,204,172,5,155,163,29,251,200,27,178,128,89,206,19,18,88,21,126,249, +210,205,0,56,239,249,99,131,156,63,250,233,249,243,223,62,238,63,124,246,112,239,229,199,151,7,79,247,30,30, +208,30,7,49,173,226,98,178,51,189,38,117,78,38,82,218,179,156,220,142,147,53,74,205,231,231,171,22,156,17, +92,14,206,240,182,10,77,194,91,222,145,135,231,223,190,115,187,247,124,103,227,120,197,35,158,97,219,29,111,107, +220,121,34,199,137,246,141,70,179,197,204,250,70,183,163,132,122,41,175,221,25,234,211,40,142,148,127,52,25,191, +105,234,16,79,122,183,168,100,73,216,14,178,89,199,191,200,197,171,180,190,120,133,47,68,130,93,129,71,157,31, +83,217,48,58,254,85,120,166,217,19,98,201,22,112,164,198,44,225,222,159,141,108,228,155,52,194,39,216,170,83, +229,84,5,220,221,96,188,191,16,33,230,61,134,198,229,46,24,74,45,22,196,132,206,194,52,39,50,196,162,166, +53,243,141,186,3,128,179,40,198,9,20,161,66,197,240,84,208,70,50,27,34,40,45,115,40,136,148,252,32,21, +250,204,0,79,210,150,107,53,44,178,73,232,54,254,117,30,186,51,89,168,81,15,136,185,49,56,210,62,17,201, +158,205,89,5,76,74,36,226,34,130,78,101,193,75,7,212,243,109,17,75,230,193,149,40,41,216,66,247,140,11, +189,104,44,226,231,64,120,132,17,61,232,73,88,135,202,46,226,107,15,139,198,49,93,16,52,122,69,228,125,188, +185,121,108,49,230,115,150,137,182,194,244,196,65,138,153,18,105,235,163,52,154,31,100,240,252,249,193,218,165,19, +125,41,193,37,151,50,104,3,250,82,252,60,201,131,90,5,209,12,79,35,247,1,72,183,141,149,22,240,150,8, +201,105,101,193,186,193,241,193,35,134,143,172,115,68,11,235,240,238,113,252,206,201,93,57,1,129,200,9,8,100, +162,176,206,218,70,228,214,177,156,136,104,222,4,13,57,99,199,142,161,141,35,28,126,71,221,99,153,114,149,48, +98,74,41,107,168,60,207,192,177,27,250,19,168,82,98,93,31,99,111,11,110,24,52,170,23,189,157,12,191,168, +171,253,57,42,104,234,170,160,142,170,133,214,86,247,39,173,170,46,105,208,1,3,69,169,162,105,80,104,85,148, +14,42,188,248,151,207,96,114,107,54,179,75,189,179,19,198,88,106,39,30,18,170,101,159,68,59,118,169,233,117, +212,82,131,47,195,94,224,217,128,91,106,42,0,195,17,219,248,201,196,45,252,52,98,7,63,137,152,225,39,22, +183,21,176,33,233,101,9,44,249,9,63,227,159,186,56,245,130,31,19,187,72,217,18,60,162,96,95,193,18,114, +134,78,183,16,102,9,217,103,11,33,227,167,98,194,95,3,228,88,241,55,66,206,95,211,96,31,253,160,84,133, +137,111,5,238,199,251,167,187,167,91,91,64,149,178,248,175,191,78,129,175,250,12,103,208,66,124,62,60,61,34, +62,245,133,45,248,41,255,10,175,8,122,73,255,13,181,139,14,203,127,236,235,255,215,162,210,55,66,106,187,21, +180,194,89,213,111,155,250,198,66,215,103,182,126,170,235,219,65,164,9,236,13,95,116,168,154,198,138,248,105,187, +42,243,27,211,32,35,213,145,14,237,155,149,171,182,156,155,160,76,144,49,162,239,179,112,73,133,169,207,147,52, +163,211,76,229,109,218,174,226,76,82,137,86,9,0,188,175,84,220,65,135,60,166,210,109,93,122,137,143,146,80, +117,6,93,0,145,174,225,115,210,197,205,174,82,156,70,121,103,12,90,248,22,47,96,63,146,60,238,171,244,172, +20,117,43,236,124,104,56,172,16,45,180,184,29,72,13,48,124,183,181,244,190,115,243,232,191,123,124,207,193,249, +56,204,255,167,134,91,19,31,2,218,48,34,185,46,54,150,54,224,181,161,35,196,88,83,168,63,23,76,206,118, +5,225,138,225,16,185,131,54,169,163,9,92,205,153,218,57,27,200,223,93,126,99,170,30,61,139,226,235,113,216, +72,170,240,95,42,40,53,42,42,74,121,227,143,234,99,69,18,72,198,31,63,34,207,48,63,76,201,124,41,88, +138,216,133,51,153,144,30,114,119,17,14,77,127,183,188,76,183,99,6,46,33,136,231,66,244,57,200,136,174,151, +231,51,39,118,223,207,231,250,76,103,183,248,12,116,159,208,69,127,213,77,171,164,105,209,219,13,191,187,35,69, +10,39,212,155,42,246,23,207,160,242,164,32,10,202,187,11,187,176,106,127,7,220,151,69,150,37,171,193,19,39, +60,179,152,88,5,219,172,252,81,174,82,138,128,161,168,212,111,214,2,100,215,1,176,247,35,95,117,17,242,48, +130,223,133,94,238,255,32,116,74,18,58,246,158,46,194,147,97,131,163,233,245,68,168,141,121,167,36,7,173,38, +130,244,81,134,172,65,250,107,180,146,167,153,196,21,255,234,227,22,206,47,244,167,107,4,41,83,243,9,103,215, +144,116,144,33,147,24,38,234,247,221,117,70,175,199,167,230,142,243,145,16,5,201,61,93,9,10,136,54,56,61, +174,6,202,244,150,0,123,62,153,221,187,154,149,81,137,238,99,18,246,156,11,168,119,145,88,82,115,56,96,226, +88,173,4,165,85,187,90,120,29,83,185,46,75,173,243,98,133,136,208,174,183,99,48,173,174,152,34,40,8,176, +12,84,58,5,54,135,26,34,45,160,10,15,196,233,189,148,253,192,187,3,115,250,210,14,93,187,139,40,26,141, +234,150,56,219,32,33,114,34,48,218,236,138,19,176,179,125,55,233,33,62,250,39,108,200,249,51,239,52,203,225, +37,233,142,231,5,191,230,143,219,243,148,229,218,221,154,178,2,133,0,213,205,77,252,61,148,96,108,170,90,232, +42,189,5,80,24,215,176,230,192,161,135,66,26,188,176,230,232,101,94,112,100,117,67,224,24,167,219,52,216,94, +241,169,175,0,79,233,188,175,76,21,198,63,234,187,233,28,210,9,62,174,0,92,139,183,239,46,134,215,190,67, +123,236,230,174,17,142,123,22,139,235,145,207,173,197,66,174,157,199,193,52,15,82,143,189,54,195,195,75,243,243, +213,188,111,171,193,242,135,120,83,98,160,254,222,90,177,94,50,125,247,4,99,158,182,113,237,195,35,248,180,74, +55,62,249,176,44,195,175,230,176,166,228,192,188,168,109,210,94,116,201,177,10,70,70,221,184,156,155,49,113,57, +40,157,172,149,20,6,33,179,170,36,150,240,214,162,127,88,250,214,126,231,148,77,81,134,195,0,219,1,169,218, +127,193,46,102,152,186,177,49,60,109,231,42,6,2,40,145,50,230,11,71,202,165,142,71,38,4,172,198,77,255, +247,175,64,135,213,202,19,85,139,146,136,207,157,94,168,132,89,205,195,126,88,208,164,73,173,124,80,209,144,8, +102,109,131,149,157,67,34,70,217,217,48,119,132,192,95,170,185,100,95,178,86,31,176,252,65,187,46,15,136,74, +54,55,187,50,81,124,215,183,167,58,232,223,225,62,149,204,146,205,77,252,29,190,14,250,163,43,151,171,46,190, +155,150,193,60,104,73,158,143,253,139,23,147,107,9,0,88,177,145,221,61,185,61,155,96,247,28,159,91,229,236, +106,36,170,53,223,90,134,250,218,46,243,198,166,110,86,109,190,78,2,253,196,36,46,197,164,226,37,71,124,41, +98,130,10,117,91,13,229,89,231,91,47,15,163,163,17,18,149,65,179,33,91,242,136,55,86,6,156,249,231,184, +44,154,149,234,152,17,71,69,143,140,39,62,47,104,88,223,62,241,4,79,76,125,213,235,4,195,165,244,211,142, +120,194,81,117,7,165,170,234,162,64,252,128,216,93,190,114,145,179,114,119,238,194,67,220,103,85,112,34,186,222, +11,135,238,107,21,80,226,114,50,13,255,131,168,141,113,79,167,206,22,134,14,1,86,142,250,17,173,145,105,166, +177,137,64,128,98,135,140,143,66,37,21,12,89,44,27,141,66,199,184,161,121,56,67,155,110,195,220,248,92,195, +220,64,188,69,184,36,240,137,199,253,144,64,79,108,78,237,80,226,2,36,67,183,6,67,200,12,46,197,69,124, +72,60,135,142,223,64,71,108,135,171,231,52,183,0,109,54,48,234,161,149,94,46,57,148,26,18,37,201,165,205, +98,222,104,135,251,82,56,135,82,52,206,1,93,186,202,153,73,115,63,17,203,214,114,226,103,22,164,200,106,89, +213,190,39,108,88,191,97,30,90,60,31,46,205,155,255,214,157,254,197,216,248,127,253,127,249,135,255,246,143,182, +126,184,185,27,177,229,181,145,16,238,106,86,218,59,59,199,95,107,164,50,157,45,193,165,56,113,211,103,67,150, +116,209,6,207,51,58,229,217,92,26,107,238,14,172,185,0,73,116,203,44,164,250,39,238,253,48,245,214,86,174, +180,123,66,162,201,65,79,184,225,244,73,135,150,136,21,119,134,27,255,90,6,146,245,3,165,29,222,212,6,2, +238,128,2,42,136,148,151,79,63,190,223,123,119,240,14,39,188,231,139,119,13,198,249,224,23,78,130,33,25,160, +77,42,204,141,22,18,25,162,193,144,98,232,238,63,201,9,123,244,122,215,230,107,181,110,114,193,218,45,122,118, +243,40,64,144,25,171,68,239,120,5,187,158,92,96,52,163,245,144,161,45,110,212,8,157,246,129,213,238,140,116, +186,224,224,234,48,80,80,237,19,73,59,247,42,204,2,228,194,53,249,21,204,220,206,203,23,91,135,170,69,143, +135,14,242,183,82,187,30,199,218,203,231,41,13,170,3,136,243,76,132,74,235,106,68,232,64,121,35,119,29,42, +166,110,226,250,94,143,179,117,77,221,200,69,141,28,141,184,49,1,149,201,165,63,146,59,225,167,84,80,122,132, +207,251,136,138,166,13,95,214,99,125,51,135,47,7,247,240,156,10,106,144,173,45,171,45,75,242,51,220,23,214, +204,158,51,123,147,241,234,91,68,173,169,217,12,111,63,240,32,48,60,144,151,233,145,155,139,211,85,224,169,150, +181,171,146,64,129,93,7,211,120,152,115,180,183,23,64,160,230,94,236,192,248,161,101,159,2,101,172,89,193,244, +115,125,148,108,79,148,165,206,189,241,212,190,71,209,240,177,180,122,230,149,138,157,149,199,5,228,177,244,209,94, +112,169,146,232,59,32,19,78,63,161,197,135,31,211,41,31,58,75,131,250,206,102,119,254,166,29,121,46,225,78, +163,69,49,106,35,172,74,215,172,116,251,116,170,229,106,133,135,69,201,181,131,150,188,114,228,117,228,132,36,51, +71,94,55,14,178,34,113,208,23,177,163,184,47,81,198,210,35,66,230,160,230,207,212,248,179,109,220,251,201,145, +245,11,29,53,83,220,243,88,149,239,133,160,193,175,78,164,237,179,234,127,111,187,203,30,216,86,115,248,168,239, +189,131,241,79,29,164,199,235,11,145,185,55,168,35,250,135,12,2,7,5,242,135,161,115,220,191,111,93,30,54, +131,0,123,133,12,2,135,230,15,148,94,178,19,97,126,111,29,213,242,169,122,94,60,153,168,12,130,183,204,171, +150,50,194,41,68,18,65,7,254,248,65,60,4,47,224,79,232,23,211,122,143,122,203,27,190,249,252,153,72,248, +79,34,27,183,94,84,254,155,200,58,95,212,30,85,108,148,240,145,104,176,252,116,12,232,33,31,196,254,56,225, +207,197,7,254,194,13,24,126,101,123,4,189,162,104,57,251,109,115,243,55,235,142,194,103,204,225,95,107,83,141, +209,128,156,1,175,201,241,207,50,50,25,151,18,12,86,73,242,95,133,103,107,246,170,71,57,5,19,202,40,152, +242,63,197,54,127,69,191,117,77,5,89,139,3,246,109,128,17,125,102,207,252,145,213,40,168,172,156,205,26,223, +55,155,9,33,158,118,33,123,92,121,177,185,57,0,24,49,139,184,103,231,162,103,221,122,85,116,110,239,232,217, +48,48,196,29,255,233,125,113,123,186,185,121,51,15,149,15,26,192,139,155,99,232,56,52,179,46,176,160,93,10, +207,218,8,225,180,135,160,101,235,136,159,30,0,136,231,98,93,116,117,249,164,249,225,75,160,31,71,76,246,0, +52,184,213,145,180,165,70,53,215,66,34,111,226,227,250,103,15,158,141,181,126,116,168,179,159,189,163,139,3,225, +222,124,0,14,221,2,204,71,64,66,15,225,115,24,132,6,230,230,195,181,183,226,69,63,103,64,69,12,198,121, +81,167,73,42,99,255,188,43,219,12,121,44,0,29,57,117,87,181,251,166,7,250,43,132,52,31,202,73,169,164, +18,155,133,248,39,15,197,196,248,124,115,235,243,13,245,13,38,60,47,242,195,144,92,191,28,110,160,102,92,156, +6,205,56,9,211,140,199,162,177,251,197,151,40,171,179,194,79,68,99,226,221,202,149,146,204,89,10,156,202,184, +61,214,208,170,137,34,52,212,223,231,238,149,87,62,31,65,23,79,230,149,40,2,70,54,246,137,141,108,243,74, +36,172,240,57,181,177,19,19,69,231,25,212,114,92,18,162,177,14,205,249,146,253,212,66,151,110,80,200,154,196, +73,244,53,202,0,226,10,88,36,242,154,85,190,63,55,145,174,138,199,124,233,7,49,218,130,37,43,28,75,143, +158,52,34,136,75,251,176,37,53,174,86,206,26,147,79,157,187,219,48,229,128,201,56,239,67,32,55,245,146,6, +213,146,214,226,18,52,196,238,47,115,150,139,53,150,193,60,245,131,72,188,125,51,33,121,142,167,87,180,82,37, +21,211,60,173,117,223,218,38,189,93,96,51,44,167,183,34,151,160,25,32,144,92,223,78,58,57,103,133,200,14, +61,58,132,91,228,124,156,23,212,53,0,108,255,231,205,205,63,152,247,147,101,62,27,230,214,13,135,71,17,149, +244,243,113,94,155,168,125,143,33,209,76,219,131,87,8,133,163,52,182,36,43,197,59,230,118,127,49,223,27,203, +51,90,236,142,53,238,117,143,205,121,237,7,116,241,103,94,171,248,255,250,209,121,65,225,85,140,61,175,235,224, +21,47,199,74,68,91,40,179,201,132,84,124,162,170,7,157,59,221,80,35,33,94,209,62,214,99,253,239,25,86, +60,186,214,187,118,147,111,71,122,97,25,184,158,248,175,52,113,179,18,152,126,47,155,102,45,77,184,189,80,128, +249,225,90,193,85,30,110,51,116,147,84,223,168,0,157,181,159,28,49,80,86,223,62,82,148,220,82,253,159,188, +80,231,18,71,6,217,50,253,17,45,191,193,96,118,116,119,80,144,54,186,224,196,216,100,213,142,234,54,34,21, +139,92,200,13,195,31,226,141,180,134,223,202,100,90,20,32,66,210,234,138,121,159,75,209,105,181,90,169,122,72, +97,93,158,89,205,146,26,91,207,165,143,74,99,43,126,231,72,109,116,206,82,10,146,83,121,78,253,119,255,103, +251,238,211,222,157,237,147,205,8,171,93,9,251,255,217,218,129,57,54,89,27,156,20,136,5,53,112,7,45,236, +27,65,246,131,169,41,95,50,211,51,71,139,157,122,91,115,194,36,77,155,228,81,98,155,243,181,135,62,113,141, +147,111,86,111,231,150,247,160,172,79,43,74,45,131,10,136,65,181,135,17,87,212,18,4,191,115,215,182,128,3, +55,119,28,25,75,230,72,117,110,177,183,253,72,240,123,99,239,228,226,3,59,53,107,210,69,2,115,18,20,98, +64,175,32,185,74,59,164,4,72,119,121,221,133,201,115,35,63,4,72,74,23,173,29,84,26,162,20,35,148,219, +23,213,129,200,220,231,165,222,229,145,248,29,137,49,198,211,137,173,238,184,232,208,199,7,122,32,94,243,56,3, +169,237,59,166,96,252,230,92,109,180,191,139,219,90,30,173,183,214,10,70,97,79,173,244,109,59,182,67,216,243, +139,100,53,210,37,197,135,65,158,36,132,120,70,235,69,63,169,114,88,21,112,88,61,7,128,134,71,131,48,95, +132,58,66,145,116,251,167,225,85,49,115,154,184,135,182,158,249,235,124,214,198,249,42,231,51,118,161,91,104,201, +209,136,53,189,92,186,178,22,21,224,3,187,251,136,188,215,174,31,104,29,22,234,122,133,18,121,201,22,61,102, +207,248,35,131,185,207,120,235,231,83,105,15,208,4,174,72,210,145,72,72,48,28,42,120,182,66,214,37,17,177, +149,156,103,48,8,82,209,224,167,106,35,192,223,20,201,92,28,68,111,226,0,112,220,164,26,217,21,180,242,73, +173,89,119,166,184,236,130,175,87,62,140,54,217,60,79,29,166,225,245,136,116,218,161,58,36,207,2,19,147,251, +206,192,37,70,165,57,13,188,130,38,25,241,65,37,115,118,193,99,248,104,172,134,151,174,137,109,123,116,22,76, +218,206,62,7,62,22,154,37,175,196,116,247,11,171,57,187,248,164,72,144,82,9,5,110,186,155,106,242,181,168, +192,138,218,75,251,57,229,193,111,222,102,116,204,161,250,241,20,30,254,154,223,184,65,46,244,28,176,91,80,43, +142,101,215,180,234,120,82,170,149,2,5,46,48,98,216,137,131,243,50,140,228,181,22,68,103,87,15,44,66,177, +182,8,253,247,46,46,190,85,183,188,80,104,220,185,22,118,174,57,43,250,115,5,207,134,135,254,122,190,210,219, +51,153,80,57,117,114,252,195,206,227,223,119,201,244,190,69,4,200,129,250,156,84,194,35,145,170,16,193,100,55, +251,177,52,246,194,110,182,181,165,31,221,136,146,34,45,187,24,160,1,134,25,191,60,98,146,55,32,80,204,121, +113,85,184,4,222,7,119,62,202,247,52,78,43,245,59,20,36,49,174,60,33,208,134,241,239,221,14,191,231,27, +226,133,227,97,76,197,90,236,153,135,230,153,67,110,226,1,234,107,195,26,80,19,10,237,43,211,159,70,10,9, +156,132,18,102,5,47,199,245,28,162,214,49,83,244,141,72,94,174,203,25,229,126,117,201,194,40,109,237,148,164, +19,117,25,248,158,200,163,48,182,70,254,133,92,88,169,64,146,57,168,104,77,148,229,236,130,16,67,100,218,9, +129,14,103,114,26,185,133,117,0,176,252,122,65,105,156,132,239,6,137,121,37,188,214,83,233,9,81,116,139,209, +199,93,172,128,241,141,250,120,21,219,17,25,146,157,46,182,134,105,204,231,69,48,176,29,60,183,180,234,132,144, +200,112,210,233,30,177,167,188,245,186,241,45,149,188,192,124,231,193,133,250,69,76,138,54,171,142,135,190,63,47, +131,106,14,114,107,177,71,66,176,92,160,197,31,12,190,73,197,82,164,156,59,75,129,207,203,115,4,69,134,23, +219,65,47,23,29,98,25,164,96,157,188,223,193,28,31,214,200,51,199,143,138,134,168,173,93,204,254,163,24,165, +133,63,117,95,67,9,117,159,104,129,13,118,80,190,145,19,47,200,212,189,250,203,37,141,115,70,18,69,66,113, +44,173,143,20,9,60,240,145,2,5,173,2,87,202,73,26,219,10,63,111,147,98,32,28,39,193,132,3,245,2, +231,105,227,58,21,63,1,85,172,110,225,11,13,131,62,11,191,56,56,49,27,81,80,92,195,85,101,150,228,69, +58,33,81,171,238,29,210,162,121,142,244,7,147,112,134,73,87,44,105,113,195,75,129,244,153,24,83,14,153,243, +185,131,24,159,124,85,70,116,202,18,95,217,209,137,235,250,226,3,154,33,193,26,203,145,208,220,13,223,99,100, +78,180,9,214,127,169,199,41,15,207,142,184,57,184,165,106,49,82,217,15,108,145,151,194,140,227,196,98,75,223, +183,196,96,33,250,20,59,247,85,88,63,7,175,98,93,215,185,186,25,73,137,11,118,114,3,16,104,236,226,100, +119,249,227,201,238,146,100,127,67,210,100,137,32,64,66,110,83,166,22,232,112,217,125,222,195,122,169,68,195,115, +37,8,113,40,134,79,186,3,30,47,244,209,184,59,249,222,233,206,153,61,211,192,58,92,142,82,123,188,191,191, +215,100,242,117,90,213,116,108,168,182,143,127,46,243,68,70,89,88,134,184,67,183,254,12,209,109,59,101,41,209, +231,30,81,181,105,120,242,238,205,122,77,115,183,174,126,80,156,202,92,85,167,28,57,23,7,234,255,227,200,146, +226,94,103,166,215,179,180,157,196,139,131,55,175,41,215,237,113,145,101,218,228,51,109,189,6,114,127,159,25,152, +164,105,217,151,232,209,181,189,145,113,26,154,97,223,164,103,18,130,67,109,27,213,145,242,28,35,79,228,77,184, +68,149,74,102,138,239,195,20,239,71,201,50,149,125,169,247,89,115,156,230,109,193,142,177,255,243,243,215,106,23, +117,55,212,223,54,103,11,89,118,245,247,68,103,251,242,216,105,40,104,244,174,234,174,21,234,122,109,232,205,108, +83,209,16,17,62,106,146,164,27,21,91,180,127,34,165,29,6,25,47,116,95,116,250,184,145,23,155,108,189,104, +34,51,75,125,38,46,21,97,54,223,195,21,29,68,106,38,251,2,89,235,155,155,189,79,201,152,171,86,5,89, +113,132,120,190,135,197,113,159,48,126,223,234,132,187,63,222,212,152,128,251,255,248,241,230,162,136,191,226,247,164, +62,203,238,255,227,255,1,204,222,188,142,16,149,2,0 +}; + +#endif diff --git a/boards/ESP32-S3-WROOM-1-N4.json b/boards/ESP32-S3-WROOM-1-N4.json new file mode 100644 index 00000000..160926b2 --- /dev/null +++ b/boards/ESP32-S3-WROOM-1-N4.json @@ -0,0 +1,39 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-D ARDUINO_USB_CDC_ON_BOOT=0", + "-D ARDUINO_USB_MSC_ON_BOOT=0", + "-D ARDUINO_USB_DFU_ON_BOOT=0", + "-D ARDUINO_USB_MODE=0", + "-D ARDUINO_RUNNING_CORE=1", + "-D ARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "ESP32-S3-WROOM-1-N4" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "ESP32-S3-WROOM-1-N4 (4 MB Flash, No PSRAM)", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 524288, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.espressif.com/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf", + "vendor": "Espressif" +} diff --git a/boards/ebyte_eora-s3.json b/boards/ebyte_eora-s3.json new file mode 100644 index 00000000..96945c1d --- /dev/null +++ b/boards/ebyte_eora-s3.json @@ -0,0 +1,45 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_LILYGO_T3_S3_V1_X", + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DARDUINO_USB_MODE=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": "Ebyte EoRa-S3-XXXTB Radio", + "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": 460800 + }, + "url": "https://www.cdebyte.com/products/EoRa-S3-900TB", + "vendor": "Chengdu Ebyte Electronic Technology Co., Ltd" +} \ No newline at end of file diff --git a/boards/esp32-s3-zero.json b/boards/esp32-s3-zero.json new file mode 100644 index 00000000..bf7aec12 --- /dev/null +++ b/boards/esp32-s3-zero.json @@ -0,0 +1,40 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-D ARDUINO_USB_CDC_ON_BOOT=1", + "-D ARDUINO_USB_MSC_ON_BOOT=0", + "-D ARDUINO_USB_DFU_ON_BOOT=0", + "-D ARDUINO_USB_MODE=1", + "-D ARDUINO_RUNNING_CORE=1", + "-D ARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "ESP32-S3-Zero", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.espressif.com", + "vendor": "Espressif" +} + diff --git a/boards/heltec_e213.json b/boards/heltec_e213.json new file mode 100644 index 00000000..81efd8f5 --- /dev/null +++ b/boards/heltec_e213.json @@ -0,0 +1,44 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [ + ["0x303A", "0x1001"], + ["0x303A", "0x0002"] + ], + "mcu": "esp32s3", + "variant": "heltec_vision_master_e213" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec Vision Master E213", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 8388608, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/project/vision-master-e213/", + "vendor": "Heltec" +} diff --git a/boards/heltec_e290.json b/boards/heltec_e290.json new file mode 100644 index 00000000..07577557 --- /dev/null +++ b/boards/heltec_e290.json @@ -0,0 +1,44 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [ + ["0x303A", "0x1001"], + ["0x303A", "0x0002"] + ], + "mcu": "esp32s3", + "variant": "heltec_vision_master_e290" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec Vision Master E290", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 8388608, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/project/vision-master-e290/", + "vendor": "Heltec" +} diff --git a/boards/heltec_mesh_pocket.json b/boards/heltec_mesh_pocket.json new file mode 100644 index 00000000..e6a04c12 --- /dev/null +++ b/boards/heltec_mesh_pocket.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_pocket", + "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": "Heltec nrf (Adafruit BSP)", + "upload": { + "maximum_ram_size": 235520, + "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://heltec.org/project/meshpocket/", + "vendor": "Heltec" + } diff --git a/boards/heltec_mesh_solar.json b/boards/heltec_mesh_solar.json new file mode 100644 index 00000000..dcd1da64 --- /dev/null +++ b/boards/heltec_mesh_solar.json @@ -0,0 +1,61 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A","0x8029"], + ["0x239A","0x0029"], + ["0x239A","0x002A"], + ["0x239A","0x802A"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_solar", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Heltec Mesh Solar Board", + "upload": { + "maximum_ram_size": 235520, + "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://heltec.org/", + "vendor": "Heltec" +} \ No newline at end of file diff --git a/boards/heltec_t096.json b/boards/heltec_t096.json new file mode 100644 index 00000000..9d4f3037 --- /dev/null +++ b/boards/heltec_t096.json @@ -0,0 +1,61 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A","0x8029"], + ["0x239A","0x0029"], + ["0x239A","0x002A"], + ["0x239A","0x802A"] + ], + "usb_product": "HT-n5262G", + "mcu": "nrf52840", + "variant": "Heltec_T096_Board", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Heltec T096 Board", + "upload": { + "maximum_ram_size": 235520, + "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://heltec.org/", + "vendor": "Heltec" +} \ No newline at end of file diff --git a/boards/heltec_t1.json b/boards/heltec_t1.json new file mode 100644 index 00000000..53bfc79a --- /dev/null +++ b/boards/heltec_t1.json @@ -0,0 +1,61 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A","0x8029"], + ["0x239A","0x0029"], + ["0x239A","0x002A"], + ["0x239A","0x802A"] + ], + "usb_product": "HT-n5262T1", + "mcu": "nrf52840", + "variant": "heltec_t1", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Heltec Mesh Node T1 Board", + "upload": { + "maximum_ram_size": 235520, + "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://heltec.org/", + "vendor": "Heltec" +} diff --git a/boards/heltec_t114.json b/boards/heltec_t114.json new file mode 100644 index 00000000..a4a61d04 --- /dev/null +++ b/boards/heltec_t114.json @@ -0,0 +1,61 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A","0x8029"], + ["0x239A","0x0029"], + ["0x239A","0x002A"], + ["0x239A","0x802A"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "Heltec_T114_Board", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Heltec T114 Board", + "upload": { + "maximum_ram_size": 235520, + "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://heltec.org/", + "vendor": "Heltec" +} \ No newline at end of file diff --git a/boards/heltec_t190.json b/boards/heltec_t190.json new file mode 100644 index 00000000..6aa674fc --- /dev/null +++ b/boards/heltec_t190.json @@ -0,0 +1,44 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [ + ["0x303A", "0x1001"], + ["0x303A", "0x0002"] + ], + "mcu": "esp32s3", + "variant": "heltec_vision_master_t190" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec Vision Master T190", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 8388608, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/project/vision-master-t190/", + "vendor": "Heltec" +} diff --git a/boards/heltec_tracker_v2.json b/boards/heltec_tracker_v2.json new file mode 100644 index 00000000..62b569e0 --- /dev/null +++ b/boards/heltec_tracker_v2.json @@ -0,0 +1,40 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-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", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_tracker_v2" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "heltec_tracker v2", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "heltec" +} \ No newline at end of file diff --git a/boards/heltec_v4.json b/boards/heltec_v4.json new file mode 100644 index 00000000..36cdfc04 --- /dev/null +++ b/boards/heltec_v4.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-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": "qspi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_v4" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "heltec_wifi_lora_32 v4 (16 MB FLASH, 2 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 2097152, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "heltec" +} \ No newline at end of file diff --git a/boards/keepteen_lt1.json b/boards/keepteen_lt1.json new file mode 100644 index 00000000..e853c633 --- /dev/null +++ b/boards/keepteen_lt1.json @@ -0,0 +1,79 @@ +{ + "build": { + "arduino":{ + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x00B3" + ], + [ + "0x239A", + "0x8029" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ], + [ + "0x239A", + "0x802A" + ] + ], + "usb_product": "Keepteen LT1", + "mcu": "nrf52840", + "variant": "Keepteen LT1", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino", + "zephyr" + ], + "name": "Keepteen LT1", + "upload": { + "maximum_ram_size": 235520, + "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": "http://www.keepteen.com/", + "vendor": "Keepteen" + } \ No newline at end of file diff --git a/boards/lilygo_t_impulse_plus_nrf52840.json b/boards/lilygo_t_impulse_plus_nrf52840.json new file mode 100644 index 00000000..3dc45011 --- /dev/null +++ b/boards/lilygo_t_impulse_plus_nrf52840.json @@ -0,0 +1,65 @@ +{ + "build": { + "arduino":{ + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_T_IMPULSE_PLUS -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x8029" + ] + ], + "usb_product": "T-Impulse-Plus-nRF52840", + "mcu": "nrf52840", + "variant": "t_impulse_plus_nrf52840", + "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": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Lilygo T-Impulse-Plus-nRF52840", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "require_upload_port": true, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "wait_for_upload_port": true + }, + "url": "https://www.lilygo.cc/", + "vendor": "Lilygo" +} diff --git a/boards/meshtiny.json b/boards/meshtiny.json new file mode 100644 index 00000000..2e38a9cc --- /dev/null +++ b/boards/meshtiny.json @@ -0,0 +1,74 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x8029" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ], + [ + "0x239A", + "0x802A" + ] + ], + "usb_product": "Meshtiny", + "mcu": "nrf52840", + "variant": "meshtiny", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": [ + "arduino", + "freertos" + ], + "name": "Meshtiny", + "upload": { + "maximum_ram_size": 235520, + "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://shop.mtoolstec.com/product/meshtiny", + "vendor": "MTools Tec" +} diff --git a/boards/minewsemi_me25ls01.json b/boards/minewsemi_me25ls01.json new file mode 100644 index 00000000..26b92d43 --- /dev/null +++ b/boards/minewsemi_me25ls01.json @@ -0,0 +1,59 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "me25ls01-BOOT", + "mcu": "nrf52840", + "variant": "minewsemi_me25ls01", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": ["arduino"], + "name": "Minewsemi ME25LS01", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 811008, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01", + "vendor": "MINEWSEMI" +} diff --git a/boards/nano-g2-ultra.json b/boards/nano-g2-ultra.json new file mode 100644 index 00000000..3246167e --- /dev/null +++ b/boards/nano-g2-ultra.json @@ -0,0 +1,73 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x8029" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ], + [ + "0x239A", + "0x802A" + ] + ], + "usb_product": "BQ nRF52840", + "mcu": "nrf52840", + "variant": "nano-g2-ultra", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "BQ nRF52840", + "upload": { + "maximum_ram_size": 235520, + "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://wiki.uniteng.com/en/meshtastic/nano-g2-ultra", + "vendor": "BQ Consulting" +} diff --git a/boards/nrf52840_s140_v6.ld b/boards/nrf52840_s140_v6.ld new file mode 100644 index 00000000..6dad975b --- /dev/null +++ b/boards/nrf52840_s140_v6.ld @@ -0,0 +1,38 @@ +/* Linker script to configure memory regions. */ + +SEARCH_DIR(.) +GROUP(-lgcc -lc -lnosys) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000 + + /* SRAM required by Softdevice depend on + * - Attribute Table Size (Number of Services and Characteristics) + * - Vendor UUID count + * - Max ATT MTU + * - Concurrent connection peripheral + central + secure links + * - Event Len, HVN queue, Write CMD queue + */ + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 +} + +SECTIONS +{ + . = ALIGN(4); + .svc_data : + { + PROVIDE(__start_svc_data = .); + KEEP(*(.svc_data)) + PROVIDE(__stop_svc_data = .); + } > RAM + + .fs_data : + { + PROVIDE(__start_fs_data = .); + KEEP(*(.fs_data)) + PROVIDE(__stop_fs_data = .); + } > RAM +} INSERT AFTER .data; + +INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v6_extrafs.ld b/boards/nrf52840_s140_v6_extrafs.ld new file mode 100644 index 00000000..35261067 --- /dev/null +++ b/boards/nrf52840_s140_v6_extrafs.ld @@ -0,0 +1,38 @@ +/* Linker script to configure memory regions. */ + +SEARCH_DIR(.) +GROUP(-lgcc -lc -lnosys) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000 + + /* SRAM required by Softdevice depend on + * - Attribute Table Size (Number of Services and Characteristics) + * - Vendor UUID count + * - Max ATT MTU + * - Concurrent connection peripheral + central + secure links + * - Event Len, HVN queue, Write CMD queue + */ + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 +} + +SECTIONS +{ + . = ALIGN(4); + .svc_data : + { + PROVIDE(__start_svc_data = .); + KEEP(*(.svc_data)) + PROVIDE(__stop_svc_data = .); + } > RAM + + .fs_data : + { + PROVIDE(__start_fs_data = .); + KEEP(*(.fs_data)) + PROVIDE(__stop_fs_data = .); + } > RAM +} INSERT AFTER .data; + +INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7.ld b/boards/nrf52840_s140_v7.ld new file mode 100644 index 00000000..6aaeb403 --- /dev/null +++ b/boards/nrf52840_s140_v7.ld @@ -0,0 +1,38 @@ +/* Linker script to configure memory regions. */ + +SEARCH_DIR(.) +GROUP(-lgcc -lc -lnosys) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000 + + /* SRAM required by Softdevice depend on + * - Attribute Table Size (Number of Services and Characteristics) + * - Vendor UUID count + * - Max ATT MTU + * - Concurrent connection peripheral + central + secure links + * - Event Len, HVN queue, Write CMD queue + */ + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 +} + +SECTIONS +{ + . = ALIGN(4); + .svc_data : + { + PROVIDE(__start_svc_data = .); + KEEP(*(.svc_data)) + PROVIDE(__stop_svc_data = .); + } > RAM + + .fs_data : + { + PROVIDE(__start_fs_data = .); + KEEP(*(.fs_data)) + PROVIDE(__stop_fs_data = .); + } > RAM +} INSERT AFTER .data; + +INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7_extrafs.ld b/boards/nrf52840_s140_v7_extrafs.ld new file mode 100644 index 00000000..5956183a --- /dev/null +++ b/boards/nrf52840_s140_v7_extrafs.ld @@ -0,0 +1,38 @@ +/* Linker script to configure memory regions. */ + +SEARCH_DIR(.) +GROUP(-lgcc -lc -lnosys) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xD4000 - 0x27000 + + /* SRAM required by Softdevice depend on + * - Attribute Table Size (Number of Services and Characteristics) + * - Vendor UUID count + * - Max ATT MTU + * - Concurrent connection peripheral + central + secure links + * - Event Len, HVN queue, Write CMD queue + */ + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 +} + +SECTIONS +{ + . = ALIGN(4); + .svc_data : + { + PROVIDE(__start_svc_data = .); + KEEP(*(.svc_data)) + PROVIDE(__stop_svc_data = .); + } > RAM + + .fs_data : + { + PROVIDE(__start_fs_data = .); + KEEP(*(.fs_data)) + PROVIDE(__stop_fs_data = .); + } > RAM +} INSERT AFTER .data; + +INCLUDE "nrf52_common.ld" diff --git a/boards/promicro_nrf52840.json b/boards/promicro_nrf52840.json new file mode 100644 index 00000000..a4460f31 --- /dev/null +++ b/boards/promicro_nrf52840.json @@ -0,0 +1,79 @@ +{ + "build": { + "arduino":{ + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x00B3" + ], + [ + "0x239A", + "0x8029" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ], + [ + "0x239A", + "0x802A" + ] + ], + "usb_product": "ProMicro NRF52840", + "mcu": "nrf52840", + "variant": "promicro_nrf52840", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino", + "zephyr" + ], + "name": "ProMicro NRF52840", + "upload": { + "maximum_ram_size": 235520, + "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.nologo.tech/en/product/otherboard/NRF52840.html", + "vendor": "Nologo" + } \ No newline at end of file diff --git a/boards/rak3172.json b/boards/rak3172.json new file mode 100644 index 00000000..dbb70b03 --- /dev/null +++ b/boards/rak3172.json @@ -0,0 +1,33 @@ +{ + "build": { + "arduino": { + "variant_h": "variant_RAK3172_MODULE.h" + }, + "core": "stm32", + "cpu": "cortex-m4", + "extra_flags": "-DSTM32WL -DSTM32WLxx -DSTM32WLE5xx", + "framework_extra_flags": { + "arduino": "-DUSE_CM4_STARTUP_FILE -DARDUINO_RAK3172_MODULE" + }, + "f_cpu": "48000000L", + "mcu": "stm32wle5ccu", + "product_line": "STM32WLE5xx", + "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U" + }, + "debug": { + "default_tools": ["stlink"], + "jlink_device": "STM32WLE5CC", + "openocd_target": "stm32wlx", + "svd_path": "STM32WLE5_CM4.svd" + }, + "frameworks": ["arduino"], + "name": "BB-STM32WL", + "upload": { + "maximum_ram_size": 65536, + "maximum_size": 262144, + "protocol": "stlink", + "protocols": ["stlink", "jlink"] + }, + "url": "https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172", + "vendor": "RAK" +} diff --git a/boards/rak3401.json b/boards/rak3401.json new file mode 100644 index 00000000..aa95b61d --- /dev/null +++ b/boards/rak3401.json @@ -0,0 +1,73 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x8029" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ], + [ + "0x239A", + "0x802A" + ] + ], + "usb_product": "WisCore RAK3401 Board", + "mcu": "nrf52840", + "variant": "WisCore_RAK3401_Board", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "WisCore RAK3401 Board", + "upload": { + "maximum_ram_size": 235520, + "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.rakwireless.com", + "vendor": "RAKwireless" +} diff --git a/boards/rak4631.json b/boards/rak4631.json new file mode 100644 index 00000000..217077f1 --- /dev/null +++ b/boards/rak4631.json @@ -0,0 +1,73 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x8029" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ], + [ + "0x239A", + "0x802A" + ] + ], + "usb_product": "WisCore RAK4631 Board", + "mcu": "nrf52840", + "variant": "WisCore_RAK4631_Board", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "WisCore RAK4631 Board", + "upload": { + "maximum_ram_size": 235520, + "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.rakwireless.com", + "vendor": "RAKwireless" +} diff --git a/boards/seeed-wio-tracker-l1.json b/boards/seeed-wio-tracker-l1.json new file mode 100644 index 00000000..b2cced19 --- /dev/null +++ b/boards/seeed-wio-tracker-l1.json @@ -0,0 +1,62 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_SEEED_WIO_TRACKER_L1 -DNRF52840_XXAA -DSEEED_WIO_TRACKER_L1 ", + "f_cpu": "64000000L", + "hwids": [ + [ "0x2886", "0x1667" ], + [ "0x2886", "0x1668" ] + ], + "mcu": "nrf52840", + "variant": "Seeed_Wio_Tracker_L1", + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bsp": { + "name": "adafruit" + }, + "bootloader": { + "settings_addr": "0xFF000" + }, + "usb_product": "Seeed Wio Tracker L1" + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "openocd_target": "nrf52.cfg", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Seeed Wio Tracker L1", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 811008, + "protocol": "nrfutil", + "speed": 115200, + "protocols": [ + "jlink", + "stlink", + "nrfjprog", + "nrfutil", + "cmsis-dap", + "sam-ba", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.seeedstudio.com/wio_tracker_l1_node/", + "vendor": "Seeed Studio" +} \ No newline at end of file diff --git a/boards/seeed-xiao-afruitnrf52-nrf52840.json b/boards/seeed-xiao-afruitnrf52-nrf52840.json new file mode 100644 index 00000000..9a60e0a7 --- /dev/null +++ b/boards/seeed-xiao-afruitnrf52-nrf52840.json @@ -0,0 +1,61 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_Seeed_XIAO_nRF52840 -DNRF52840_XXAA -DSEEED_XIAO_NRF52840 ", + "f_cpu": "64000000L", + "hwids": [ + [ "0x2886", "0x8044" ], + [ "0x2886", "0x0044" ] + ], + "mcu": "nrf52840", + "variant": "Seeed_XIAO_nRF52840", + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bsp": { + "name": "adafruit" + }, + "bootloader": { + "settings_addr": "0xFF000" + }, + "usb_product": "XIAO nRF52840" + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "openocd_target": "nrf52.cfg", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Seeed Studio XIAO nRF52840", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 811008, + "protocol": "nrfutil", + "speed": 115200, + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "cmsis-dap", + "sam-ba", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.seeedstudio.com/XIAO_BLE", + "vendor": "Seeed Studio" +} diff --git a/boards/seeed_sensecap_solar.json b/boards/seeed_sensecap_solar.json new file mode 100644 index 00000000..50f08976 --- /dev/null +++ b/boards/seeed_sensecap_solar.json @@ -0,0 +1,60 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_Seeed_XIAO_nRF52840 -DNRF52840_XXAA -DSEEED_XIAO_NRF52840 ", + "f_cpu": "64000000L", + "hwids": [ + [ "0x2886", "0x0059" ] + ], + "mcu": "nrf52840", + "variant": "Seeed_XIAO_nRF52840", + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bsp": { + "name": "adafruit" + }, + "bootloader": { + "settings_addr": "0xFF000" + }, + "usb_product": "XIAO nRF52840" + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "openocd_target": "nrf52.cfg", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Seeed Studio XIAO nRF52840", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 811008, + "protocol": "nrfutil", + "speed": 115200, + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "cmsis-dap", + "sam-ba", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.seeedstudio.com/meshtastic_solar_node/", + "vendor": "Seeed Studio" +} \ No newline at end of file diff --git a/boards/station-g2.json b/boards/station-g2.json new file mode 100644 index 00000000..8ea6ebc6 --- /dev/null +++ b/boards/station-g2.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "BQ Station G2", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://wiki.uniteng.com/en/meshtastic/station-g2", + "vendor": "BQ Consulting" +} diff --git a/boards/station-g3-esp32.json b/boards/station-g3-esp32.json new file mode 100644 index 00000000..abfe5ece --- /dev/null +++ b/boards/station-g3-esp32.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Station G3 ESP32", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://wiki.bqvoy.com/en/devkits/station-g3", + "vendor": "BQ Consulting" +} diff --git a/boards/t-deck.json b/boards/t-deck.json new file mode 100644 index 00000000..6942ab01 --- /dev/null +++ b/boards/t-deck.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo T-Deck (16M Flash 8M PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.lilygo.cc", + "vendor": "LilyGo" +} \ No newline at end of file diff --git a/boards/t-echo.json b/boards/t-echo.json new file mode 100644 index 00000000..e0df6eac --- /dev/null +++ b/boards/t-echo.json @@ -0,0 +1,66 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_PCA10056 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x8029" + ] + ], + "usb_product": "NRF52 DK", + "mcu": "nrf52840", + "variant": "pca10056", + "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": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "LilyGo T-ECHO", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 815104, + "require_upload_port": true, + "speed": 115200, + "protocol": "jlink", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "wait_for_upload_port": true + }, + "url": "https://os.mbed.com/platforms/Nordic-nRF52840-DK/", + "vendor": "Nordic" +} diff --git a/boards/t3_s3_v1_x.json b/boards/t3_s3_v1_x.json new file mode 100644 index 00000000..d09c354a --- /dev/null +++ b/boards/t3_s3_v1_x.json @@ -0,0 +1,45 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_LILYGO_T3_S3_V1_X", + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DARDUINO_USB_MODE=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": "LilyGo T3-S3 Radio", + "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": 460800 + }, + "url": "https://www.lilygo.cc", + "vendor": "LilyGo" +} \ No newline at end of file diff --git a/boards/t_beam_1w.json b/boards/t_beam_1w.json new file mode 100644 index 00000000..2f1159aa --- /dev/null +++ b/boards/t_beam_1w.json @@ -0,0 +1,50 @@ +{ + "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": "lilygo_tbeam_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/boards/t_beams3_supreme.json b/boards/t_beams3_supreme.json new file mode 100644 index 00000000..3eb9c016 --- /dev/null +++ b/boards/t_beams3_supreme.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_USB_MODE=1", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "LilyGo T-Beam supreme (8MB Flash 8MB PSRAM)", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 8388608, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.lilygo.cc/products/t-beamsupreme-m", + "vendor": "LilyGo" + } \ No newline at end of file diff --git a/boards/thinknode_m1.json b/boards/thinknode_m1.json new file mode 100644 index 00000000..5313d2a1 --- /dev/null +++ b/boards/thinknode_m1.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_TTGO_EINK -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "elecrow_eink", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M1", + "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": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "elecrow eink", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/Elecrow-RD", + "vendor": "ELECROW" +} \ No newline at end of file diff --git a/boards/thinknode_m3.json b/boards/thinknode_m3.json new file mode 100644 index 00000000..2920b82d --- /dev/null +++ b/boards/thinknode_m3.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "elecrow_eink", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M3", + "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": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "elecrow nrf", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/Elecrow-RD", + "vendor": "ELECROW" +} \ No newline at end of file diff --git a/boards/thinknode_m6.json b/boards/thinknode_m6.json new file mode 100644 index 00000000..da07b3d8 --- /dev/null +++ b/boards/thinknode_m6.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_ELECROW_M6 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "elecrow_solar", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M6", + "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": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "elecrow solar", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/Elecrow-RD", + "vendor": "ELECROW" +} diff --git a/boards/tiny_relay.json b/boards/tiny_relay.json new file mode 100644 index 00000000..517d11f0 --- /dev/null +++ b/boards/tiny_relay.json @@ -0,0 +1,33 @@ +{ + "build": { + "arduino": { + "variant_h": "variant_RAK3172_MODULE.h" + }, + "core": "stm32", + "cpu": "cortex-m4", + "extra_flags": "-DSTM32WL -DSTM32WLxx -DSTM32WLE5xx", + "framework_extra_flags": { + "arduino": "-DUSE_CM4_STARTUP_FILE -DARDUINO_RAK3172_MODULE" + }, + "f_cpu": "48000000L", + "mcu": "stm32wle5ccu", + "product_line": "STM32WLE5xx", + "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U" + }, + "debug": { + "default_tools": ["stlink"], + "jlink_device": "STM32WLE5CC", + "openocd_target": "stm32wlx", + "svd_path": "STM32WLE5_CM4.svd" + }, + "frameworks": ["arduino"], + "name": "BB-STM32WL", + "upload": { + "maximum_ram_size": 65536, + "maximum_size": 262144, + "protocol": "stlink", + "protocols": ["stlink", "jlink"] + }, + "url": "https://www.st.com/en/microcontrollers-microprocessors/stm32wle5cc.html", + "vendor": "YAOYAO" +} diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json new file mode 100644 index 00000000..330455f5 --- /dev/null +++ b/boards/tracker-t1000-e.json @@ -0,0 +1,59 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "T1000-E-BOOT", + "mcu": "nrf52840", + "variant": "Seeed_T1000-E", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": ["arduino"], + "name": "Seeed T1000-E", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 811008, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-E-for-Meshtastic-p-5913.html", + "vendor": "Seeed Studio" +} diff --git a/include/LvglPsramAlloc.h b/include/LvglPsramAlloc.h new file mode 100644 index 00000000..1f3038cd --- /dev/null +++ b/include/LvglPsramAlloc.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// LVGL's general allocator routed to prefer PSRAM. Internal DRAM is precious +// on this board (320 KB total, WiFi DMA wants ~10–15 KB for association), so +// the ~60 KB of widget state LVGL grows during buildUiTree goes to the 8 MB +// of PSRAM instead. Falls back to default malloc if PSRAM allocation fails +// (e.g. tiny allocations the heap may refuse to satisfy from SPIRAM). +void* lvglPsramAlloc(size_t size); +void lvglPsramFree(void* ptr); +void* lvglPsramRealloc(void* ptr, size_t size); + +#ifdef __cplusplus +} +#endif diff --git a/include/README b/include/README new file mode 100644 index 00000000..194dcd43 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/include/lv_conf.h b/include/lv_conf.h new file mode 100644 index 00000000..25cb5a95 --- /dev/null +++ b/include/lv_conf.h @@ -0,0 +1,113 @@ +#pragma once + +#define LV_COLOR_DEPTH 16 +#define LV_COLOR_16_SWAP 0 + +// Route LVGL's general allocator through PSRAM-preferred wrappers (defined in +// LvglPsramAlloc.cpp). The board has 8 MB of PSRAM and the ~60 KB of widget +// state LVGL grows during buildUiTree would otherwise come out of the 320 KB +// internal DRAM that WiFi DMA also needs — without this redirect, DMA-capable +// free hovers around 5 KB after boot and association can't complete. +#define LV_MEM_CUSTOM 1 +#define LV_MEM_CUSTOM_INCLUDE "LvglPsramAlloc.h" +#define LV_MEM_CUSTOM_ALLOC lvglPsramAlloc +#define LV_MEM_CUSTOM_FREE lvglPsramFree +#define LV_MEM_CUSTOM_REALLOC lvglPsramRealloc + +// LVGL reads elapsed time straight from the ESP32 hardware timer instead of +// relying on manual lv_tick_inc() calls in the main loop. Means animations +// stay correctly-timed even when the main loop bursts (mesh packet handling, +// long renders) — no more "skipped frames" or "fast-forward catch-up" after +// a stall. +#define LV_TICK_CUSTOM 1 +#define LV_TICK_CUSTOM_INCLUDE "esp_timer.h" +#define LV_TICK_CUSTOM_SYS_TIME_EXPR ((uint32_t)(esp_timer_get_time() / 1000)) + +// 16 ms ≈ 60 Hz refresh. We have CPU headroom (160-200 MHz) and a +// double-buffered internal-DRAM draw setup, so driving the display at +// 60 fps gives noticeably smoother animations / pan / scroll. Higher +// values were chosen earlier when CPU was 80 MHz; on 160+ they're +// leaving frame budget unused. +#define LV_DISP_DEF_REFR_PERIOD 16 +// 15 ms (was 30 ms): the touch driver runs async at 125 Hz so the cached state +// is always fresh. Polling LVGL at 67 Hz catches brief taps that the previous +// 30 ms cadence missed (felt as needing to click twice). +#define LV_INDEV_DEF_READ_PERIOD 15 + +// Scroll threshold (px the finger must travel before a press becomes a scroll). +// Default 10 was too twitchy on the cap-touch panel: a tap on a row inside a +// scrollable container (e.g. the Settings tab, whose section buttons are direct +// children of a vertical-scroll view) would jitter past 10 px and turn into a +// scroll, cancelling the click — the button showed its press style but the +// CLICKED handler never fired ("nothing happens"). 24 px keeps taps reliable +// while still allowing a deliberate drag to scroll. +#define LV_INDEV_DEF_SCROLL_LIMIT 24 + +#define LV_USE_LOG 0 + +#define LV_FONT_MONTSERRAT_12 1 +#define LV_FONT_MONTSERRAT_14 1 +#define LV_FONT_MONTSERRAT_16 1 +/* 28 px Montserrat for the boot splash title — keeps the rest of the UI on + * the smaller fonts so the .data cost stays modest. */ +#define LV_FONT_MONTSERRAT_28 1 +/* Pixelated UNSCII bitmap font for the boot splash. Matches the retro/ + * pixelated wordmark the bootloader paints, so the splash reads as a + * direct continuation of the boot sequence rather than a different style + * of "designed" screen. Both sizes get linked since we use 16 for the + * title and 8 for the subtitle. */ +#define LV_FONT_UNSCII_8 1 +#define LV_FONT_UNSCII_16 1 + +/* + * Unicode / emoji: bundled Montserrat only includes a small subset (see each lv_font_montserrat_*.c header). + * More letters or emoji need extra glyph data — e.g. run LVGL’s lv_font_conv on Montserrat/Noto TTF with + * the ranges you want, add the generated .c here, and wire it in UITask (same hardware, flash tradeoff). + */ + +#define LV_USE_LABEL 1 +#define LV_USE_BTN 1 +#define LV_USE_LIST 1 +#define LV_USE_TEXTAREA 1 +#define LV_USE_KEYBOARD 1 +#define LV_USE_TABVIEW 1 +#define LV_USE_ROLLER 1 +#define LV_USE_BAR 1 +#define LV_USE_ARC 1 +#define LV_USE_SWITCH 1 +#define LV_USE_CANVAS 1 +/* QR-code widget for the "share my contact" popup in the Chats tab. Needs + * LV_USE_CANVAS since lv_qrcode renders into a canvas. */ +#define LV_USE_QRCODE 1 +/* Split-JPEG decoder for the Map tab's slippy tiles. SJPG decodes JPEG + * incrementally in stripes so a 256×256 tile only needs ~10-20 KB of + * working RAM rather than the whole frame at once. */ +#define LV_USE_SJPG 1 +/* PNG decoder via lodepng. OSM tile.openstreetmap.org serves PNG; the + * in-firmware Wi-Fi tile fetcher (Phase 4.1c) writes them straight into + * the tiles LittleFS partition as .png. Tile loader tries .jpg first + * (offline packs) and falls back to .png (fetched). */ +#define LV_USE_PNG 1 +/* Decoded-image cache. We pre-decode map tiles to RGB565 ourselves and + * present them as CF_TRUE_COLOR, so the cache mostly tracks open-decoder + * metadata rather than holding decoded pixel buffers. 8 entries is plenty + * for the chat / status icons that DO go through the lazy decoder; the + * map's 9 visible tiles are managed by our own per-slot LRU. Smaller + * cache also means less state to keep coherent across slot reuse. */ +#define LV_IMG_CACHE_DEF_SIZE 8 + +/* Image-as-font (lv_imgfont): maps Unicode codepoints to colour images so we can + * render full-colour emoji inline with text. The emoji set is pre-baked to + * RGB565+alpha lv_img_dsc_t C-arrays (emoji_data.c) and wired as the tail of the + * font fallback chain in UITask (montserrat -> extras -> emoji). */ +#define LV_USE_IMGFONT 1 + +/* Bi-directional text + Arabic/Persian contextual shaping. Lets Arabic (and + * other RTL scripts) render right-to-left with correctly joined letter forms. + * No-op for LTR text (Latin/Cyrillic/Greek auto-detect as LTR). The fallback + * fonts carry the Arabic presentation forms (0xFE70-0xFEFF) that the shaper + * substitutes in. */ +#define LV_USE_BIDI 1 +#define LV_BIDI_BASE_DIR_DEF LV_BASE_DIR_AUTO +#define LV_USE_ARABIC_PERSIAN_CHARS 1 + diff --git a/lib/ed25519/add_scalar.c b/lib/ed25519/add_scalar.c new file mode 100644 index 00000000..77f1fd8c --- /dev/null +++ b/lib/ed25519/add_scalar.c @@ -0,0 +1,69 @@ +#include "ed_25519.h" +#include "ge.h" +#include "sc.h" +#include "sha512.h" + + +/* see http://crypto.stackexchange.com/a/6215/4697 */ +void ed25519_add_scalar(unsigned char *public_key, unsigned char *private_key, const unsigned char *scalar) { + const unsigned char SC_1[32] = {1}; /* scalar with value 1 */ + + unsigned char n[32]; + ge_p3 nB; + ge_p1p1 A_p1p1; + ge_p3 A; + ge_p3 public_key_unpacked; + ge_cached T; + + sha512_context hash; + unsigned char hashbuf[64]; + + int i; + + /* copy the scalar and clear highest bit */ + for (i = 0; i < 31; ++i) { + n[i] = scalar[i]; + } + n[31] = scalar[31] & 127; + + /* private key: a = n + t */ + if (private_key) { + sc_muladd(private_key, SC_1, n, private_key); + + // https://github.com/orlp/ed25519/issues/3 + sha512_init(&hash); + sha512_update(&hash, private_key + 32, 32); + sha512_update(&hash, scalar, 32); + sha512_final(&hash, hashbuf); + for (i = 0; i < 32; ++i) { + private_key[32 + i] = hashbuf[i]; + } + } + + /* public key: A = nB + T */ + if (public_key) { + /* if we know the private key we don't need a point addition, which is faster */ + /* using a "timing attack" you could find out wether or not we know the private + key, but this information seems rather useless - if this is important pass + public_key and private_key seperately in 2 function calls */ + if (private_key) { + ge_scalarmult_base(&A, private_key); + } else { + /* unpack public key into T */ + ge_frombytes_negate_vartime(&public_key_unpacked, public_key); + fe_neg(public_key_unpacked.X, public_key_unpacked.X); /* undo negate */ + fe_neg(public_key_unpacked.T, public_key_unpacked.T); /* undo negate */ + ge_p3_to_cached(&T, &public_key_unpacked); + + /* calculate n*B */ + ge_scalarmult_base(&nB, n); + + /* A = n*B + T */ + ge_add(&A_p1p1, &nB, &T); + ge_p1p1_to_p3(&A, &A_p1p1); + } + + /* pack public key */ + ge_p3_tobytes(public_key, &A); + } +} diff --git a/lib/ed25519/ed_25519.h b/lib/ed25519/ed_25519.h new file mode 100644 index 00000000..5ac015e3 --- /dev/null +++ b/lib/ed25519/ed_25519.h @@ -0,0 +1,41 @@ +#ifndef ED25519_H +#define ED25519_H + +// Nightcracker's Ed25519 - https://github.com/orlp/ed25519 + +#include + +#if defined(_WIN32) + #if defined(ED25519_BUILD_DLL) + #define ED25519_DECLSPEC __declspec(dllexport) + #elif defined(ED25519_DLL) + #define ED25519_DECLSPEC __declspec(dllimport) + #else + #define ED25519_DECLSPEC + #endif +#else + #define ED25519_DECLSPEC +#endif + + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef ED25519_NO_SEED +int ED25519_DECLSPEC ed25519_create_seed(unsigned char *seed); +#endif + +void ED25519_DECLSPEC ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key, const unsigned char *seed); +void ED25519_DECLSPEC ed25519_derive_pub(unsigned char *public_key, const unsigned char *private_key); +void ED25519_DECLSPEC ed25519_sign(unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key, const unsigned char *private_key); +int ED25519_DECLSPEC ed25519_verify(const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key); +void ED25519_DECLSPEC ed25519_add_scalar(unsigned char *public_key, unsigned char *private_key, const unsigned char *scalar); +void ED25519_DECLSPEC ed25519_key_exchange(unsigned char *shared_secret, const unsigned char *public_key, const unsigned char *private_key); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/lib/ed25519/fe.c b/lib/ed25519/fe.c new file mode 100644 index 00000000..2105eb7b --- /dev/null +++ b/lib/ed25519/fe.c @@ -0,0 +1,1491 @@ +#include "fixedint.h" +#include "fe.h" + + +/* + helper functions +*/ +static uint64_t load_3(const unsigned char *in) { + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + + return result; +} + +static uint64_t load_4(const unsigned char *in) { + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + result |= ((uint64_t) in[3]) << 24; + + return result; +} + + + +/* + h = 0 +*/ + +void fe_0(fe h) { + h[0] = 0; + h[1] = 0; + h[2] = 0; + h[3] = 0; + h[4] = 0; + h[5] = 0; + h[6] = 0; + h[7] = 0; + h[8] = 0; + h[9] = 0; +} + + + +/* + h = 1 +*/ + +void fe_1(fe h) { + h[0] = 1; + h[1] = 0; + h[2] = 0; + h[3] = 0; + h[4] = 0; + h[5] = 0; + h[6] = 0; + h[7] = 0; + h[8] = 0; + h[9] = 0; +} + + + +/* + h = f + g + Can overlap h with f or g. + + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + + Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + +void fe_add(fe h, const fe f, const fe g) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t h0 = f0 + g0; + int32_t h1 = f1 + g1; + int32_t h2 = f2 + g2; + int32_t h3 = f3 + g3; + int32_t h4 = f4 + g4; + int32_t h5 = f5 + g5; + int32_t h6 = f6 + g6; + int32_t h7 = f7 + g7; + int32_t h8 = f8 + g8; + int32_t h9 = f9 + g9; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + + + +/* + Replace (f,g) with (g,g) if b == 1; + replace (f,g) with (f,g) if b == 0. + + Preconditions: b in {0,1}. +*/ + +void fe_cmov(fe f, const fe g, unsigned int b) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t x0 = f0 ^ g0; + int32_t x1 = f1 ^ g1; + int32_t x2 = f2 ^ g2; + int32_t x3 = f3 ^ g3; + int32_t x4 = f4 ^ g4; + int32_t x5 = f5 ^ g5; + int32_t x6 = f6 ^ g6; + int32_t x7 = f7 ^ g7; + int32_t x8 = f8 ^ g8; + int32_t x9 = f9 ^ g9; + + b = (unsigned int) (- (int) b); /* silence warning */ + x0 &= b; + x1 &= b; + x2 &= b; + x3 &= b; + x4 &= b; + x5 &= b; + x6 &= b; + x7 &= b; + x8 &= b; + x9 &= b; + + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; +} + +/* + Replace (f,g) with (g,f) if b == 1; + replace (f,g) with (f,g) if b == 0. + + Preconditions: b in {0,1}. +*/ + +void fe_cswap(fe f,fe g,unsigned int b) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t x0 = f0 ^ g0; + int32_t x1 = f1 ^ g1; + int32_t x2 = f2 ^ g2; + int32_t x3 = f3 ^ g3; + int32_t x4 = f4 ^ g4; + int32_t x5 = f5 ^ g5; + int32_t x6 = f6 ^ g6; + int32_t x7 = f7 ^ g7; + int32_t x8 = f8 ^ g8; + int32_t x9 = f9 ^ g9; + b = (unsigned int) (- (int) b); /* silence warning */ + x0 &= b; + x1 &= b; + x2 &= b; + x3 &= b; + x4 &= b; + x5 &= b; + x6 &= b; + x7 &= b; + x8 &= b; + x9 &= b; + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; + g[0] = g0 ^ x0; + g[1] = g1 ^ x1; + g[2] = g2 ^ x2; + g[3] = g3 ^ x3; + g[4] = g4 ^ x4; + g[5] = g5 ^ x5; + g[6] = g6 ^ x6; + g[7] = g7 ^ x7; + g[8] = g8 ^ x8; + g[9] = g9 ^ x9; +} + + + +/* + h = f +*/ + +void fe_copy(fe h, const fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + h[0] = f0; + h[1] = f1; + h[2] = f2; + h[3] = f3; + h[4] = f4; + h[5] = f5; + h[6] = f6; + h[7] = f7; + h[8] = f8; + h[9] = f9; +} + + + +/* + Ignores top bit of h. +*/ + +void fe_frombytes(fe h, const unsigned char *s) { + int64_t h0 = load_4(s); + int64_t h1 = load_3(s + 4) << 6; + int64_t h2 = load_3(s + 7) << 5; + int64_t h3 = load_3(s + 10) << 3; + int64_t h4 = load_3(s + 13) << 2; + int64_t h5 = load_4(s + 16); + int64_t h6 = load_3(s + 20) << 7; + int64_t h7 = load_3(s + 23) << 5; + int64_t h8 = load_3(s + 26) << 4; + int64_t h9 = (load_3(s + 29) & 8388607) << 2; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry9 = (h9 + (int64_t) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry1 = (h1 + (int64_t) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry3 = (h3 + (int64_t) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry5 = (h5 + (int64_t) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry7 = (h7 + (int64_t) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry2 = (h2 + (int64_t) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry6 = (h6 + (int64_t) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry8 = (h8 + (int64_t) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + + +void fe_invert(fe out, const fe z) { + fe t0; + fe t1; + fe t2; + fe t3; + int i; + + fe_sq(t0, z); + + for (i = 1; i < 1; ++i) { + fe_sq(t0, t0); + } + + fe_sq(t1, t0); + + for (i = 1; i < 2; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t1, z, t1); + fe_mul(t0, t0, t1); + fe_sq(t2, t0); + + for (i = 1; i < 1; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t1, t2); + fe_sq(t2, t1); + + for (i = 1; i < 5; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t2, t1); + + for (i = 1; i < 10; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t2, t2, t1); + fe_sq(t3, t2); + + for (i = 1; i < 20; ++i) { + fe_sq(t3, t3); + } + + fe_mul(t2, t3, t2); + fe_sq(t2, t2); + + for (i = 1; i < 10; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t2, t1); + + for (i = 1; i < 50; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t2, t2, t1); + fe_sq(t3, t2); + + for (i = 1; i < 100; ++i) { + fe_sq(t3, t3); + } + + fe_mul(t2, t3, t2); + fe_sq(t2, t2); + + for (i = 1; i < 50; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t1, t1); + + for (i = 1; i < 5; ++i) { + fe_sq(t1, t1); + } + + fe_mul(out, t1, t0); +} + + + +/* + return 1 if f is in {1,3,5,...,q-2} + return 0 if f is in {0,2,4,...,q-1} + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + +int fe_isnegative(const fe f) { + unsigned char s[32]; + + fe_tobytes(s, f); + + return s[0] & 1; +} + + + +/* + return 1 if f == 0 + return 0 if f != 0 + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + +int fe_isnonzero(const fe f) { + unsigned char s[32]; + unsigned char r; + + fe_tobytes(s, f); + + r = s[0]; + #define F(i) r |= s[i] + F(1); + F(2); + F(3); + F(4); + F(5); + F(6); + F(7); + F(8); + F(9); + F(10); + F(11); + F(12); + F(13); + F(14); + F(15); + F(16); + F(17); + F(18); + F(19); + F(20); + F(21); + F(22); + F(23); + F(24); + F(25); + F(26); + F(27); + F(28); + F(29); + F(30); + F(31); + #undef F + + return r != 0; +} + + + +/* + h = f * g + Can overlap h with f or g. + + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + |g| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + + /* + Notes on implementation strategy: + + Using schoolbook multiplication. + Karatsuba would save a little in some cost models. + + Most multiplications by 2 and 19 are 32-bit precomputations; + cheaper than 64-bit postcomputations. + + There is one remaining multiplication by 19 in the carry chain; + one *19 precomputation can be merged into this, + but the resulting data flow is considerably less clean. + + There are 12 carries below. + 10 of them are 2-way parallelizable and vectorizable. + Can get away with 11 carries, but then data flow is much deeper. + + With tighter constraints on inputs can squeeze carries into int32. +*/ + +void fe_mul(fe h, const fe f, const fe g) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t g1_19 = 19 * g1; /* 1.959375*2^29 */ + int32_t g2_19 = 19 * g2; /* 1.959375*2^30; still ok */ + int32_t g3_19 = 19 * g3; + int32_t g4_19 = 19 * g4; + int32_t g5_19 = 19 * g5; + int32_t g6_19 = 19 * g6; + int32_t g7_19 = 19 * g7; + int32_t g8_19 = 19 * g8; + int32_t g9_19 = 19 * g9; + int32_t f1_2 = 2 * f1; + int32_t f3_2 = 2 * f3; + int32_t f5_2 = 2 * f5; + int32_t f7_2 = 2 * f7; + int32_t f9_2 = 2 * f9; + int64_t f0g0 = f0 * (int64_t) g0; + int64_t f0g1 = f0 * (int64_t) g1; + int64_t f0g2 = f0 * (int64_t) g2; + int64_t f0g3 = f0 * (int64_t) g3; + int64_t f0g4 = f0 * (int64_t) g4; + int64_t f0g5 = f0 * (int64_t) g5; + int64_t f0g6 = f0 * (int64_t) g6; + int64_t f0g7 = f0 * (int64_t) g7; + int64_t f0g8 = f0 * (int64_t) g8; + int64_t f0g9 = f0 * (int64_t) g9; + int64_t f1g0 = f1 * (int64_t) g0; + int64_t f1g1_2 = f1_2 * (int64_t) g1; + int64_t f1g2 = f1 * (int64_t) g2; + int64_t f1g3_2 = f1_2 * (int64_t) g3; + int64_t f1g4 = f1 * (int64_t) g4; + int64_t f1g5_2 = f1_2 * (int64_t) g5; + int64_t f1g6 = f1 * (int64_t) g6; + int64_t f1g7_2 = f1_2 * (int64_t) g7; + int64_t f1g8 = f1 * (int64_t) g8; + int64_t f1g9_38 = f1_2 * (int64_t) g9_19; + int64_t f2g0 = f2 * (int64_t) g0; + int64_t f2g1 = f2 * (int64_t) g1; + int64_t f2g2 = f2 * (int64_t) g2; + int64_t f2g3 = f2 * (int64_t) g3; + int64_t f2g4 = f2 * (int64_t) g4; + int64_t f2g5 = f2 * (int64_t) g5; + int64_t f2g6 = f2 * (int64_t) g6; + int64_t f2g7 = f2 * (int64_t) g7; + int64_t f2g8_19 = f2 * (int64_t) g8_19; + int64_t f2g9_19 = f2 * (int64_t) g9_19; + int64_t f3g0 = f3 * (int64_t) g0; + int64_t f3g1_2 = f3_2 * (int64_t) g1; + int64_t f3g2 = f3 * (int64_t) g2; + int64_t f3g3_2 = f3_2 * (int64_t) g3; + int64_t f3g4 = f3 * (int64_t) g4; + int64_t f3g5_2 = f3_2 * (int64_t) g5; + int64_t f3g6 = f3 * (int64_t) g6; + int64_t f3g7_38 = f3_2 * (int64_t) g7_19; + int64_t f3g8_19 = f3 * (int64_t) g8_19; + int64_t f3g9_38 = f3_2 * (int64_t) g9_19; + int64_t f4g0 = f4 * (int64_t) g0; + int64_t f4g1 = f4 * (int64_t) g1; + int64_t f4g2 = f4 * (int64_t) g2; + int64_t f4g3 = f4 * (int64_t) g3; + int64_t f4g4 = f4 * (int64_t) g4; + int64_t f4g5 = f4 * (int64_t) g5; + int64_t f4g6_19 = f4 * (int64_t) g6_19; + int64_t f4g7_19 = f4 * (int64_t) g7_19; + int64_t f4g8_19 = f4 * (int64_t) g8_19; + int64_t f4g9_19 = f4 * (int64_t) g9_19; + int64_t f5g0 = f5 * (int64_t) g0; + int64_t f5g1_2 = f5_2 * (int64_t) g1; + int64_t f5g2 = f5 * (int64_t) g2; + int64_t f5g3_2 = f5_2 * (int64_t) g3; + int64_t f5g4 = f5 * (int64_t) g4; + int64_t f5g5_38 = f5_2 * (int64_t) g5_19; + int64_t f5g6_19 = f5 * (int64_t) g6_19; + int64_t f5g7_38 = f5_2 * (int64_t) g7_19; + int64_t f5g8_19 = f5 * (int64_t) g8_19; + int64_t f5g9_38 = f5_2 * (int64_t) g9_19; + int64_t f6g0 = f6 * (int64_t) g0; + int64_t f6g1 = f6 * (int64_t) g1; + int64_t f6g2 = f6 * (int64_t) g2; + int64_t f6g3 = f6 * (int64_t) g3; + int64_t f6g4_19 = f6 * (int64_t) g4_19; + int64_t f6g5_19 = f6 * (int64_t) g5_19; + int64_t f6g6_19 = f6 * (int64_t) g6_19; + int64_t f6g7_19 = f6 * (int64_t) g7_19; + int64_t f6g8_19 = f6 * (int64_t) g8_19; + int64_t f6g9_19 = f6 * (int64_t) g9_19; + int64_t f7g0 = f7 * (int64_t) g0; + int64_t f7g1_2 = f7_2 * (int64_t) g1; + int64_t f7g2 = f7 * (int64_t) g2; + int64_t f7g3_38 = f7_2 * (int64_t) g3_19; + int64_t f7g4_19 = f7 * (int64_t) g4_19; + int64_t f7g5_38 = f7_2 * (int64_t) g5_19; + int64_t f7g6_19 = f7 * (int64_t) g6_19; + int64_t f7g7_38 = f7_2 * (int64_t) g7_19; + int64_t f7g8_19 = f7 * (int64_t) g8_19; + int64_t f7g9_38 = f7_2 * (int64_t) g9_19; + int64_t f8g0 = f8 * (int64_t) g0; + int64_t f8g1 = f8 * (int64_t) g1; + int64_t f8g2_19 = f8 * (int64_t) g2_19; + int64_t f8g3_19 = f8 * (int64_t) g3_19; + int64_t f8g4_19 = f8 * (int64_t) g4_19; + int64_t f8g5_19 = f8 * (int64_t) g5_19; + int64_t f8g6_19 = f8 * (int64_t) g6_19; + int64_t f8g7_19 = f8 * (int64_t) g7_19; + int64_t f8g8_19 = f8 * (int64_t) g8_19; + int64_t f8g9_19 = f8 * (int64_t) g9_19; + int64_t f9g0 = f9 * (int64_t) g0; + int64_t f9g1_38 = f9_2 * (int64_t) g1_19; + int64_t f9g2_19 = f9 * (int64_t) g2_19; + int64_t f9g3_38 = f9_2 * (int64_t) g3_19; + int64_t f9g4_19 = f9 * (int64_t) g4_19; + int64_t f9g5_38 = f9_2 * (int64_t) g5_19; + int64_t f9g6_19 = f9 * (int64_t) g6_19; + int64_t f9g7_38 = f9_2 * (int64_t) g7_19; + int64_t f9g8_19 = f9 * (int64_t) g8_19; + int64_t f9g9_38 = f9_2 * (int64_t) g9_19; + int64_t h0 = f0g0 + f1g9_38 + f2g8_19 + f3g7_38 + f4g6_19 + f5g5_38 + f6g4_19 + f7g3_38 + f8g2_19 + f9g1_38; + int64_t h1 = f0g1 + f1g0 + f2g9_19 + f3g8_19 + f4g7_19 + f5g6_19 + f6g5_19 + f7g4_19 + f8g3_19 + f9g2_19; + int64_t h2 = f0g2 + f1g1_2 + f2g0 + f3g9_38 + f4g8_19 + f5g7_38 + f6g6_19 + f7g5_38 + f8g4_19 + f9g3_38; + int64_t h3 = f0g3 + f1g2 + f2g1 + f3g0 + f4g9_19 + f5g8_19 + f6g7_19 + f7g6_19 + f8g5_19 + f9g4_19; + int64_t h4 = f0g4 + f1g3_2 + f2g2 + f3g1_2 + f4g0 + f5g9_38 + f6g8_19 + f7g7_38 + f8g6_19 + f9g5_38; + int64_t h5 = f0g5 + f1g4 + f2g3 + f3g2 + f4g1 + f5g0 + f6g9_19 + f7g8_19 + f8g7_19 + f9g6_19; + int64_t h6 = f0g6 + f1g5_2 + f2g4 + f3g3_2 + f4g2 + f5g1_2 + f6g0 + f7g9_38 + f8g8_19 + f9g7_38; + int64_t h7 = f0g7 + f1g6 + f2g5 + f3g4 + f4g3 + f5g2 + f6g1 + f7g0 + f8g9_19 + f9g8_19; + int64_t h8 = f0g8 + f1g7_2 + f2g6 + f3g5_2 + f4g4 + f5g3_2 + f6g2 + f7g1_2 + f8g0 + f9g9_38; + int64_t h9 = f0g9 + f1g8 + f2g7 + f3g6 + f4g5 + f5g4 + f6g3 + f7g2 + f8g1 + f9g0 ; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + + carry1 = (h1 + (int64_t) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (int64_t) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + + carry2 = (h2 + (int64_t) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (int64_t) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + + carry3 = (h3 + (int64_t) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (int64_t) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (int64_t) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + carry9 = (h9 + (int64_t) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + +/* +h = f * 121666 +Can overlap h with f. + +Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + +Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +*/ + +void fe_mul121666(fe h, fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int64_t h0 = f0 * (int64_t) 121666; + int64_t h1 = f1 * (int64_t) 121666; + int64_t h2 = f2 * (int64_t) 121666; + int64_t h3 = f3 * (int64_t) 121666; + int64_t h4 = f4 * (int64_t) 121666; + int64_t h5 = f5 * (int64_t) 121666; + int64_t h6 = f6 * (int64_t) 121666; + int64_t h7 = f7 * (int64_t) 121666; + int64_t h8 = f8 * (int64_t) 121666; + int64_t h9 = f9 * (int64_t) 121666; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry9 = (h9 + (int64_t) (1<<24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; + carry1 = (h1 + (int64_t) (1<<24)) >> 25; h2 += carry1; h1 -= carry1 << 25; + carry3 = (h3 + (int64_t) (1<<24)) >> 25; h4 += carry3; h3 -= carry3 << 25; + carry5 = (h5 + (int64_t) (1<<24)) >> 25; h6 += carry5; h5 -= carry5 << 25; + carry7 = (h7 + (int64_t) (1<<24)) >> 25; h8 += carry7; h7 -= carry7 << 25; + + carry0 = (h0 + (int64_t) (1<<25)) >> 26; h1 += carry0; h0 -= carry0 << 26; + carry2 = (h2 + (int64_t) (1<<25)) >> 26; h3 += carry2; h2 -= carry2 << 26; + carry4 = (h4 + (int64_t) (1<<25)) >> 26; h5 += carry4; h4 -= carry4 << 26; + carry6 = (h6 + (int64_t) (1<<25)) >> 26; h7 += carry6; h6 -= carry6 << 26; + carry8 = (h8 + (int64_t) (1<<25)) >> 26; h9 += carry8; h8 -= carry8 << 26; + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + +/* +h = -f + +Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + +Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +*/ + +void fe_neg(fe h, const fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t h0 = -f0; + int32_t h1 = -f1; + int32_t h2 = -f2; + int32_t h3 = -f3; + int32_t h4 = -f4; + int32_t h5 = -f5; + int32_t h6 = -f6; + int32_t h7 = -f7; + int32_t h8 = -f8; + int32_t h9 = -f9; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + + +void fe_pow22523(fe out, const fe z) { + fe t0; + fe t1; + fe t2; + int i; + fe_sq(t0, z); + + for (i = 1; i < 1; ++i) { + fe_sq(t0, t0); + } + + fe_sq(t1, t0); + + for (i = 1; i < 2; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t1, z, t1); + fe_mul(t0, t0, t1); + fe_sq(t0, t0); + + for (i = 1; i < 1; ++i) { + fe_sq(t0, t0); + } + + fe_mul(t0, t1, t0); + fe_sq(t1, t0); + + for (i = 1; i < 5; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t0, t1, t0); + fe_sq(t1, t0); + + for (i = 1; i < 10; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t1, t1, t0); + fe_sq(t2, t1); + + for (i = 1; i < 20; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t1, t1); + + for (i = 1; i < 10; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t0, t1, t0); + fe_sq(t1, t0); + + for (i = 1; i < 50; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t1, t1, t0); + fe_sq(t2, t1); + + for (i = 1; i < 100; ++i) { + fe_sq(t2, t2); + } + + fe_mul(t1, t2, t1); + fe_sq(t1, t1); + + for (i = 1; i < 50; ++i) { + fe_sq(t1, t1); + } + + fe_mul(t0, t1, t0); + fe_sq(t0, t0); + + for (i = 1; i < 2; ++i) { + fe_sq(t0, t0); + } + + fe_mul(out, t0, z); + return; +} + + +/* +h = f * f +Can overlap h with f. + +Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + +Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. +*/ + +/* +See fe_mul.c for discussion of implementation strategy. +*/ + +void fe_sq(fe h, const fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t f0_2 = 2 * f0; + int32_t f1_2 = 2 * f1; + int32_t f2_2 = 2 * f2; + int32_t f3_2 = 2 * f3; + int32_t f4_2 = 2 * f4; + int32_t f5_2 = 2 * f5; + int32_t f6_2 = 2 * f6; + int32_t f7_2 = 2 * f7; + int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ + int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ + int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ + int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ + int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ + int64_t f0f0 = f0 * (int64_t) f0; + int64_t f0f1_2 = f0_2 * (int64_t) f1; + int64_t f0f2_2 = f0_2 * (int64_t) f2; + int64_t f0f3_2 = f0_2 * (int64_t) f3; + int64_t f0f4_2 = f0_2 * (int64_t) f4; + int64_t f0f5_2 = f0_2 * (int64_t) f5; + int64_t f0f6_2 = f0_2 * (int64_t) f6; + int64_t f0f7_2 = f0_2 * (int64_t) f7; + int64_t f0f8_2 = f0_2 * (int64_t) f8; + int64_t f0f9_2 = f0_2 * (int64_t) f9; + int64_t f1f1_2 = f1_2 * (int64_t) f1; + int64_t f1f2_2 = f1_2 * (int64_t) f2; + int64_t f1f3_4 = f1_2 * (int64_t) f3_2; + int64_t f1f4_2 = f1_2 * (int64_t) f4; + int64_t f1f5_4 = f1_2 * (int64_t) f5_2; + int64_t f1f6_2 = f1_2 * (int64_t) f6; + int64_t f1f7_4 = f1_2 * (int64_t) f7_2; + int64_t f1f8_2 = f1_2 * (int64_t) f8; + int64_t f1f9_76 = f1_2 * (int64_t) f9_38; + int64_t f2f2 = f2 * (int64_t) f2; + int64_t f2f3_2 = f2_2 * (int64_t) f3; + int64_t f2f4_2 = f2_2 * (int64_t) f4; + int64_t f2f5_2 = f2_2 * (int64_t) f5; + int64_t f2f6_2 = f2_2 * (int64_t) f6; + int64_t f2f7_2 = f2_2 * (int64_t) f7; + int64_t f2f8_38 = f2_2 * (int64_t) f8_19; + int64_t f2f9_38 = f2 * (int64_t) f9_38; + int64_t f3f3_2 = f3_2 * (int64_t) f3; + int64_t f3f4_2 = f3_2 * (int64_t) f4; + int64_t f3f5_4 = f3_2 * (int64_t) f5_2; + int64_t f3f6_2 = f3_2 * (int64_t) f6; + int64_t f3f7_76 = f3_2 * (int64_t) f7_38; + int64_t f3f8_38 = f3_2 * (int64_t) f8_19; + int64_t f3f9_76 = f3_2 * (int64_t) f9_38; + int64_t f4f4 = f4 * (int64_t) f4; + int64_t f4f5_2 = f4_2 * (int64_t) f5; + int64_t f4f6_38 = f4_2 * (int64_t) f6_19; + int64_t f4f7_38 = f4 * (int64_t) f7_38; + int64_t f4f8_38 = f4_2 * (int64_t) f8_19; + int64_t f4f9_38 = f4 * (int64_t) f9_38; + int64_t f5f5_38 = f5 * (int64_t) f5_38; + int64_t f5f6_38 = f5_2 * (int64_t) f6_19; + int64_t f5f7_76 = f5_2 * (int64_t) f7_38; + int64_t f5f8_38 = f5_2 * (int64_t) f8_19; + int64_t f5f9_76 = f5_2 * (int64_t) f9_38; + int64_t f6f6_19 = f6 * (int64_t) f6_19; + int64_t f6f7_38 = f6 * (int64_t) f7_38; + int64_t f6f8_38 = f6_2 * (int64_t) f8_19; + int64_t f6f9_38 = f6 * (int64_t) f9_38; + int64_t f7f7_38 = f7 * (int64_t) f7_38; + int64_t f7f8_38 = f7_2 * (int64_t) f8_19; + int64_t f7f9_76 = f7_2 * (int64_t) f9_38; + int64_t f8f8_19 = f8 * (int64_t) f8_19; + int64_t f8f9_38 = f8 * (int64_t) f9_38; + int64_t f9f9_38 = f9 * (int64_t) f9_38; + int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry1 = (h1 + (int64_t) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (int64_t) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry2 = (h2 + (int64_t) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (int64_t) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry3 = (h3 + (int64_t) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (int64_t) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (int64_t) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + carry9 = (h9 + (int64_t) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + +/* +h = 2 * f * f +Can overlap h with f. + +Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + +Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. +*/ + +/* +See fe_mul.c for discussion of implementation strategy. +*/ + +void fe_sq2(fe h, const fe f) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t f0_2 = 2 * f0; + int32_t f1_2 = 2 * f1; + int32_t f2_2 = 2 * f2; + int32_t f3_2 = 2 * f3; + int32_t f4_2 = 2 * f4; + int32_t f5_2 = 2 * f5; + int32_t f6_2 = 2 * f6; + int32_t f7_2 = 2 * f7; + int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ + int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ + int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ + int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ + int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ + int64_t f0f0 = f0 * (int64_t) f0; + int64_t f0f1_2 = f0_2 * (int64_t) f1; + int64_t f0f2_2 = f0_2 * (int64_t) f2; + int64_t f0f3_2 = f0_2 * (int64_t) f3; + int64_t f0f4_2 = f0_2 * (int64_t) f4; + int64_t f0f5_2 = f0_2 * (int64_t) f5; + int64_t f0f6_2 = f0_2 * (int64_t) f6; + int64_t f0f7_2 = f0_2 * (int64_t) f7; + int64_t f0f8_2 = f0_2 * (int64_t) f8; + int64_t f0f9_2 = f0_2 * (int64_t) f9; + int64_t f1f1_2 = f1_2 * (int64_t) f1; + int64_t f1f2_2 = f1_2 * (int64_t) f2; + int64_t f1f3_4 = f1_2 * (int64_t) f3_2; + int64_t f1f4_2 = f1_2 * (int64_t) f4; + int64_t f1f5_4 = f1_2 * (int64_t) f5_2; + int64_t f1f6_2 = f1_2 * (int64_t) f6; + int64_t f1f7_4 = f1_2 * (int64_t) f7_2; + int64_t f1f8_2 = f1_2 * (int64_t) f8; + int64_t f1f9_76 = f1_2 * (int64_t) f9_38; + int64_t f2f2 = f2 * (int64_t) f2; + int64_t f2f3_2 = f2_2 * (int64_t) f3; + int64_t f2f4_2 = f2_2 * (int64_t) f4; + int64_t f2f5_2 = f2_2 * (int64_t) f5; + int64_t f2f6_2 = f2_2 * (int64_t) f6; + int64_t f2f7_2 = f2_2 * (int64_t) f7; + int64_t f2f8_38 = f2_2 * (int64_t) f8_19; + int64_t f2f9_38 = f2 * (int64_t) f9_38; + int64_t f3f3_2 = f3_2 * (int64_t) f3; + int64_t f3f4_2 = f3_2 * (int64_t) f4; + int64_t f3f5_4 = f3_2 * (int64_t) f5_2; + int64_t f3f6_2 = f3_2 * (int64_t) f6; + int64_t f3f7_76 = f3_2 * (int64_t) f7_38; + int64_t f3f8_38 = f3_2 * (int64_t) f8_19; + int64_t f3f9_76 = f3_2 * (int64_t) f9_38; + int64_t f4f4 = f4 * (int64_t) f4; + int64_t f4f5_2 = f4_2 * (int64_t) f5; + int64_t f4f6_38 = f4_2 * (int64_t) f6_19; + int64_t f4f7_38 = f4 * (int64_t) f7_38; + int64_t f4f8_38 = f4_2 * (int64_t) f8_19; + int64_t f4f9_38 = f4 * (int64_t) f9_38; + int64_t f5f5_38 = f5 * (int64_t) f5_38; + int64_t f5f6_38 = f5_2 * (int64_t) f6_19; + int64_t f5f7_76 = f5_2 * (int64_t) f7_38; + int64_t f5f8_38 = f5_2 * (int64_t) f8_19; + int64_t f5f9_76 = f5_2 * (int64_t) f9_38; + int64_t f6f6_19 = f6 * (int64_t) f6_19; + int64_t f6f7_38 = f6 * (int64_t) f7_38; + int64_t f6f8_38 = f6_2 * (int64_t) f8_19; + int64_t f6f9_38 = f6 * (int64_t) f9_38; + int64_t f7f7_38 = f7 * (int64_t) f7_38; + int64_t f7f8_38 = f7_2 * (int64_t) f8_19; + int64_t f7f9_76 = f7_2 * (int64_t) f9_38; + int64_t f8f8_19 = f8 * (int64_t) f8_19; + int64_t f8f9_38 = f8 * (int64_t) f9_38; + int64_t f9f9_38 = f9 * (int64_t) f9_38; + int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + h0 += h0; + h1 += h1; + h2 += h2; + h3 += h3; + h4 += h4; + h5 += h5; + h6 += h6; + h7 += h7; + h8 += h8; + h9 += h9; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry1 = (h1 + (int64_t) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (int64_t) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry2 = (h2 + (int64_t) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (int64_t) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry3 = (h3 + (int64_t) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (int64_t) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry4 = (h4 + (int64_t) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (int64_t) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + carry9 = (h9 + (int64_t) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry0 = (h0 + (int64_t) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + + +/* +h = f - g +Can overlap h with f or g. + +Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + +Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + +void fe_sub(fe h, const fe f, const fe g) { + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + int32_t h0 = f0 - g0; + int32_t h1 = f1 - g1; + int32_t h2 = f2 - g2; + int32_t h3 = f3 - g3; + int32_t h4 = f4 - g4; + int32_t h5 = f5 - g5; + int32_t h6 = f6 - g6; + int32_t h7 = f7 - g7; + int32_t h8 = f8 - g8; + int32_t h9 = f9 - g9; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + + + +/* +Preconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + +Write p=2^255-19; q=floor(h/p). +Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). + +Proof: + Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. + Also have |h-2^230 h9|<2^231 so |19 2^(-255)(h-2^230 h9)|<1/4. + + Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). + Then 0> 25; + q = (h0 + q) >> 26; + q = (h1 + q) >> 25; + q = (h2 + q) >> 26; + q = (h3 + q) >> 25; + q = (h4 + q) >> 26; + q = (h5 + q) >> 25; + q = (h6 + q) >> 26; + q = (h7 + q) >> 25; + q = (h8 + q) >> 26; + q = (h9 + q) >> 25; + /* Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. */ + h0 += 19 * q; + /* Goal: Output h-2^255 q, which is between 0 and 2^255-20. */ + carry0 = h0 >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry1 = h1 >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry2 = h2 >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry3 = h3 >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry4 = h4 >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry5 = h5 >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry6 = h6 >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry7 = h7 >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry8 = h8 >> 26; + h9 += carry8; + h8 -= carry8 << 26; + carry9 = h9 >> 25; + h9 -= carry9 << 25; + + /* h10 = carry9 */ + /* + Goal: Output h0+...+2^255 h10-2^255 q, which is between 0 and 2^255-20. + Have h0+...+2^230 h9 between 0 and 2^255-1; + evidently 2^255 h10-2^255 q = 0. + Goal: Output h0+...+2^230 h9. + */ + s[0] = (unsigned char) (h0 >> 0); + s[1] = (unsigned char) (h0 >> 8); + s[2] = (unsigned char) (h0 >> 16); + s[3] = (unsigned char) ((h0 >> 24) | (h1 << 2)); + s[4] = (unsigned char) (h1 >> 6); + s[5] = (unsigned char) (h1 >> 14); + s[6] = (unsigned char) ((h1 >> 22) | (h2 << 3)); + s[7] = (unsigned char) (h2 >> 5); + s[8] = (unsigned char) (h2 >> 13); + s[9] = (unsigned char) ((h2 >> 21) | (h3 << 5)); + s[10] = (unsigned char) (h3 >> 3); + s[11] = (unsigned char) (h3 >> 11); + s[12] = (unsigned char) ((h3 >> 19) | (h4 << 6)); + s[13] = (unsigned char) (h4 >> 2); + s[14] = (unsigned char) (h4 >> 10); + s[15] = (unsigned char) (h4 >> 18); + s[16] = (unsigned char) (h5 >> 0); + s[17] = (unsigned char) (h5 >> 8); + s[18] = (unsigned char) (h5 >> 16); + s[19] = (unsigned char) ((h5 >> 24) | (h6 << 1)); + s[20] = (unsigned char) (h6 >> 7); + s[21] = (unsigned char) (h6 >> 15); + s[22] = (unsigned char) ((h6 >> 23) | (h7 << 3)); + s[23] = (unsigned char) (h7 >> 5); + s[24] = (unsigned char) (h7 >> 13); + s[25] = (unsigned char) ((h7 >> 21) | (h8 << 4)); + s[26] = (unsigned char) (h8 >> 4); + s[27] = (unsigned char) (h8 >> 12); + s[28] = (unsigned char) ((h8 >> 20) | (h9 << 6)); + s[29] = (unsigned char) (h9 >> 2); + s[30] = (unsigned char) (h9 >> 10); + s[31] = (unsigned char) (h9 >> 18); +} diff --git a/lib/ed25519/fe.h b/lib/ed25519/fe.h new file mode 100644 index 00000000..b4b62d28 --- /dev/null +++ b/lib/ed25519/fe.h @@ -0,0 +1,41 @@ +#ifndef FE_H +#define FE_H + +#include "fixedint.h" + + +/* + fe means field element. + Here the field is \Z/(2^255-19). + An element t, entries t[0]...t[9], represents the integer + t[0]+2^26 t[1]+2^51 t[2]+2^77 t[3]+2^102 t[4]+...+2^230 t[9]. + Bounds on each t[i] vary depending on context. +*/ + + +typedef int32_t fe[10]; + + +void fe_0(fe h); +void fe_1(fe h); + +void fe_frombytes(fe h, const unsigned char *s); +void fe_tobytes(unsigned char *s, const fe h); + +void fe_copy(fe h, const fe f); +int fe_isnegative(const fe f); +int fe_isnonzero(const fe f); +void fe_cmov(fe f, const fe g, unsigned int b); +void fe_cswap(fe f, fe g, unsigned int b); + +void fe_neg(fe h, const fe f); +void fe_add(fe h, const fe f, const fe g); +void fe_invert(fe out, const fe z); +void fe_sq(fe h, const fe f); +void fe_sq2(fe h, const fe f); +void fe_mul(fe h, const fe f, const fe g); +void fe_mul121666(fe h, fe f); +void fe_pow22523(fe out, const fe z); +void fe_sub(fe h, const fe f, const fe g); + +#endif diff --git a/lib/ed25519/fixedint.h b/lib/ed25519/fixedint.h new file mode 100644 index 00000000..1a8745b1 --- /dev/null +++ b/lib/ed25519/fixedint.h @@ -0,0 +1,72 @@ +/* + Portable header to provide the 32 and 64 bits type. + + Not a compatible replacement for , do not blindly use it as such. +*/ + +#if ((defined(__STDC__) && __STDC__ && __STDC_VERSION__ >= 199901L) || (defined(__WATCOMC__) && (defined(_STDINT_H_INCLUDED) || __WATCOMC__ >= 1250)) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_) || defined(__UINT_FAST64_TYPE__)) )) && !defined(FIXEDINT_H_INCLUDED) + #include + #define FIXEDINT_H_INCLUDED + + #if defined(__WATCOMC__) && __WATCOMC__ >= 1250 && !defined(UINT64_C) + #include + #define UINT64_C(x) (x + (UINT64_MAX - UINT64_MAX)) + #endif +#endif + + +#ifndef FIXEDINT_H_INCLUDED + #define FIXEDINT_H_INCLUDED + + #include + + /* (u)int32_t */ + #ifndef uint32_t + #if (ULONG_MAX == 0xffffffffUL) + typedef unsigned long uint32_t; + #elif (UINT_MAX == 0xffffffffUL) + typedef unsigned int uint32_t; + #elif (USHRT_MAX == 0xffffffffUL) + typedef unsigned short uint32_t; + #endif + #endif + + + #ifndef int32_t + #if (LONG_MAX == 0x7fffffffL) + typedef signed long int32_t; + #elif (INT_MAX == 0x7fffffffL) + typedef signed int int32_t; + #elif (SHRT_MAX == 0x7fffffffL) + typedef signed short int32_t; + #endif + #endif + + + /* (u)int64_t */ + #if (defined(__STDC__) && defined(__STDC_VERSION__) && __STDC__ && __STDC_VERSION__ >= 199901L) + typedef long long int64_t; + typedef unsigned long long uint64_t; + + #define UINT64_C(v) v ##ULL + #define INT64_C(v) v ##LL + #elif defined(__GNUC__) + __extension__ typedef long long int64_t; + __extension__ typedef unsigned long long uint64_t; + + #define UINT64_C(v) v ##ULL + #define INT64_C(v) v ##LL + #elif defined(__MWERKS__) || defined(__SUNPRO_C) || defined(__SUNPRO_CC) || defined(__APPLE_CC__) || defined(_LONG_LONG) || defined(_CRAYC) + typedef long long int64_t; + typedef unsigned long long uint64_t; + + #define UINT64_C(v) v ##ULL + #define INT64_C(v) v ##LL + #elif (defined(__WATCOMC__) && defined(__WATCOM_INT64__)) || (defined(_MSC_VER) && _INTEGRAL_MAX_BITS >= 64) || (defined(__BORLANDC__) && __BORLANDC__ > 0x460) || defined(__alpha) || defined(__DECC) + typedef __int64 int64_t; + typedef unsigned __int64 uint64_t; + + #define UINT64_C(v) v ##UI64 + #define INT64_C(v) v ##I64 + #endif +#endif diff --git a/lib/ed25519/ge.c b/lib/ed25519/ge.c new file mode 100644 index 00000000..87c691bf --- /dev/null +++ b/lib/ed25519/ge.c @@ -0,0 +1,467 @@ +#include "ge.h" +#include "precomp_data.h" + + +/* +r = p + q +*/ + +void ge_add(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q) { + fe t0; + fe_add(r->X, p->Y, p->X); + fe_sub(r->Y, p->Y, p->X); + fe_mul(r->Z, r->X, q->YplusX); + fe_mul(r->Y, r->Y, q->YminusX); + fe_mul(r->T, q->T2d, p->T); + fe_mul(r->X, p->Z, q->Z); + fe_add(t0, r->X, r->X); + fe_sub(r->X, r->Z, r->Y); + fe_add(r->Y, r->Z, r->Y); + fe_add(r->Z, t0, r->T); + fe_sub(r->T, t0, r->T); +} + + +static void slide(signed char *r, const unsigned char *a) { + int i; + int b; + int k; + + for (i = 0; i < 256; ++i) { + r[i] = 1 & (a[i >> 3] >> (i & 7)); + } + + for (i = 0; i < 256; ++i) + if (r[i]) { + for (b = 1; b <= 6 && i + b < 256; ++b) { + if (r[i + b]) { + if (r[i] + (r[i + b] << b) <= 15) { + r[i] += r[i + b] << b; + r[i + b] = 0; + } else if (r[i] - (r[i + b] << b) >= -15) { + r[i] -= r[i + b] << b; + + for (k = i + b; k < 256; ++k) { + if (!r[k]) { + r[k] = 1; + break; + } + + r[k] = 0; + } + } else { + break; + } + } + } + } +} + +/* +r = a * A + b * B +where a = a[0]+256*a[1]+...+256^31 a[31]. +and b = b[0]+256*b[1]+...+256^31 b[31]. +B is the Ed25519 base point (x,4/5) with x positive. +*/ + +void ge_double_scalarmult_vartime(ge_p2 *r, const unsigned char *a, const ge_p3 *A, const unsigned char *b) { + signed char aslide[256]; + signed char bslide[256]; + ge_cached Ai[8]; /* A,3A,5A,7A,9A,11A,13A,15A */ + ge_p1p1 t; + ge_p3 u; + ge_p3 A2; + int i; + slide(aslide, a); + slide(bslide, b); + ge_p3_to_cached(&Ai[0], A); + ge_p3_dbl(&t, A); + ge_p1p1_to_p3(&A2, &t); + ge_add(&t, &A2, &Ai[0]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[1], &u); + ge_add(&t, &A2, &Ai[1]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[2], &u); + ge_add(&t, &A2, &Ai[2]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[3], &u); + ge_add(&t, &A2, &Ai[3]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[4], &u); + ge_add(&t, &A2, &Ai[4]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[5], &u); + ge_add(&t, &A2, &Ai[5]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[6], &u); + ge_add(&t, &A2, &Ai[6]); + ge_p1p1_to_p3(&u, &t); + ge_p3_to_cached(&Ai[7], &u); + ge_p2_0(r); + + for (i = 255; i >= 0; --i) { + if (aslide[i] || bslide[i]) { + break; + } + } + + for (; i >= 0; --i) { + ge_p2_dbl(&t, r); + + if (aslide[i] > 0) { + ge_p1p1_to_p3(&u, &t); + ge_add(&t, &u, &Ai[aslide[i] / 2]); + } else if (aslide[i] < 0) { + ge_p1p1_to_p3(&u, &t); + ge_sub(&t, &u, &Ai[(-aslide[i]) / 2]); + } + + if (bslide[i] > 0) { + ge_p1p1_to_p3(&u, &t); + ge_madd(&t, &u, &Bi[bslide[i] / 2]); + } else if (bslide[i] < 0) { + ge_p1p1_to_p3(&u, &t); + ge_msub(&t, &u, &Bi[(-bslide[i]) / 2]); + } + + ge_p1p1_to_p2(r, &t); + } +} + + +static const fe d = { + -10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116 +}; + +static const fe sqrtm1 = { + -32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482 +}; + +int ge_frombytes_negate_vartime(ge_p3 *h, const unsigned char *s) { + fe u; + fe v; + fe v3; + fe vxx; + fe check; + fe_frombytes(h->Y, s); + fe_1(h->Z); + fe_sq(u, h->Y); + fe_mul(v, u, d); + fe_sub(u, u, h->Z); /* u = y^2-1 */ + fe_add(v, v, h->Z); /* v = dy^2+1 */ + fe_sq(v3, v); + fe_mul(v3, v3, v); /* v3 = v^3 */ + fe_sq(h->X, v3); + fe_mul(h->X, h->X, v); + fe_mul(h->X, h->X, u); /* x = uv^7 */ + fe_pow22523(h->X, h->X); /* x = (uv^7)^((q-5)/8) */ + fe_mul(h->X, h->X, v3); + fe_mul(h->X, h->X, u); /* x = uv^3(uv^7)^((q-5)/8) */ + fe_sq(vxx, h->X); + fe_mul(vxx, vxx, v); + fe_sub(check, vxx, u); /* vx^2-u */ + + if (fe_isnonzero(check)) { + fe_add(check, vxx, u); /* vx^2+u */ + + if (fe_isnonzero(check)) { + return -1; + } + + fe_mul(h->X, h->X, sqrtm1); + } + + if (fe_isnegative(h->X) == (s[31] >> 7)) { + fe_neg(h->X, h->X); + } + + fe_mul(h->T, h->X, h->Y); + return 0; +} + + +/* +r = p + q +*/ + +void ge_madd(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q) { + fe t0; + fe_add(r->X, p->Y, p->X); + fe_sub(r->Y, p->Y, p->X); + fe_mul(r->Z, r->X, q->yplusx); + fe_mul(r->Y, r->Y, q->yminusx); + fe_mul(r->T, q->xy2d, p->T); + fe_add(t0, p->Z, p->Z); + fe_sub(r->X, r->Z, r->Y); + fe_add(r->Y, r->Z, r->Y); + fe_add(r->Z, t0, r->T); + fe_sub(r->T, t0, r->T); +} + + +/* +r = p - q +*/ + +void ge_msub(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q) { + fe t0; + + fe_add(r->X, p->Y, p->X); + fe_sub(r->Y, p->Y, p->X); + fe_mul(r->Z, r->X, q->yminusx); + fe_mul(r->Y, r->Y, q->yplusx); + fe_mul(r->T, q->xy2d, p->T); + fe_add(t0, p->Z, p->Z); + fe_sub(r->X, r->Z, r->Y); + fe_add(r->Y, r->Z, r->Y); + fe_sub(r->Z, t0, r->T); + fe_add(r->T, t0, r->T); +} + + +/* +r = p +*/ + +void ge_p1p1_to_p2(ge_p2 *r, const ge_p1p1 *p) { + fe_mul(r->X, p->X, p->T); + fe_mul(r->Y, p->Y, p->Z); + fe_mul(r->Z, p->Z, p->T); +} + + + +/* +r = p +*/ + +void ge_p1p1_to_p3(ge_p3 *r, const ge_p1p1 *p) { + fe_mul(r->X, p->X, p->T); + fe_mul(r->Y, p->Y, p->Z); + fe_mul(r->Z, p->Z, p->T); + fe_mul(r->T, p->X, p->Y); +} + + +void ge_p2_0(ge_p2 *h) { + fe_0(h->X); + fe_1(h->Y); + fe_1(h->Z); +} + + + +/* +r = 2 * p +*/ + +void ge_p2_dbl(ge_p1p1 *r, const ge_p2 *p) { + fe t0; + + fe_sq(r->X, p->X); + fe_sq(r->Z, p->Y); + fe_sq2(r->T, p->Z); + fe_add(r->Y, p->X, p->Y); + fe_sq(t0, r->Y); + fe_add(r->Y, r->Z, r->X); + fe_sub(r->Z, r->Z, r->X); + fe_sub(r->X, t0, r->Y); + fe_sub(r->T, r->T, r->Z); +} + + +void ge_p3_0(ge_p3 *h) { + fe_0(h->X); + fe_1(h->Y); + fe_1(h->Z); + fe_0(h->T); +} + + +/* +r = 2 * p +*/ + +void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p) { + ge_p2 q; + ge_p3_to_p2(&q, p); + ge_p2_dbl(r, &q); +} + + + +/* +r = p +*/ + +static const fe d2 = { + -21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199 +}; + +void ge_p3_to_cached(ge_cached *r, const ge_p3 *p) { + fe_add(r->YplusX, p->Y, p->X); + fe_sub(r->YminusX, p->Y, p->X); + fe_copy(r->Z, p->Z); + fe_mul(r->T2d, p->T, d2); +} + + +/* +r = p +*/ + +void ge_p3_to_p2(ge_p2 *r, const ge_p3 *p) { + fe_copy(r->X, p->X); + fe_copy(r->Y, p->Y); + fe_copy(r->Z, p->Z); +} + + +void ge_p3_tobytes(unsigned char *s, const ge_p3 *h) { + fe recip; + fe x; + fe y; + fe_invert(recip, h->Z); + fe_mul(x, h->X, recip); + fe_mul(y, h->Y, recip); + fe_tobytes(s, y); + s[31] ^= fe_isnegative(x) << 7; +} + + +static unsigned char equal(signed char b, signed char c) { + unsigned char ub = b; + unsigned char uc = c; + unsigned char x = ub ^ uc; /* 0: yes; 1..255: no */ + uint64_t y = x; /* 0: yes; 1..255: no */ + y -= 1; /* large: yes; 0..254: no */ + y >>= 63; /* 1: yes; 0: no */ + return (unsigned char) y; +} + +static unsigned char negative(signed char b) { + uint64_t x = b; /* 18446744073709551361..18446744073709551615: yes; 0..255: no */ + x >>= 63; /* 1: yes; 0: no */ + return (unsigned char) x; +} + +static void cmov(ge_precomp *t, const ge_precomp *u, unsigned char b) { + fe_cmov(t->yplusx, u->yplusx, b); + fe_cmov(t->yminusx, u->yminusx, b); + fe_cmov(t->xy2d, u->xy2d, b); +} + + +static void select(ge_precomp *t, int pos, signed char b) { + ge_precomp minust; + unsigned char bnegative = negative(b); + unsigned char babs = b - (((-bnegative) & b) << 1); + fe_1(t->yplusx); + fe_1(t->yminusx); + fe_0(t->xy2d); + cmov(t, &base[pos][0], equal(babs, 1)); + cmov(t, &base[pos][1], equal(babs, 2)); + cmov(t, &base[pos][2], equal(babs, 3)); + cmov(t, &base[pos][3], equal(babs, 4)); + cmov(t, &base[pos][4], equal(babs, 5)); + cmov(t, &base[pos][5], equal(babs, 6)); + cmov(t, &base[pos][6], equal(babs, 7)); + cmov(t, &base[pos][7], equal(babs, 8)); + fe_copy(minust.yplusx, t->yminusx); + fe_copy(minust.yminusx, t->yplusx); + fe_neg(minust.xy2d, t->xy2d); + cmov(t, &minust, bnegative); +} + +/* +h = a * B +where a = a[0]+256*a[1]+...+256^31 a[31] +B is the Ed25519 base point (x,4/5) with x positive. + +Preconditions: + a[31] <= 127 +*/ + +void ge_scalarmult_base(ge_p3 *h, const unsigned char *a) { + signed char e[64]; + signed char carry; + ge_p1p1 r; + ge_p2 s; + ge_precomp t; + int i; + + for (i = 0; i < 32; ++i) { + e[2 * i + 0] = (a[i] >> 0) & 15; + e[2 * i + 1] = (a[i] >> 4) & 15; + } + + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + carry = 0; + + for (i = 0; i < 63; ++i) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry << 4; + } + + e[63] += carry; + /* each e[i] is between -8 and 8 */ + ge_p3_0(h); + + for (i = 1; i < 64; i += 2) { + select(&t, i / 2, e[i]); + ge_madd(&r, h, &t); + ge_p1p1_to_p3(h, &r); + } + + ge_p3_dbl(&r, h); + ge_p1p1_to_p2(&s, &r); + ge_p2_dbl(&r, &s); + ge_p1p1_to_p2(&s, &r); + ge_p2_dbl(&r, &s); + ge_p1p1_to_p2(&s, &r); + ge_p2_dbl(&r, &s); + ge_p1p1_to_p3(h, &r); + + for (i = 0; i < 64; i += 2) { + select(&t, i / 2, e[i]); + ge_madd(&r, h, &t); + ge_p1p1_to_p3(h, &r); + } +} + + +/* +r = p - q +*/ + +void ge_sub(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q) { + fe t0; + + fe_add(r->X, p->Y, p->X); + fe_sub(r->Y, p->Y, p->X); + fe_mul(r->Z, r->X, q->YminusX); + fe_mul(r->Y, r->Y, q->YplusX); + fe_mul(r->T, q->T2d, p->T); + fe_mul(r->X, p->Z, q->Z); + fe_add(t0, r->X, r->X); + fe_sub(r->X, r->Z, r->Y); + fe_add(r->Y, r->Z, r->Y); + fe_sub(r->Z, t0, r->T); + fe_add(r->T, t0, r->T); +} + + +void ge_tobytes(unsigned char *s, const ge_p2 *h) { + fe recip; + fe x; + fe y; + fe_invert(recip, h->Z); + fe_mul(x, h->X, recip); + fe_mul(y, h->Y, recip); + fe_tobytes(s, y); + s[31] ^= fe_isnegative(x) << 7; +} diff --git a/lib/ed25519/ge.h b/lib/ed25519/ge.h new file mode 100644 index 00000000..17fde2df --- /dev/null +++ b/lib/ed25519/ge.h @@ -0,0 +1,74 @@ +#ifndef GE_H +#define GE_H + +#include "fe.h" + + +/* +ge means group element. + +Here the group is the set of pairs (x,y) of field elements (see fe.h) +satisfying -x^2 + y^2 = 1 + d x^2y^2 +where d = -121665/121666. + +Representations: + ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z + ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT + ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T + ge_precomp (Duif): (y+x,y-x,2dxy) +*/ + +typedef struct { + fe X; + fe Y; + fe Z; +} ge_p2; + +typedef struct { + fe X; + fe Y; + fe Z; + fe T; +} ge_p3; + +typedef struct { + fe X; + fe Y; + fe Z; + fe T; +} ge_p1p1; + +typedef struct { + fe yplusx; + fe yminusx; + fe xy2d; +} ge_precomp; + +typedef struct { + fe YplusX; + fe YminusX; + fe Z; + fe T2d; +} ge_cached; + +void ge_p3_tobytes(unsigned char *s, const ge_p3 *h); +void ge_tobytes(unsigned char *s, const ge_p2 *h); +int ge_frombytes_negate_vartime(ge_p3 *h, const unsigned char *s); + +void ge_add(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q); +void ge_sub(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q); +void ge_double_scalarmult_vartime(ge_p2 *r, const unsigned char *a, const ge_p3 *A, const unsigned char *b); +void ge_madd(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q); +void ge_msub(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q); +void ge_scalarmult_base(ge_p3 *h, const unsigned char *a); + +void ge_p1p1_to_p2(ge_p2 *r, const ge_p1p1 *p); +void ge_p1p1_to_p3(ge_p3 *r, const ge_p1p1 *p); +void ge_p2_0(ge_p2 *h); +void ge_p2_dbl(ge_p1p1 *r, const ge_p2 *p); +void ge_p3_0(ge_p3 *h); +void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p); +void ge_p3_to_cached(ge_cached *r, const ge_p3 *p); +void ge_p3_to_p2(ge_p2 *r, const ge_p3 *p); + +#endif diff --git a/lib/ed25519/key_exchange.c b/lib/ed25519/key_exchange.c new file mode 100644 index 00000000..984c2c8b --- /dev/null +++ b/lib/ed25519/key_exchange.c @@ -0,0 +1,79 @@ +#include "ed_25519.h" +#include "fe.h" + +void ed25519_key_exchange(unsigned char *shared_secret, const unsigned char *public_key, const unsigned char *private_key) { + unsigned char e[32]; + unsigned int i; + + fe x1; + fe x2; + fe z2; + fe x3; + fe z3; + fe tmp0; + fe tmp1; + + int pos; + unsigned int swap; + unsigned int b; + + /* copy the private key and make sure it's valid */ + for (i = 0; i < 32; ++i) { + e[i] = private_key[i]; + } + + e[0] &= 248; + e[31] &= 63; + e[31] |= 64; + + /* unpack the public key and convert edwards to montgomery */ + /* due to CodesInChaos: montgomeryX = (edwardsY + 1)*inverse(1 - edwardsY) mod p */ + fe_frombytes(x1, public_key); + fe_1(tmp1); + fe_add(tmp0, x1, tmp1); + fe_sub(tmp1, tmp1, x1); + fe_invert(tmp1, tmp1); + fe_mul(x1, tmp0, tmp1); + + fe_1(x2); + fe_0(z2); + fe_copy(x3, x1); + fe_1(z3); + + swap = 0; + for (pos = 254; pos >= 0; --pos) { + b = e[pos / 8] >> (pos & 7); + b &= 1; + swap ^= b; + fe_cswap(x2, x3, swap); + fe_cswap(z2, z3, swap); + swap = b; + + /* from montgomery.h */ + fe_sub(tmp0, x3, z3); + fe_sub(tmp1, x2, z2); + fe_add(x2, x2, z2); + fe_add(z2, x3, z3); + fe_mul(z3, tmp0, x2); + fe_mul(z2, z2, tmp1); + fe_sq(tmp0, tmp1); + fe_sq(tmp1, x2); + fe_add(x3, z3, z2); + fe_sub(z2, z3, z2); + fe_mul(x2, tmp1, tmp0); + fe_sub(tmp1, tmp1, tmp0); + fe_sq(z2, z2); + fe_mul121666(z3, tmp1); + fe_sq(x3, x3); + fe_add(tmp0, tmp0, z3); + fe_mul(z3, x1, z2); + fe_mul(z2, tmp1, tmp0); + } + + fe_cswap(x2, x3, swap); + fe_cswap(z2, z3, swap); + + fe_invert(z2, z2); + fe_mul(x2, x2, z2); + fe_tobytes(shared_secret, x2); +} diff --git a/lib/ed25519/keypair.c b/lib/ed25519/keypair.c new file mode 100644 index 00000000..da789f75 --- /dev/null +++ b/lib/ed25519/keypair.c @@ -0,0 +1,23 @@ +#include "ed_25519.h" +#include "sha512.h" +#include "ge.h" + + +void ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key, const unsigned char *seed) { + ge_p3 A; + + sha512(seed, 32, private_key); + private_key[0] &= 248; + private_key[31] &= 63; + private_key[31] |= 64; + + ge_scalarmult_base(&A, private_key); + ge_p3_tobytes(public_key, &A); +} + +void ed25519_derive_pub(unsigned char *public_key, const unsigned char *private_key) { + ge_p3 A; + + ge_scalarmult_base(&A, private_key); + ge_p3_tobytes(public_key, &A); +} diff --git a/lib/ed25519/license.txt b/lib/ed25519/license.txt new file mode 100644 index 00000000..c1503f91 --- /dev/null +++ b/lib/ed25519/license.txt @@ -0,0 +1,16 @@ +Copyright (c) 2015 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In no event will the +authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the + original software. If you use this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as + being the original software. + +3. This notice may not be removed or altered from any source distribution. diff --git a/lib/ed25519/precomp_data.h b/lib/ed25519/precomp_data.h new file mode 100644 index 00000000..ff23986c --- /dev/null +++ b/lib/ed25519/precomp_data.h @@ -0,0 +1,1391 @@ +static const ge_precomp Bi[8] = { + { + { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, + { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, + { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 }, + }, + { + { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, + { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, + { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 }, + }, + { + { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, + { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, + { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 }, + }, + { + { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, + { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, + { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 }, + }, + { + { -22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877 }, + { -6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951 }, + { 4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784 }, + }, + { + { -25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436 }, + { 25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918 }, + { 23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877 }, + }, + { + { -33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800 }, + { -25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305 }, + { -13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300 }, + }, + { + { -3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876 }, + { -24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619 }, + { -3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683 }, + }, +}; + + +/* base[i][j] = (j+1)*256^i*B */ +static const ge_precomp base[32][8] = { + { + { + { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, + { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, + { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 }, + }, + { + { -12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, -3814571, -358445, -10211303 }, + { -21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, -26829678, -5319081 }, + { 26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, -15472047, -4166697 }, + }, + { + { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, + { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, + { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 }, + }, + { + { -17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, 3284568, -6276540 }, + { 23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, 13059162, 10374397 }, + { 7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, -641708, -101325 }, + }, + { + { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, + { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, + { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 }, + }, + { + { -15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, -9818940, -12085777 }, + { -8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, 1762328, 14866737 }, + { -18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, 27914454, 4383652 }, + }, + { + { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, + { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, + { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 }, + }, + { + { 14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, -33154098, 2381726 }, + { -7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, -32300832, 15351955 }, + { 27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425 }, + }, + }, + { + { + { -13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171 }, + { 27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, -17810127, 12803510 }, + { 17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, 18016357, 4397660 }, + }, + { + { -10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, -17477504, 982639 }, + { 29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, -21047696, 9934963 }, + { 5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, -30123922, -10897950 }, + }, + { + { -27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, -13338309, 5500568 }, + { 12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, 10874051, 13524335 }, + { 25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, -22528059, 5376628 }, + }, + { + { -26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, 4535768, 1569007 }, + { -2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, 31848280, 12543772 }, + { -22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, 7718482, 14474653 }, + }, + { + { 2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, 24316168, -5253567 }, + { 13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, -20729456, 8321686 }, + { 21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, -9984458, 608372 }, + }, + { + { -13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, 27229399, 23887 }, + { -23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, -1633405, 16678954 }, + { -29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, -17169265, 4904953 }, + }, + { + { 24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, 2128236, -4326833 }, + { -16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, -6882542, -2986532 }, + { -22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, 15736322, 4143876 }, + }, + { + { 2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, 23527084, -16458268 }, + { 33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, -15188911, -14416214 }, + { 1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, -4774191, -16323038 }, + }, + }, + { + { + { 6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, 7390890, 2759800 }, + { 4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, 1244380, -12919645 }, + { -4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, 15886429, 16489664 }, + }, + { + { 1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, -30606455, -712933 }, + { -25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, -30836462, 5113182 }, + { -17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, 13847711, 5387222 }, + }, + { + { -18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, -19600662, 10370991 }, + { 20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, 32232924, 16763880 }, + { 9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, 3140038, -16510092 }, + }, + { + { -16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, -28774454, 366295 }, + { 19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, 719605, 11671788 }, + { 8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, 27000813, -10195553 }, + }, + { + { -15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, 6750977, -14521026 }, + { 11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, 8169720, 16220347 }, + { -18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, -5990708, -14166033 }, + }, + { + { -23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, 2655861, 1738395 }, + { -27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, -19087449, -11005278 }, + { 1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, 10478196, 8544890 }, + }, + { + { 32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, 17789017, -3395995 }, + { -30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, 30460520, 1052596 }, + { -11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, 3179268, -9478891 }, + }, + { + { 31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, -16411740, 19072640, -9511060 }, + { 11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, 473099, 5040608 }, + { -20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, -19558642, -3131606 }, + }, + }, + { + { + { 7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, -27868496, 11538389 }, + { -19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, -5676054, 5797016 }, + { -11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, -2369172, -5877341 }, + }, + { + { -22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, 15123619, 10811505 }, + { 14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, -28886779, -11974553 }, + { -28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, 4714547, -9600655 }, + }, + { + { 15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, -11745876, 12340220 }, + { 12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, 15370987, 9608631 }, + { -4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, -28210621, -8814099 }, + }, + { + { 26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, 20571223, 8420556 }, + { 14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, -21574435, -12476749 }, + { 236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, 15069930 }, + }, + { + { 1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, -30663140, -2331391 }, + { 5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, -33069337, 9234253 }, + { 20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, 18979186, 13396066 }, + }, + { + { 24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, 33514650, 7021958 }, + { -11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, -21480480, 12868082 }, + { -28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, -12455797, -8089383 }, + }, + { + { -30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, -15233648, 5540521 }, + { -11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, -16128528, -14962807 }, + { 23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, -11513277, -15205948 }, + }, + { + { 9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, -19179614, 5867134 }, + { -32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, -29749703, -16108455 }, + { 27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, -19526700, 7734629 }, + }, + }, + { + { + { -8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, 18549497, 15302069 }, + { -32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, -8872242, 8424746 }, + { 24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, -11284482, -828919 }, + }, + { + { 11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, -5066034, 16498837 }, + { 8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, 6536641, 10543906 }, + { -28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, -27235709, 8876771 }, + }, + { + { -25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, -604426, -9039817 }, + { 10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, 3084098 }, + { 10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, -30233575, 15272409 }, + }, + { + { -12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, -25014757, 1950504 }, + { -26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, 15341279, 8373727 }, + { 28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, -2878207, 15190420 }, + }, + { + { -32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, -26033313, -6875003 }, + { -1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, -16247779, -12154742, 6048605 }, + { -30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, 11213262, 9168384 }, + }, + { + { -26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, 20470157, -16398701 }, + { -23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, 14042978, 5230683 }, + { 29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, -5933891, -12449708 }, + }, + { + { -3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, -28539189, 15326563 }, + { -19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, -20514118, 9168260 }, + { -5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, 33087103, -9011387 }, + }, + { + { -19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, -15000531, -5996870, 15664672 }, + { 23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, 18151676, 13417686 }, + { -24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, 15271676, -15452665 }, + }, + }, + { + { + { 11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, -6921301, -13440182 }, + { -31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, 12215110, 12028277 }, + { 14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, -16900089, -655628 }, + }, + { + { -4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, 23973261, -12684474 }, + { -26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, 32013174, -10103539 }, + { -25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, 4425632, -837822 }, + }, + { + { -10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, 4725005, 14044970 }, + { 19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, -19522291, -16109756 }, + { -24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, 9282262, 10282508 }, + }, + { + { -26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, 5542595, -10702683 }, + { -10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, -18093885, 10114655 }, + { -20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, 25953725, -106158 }, + }, + { + { -4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, -3315279, 12831125 }, + { -15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, 7381791, -2421839 }, + { -20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, -15362489, 12339664 }, + }, + { + { 27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, 25091167, 14856294 }, + { -18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, -22182473, -9012899 }, + { -11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, 30146206, 9142070 }, + }, + { + { 3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, -8237858, 4171294 }, + { -15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, 345228, -5462949 }, + { -21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, -16253183, -13582083 }, + }, + { + { 31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, 28012650, -16405420 }, + { -5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, -8867157, 3507940 }, + { 29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, -9164929, 6580396 }, + }, + }, + { + { + { -12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, -2095802, 9304567 }, + { 20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, 5289421, -477127 }, + { -16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, 26939669, -3752294 }, + }, + { + { -12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, -3698806, 117887 }, + { 22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, -23319780, 541964 }, + { 16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, -13337066, -13552195 }, + }, + { + { 9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, 690426, 14876244 }, + { 24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, -22380984, 6618999 }, + { -1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, 32239829, -5652762 }, + }, + { + { -18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, 32779359, 5095274 }, + { -33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, 21639561, -2630236 }, + { -16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, 17874574, 558605 }, + }, + { + { -13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, 2085892, 5119761 }, + { -22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, -31999993, -5759884 }, + { -6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, -22351378, 12961482 }, + }, + { + { -24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, -30194652, -5159638 }, + { -11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, 7589640, 8945490 }, + { -32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, 24099109, -14456170 }, + }, + { + { 5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, -6231896, -10280736 }, + { 10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, -3363451, -6441124 }, + { -17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, -13866408, 7421392 }, + }, + { + { 8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, 28826358, -4123029 }, + { 6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, 13746021, -1742048 }, + { 28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, 4771362, -8419958 }, + }, + }, + { + { + { 24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, 3569627, 11342593 }, + { 26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, 7325975, -14801071 }, + { -11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, -17646694, -8186692 }, + }, + { + { 11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, 9280358, -3973687 }, + { -160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, -11181622, -15545091, 4387441 }, + { -20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, 20617071, -7482001 }, + }, + { + { -938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, 24345683, 10325460 }, + { -19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, 4766743, 3552007 }, + { -21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, 10988822, -3994762 }, + }, + { + { 15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, -8295852, 13296005 }, + { -9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, -30000824, 12074674 }, + { 4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, 29832613, -16391035 }, + }, + { + { 7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, -27122660, 13101590 }, + { -2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, -30356070, -4190957 }, + { -30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, 19568978, 9628812 }, + }, + { + { 33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, -6291786, 3437740 }, + { -18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, 7463304, 4176122 }, + { -27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, 24216882, 5944158 }, + }, + { + { 8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, 11632993, 5847885 }, + { 26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, -9727230, 4782140 }, + { 19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, 6358847, -1873857 }, + }, + { + { 801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, -13538503, 1387155 }, + { 19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, -21866831, 11835260 }, + { 19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, -26560550, 5052483 }, + }, + }, + { + { + { -3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, 12228557, -7003677 }, + { 32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, -6114803, 8653815 }, + { 22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, 28483275, 2841751 }, + }, + { + { -16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, 19932058, -12739203 }, + { -11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, -4777469, -13910208 }, + { 1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, -14013818, 3093230 }, + }, + { + { 16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, -24049421, -6691850 }, + { -21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, -21652117, -16739389 }, + { -9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, 17361620, 11864968 }, + }, + { + { -11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, -25578460, -16240689 }, + { 14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, 9145645, -6443880 }, + { 5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, -5652273, -3050304 }, + }, + { + { 30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, -27999779, 1803632 }, + { -3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, -21405489, 7047412 }, + { 20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, -32343828, -10257566 }, + }, + { + { -20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, -21679658, 2288038 }, + { -26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, -24819617, 12570232 }, + { -1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, -11974704, 4724943 }, + }, + { + { 17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, -29576300, 1903856 }, + { 23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, 26955097, 14109738 }, + { 15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, 31960942, 11934971 }, + }, + { + { -27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, 10491392, 1379718 }, + { -13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, 21432314, 12180697 }, + { -11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, -10301319, -13872883 }, + }, + }, + { + { + { 5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, 26128231, 6032912 }, + { -26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, -25875034, -10437358 }, + { 3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, 12376617, 3188849 }, + }, + { + { 29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, -16109234, -9852307 }, + { -14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, 18640741, -960977 }, + { -6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, 10530747, 1053335 }, + }, + { + { -29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, -2948985, 24018831, 15026644 }, + { -22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, -2314791, -15145616 }, + { -27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, -23166419, -2531735 }, + }, + { + { -21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, -5070217, 726099 }, + { 29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, 30007388, -15823341 }, + { -936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, 4555336 }, + }, + { + { -23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, -11985298, 12422646 }, + { 31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, 29120153, 13924425 }, + { -17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, 7240931, -237388 }, + }, + { + { -31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, 3293637, -15551743 }, + { -16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, 12796905, -6335822 }, + { -8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, 3222231, -11160462 }, + }, + { + { 18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, -7550776, 2484985 }, + { 9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, -19802075, -3034702 }, + { -22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, -30097688, -10618797 }, + }, + { + { 21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, -7360503, -4109293 }, + { 27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, -23659143, -8132100 }, + { 19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, 13243957, 8709688 }, + }, + }, + { + { + { 12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, 5150968, 7274186 }, + { 2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, 31021603, -9793610 }, + { -2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, 15067285, -14147707 }, + }, + { + { 7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, -19379462, -12403220 }, + { 915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, -7220461, 16527025 }, + { 32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, 23834301, 6588044 }, + }, + { + { 32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, -20314580, -1305992 }, + { -4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, 1976123, -7249027 }, + { 21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, 12331345, -8237197 }, + }, + { + { 8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, 10008136, -4667901 }, + { 31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, 8684155, -10532952 }, + { 19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, 31316348, 14219878 }, + }, + { + { -28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, 29126555, 9207390 }, + { 32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, -7957600, -14435730 }, + { 2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, 28346258, 1994730 }, + }, + { + { -19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, 22628102, 8115180 }, + { -30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, -9943017, 930272 }, + { -15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, -22765376, -10650715 }, + }, + { + { -22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, -16554220, -1867018, 8398970 }, + { -31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, 18009408, -15772772 }, + { -17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, 19288549, 1325865 }, + }, + { + { 15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, 33166107, -8042750 }, + { 20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, 2213263, -13878373 }, + { 32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, -5766928, 8371348 }, + }, + }, + { + { + { 9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, -26442943, 10486144 }, + { -22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, -23921530, -11455195 }, + { 5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, 19985175, -3436086 }, + }, + { + { -13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, 65030, 8370684 }, + { -8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, 12743482, -9800518 }, + { -2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, 18800704, 255233 }, + }, + { + { -5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, 19258688, -14753793 }, + { -2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, -1524053, 2055794 }, + { 580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, -30021019, 7394435 }, + }, + { + { 23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, -4821741, 14799921 }, + { 13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, -9056018, 7402518 }, + { 2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, 8205061, 1607563 }, + }, + { + { 14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, 30019587, -9029278 }, + { -27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, 9944378, 8024 }, + { 4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, -8142388, 5640030 }, + }, + { + { 10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, 15219798, -14327783 }, + { 27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, -18342183, 9742717 }, + { 6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, 1994844 }, + }, + { + { 14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, 24319929, -6446333 }, + { 16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, -30102368, -4739048 }, + { 22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, 21880021, -12250760 }, + }, + { + { -4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, 8840445, 4957760 }, + { -15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, -14796503, 5005757 }, + { -2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, -16395286, -2176112 }, + }, + }, + { + { + { -19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, 22208662, 2000468 }, + { 3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, -3691214, -7414184 }, + { 10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, -3132688, 16400289 }, + }, + { + { 15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, 26894937, 9132066 }, + { 24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, -6817084, -2692882 }, + { 13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, -4688006, 2364226 }, + }, + { + { 16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, 15445875, -7798101 }, + { 29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, -278050, -15759279 }, + { -6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, -29828287, -10815811 }, + }, + { + { 27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, 17293797, 960709 }, + { 20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, -19384511, 7639714 }, + { -2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, 29994677, -15808121 }, + }, + { + { 9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, -31184575, 709464 }, + { 12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, 16102007, 13205847 }, + { 13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, 10151379, 10394400 }, + }, + { + { 4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, -33302334, 8934414 }, + { -15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, -11536886, 11721158 }, + { 17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, -9205489, -1280045 }, + }, + { + { -461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, 6108462, -6183415 }, + { -5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, -26898490, -7867459 }, + { -31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, 7592688, -14992079 }, + }, + { + { 21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, -28256052, 4298412 }, + { -20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, 13717174, 10805743 }, + { -14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, -26938930, -5863836 }, + }, + }, + { + { + { 12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, -4381056, 9882022 }, + { 18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, -23967430, -3299429 }, + { -6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, -2403099, 5276065 }, + }, + { + { 30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, 1449224, 13082861 }, + { 10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, -21016438, -8202000 }, + { -33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, -10300080, -11060101 }, + }, + { + { 32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, -22664288, 6904815 }, + { 29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, -29003639, -6657642 }, + { 10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, -25584551, 5181966 }, + }, + { + { 25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, -2322852, -12388574 }, + { -21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, 20603771, -6561742 }, + { -18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, 4662781, 7820689 }, + }, + { + { 12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, 16136752, 15264020 }, + { -10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, 19012087, 3772772 }, + { 3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, 20527771, 12988982 }, + }, + { + { -14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, 3299665, -12424953 }, + { -16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, -25583147, 8991218 }, + { -17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, -30242287, 1507265 }, + }, + { + { 29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, 14606362, -10647073 }, + { -3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, 4170404, -2085325 }, + { -11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, -14276837, -8400798 }, + }, + { + { -4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, 13240845, 10965870 }, + { -7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, 29514390, 4302863 }, + { -13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, 17846988, -13971927 }, + }, + }, + { + { + { -2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, -5222701, 12650267 }, + { -9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, -19134326, 10958663 }, + { 22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, -25083830, 4271862 }, + }, + { + { -25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, -32526221, 8469673 }, + { 15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, 24154791, -9460943 }, + { 15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, 24913809, 9815020 }, + }, + { + { -4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, 18735128, 9466238 }, + { 11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, -22728655, 16199064 }, + { 14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, 30801119, 2164795 }, + }, + { + { 15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, -13492807, 1268052 }, + { -10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, -17055369, 3565904 }, + { 29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, -27162222, -14030531 }, + }, + { + { -13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, 2244111, -14001979 }, + { -5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, 13491506, 4641841 }, + { 10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, 14476989, -12767431 }, + }, + { + { 10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, -27912810, 12651324 }, + { -31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, 21721536, -3148940 }, + { 10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, 13216206, 14842320 }, + }, + { + { -15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, -27434397, 9900184 }, + { 14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, -7051866, -8040114 }, + { 30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, 9524356, -7018878 }, + }, + { + { 12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, 15663516, 4035784 }, + { -2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, -22258478, 4659091 }, + { -16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, -2178256, -13455585 }, + }, + }, + { + { + { -8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, 23440562, -290208 }, + { 10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, -16571960, -7442864 }, + { 17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, -3646624, 3898661 }, + }, + { + { 7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, 21831162, 1245233 }, + { 26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, -30402091, -16716212 }, + { -12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, -22338025, 13987525 }, + }, + { + { -24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, -7469781, -2858068 }, + { 9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, -14581012, 4091397 }, + { -8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, 29077877, -14741988 }, + }, + { + { 5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, -20017847, 2357889 }, + { 32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, 5727338, 189038 }, + { 14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, -25888931, -14884697 }, + }, + { + { 20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, 13913677, -5137875 }, + { -25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, 12878652, 8511905 }, + { -25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, 5568676, -3127656 }, + }, + { + { 11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, -14395196, 8070818 }, + { 27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, -11552749, -1028714 }, + { 10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, -27493162, 15431203 }, + }, + { + { 20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, -15056790, -7935931 }, + { -30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, -3356550, 9594024 }, + { -23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, 13352335, -10977084 }, + }, + { + { -1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, -13215537, -319204 }, + { 20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, -22750759, 14523817 }, + { 27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, -30172742, -4805667 }, + }, + }, + { + { + { 11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, -27727044, 11358504 }, + { -12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, -26123651, 4985768 }, + { -19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, -19414307, -15621255 }, + }, + { + { 6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, -1691065, -9004790 }, + { 1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, -16796596, -5031438 }, + { -22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, -99853, -10237333 }, + }, + { + { 17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, -29260363, -5511971 }, + { 31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, 4882242, -10626905 }, + { 29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, -5190932, -4162409 }, + }, + { + { 12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, 330070, -3659409 }, + { 6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, -271295, 12071499 }, + { -8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, -5159697, 3829363 }, + }, + { + { 28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, 26333140, 14267664 }, + { -11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, -21761224, 8930324 }, + { -21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, -26491501, -16408940 }, + }, + { + { 13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, -15943699, 13814990 }, + { -17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, -1956526, -1776914 }, + { -25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, -18685978, 4578290 }, + }, + { + { 24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, 21964432, 8235257 }, + { -6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, 13006806, 2355433 }, + { -16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, 1141648, -12796236 }, + }, + { + { -12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, 32674895, 13715045 }, + { 11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, -28740881, -15642093 }, + { -1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, -27245943, 4383347 }, + }, + }, + { + { + { -28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, 27193557, 6245191 }, + { -15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, 22510453, 8577507 }, + { -12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, -29571492, -3635906 }, + }, + { + { 3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, 5611860, 8164018 }, + { -16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, 5773085, -8422109 }, + { -23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, 31632953, 190926 }, + }, + { + { -24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, -25341555, -3627528 }, + { 8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, -1672488, 827625 }, + { -32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, -24878478, 1541286 }, + }, + { + { 2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, 20456845, -1885033 }, + { 27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, 29439641, 15138866 }, + { 21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, 7779328, 109896 }, + }, + { + { 30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, 23177719, -554075 }, + { 26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, 4439158, -13279347 }, + { -22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, -17717757, -5461437 }, + }, + { + { -5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, 8550082, -15114165 }, + { -18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, -23260460, -8428588 }, + { -32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, -21902570, 1494193 }, + }, + { + { -19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, 8742704, 12967017 }, + { -28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, -29903758, 15553883 }, + { 21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, 19375923, -12647961 }, + }, + { + { 8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, 2862653, 9455043 }, + { 29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, 15511449, 4789663 }, + { -20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, 23513200, 16652362 }, + }, + }, + { + { + { -33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, -18319198, 10212860 }, + { 2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, -9988298, -12506466 }, + { -24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, -2499677, -16710063 }, + }, + { + { -26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, 2719757, 4940997 }, + { -1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, 26699843, 5276295 }, + { -13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, -15452774, 7159369 }, + }, + { + { 9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, 22477218, -8403385 }, + { 18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, -22548173, 9334109 }, + { 2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, -22587149, 536906 }, + }, + { + { 4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, 10838060, -15420424 }, + { -19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, 6378260, 699185 }, + { 7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, -5045064, -13373962 }, + }, + { + { -7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, -3918636, -9669325 }, + { 10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, -21786507, 5427593 }, + { 696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, 19236243, 12477404 }, + }, + { + { -11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, -23169516, 7733644 }, + { 17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, 10322027, 15313801 }, + { 26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, -24449242, 10890804 }, + }, + { + { -31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, 25180797, -5334884 }, + { -586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, 316878, 13820577 }, + { -9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, 30696930, -3712849 }, + }, + { + { 32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, -32349517, 7392473 }, + { -8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, 25112947, -2926644 }, + { -2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, 5537438, -13914319 }, + }, + }, + { + { + { -11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, 31234590, 6090599 }, + { -9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, -6970405, -9034768 }, + { -27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, 20650474, 1804084 }, + }, + { + { -27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, 27842616, -202328 }, + { -15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, -11375082, 12714369 }, + { 20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, -19733229, 12796920 }, + }, + { + { 12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, -12899438, 3480665, -15182815 }, + { -32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, -33374054, 2771025 }, + { -21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, -24552282, 15673397 }, + }, + { + { -20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, 3727144, -12934448 }, + { 6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, 31123697, -10958981 }, + { 30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, -16432438, 9648165 }, + }, + { + { 32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, -26008332, -11377501 }, + { 17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, 2662509, -16297073 }, + { -1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, 32247248, -14389861 }, + }, + { + { 14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, -16027623, -13378845 }, + { -1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, 17597934, 2346211 }, + { 18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, 3059833, -11782870 }, + }, + { + { 10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, -33502212, 3592096 }, + { 33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, -982665, -6837803 }, + { -32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, 26353178, 693168 }, + }, + { + { 30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, 31375464, 14369965 }, + { -14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, 17901441, 16011505 }, + { 18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, -16046376, 8764035, 12309598 }, + }, + }, + { + { + { 5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, 17544096, -10593782 }, + { 5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, -5565381, 12348900 }, + { -31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, -10632534, -585479 }, + }, + { + { -12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, 30944593, 1130208 }, + { 8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, 23550156, -271232 }, + { 17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, -408818, -137719 }, + }, + { + { 16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, 3260525, -7166271 }, + { -4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, -12748722, 2701326 }, + { -8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, 11329923, 1862132 }, + }, + { + { 14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, -9037963, -940300 }, + { 8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, -15438168, 11595570 }, + { 15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, -13489462, -4363670 }, + }, + { + { -2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, 977108, 699994 }, + { -12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, -30534533, -14337913 }, + { 31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, 12493932, -5409317 }, + }, + { + { -25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, 29375955, 6024730 }, + { 842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, -9940361, 2854096 }, + { -4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, 12121869, 16648078 }, + }, + { + { -15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, 20237806, 2838411 }, + { -19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, 1068881, -12499905 }, + { -9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, 12521378, 4845654 }, + }, + { + { -28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, -6482306, -12885870 }, + { -23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, -3113227, 13974498 }, + { 12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, 18895762, 12629579 }, + }, + }, + { + { + { 14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, 32195181, 3895677 }, + { 10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, -8982069, 4429647 }, + { -2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, 18047436, -15281743 }, + }, + { + { -25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, -12850837, -17620701, -9408468 }, + { 21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, -29958985, 8773375 }, + { -25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, -7874389, -13920155 }, + }, + { + { 6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, -1721788, -2776725 }, + { -12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, 28408820, 6816612 }, + { -10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, -10339570, 5067943 }, + }, + { + { -30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, 5331210, -10105944 }, + { 30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, 27679908, -1648928 }, + { 9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, 22611444, -12715406 }, + }, + { + { 22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, 4854859, 6622139 }, + { -8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, 26404409, 13001963 }, + { -31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, -15405155, 11020693 }, + }, + { + { 1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, 28761762, 1406734 }, + { -448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, 25577411, -13378680 }, + { -24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, -9368926, 4745410 }, + }, + { + { -9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, 10931924, -11931931 }, + { -16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, -20470770, 13434654 }, + { 22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, 28445307, -5364710 }, + }, + { + { 29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, 9074234, 1167180 }, + { -26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, 3843903, 9367684 }, + { -10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, -139197, 4242895 }, + }, + }, + { + { + { 22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, -27417366, 16625501 }, + { -11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, -27351616, 14247413 }, + { 6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, 27108877, -1180880 }, + }, + { + { -8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, -27148451, 981874 }, + { 22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, -10479804, -16197962 }, + { -7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, -15933690, 3797899 }, + }, + { + { 21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, 20491983, -8042152 }, + { 9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, 30997318, -6703063 }, + { 7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, 17649998, -250080 }, + }, + { + { -9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, -2233253, 7662146 }, + { -17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, -3174674, 3440183 }, + { -19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, 4862400, 1133 }, + }, + { + { -32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, -30594991, -7379421 }, + { -3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, 18278453, 15405622 }, + { -4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, -21875062, 13626197 }, + }, + { + { 2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, 13340839, 15928663 }, + { 31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, -23452306, 5964753 }, + { 4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, 2538215, -7570755 }, + }, + { + { -9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, -629256, -15958862 }, + { -26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, 11659922, -11115118 }, + { 26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, 33100372, -1306171 }, + }, + { + { 15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, -5931810, -14466380 }, + { 16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, -10735770, -10039824 }, + { 28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, 18036436, 5803270 }, + }, + }, + { + { + { -817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, -6159431, -14117438 }, + { -31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, -20095739, 11763584 }, + { -594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, -19773211, -10713562 }, + }, + { + { 30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, -17020157, 992471 }, + { 18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, -11574335, -10601610 }, + { 19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, 26224780, 16452269 }, + }, + { + { -30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, -20533829, 3698650 }, + { 14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, 27394301, 12015369 }, + { 19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, -13259127, -3402461 }, + }, + { + { 30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, -8550524, -10393462 }, + { -5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, -4976164, 12651793 }, + { -2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, 9768698, -2533218 }, + }, + { + { -24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, 4940095, 10678226 }, + { 18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, -24372348, 16582019 }, + { -15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, -11003761, 7989037 }, + }, + { + { 31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, -19686113, -14800171 }, + { -17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, 28550068, -6857132 }, + { -28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, 1155602, 5988841 }, + }, + { + { 21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, 24618407, 8283181 }, + { -33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, 1466169, 10740210 }, + { -1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, -32164649, -15327040 }, + }, + { + { 3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, -3260321, -573935 }, + { 24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, -7380369, -6144105 }, + { -28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, -14453128, -1625486, -6494814 }, + }, + }, + { + { + { 793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, -9906200, -621852 }, + { 5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, -6171428, -15186581 }, + { -4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, -1558923, -9863646 }, + }, + { + { 10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, -30581476, -15757844 }, + { 10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, 24003793, 14264025 }, + { 27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, 13958495, -5732453 }, + }, + { + { -23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, 7340521, -15410068 }, + { 4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, -15066784, -10375192 }, + { -17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, -8843694, 3849921 }, + }, + { + { -9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, 5002294, -15550259 }, + { -12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, -25845716, 12741426 }, + { -5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, 28834118, -7646072 }, + }, + { + { -17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, 20461858, 5491305 }, + { 13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, 11293807, -28588204, -9421832 }, + { 28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, 29867745, -8795943 }, + }, + { + { -16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, 22853429, -4012011 }, + { 24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, 12876623, -2112447 }, + { 17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, 16031844, 3723494 }, + }, + { + { -28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, 23896954, -4314245 }, + { -20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, -9919646, -8826859 }, + { 28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, -18051778, -2082915 }, + }, + { + { 16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, -19715240, 7847707 }, + { 10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, -10020567, 3852848 }, + { -11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, 29330899, -15076224 }, + }, + }, + { + { + { -25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, -27548796, 12314391 }, + { 15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, 16103996, -3731215 }, + { -23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, -29815713, -9841101 }, + }, + { + { 23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, -25205859, 2739713 }, + { 21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, 4931255, 11987849 }, + { -7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, -30076310, 10117930 }, + }, + { + { -29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, 12890019, 15728940 }, + { -21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, 12885167, 8311031 }, + { -17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, 1888765, -5435404 }, + }, + { + { -25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, -12707869, -19464434, -3340243 }, + { -23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, 17151279, -9854116 }, + { -24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, -16866484, 16176525 }, + }, + { + { -23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, -28746253, -10197509 }, + { -10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, 32808310, 1099883 }, + { 15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, 2051441, -15225865 }, + }, + { + { -3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, -22686354, 16633660 }, + { 4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, 8402478, -9864273 }, + { -28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, 17983010, 9967138 }, + }, + { + { -25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, 17507396, 3651560 }, + { -10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, -18553322, -11357135 }, + { 2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, -5605463, -7621941 }, + }, + { + { -4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, 17258084, -7977739 }, + { 18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, 26908270, 12150756 }, + { -30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, -32302074, 16215819 }, + }, + }, + { + { + { -6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, -7503072, -8675347 }, + { -27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, 21676107, -1943028 }, + { 21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, -3556191, -7913075 }, + }, + { + { 16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, -18907032, -9662799 }, + { -2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, 25459437, -4564609 }, + { -25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, -6432418, -1644817 }, + }, + { + { -23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, 6335692, 7249989 }, + { -30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, 25993170, -12478523 }, + { 4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, 31820368, 15075278 }, + }, + { + { 31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, -17970238, 12833045 }, + { 19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, -9413939, 14905377 }, + { 24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, 27628530, -7555480 }, + }, + { + { 17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, -14110875, 15297016 }, + { 510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, 8683221, 2921426 }, + { 18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, 4674690, 13890525 }, + }, + { + { 13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, 4389687, 288396 }, + { 9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, 23388070, 16052080 }, + { 12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, -14754271, -10812892 }, + }, + { + { 15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, -28822128, 929275 }, + { 11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, -9371869, -21393143, 2465074 }, + { 20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, 2463391, -4622140 }, + }, + { + { -16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, 4003896, 12673717 }, + { -1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, 7903886, 2348101 }, + { 24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, -28377538, 10048127 }, + }, + }, + { + { + { -12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, -32297756, 15221632 }, + { -26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, 29769758, 6593415 }, + { -31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, 30958054, 8292160 }, + }, + { + { 31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, 26143136, -3148876 }, + { 22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, -3758243, -2304625 }, + { -15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, -16664475, 8194478 }, + }, + { + { 27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, 28572286, 3005164 }, + { 26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, -26094821, -13079595 }, + { -7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, 32208683, -1198248 }, + }, + { + { -16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, -27672585, -11539858 }, + { 15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, 15397331, -4130193 }, + { 8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, -27505566, 15087184 }, + }, + { + { -18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, 16788528, -5868942 }, + { -1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, 31322514, -11615635 }, + { 21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, 13040862, -12112948 }, + }, + { + { 11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, 1613711, 4896935 }, + { -25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, -23971776, -11267415 }, + { -15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, -23753030, -8436416 }, + }, + { + { -7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, 23097949, -566018 }, + { 4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, -18512313, 2424778 }, + { 366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, -3642553, 12412659 }, + }, + { + { -24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, -3141501, 11179385 }, + { 18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, -18684645, -11443503 }, + { 476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, 11052904, 5219329 }, + }, + }, + { + { + { 20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, 9014762, -8579056 }, + { -13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, 8473550, -3256838 }, + { 24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, -24568481, 11788948 }, + }, + { + { -3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, -24568470, -1458691 }, + { -15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, -18513325, 8464118 }, + { -23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, 33313881, -8370517 }, + }, + { + { -20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, -8940735, -5818269 }, + { -6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, 28358192, -4253904 }, + { -23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, -5351664, 5596589 }, + }, + { + { -28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, 1768301, 15373193 }, + { -7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, -24660491, 3442910 }, + { -30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, -18585478, 13365930 }, + }, + { + { -7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, -9689599, -3031667 }, + { 25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, 15723479, -15163481 }, + { -9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, 519526, -1235876 }, + }, + { + { 22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, 16648397, 7605640 }, + { -8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, -9468848, 4763278 }, + { -21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, -11356529, 728112 }, + }, + { + { 26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, -29365436, -5498272 }, + { 17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, 10750447, 10014012 }, + { -10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, 3424691, 7540221 }, + }, + { + { 30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, -9244265, 15258046 }, + { 13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, 1062915, -5136345 }, + { -19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, 32960380, 1459310 }, + }, + }, + { + { + { 19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, 18638003, -11174937 }, + { 31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, -6643087, -5442636 }, + { -9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, -32294889, -6456008 }, + }, + { + { -2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, -7852844, -8138429 }, + { -15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, -9431476, 5203576 }, + { 31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, 31809243, 7347066 }, + }, + { + { -9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, 15192876, -2087490 }, + { -12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, 33473243, -13382104 }, + { 33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, -6135662, 14480053 }, + }, + { + { 31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, 27595050, 8737275 }, + { -20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, 26498114, 66511 }, + { 22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, -13327787, -7515095 }, + }, + { + { -28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, -17127465, 15115439 }, + { 23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, 8236921, 16492939 }, + { -23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, 6708380, -6222424 }, + }, + { + { 2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, 29955601, -11678310 }, + { 3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, -9917575, 6216608 }, + { -32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, -26767480, 7525079 }, + }, + { + { -23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, -19386633, 11994101 }, + { 21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, -7477437, 13381418 }, + { 18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, 28535282, 15779576 }, + }, + { + { 30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, 9955285, -16303356 }, + { 9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, 20194861, 13380996 }, + { -26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, 26342023, 10146099 }, + }, + }, + { + { + { -26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, 21612326, -545728 }, + { -13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, -20856566, 11649658 }, + { -10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, 33514190, 2333242 }, + }, + { + { -21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, 24350578, -13450001 }, + { -4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, 8317860, 12352766 }, + { 18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, 20712163, 6719373 }, + }, + { + { 26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, 26224235, -3297458 }, + { -17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, 18620611, -16428628 }, + { -13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, -30701573, -16479657 }, + }, + { + { -23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, 13735342, 1934062 }, + { 25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, -21277997, 5473616 }, + { 31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, 29111212, -5451014 }, + }, + { + { 24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, -10295587, 452383 }, + { -25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, 15766062, 8407814 }, + { -20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, -8067909, 2276718 }, + }, + { + { 30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, 22740376, -7303417 }, + { 2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, -15637599, 13295222 }, + { 33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, -4451357, -14669444 }, + }, + { + { -20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, 7770569, 9620597 }, + { 23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, -33502340, -14767970 }, + { 1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, 30494170, -11440799 }, + }, + { + { -5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, -1684339, -13333647 }, + { 13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, 3353509, 4033511 }, + { -29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, 27485041, 7356032 }, + }, + }, + { + { + { 9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, 28431036, -16771834 }, + { -23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, 15970762, 4099461 }, + { 29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, 11465739, 8317062 }, + }, + { + { -25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, 20969334, -5157516 }, + { -20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, 11177095, 14989547 }, + { -24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, -28487508, 9930240 }, + }, + { + { -17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, -13403753, 16291481, -5314038 }, + { -33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, 4368891, 9788741 }, + { 16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, 16250552, -11111103 }, + }, + { + { -19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, -30190403, 4782747 }, + { -1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, 22546403, 437323 }, + { 31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, -30633590, -14097016 }, + }, + { + { -14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, -19284671, -6114373 }, + { 15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, -29380133, 11754228 }, + { -2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, 22761615, -10134141 }, + }, + { + { 16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, 32851222, 11717399 }, + { 11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, -1193175, -4030831 }, + { -185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, -19985279, -3948376 }, + }, + { + { -32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, -30772034, -15486313 }, + { -18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, 10928917, 3011958 }, + { -6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, 18008031, 10258577 }, + }, + { + { -22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, 25127874, 6671743 }, + { 29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, 25838796, 4642684 }, + { -20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476 }, + }, + }, +}; diff --git a/lib/ed25519/sc.c b/lib/ed25519/sc.c new file mode 100644 index 00000000..ca5bad2c --- /dev/null +++ b/lib/ed25519/sc.c @@ -0,0 +1,809 @@ +#include "fixedint.h" +#include "sc.h" + +static uint64_t load_3(const unsigned char *in) { + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + + return result; +} + +static uint64_t load_4(const unsigned char *in) { + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + result |= ((uint64_t) in[3]) << 24; + + return result; +} + +/* +Input: + s[0]+256*s[1]+...+256^63*s[63] = s + +Output: + s[0]+256*s[1]+...+256^31*s[31] = s mod l + where l = 2^252 + 27742317777372353535851937790883648493. + Overwrites s in place. +*/ + +void sc_reduce(unsigned char *s) { + int64_t s0 = 2097151 & load_3(s); + int64_t s1 = 2097151 & (load_4(s + 2) >> 5); + int64_t s2 = 2097151 & (load_3(s + 5) >> 2); + int64_t s3 = 2097151 & (load_4(s + 7) >> 7); + int64_t s4 = 2097151 & (load_4(s + 10) >> 4); + int64_t s5 = 2097151 & (load_3(s + 13) >> 1); + int64_t s6 = 2097151 & (load_4(s + 15) >> 6); + int64_t s7 = 2097151 & (load_3(s + 18) >> 3); + int64_t s8 = 2097151 & load_3(s + 21); + int64_t s9 = 2097151 & (load_4(s + 23) >> 5); + int64_t s10 = 2097151 & (load_3(s + 26) >> 2); + int64_t s11 = 2097151 & (load_4(s + 28) >> 7); + int64_t s12 = 2097151 & (load_4(s + 31) >> 4); + int64_t s13 = 2097151 & (load_3(s + 34) >> 1); + int64_t s14 = 2097151 & (load_4(s + 36) >> 6); + int64_t s15 = 2097151 & (load_3(s + 39) >> 3); + int64_t s16 = 2097151 & load_3(s + 42); + int64_t s17 = 2097151 & (load_4(s + 44) >> 5); + int64_t s18 = 2097151 & (load_3(s + 47) >> 2); + int64_t s19 = 2097151 & (load_4(s + 49) >> 7); + int64_t s20 = 2097151 & (load_4(s + 52) >> 4); + int64_t s21 = 2097151 & (load_3(s + 55) >> 1); + int64_t s22 = 2097151 & (load_4(s + 57) >> 6); + int64_t s23 = (load_4(s + 60) >> 3); + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s23 = 0; + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s22 = 0; + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s21 = 0; + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s20 = 0; + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s19 = 0; + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + s18 = 0; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s17 = 0; + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s16 = 0; + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s15 = 0; + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s14 = 0; + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s13 = 0; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 << 21; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + s[0] = (unsigned char) (s0 >> 0); + s[1] = (unsigned char) (s0 >> 8); + s[2] = (unsigned char) ((s0 >> 16) | (s1 << 5)); + s[3] = (unsigned char) (s1 >> 3); + s[4] = (unsigned char) (s1 >> 11); + s[5] = (unsigned char) ((s1 >> 19) | (s2 << 2)); + s[6] = (unsigned char) (s2 >> 6); + s[7] = (unsigned char) ((s2 >> 14) | (s3 << 7)); + s[8] = (unsigned char) (s3 >> 1); + s[9] = (unsigned char) (s3 >> 9); + s[10] = (unsigned char) ((s3 >> 17) | (s4 << 4)); + s[11] = (unsigned char) (s4 >> 4); + s[12] = (unsigned char) (s4 >> 12); + s[13] = (unsigned char) ((s4 >> 20) | (s5 << 1)); + s[14] = (unsigned char) (s5 >> 7); + s[15] = (unsigned char) ((s5 >> 15) | (s6 << 6)); + s[16] = (unsigned char) (s6 >> 2); + s[17] = (unsigned char) (s6 >> 10); + s[18] = (unsigned char) ((s6 >> 18) | (s7 << 3)); + s[19] = (unsigned char) (s7 >> 5); + s[20] = (unsigned char) (s7 >> 13); + s[21] = (unsigned char) (s8 >> 0); + s[22] = (unsigned char) (s8 >> 8); + s[23] = (unsigned char) ((s8 >> 16) | (s9 << 5)); + s[24] = (unsigned char) (s9 >> 3); + s[25] = (unsigned char) (s9 >> 11); + s[26] = (unsigned char) ((s9 >> 19) | (s10 << 2)); + s[27] = (unsigned char) (s10 >> 6); + s[28] = (unsigned char) ((s10 >> 14) | (s11 << 7)); + s[29] = (unsigned char) (s11 >> 1); + s[30] = (unsigned char) (s11 >> 9); + s[31] = (unsigned char) (s11 >> 17); +} + + + +/* +Input: + a[0]+256*a[1]+...+256^31*a[31] = a + b[0]+256*b[1]+...+256^31*b[31] = b + c[0]+256*c[1]+...+256^31*c[31] = c + +Output: + s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l + where l = 2^252 + 27742317777372353535851937790883648493. +*/ + +void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b, const unsigned char *c) { + int64_t a0 = 2097151 & load_3(a); + int64_t a1 = 2097151 & (load_4(a + 2) >> 5); + int64_t a2 = 2097151 & (load_3(a + 5) >> 2); + int64_t a3 = 2097151 & (load_4(a + 7) >> 7); + int64_t a4 = 2097151 & (load_4(a + 10) >> 4); + int64_t a5 = 2097151 & (load_3(a + 13) >> 1); + int64_t a6 = 2097151 & (load_4(a + 15) >> 6); + int64_t a7 = 2097151 & (load_3(a + 18) >> 3); + int64_t a8 = 2097151 & load_3(a + 21); + int64_t a9 = 2097151 & (load_4(a + 23) >> 5); + int64_t a10 = 2097151 & (load_3(a + 26) >> 2); + int64_t a11 = (load_4(a + 28) >> 7); + int64_t b0 = 2097151 & load_3(b); + int64_t b1 = 2097151 & (load_4(b + 2) >> 5); + int64_t b2 = 2097151 & (load_3(b + 5) >> 2); + int64_t b3 = 2097151 & (load_4(b + 7) >> 7); + int64_t b4 = 2097151 & (load_4(b + 10) >> 4); + int64_t b5 = 2097151 & (load_3(b + 13) >> 1); + int64_t b6 = 2097151 & (load_4(b + 15) >> 6); + int64_t b7 = 2097151 & (load_3(b + 18) >> 3); + int64_t b8 = 2097151 & load_3(b + 21); + int64_t b9 = 2097151 & (load_4(b + 23) >> 5); + int64_t b10 = 2097151 & (load_3(b + 26) >> 2); + int64_t b11 = (load_4(b + 28) >> 7); + int64_t c0 = 2097151 & load_3(c); + int64_t c1 = 2097151 & (load_4(c + 2) >> 5); + int64_t c2 = 2097151 & (load_3(c + 5) >> 2); + int64_t c3 = 2097151 & (load_4(c + 7) >> 7); + int64_t c4 = 2097151 & (load_4(c + 10) >> 4); + int64_t c5 = 2097151 & (load_3(c + 13) >> 1); + int64_t c6 = 2097151 & (load_4(c + 15) >> 6); + int64_t c7 = 2097151 & (load_3(c + 18) >> 3); + int64_t c8 = 2097151 & load_3(c + 21); + int64_t c9 = 2097151 & (load_4(c + 23) >> 5); + int64_t c10 = 2097151 & (load_3(c + 26) >> 2); + int64_t c11 = (load_4(c + 28) >> 7); + int64_t s0; + int64_t s1; + int64_t s2; + int64_t s3; + int64_t s4; + int64_t s5; + int64_t s6; + int64_t s7; + int64_t s8; + int64_t s9; + int64_t s10; + int64_t s11; + int64_t s12; + int64_t s13; + int64_t s14; + int64_t s15; + int64_t s16; + int64_t s17; + int64_t s18; + int64_t s19; + int64_t s20; + int64_t s21; + int64_t s22; + int64_t s23; + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + int64_t carry17; + int64_t carry18; + int64_t carry19; + int64_t carry20; + int64_t carry21; + int64_t carry22; + + s0 = c0 + a0 * b0; + s1 = c1 + a0 * b1 + a1 * b0; + s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; + s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; + s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; + s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + a8 * b0; + s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; + s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + carry18 = (s18 + (1 << 20)) >> 21; + s19 += carry18; + s18 -= carry18 << 21; + carry20 = (s20 + (1 << 20)) >> 21; + s21 += carry20; + s20 -= carry20 << 21; + carry22 = (s22 + (1 << 20)) >> 21; + s23 += carry22; + s22 -= carry22 << 21; + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + carry17 = (s17 + (1 << 20)) >> 21; + s18 += carry17; + s17 -= carry17 << 21; + carry19 = (s19 + (1 << 20)) >> 21; + s20 += carry19; + s19 -= carry19 << 21; + carry21 = (s21 + (1 << 20)) >> 21; + s22 += carry21; + s21 -= carry21 << 21; + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s23 = 0; + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s22 = 0; + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s21 = 0; + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s20 = 0; + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s19 = 0; + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + s18 = 0; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s17 = 0; + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s16 = 0; + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s15 = 0; + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s14 = 0; + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s13 = 0; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 << 21; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + s[0] = (unsigned char) (s0 >> 0); + s[1] = (unsigned char) (s0 >> 8); + s[2] = (unsigned char) ((s0 >> 16) | (s1 << 5)); + s[3] = (unsigned char) (s1 >> 3); + s[4] = (unsigned char) (s1 >> 11); + s[5] = (unsigned char) ((s1 >> 19) | (s2 << 2)); + s[6] = (unsigned char) (s2 >> 6); + s[7] = (unsigned char) ((s2 >> 14) | (s3 << 7)); + s[8] = (unsigned char) (s3 >> 1); + s[9] = (unsigned char) (s3 >> 9); + s[10] = (unsigned char) ((s3 >> 17) | (s4 << 4)); + s[11] = (unsigned char) (s4 >> 4); + s[12] = (unsigned char) (s4 >> 12); + s[13] = (unsigned char) ((s4 >> 20) | (s5 << 1)); + s[14] = (unsigned char) (s5 >> 7); + s[15] = (unsigned char) ((s5 >> 15) | (s6 << 6)); + s[16] = (unsigned char) (s6 >> 2); + s[17] = (unsigned char) (s6 >> 10); + s[18] = (unsigned char) ((s6 >> 18) | (s7 << 3)); + s[19] = (unsigned char) (s7 >> 5); + s[20] = (unsigned char) (s7 >> 13); + s[21] = (unsigned char) (s8 >> 0); + s[22] = (unsigned char) (s8 >> 8); + s[23] = (unsigned char) ((s8 >> 16) | (s9 << 5)); + s[24] = (unsigned char) (s9 >> 3); + s[25] = (unsigned char) (s9 >> 11); + s[26] = (unsigned char) ((s9 >> 19) | (s10 << 2)); + s[27] = (unsigned char) (s10 >> 6); + s[28] = (unsigned char) ((s10 >> 14) | (s11 << 7)); + s[29] = (unsigned char) (s11 >> 1); + s[30] = (unsigned char) (s11 >> 9); + s[31] = (unsigned char) (s11 >> 17); +} diff --git a/lib/ed25519/sc.h b/lib/ed25519/sc.h new file mode 100644 index 00000000..e29e7fa5 --- /dev/null +++ b/lib/ed25519/sc.h @@ -0,0 +1,12 @@ +#ifndef SC_H +#define SC_H + +/* +The set of scalars is \Z/l +where l = 2^252 + 27742317777372353535851937790883648493. +*/ + +void sc_reduce(unsigned char *s); +void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b, const unsigned char *c); + +#endif diff --git a/lib/ed25519/seed.c b/lib/ed25519/seed.c new file mode 100644 index 00000000..95ddc980 --- /dev/null +++ b/lib/ed25519/seed.c @@ -0,0 +1,40 @@ +#include "ed_25519.h" + +#ifndef ED25519_NO_SEED + +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +int ed25519_create_seed(unsigned char *seed) { +#ifdef _WIN32 + HCRYPTPROV prov; + + if (!CryptAcquireContext(&prov, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { + return 1; + } + + if (!CryptGenRandom(prov, 32, seed)) { + CryptReleaseContext(prov, 0); + return 1; + } + + CryptReleaseContext(prov, 0); +#else + FILE *f = fopen("/dev/urandom", "rb"); + + if (f == NULL) { + return 1; + } + + fread(seed, 1, 32, f); + fclose(f); +#endif + + return 0; +} + +#endif diff --git a/lib/ed25519/sha512.c b/lib/ed25519/sha512.c new file mode 100644 index 00000000..cb8ae717 --- /dev/null +++ b/lib/ed25519/sha512.c @@ -0,0 +1,275 @@ +/* LibTomCrypt, modular cryptographic library -- Tom St Denis + * + * LibTomCrypt is a library that provides various cryptographic + * algorithms in a highly modular and flexible manner. + * + * The library is free for all purposes without any express + * guarantee it works. + * + * Tom St Denis, tomstdenis@gmail.com, http://libtom.org + */ + +#include "fixedint.h" +#include "sha512.h" + +/* the K array */ +static const uint64_t K[80] = { + UINT64_C(0x428a2f98d728ae22), UINT64_C(0x7137449123ef65cd), + UINT64_C(0xb5c0fbcfec4d3b2f), UINT64_C(0xe9b5dba58189dbbc), + UINT64_C(0x3956c25bf348b538), UINT64_C(0x59f111f1b605d019), + UINT64_C(0x923f82a4af194f9b), UINT64_C(0xab1c5ed5da6d8118), + UINT64_C(0xd807aa98a3030242), UINT64_C(0x12835b0145706fbe), + UINT64_C(0x243185be4ee4b28c), UINT64_C(0x550c7dc3d5ffb4e2), + UINT64_C(0x72be5d74f27b896f), UINT64_C(0x80deb1fe3b1696b1), + UINT64_C(0x9bdc06a725c71235), UINT64_C(0xc19bf174cf692694), + UINT64_C(0xe49b69c19ef14ad2), UINT64_C(0xefbe4786384f25e3), + UINT64_C(0x0fc19dc68b8cd5b5), UINT64_C(0x240ca1cc77ac9c65), + UINT64_C(0x2de92c6f592b0275), UINT64_C(0x4a7484aa6ea6e483), + UINT64_C(0x5cb0a9dcbd41fbd4), UINT64_C(0x76f988da831153b5), + UINT64_C(0x983e5152ee66dfab), UINT64_C(0xa831c66d2db43210), + UINT64_C(0xb00327c898fb213f), UINT64_C(0xbf597fc7beef0ee4), + UINT64_C(0xc6e00bf33da88fc2), UINT64_C(0xd5a79147930aa725), + UINT64_C(0x06ca6351e003826f), UINT64_C(0x142929670a0e6e70), + UINT64_C(0x27b70a8546d22ffc), UINT64_C(0x2e1b21385c26c926), + UINT64_C(0x4d2c6dfc5ac42aed), UINT64_C(0x53380d139d95b3df), + UINT64_C(0x650a73548baf63de), UINT64_C(0x766a0abb3c77b2a8), + UINT64_C(0x81c2c92e47edaee6), UINT64_C(0x92722c851482353b), + UINT64_C(0xa2bfe8a14cf10364), UINT64_C(0xa81a664bbc423001), + UINT64_C(0xc24b8b70d0f89791), UINT64_C(0xc76c51a30654be30), + UINT64_C(0xd192e819d6ef5218), UINT64_C(0xd69906245565a910), + UINT64_C(0xf40e35855771202a), UINT64_C(0x106aa07032bbd1b8), + UINT64_C(0x19a4c116b8d2d0c8), UINT64_C(0x1e376c085141ab53), + UINT64_C(0x2748774cdf8eeb99), UINT64_C(0x34b0bcb5e19b48a8), + UINT64_C(0x391c0cb3c5c95a63), UINT64_C(0x4ed8aa4ae3418acb), + UINT64_C(0x5b9cca4f7763e373), UINT64_C(0x682e6ff3d6b2b8a3), + UINT64_C(0x748f82ee5defb2fc), UINT64_C(0x78a5636f43172f60), + UINT64_C(0x84c87814a1f0ab72), UINT64_C(0x8cc702081a6439ec), + UINT64_C(0x90befffa23631e28), UINT64_C(0xa4506cebde82bde9), + UINT64_C(0xbef9a3f7b2c67915), UINT64_C(0xc67178f2e372532b), + UINT64_C(0xca273eceea26619c), UINT64_C(0xd186b8c721c0c207), + UINT64_C(0xeada7dd6cde0eb1e), UINT64_C(0xf57d4f7fee6ed178), + UINT64_C(0x06f067aa72176fba), UINT64_C(0x0a637dc5a2c898a6), + UINT64_C(0x113f9804bef90dae), UINT64_C(0x1b710b35131c471b), + UINT64_C(0x28db77f523047d84), UINT64_C(0x32caab7b40c72493), + UINT64_C(0x3c9ebe0a15c9bebc), UINT64_C(0x431d67c49c100d4c), + UINT64_C(0x4cc5d4becb3e42b6), UINT64_C(0x597f299cfc657e2a), + UINT64_C(0x5fcb6fab3ad6faec), UINT64_C(0x6c44198c4a475817) +}; + +/* Various logical functions */ + +#define ROR64c(x, y) \ + ( ((((x)&UINT64_C(0xFFFFFFFFFFFFFFFF))>>((uint64_t)(y)&UINT64_C(63))) | \ + ((x)<<((uint64_t)(64-((y)&UINT64_C(63)))))) & UINT64_C(0xFFFFFFFFFFFFFFFF)) + +#define STORE64H(x, y) \ + { (y)[0] = (unsigned char)(((x)>>56)&255); (y)[1] = (unsigned char)(((x)>>48)&255); \ + (y)[2] = (unsigned char)(((x)>>40)&255); (y)[3] = (unsigned char)(((x)>>32)&255); \ + (y)[4] = (unsigned char)(((x)>>24)&255); (y)[5] = (unsigned char)(((x)>>16)&255); \ + (y)[6] = (unsigned char)(((x)>>8)&255); (y)[7] = (unsigned char)((x)&255); } + +#define LOAD64H(x, y) \ + { x = (((uint64_t)((y)[0] & 255))<<56)|(((uint64_t)((y)[1] & 255))<<48) | \ + (((uint64_t)((y)[2] & 255))<<40)|(((uint64_t)((y)[3] & 255))<<32) | \ + (((uint64_t)((y)[4] & 255))<<24)|(((uint64_t)((y)[5] & 255))<<16) | \ + (((uint64_t)((y)[6] & 255))<<8)|(((uint64_t)((y)[7] & 255))); } + + +#define Ch(x,y,z) (z ^ (x & (y ^ z))) +#define Maj(x,y,z) (((x | y) & z) | (x & y)) +#define S(x, n) ROR64c(x, n) +#define R(x, n) (((x) &UINT64_C(0xFFFFFFFFFFFFFFFF))>>((uint64_t)n)) +#define Sigma0(x) (S(x, 28) ^ S(x, 34) ^ S(x, 39)) +#define Sigma1(x) (S(x, 14) ^ S(x, 18) ^ S(x, 41)) +#define Gamma0(x) (S(x, 1) ^ S(x, 8) ^ R(x, 7)) +#define Gamma1(x) (S(x, 19) ^ S(x, 61) ^ R(x, 6)) +#ifndef MIN + #define MIN(x, y) ( ((x)<(y))?(x):(y) ) +#endif + +/* compress 1024-bits */ +static int sha512_compress(sha512_context *md, unsigned char *buf) +{ + uint64_t S[8], W[80], t0, t1; + int i; + + /* copy state into S */ + for (i = 0; i < 8; i++) { + S[i] = md->state[i]; + } + + /* copy the state into 1024-bits into W[0..15] */ + for (i = 0; i < 16; i++) { + LOAD64H(W[i], buf + (8*i)); + } + + /* fill W[16..79] */ + for (i = 16; i < 80; i++) { + W[i] = Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]; + } + +/* Compress */ + #define RND(a,b,c,d,e,f,g,h,i) \ + t0 = h + Sigma1(e) + Ch(e, f, g) + K[i] + W[i]; \ + t1 = Sigma0(a) + Maj(a, b, c);\ + d += t0; \ + h = t0 + t1; + + for (i = 0; i < 80; i += 8) { + RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],i+0); + RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],i+1); + RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],i+2); + RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],i+3); + RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],i+4); + RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],i+5); + RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],i+6); + RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],i+7); + } + + #undef RND + + + + /* feedback */ + for (i = 0; i < 8; i++) { + md->state[i] = md->state[i] + S[i]; + } + + return 0; +} + + +/** + Initialize the hash state + @param md The hash state you wish to initialize + @return 0 if successful +*/ +int sha512_init(sha512_context * md) { + if (md == NULL) return 1; + + md->curlen = 0; + md->length = 0; + md->state[0] = UINT64_C(0x6a09e667f3bcc908); + md->state[1] = UINT64_C(0xbb67ae8584caa73b); + md->state[2] = UINT64_C(0x3c6ef372fe94f82b); + md->state[3] = UINT64_C(0xa54ff53a5f1d36f1); + md->state[4] = UINT64_C(0x510e527fade682d1); + md->state[5] = UINT64_C(0x9b05688c2b3e6c1f); + md->state[6] = UINT64_C(0x1f83d9abfb41bd6b); + md->state[7] = UINT64_C(0x5be0cd19137e2179); + + return 0; +} + +/** + Process a block of memory though the hash + @param md The hash state + @param in The data to hash + @param inlen The length of the data (octets) + @return 0 if successful +*/ +int sha512_update (sha512_context * md, const unsigned char *in, size_t inlen) +{ + size_t n; + size_t i; + int err; + if (md == NULL) return 1; + if (in == NULL) return 1; + if (md->curlen > sizeof(md->buf)) { + return 1; + } + while (inlen > 0) { + if (md->curlen == 0 && inlen >= 128) { + if ((err = sha512_compress (md, (unsigned char *)in)) != 0) { + return err; + } + md->length += 128 * 8; + in += 128; + inlen -= 128; + } else { + n = MIN(inlen, (128 - md->curlen)); + + for (i = 0; i < n; i++) { + md->buf[i + md->curlen] = in[i]; + } + + + md->curlen += n; + in += n; + inlen -= n; + if (md->curlen == 128) { + if ((err = sha512_compress (md, md->buf)) != 0) { + return err; + } + md->length += 8*128; + md->curlen = 0; + } + } + } + return 0; +} + +/** + Terminate the hash to get the digest + @param md The hash state + @param out [out] The destination of the hash (64 bytes) + @return 0 if successful +*/ + int sha512_final(sha512_context * md, unsigned char *out) + { + int i; + + if (md == NULL) return 1; + if (out == NULL) return 1; + + if (md->curlen >= sizeof(md->buf)) { + return 1; + } + + /* increase the length of the message */ + md->length += md->curlen * UINT64_C(8); + + /* append the '1' bit */ + md->buf[md->curlen++] = (unsigned char)0x80; + + /* if the length is currently above 112 bytes we append zeros + * then compress. Then we can fall back to padding zeros and length + * encoding like normal. + */ + if (md->curlen > 112) { + while (md->curlen < 128) { + md->buf[md->curlen++] = (unsigned char)0; + } + sha512_compress(md, md->buf); + md->curlen = 0; + } + + /* pad upto 120 bytes of zeroes + * note: that from 112 to 120 is the 64 MSB of the length. We assume that you won't hash + * > 2^64 bits of data... :-) + */ +while (md->curlen < 120) { + md->buf[md->curlen++] = (unsigned char)0; +} + + /* store length */ +STORE64H(md->length, md->buf+120); +sha512_compress(md, md->buf); + + /* copy output */ +for (i = 0; i < 8; i++) { + STORE64H(md->state[i], out+(8*i)); +} + +return 0; +} + +int sha512(const unsigned char *message, size_t message_len, unsigned char *out) +{ + sha512_context ctx; + int ret; + if ((ret = sha512_init(&ctx))) return ret; + if ((ret = sha512_update(&ctx, message, message_len))) return ret; + if ((ret = sha512_final(&ctx, out))) return ret; + return 0; +} diff --git a/lib/ed25519/sha512.h b/lib/ed25519/sha512.h new file mode 100644 index 00000000..a34dd5e4 --- /dev/null +++ b/lib/ed25519/sha512.h @@ -0,0 +1,21 @@ +#ifndef SHA512_H +#define SHA512_H + +#include + +#include "fixedint.h" + +/* state */ +typedef struct sha512_context_ { + uint64_t length, state[8]; + size_t curlen; + unsigned char buf[128]; +} sha512_context; + + +int sha512_init(sha512_context * md); +int sha512_final(sha512_context * md, unsigned char *out); +int sha512_update(sha512_context * md, const unsigned char *in, size_t inlen); +int sha512(const unsigned char *message, size_t message_len, unsigned char *out); + +#endif diff --git a/lib/ed25519/sign.c b/lib/ed25519/sign.c new file mode 100644 index 00000000..463b02bf --- /dev/null +++ b/lib/ed25519/sign.c @@ -0,0 +1,31 @@ +#include "ed_25519.h" +#include "sha512.h" +#include "ge.h" +#include "sc.h" + + +void ed25519_sign(unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key, const unsigned char *private_key) { + sha512_context hash; + unsigned char hram[64]; + unsigned char r[64]; + ge_p3 R; + + + sha512_init(&hash); + sha512_update(&hash, private_key + 32, 32); + sha512_update(&hash, message, message_len); + sha512_final(&hash, r); + + sc_reduce(r); + ge_scalarmult_base(&R, r); + ge_p3_tobytes(signature, &R); + + sha512_init(&hash); + sha512_update(&hash, signature, 32); + sha512_update(&hash, public_key, 32); + sha512_update(&hash, message, message_len); + sha512_final(&hash, hram); + + sc_reduce(hram); + sc_muladd(signature + 32, hram, private_key, r); +} diff --git a/lib/ed25519/verify.c b/lib/ed25519/verify.c new file mode 100644 index 00000000..4d8de253 --- /dev/null +++ b/lib/ed25519/verify.c @@ -0,0 +1,77 @@ +#include "ed_25519.h" +#include "sha512.h" +#include "ge.h" +#include "sc.h" + +static int consttime_equal(const unsigned char *x, const unsigned char *y) { + unsigned char r = 0; + + r = x[0] ^ y[0]; + #define F(i) r |= x[i] ^ y[i] + F(1); + F(2); + F(3); + F(4); + F(5); + F(6); + F(7); + F(8); + F(9); + F(10); + F(11); + F(12); + F(13); + F(14); + F(15); + F(16); + F(17); + F(18); + F(19); + F(20); + F(21); + F(22); + F(23); + F(24); + F(25); + F(26); + F(27); + F(28); + F(29); + F(30); + F(31); + #undef F + + return !r; +} + +int ed25519_verify(const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key) { + unsigned char h[64]; + unsigned char checker[32]; + sha512_context hash; + ge_p3 A; + ge_p2 R; + + if (signature[63] & 224) { + return 0; + } + + if (ge_frombytes_negate_vartime(&A, public_key) != 0) { + return 0; + } + + sha512_init(&hash); + sha512_update(&hash, signature, 32); + sha512_update(&hash, public_key, 32); + sha512_update(&hash, message, message_len); + sha512_final(&hash, h); + + sc_reduce(h); + ge_double_scalarmult_vartime(&R, h, &A, signature + 32); + ge_tobytes(checker, &R); + + if (!consttime_equal(checker, signature)) { + return 0; + } + + return 1; +} diff --git a/partitions_tft_touch.csv b/partitions_tft_touch.csv new file mode 100644 index 00000000..6e9771ae --- /dev/null +++ b/partitions_tft_touch.csv @@ -0,0 +1,37 @@ +# Heltec V4 TFT touch firmware — custom partition table. +# +# 16 MB layout with TWO EQUAL OTA app slots so the touch firmware can +# self-update (HTTP OTA via the meshcomod proxy). The app region (app0+app1) +# is split evenly at 3.875 MB each — the firmware is ~2.4–2.5 MB, leaving +# ~1.3 MB of growth headroom per slot. The tiles + spiffs offsets are UNCHANGED, +# so an OTA (or the one-time USB re-flash that installs this table) keeps the +# map tile cache and all user data (/new_prefs, /contacts3, /channels2, chat +# history) intact. +# - app0 3.875 MB (ota_0) ┐ equal A/B slots; OTA ping-pongs between them +# - app1 3.875 MB (ota_1) ┘ (was 1.5 MB — too small to hold the image) +# - "tiles" 4.75 MB (subtype 0x83 = LittleFS) for the map tab's tile pack +# - spiffs 3.375 MB for user data +# +# Layout (16 MB total): +# 0x000000–0x009000 bootloader + partition table +# 0x009000–0x00E000 nvs (20 KB, unchanged) +# 0x00E000–0x010000 otadata (8 KB, unchanged) +# 0x010000–0x3F0000 app0 (3.875 MB, ota_0) +# 0x3F0000–0x7D0000 app1 (3.875 MB, ota_1 — grown from 1.5 MB) +# 0x7D0000–0xC90000 tiles (4.75 MB, LittleFS, type 0x83) [unchanged] +# 0xC90000–0xFF0000 spiffs (3.375 MB) [unchanged] +# 0xFF0000–0x1000000 coredump (64 KB, unchanged) +# +# Subtype 0x83 = ESP-IDF LittleFS subtype. The Arduino LittleFSFS instance +# in UITask.cpp mounts this by partition label "tiles". SPIFFS.begin() +# matches the first partition with subtype `spiffs` (== our "spiffs" +# partition), so the two filesystems don't collide. + +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x3E0000, +app1, app, ota_1, 0x3F0000, 0x3E0000, +tiles, data, 0x83, 0x7D0000, 0x4C0000, +spiffs, data, spiffs, 0xC90000, 0x360000, +coredump, data, coredump, 0xFF0000, 0x10000, diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 00000000..594c2577 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,190 @@ +; wadamesh — Heltec V4 TFT touch firmware. +; Standalone PlatformIO project: the MeshCore core is consumed as a library +; (lib/MeshCore here; a git-tagged fork once published). This env is the flattened +; equivalent of meshcomod's heltec_v4_tft_companion_radio_usb_tcp_touch. + +[platformio] +default_envs = heltec_v4_tft_companion_radio_usb_tcp_touch + +[env:heltec_v4_tft_companion_radio_usb_tcp_touch] +platform = platformio/espressif32@6.11.0 +framework = arduino +board = heltec_v4 +monitor_speed = 115200 +board_build.partitions = partitions_tft_touch.csv +extra_scripts = pre:scripts/inject_wifi_env.py + +build_flags = + -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 + -D LORA_FREQ=869.618 + -D LORA_BW=62.5 + -D LORA_SF=8 + -D ENABLE_PRIVATE_KEY_IMPORT=1 + -D ENABLE_PRIVATE_KEY_EXPORT=1 + -D RADIOLIB_EXCLUDE_CC1101=1 + -D RADIOLIB_EXCLUDE_RF69=1 + -D RADIOLIB_EXCLUDE_SX1231=1 + -D RADIOLIB_EXCLUDE_SI443X=1 + -D RADIOLIB_EXCLUDE_RFM2X=1 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D RADIOLIB_EXCLUDE_AFSK=1 + -D RADIOLIB_EXCLUDE_AX25=1 + -D RADIOLIB_EXCLUDE_HELLSCHREIBER=1 + -D RADIOLIB_EXCLUDE_MORSE=1 + -D RADIOLIB_EXCLUDE_APRS=1 + -D RADIOLIB_EXCLUDE_BELL=1 + -D RADIOLIB_EXCLUDE_RTTY=1 + -D RADIOLIB_EXCLUDE_SSTV=1 + -D ESP32_PLATFORM + ; --- sensors --- + -D ENV_INCLUDE_GPS=1 + -D ENV_INCLUDE_AHTX0=1 + -D ENV_INCLUDE_BME280=1 + -D ENV_INCLUDE_BMP280=1 + -D ENV_INCLUDE_SHTC3=1 + -D ENV_INCLUDE_SHT4X=1 + -D ENV_INCLUDE_LPS22HB=1 + -D ENV_INCLUDE_INA3221=1 + -D ENV_INCLUDE_INA219=1 + -D ENV_INCLUDE_INA226=1 + -D ENV_INCLUDE_INA260=1 + -D ENV_INCLUDE_MLX90614=1 + -D ENV_INCLUDE_VL53L0X=1 + -D ENV_INCLUDE_BME680=1 + -D ENV_INCLUDE_BMP085=1 + ; --- Heltec LoRa32 V4 board --- + -I variants/heltec_v4 + -D HELTEC_LORA_V4 + -D USE_SX1262 + -D ESP32_CPU_FREQ=80 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_TX_LED=35 + -D P_LORA_DIO_1=14 + -D P_LORA_NSS=8 + -D P_LORA_RESET=12 + -D P_LORA_BUSY=13 + -D P_LORA_SCLK=9 + -D P_LORA_MISO=11 + -D P_LORA_MOSI=10 + -D P_LORA_PA_POWER=7 + -D P_LORA_PA_EN=2 + -D P_LORA_PA_TX_EN=46 + -D P_LORA_GC1109_PA_EN=2 + -D P_LORA_GC1109_PA_TX_EN=46 + -D P_LORA_KCT8103L_PA_CSD=2 + -D P_LORA_KCT8103L_PA_CTX=5 + -D PIN_USER_BTN=0 + -D PIN_VEXT_EN=36 + -D PIN_VEXT_EN_ACTIVE=HIGH + -D LORA_TX_POWER=10 + -D MAX_LORA_TX_POWER=22 + -D SX126X_REGISTER_PATCH=1 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_GPS_RX=38 + -D PIN_GPS_TX=39 + -D PIN_GPS_RESET=42 + -D PIN_GPS_RESET_ACTIVE=LOW + -D PIN_GPS_EN=34 + -D PIN_GPS_EN_ACTIVE=LOW + -D PIN_ADC_CTRL=37 + -D PIN_VBAT_READ=1 + ; --- TFT --- + -D HELTEC_LORA_V4_TFT + -D PIN_BOARD_SDA=4 + -D PIN_BOARD_SCL=3 + -D DISPLAY_ROTATION=2 + -D DISPLAY_SCALE_X=1.875 + -D DISPLAY_SCALE_Y=5 + -D PIN_TFT_RST=18 + -D PIN_TFT_VDD_CTL=-1 + -D PIN_TFT_LEDA_CTL=21 + -D PIN_TFT_LEDA_CTL_ACTIVE=HIGH + -D PIN_TFT_CS=15 + -D PIN_TFT_DC=16 + -D PIN_TFT_SCL=17 + -D PIN_TFT_SDA=33 + ; --- CHSC6x cap touch --- + -D HAS_HELTEC_V4_CAP_TOUCH=1 + -D PIN_TOUCH_SDA=47 + -D PIN_TOUCH_SCL=48 + -D PIN_TOUCH_RST=44 + -D HELTEC_V4_BUZZER_PIN=6 + ; --- touch UI / companion stack --- + -I include + -I src/ui-touch + -D UI_LVGL=1 + -D HAS_TOUCH_UI=1 + -D FIRMWARE_OTA_ENV='"heltec_v4_tft_companion_radio_usb_tcp_touch"' + -D ENABLE_ADVERT_ON_BOOT=0 + -D LV_CONF_PATH=lv_conf.h + -D LV_CONF_INCLUDE_SIMPLE=1 + -D USER_SETUP_LOADED=1 + -D ST7789_DRIVER=1 + -D TFT_WIDTH=240 + -D TFT_HEIGHT=320 + -D TFT_MOSI=33 + -D TFT_SCLK=17 + -D TFT_CS=15 + -D TFT_DC=16 + -D TFT_RST=18 + -D TFT_MISO=-1 + -D SPI_FREQUENCY=80000000 + -D TFT_RGB_ORDER=TFT_RGB + -D TFT_INVERSION_ON=1 + -D TFT_BL=21 + -D TFT_BACKLIGHT_ON=1 + -D MAX_CONTACTS=2000 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=ST7789LCDDisplay + -D ADMIN_PASSWORD='"password"' + -D MULTI_TRANSPORT_COMPANION=1 + -D TCP_PORT=5000 + -D WS_PORT=8765 + -D BLE_PIN_CODE=123456 + -D CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=1 + -D CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 + -D CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 + +build_src_filter = + +<*.cpp> + + + + + +<../variants/heltec_v4/*.cpp> + +lib_deps = + ; MeshCore core — the upstreamable fork, pinned by tag (no vendored src/ in this repo) + https://github.com/ALLFATHER-BV/MeshCore.git#v1.16.0-wada.0 + SPI + Wire + jgromes/RadioLib @ ^7.6.0 + rweather/Crypto @ ^0.4.0 + adafruit/RTClib @ ^2.1.3 + melopero/Melopero RV3028 @ ^1.1.0 + electroniccats/CayenneLPP @ 1.6.1 + h2zero/NimBLE-Arduino @ 1.4.3 + adafruit/Adafruit INA3221 Library @ ^1.0.1 + adafruit/Adafruit INA219 @ ^1.2.3 + robtillaart/INA226 @ ^0.6.4 + adafruit/Adafruit INA260 Library @ ^1.5.3 + adafruit/Adafruit AHTX0 @ ^2.0.5 + adafruit/Adafruit BME280 Library @ ^2.3.0 + adafruit/Adafruit BMP280 Library @ ^2.6.8 + adafruit/Adafruit SHTC3 Library @ ^1.0.1 + sensirion/Sensirion I2C SHT4x @ ^1.1.2 + arduino-libraries/Arduino_LPS22HB @ ^1.0.2 + adafruit/Adafruit MLX90614 Library @ ^2.1.5 + adafruit/Adafruit_VL53L0X @ ^1.2.4 + stevemarple/MicroNMEA @ ^2.0.6 + adafruit/Adafruit BME680 Library @ ^2.0.4 + adafruit/Adafruit BMP085 Library @ ^1.2.4 + adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip + ESP32Async/ESPAsyncWebServer @ 3.10.3 + file://arch/esp32/AsyncElegantOTA + lvgl/lvgl @ ^8.3.11 + bodmer/TFT_eSPI @ ^2.5.43 + densaugeo/base64 @ ~1.4.0 diff --git a/scripts/inject_wifi_env.py b/scripts/inject_wifi_env.py new file mode 100644 index 00000000..84fce294 --- /dev/null +++ b/scripts/inject_wifi_env.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Inject WIFI_SSID and WIFI_PWD from environment into build. Never put real credentials in the repo.""" + +import os + +Import("env") + +ssid = os.environ.get("WIFI_SSID", "YourSSID").replace("\\", "\\\\").replace('"', '\\"') +pwd = os.environ.get("WIFI_PWD", "YourPassword").replace("\\", "\\\\").replace('"', '\\"') + +# Write a generated header so we avoid -D quote escaping; force-include it. +out_dir = os.path.join(env.get("PROJECT_DIR", "."), "scripts", "build") +os.makedirs(out_dir, exist_ok=True) +header_path = os.path.join(out_dir, "wifi_secrets.h") +with open(header_path, "w") as f: + f.write("/* Generated at build from WIFI_SSID/WIFI_PWD env - do not commit */\n") + f.write("#ifndef WIFI_SECRETS_H\n#define WIFI_SECRETS_H\n") + f.write('#define WIFI_SSID "%s"\n' % ssid) + f.write('#define WIFI_PWD "%s"\n' % pwd) + f.write("#endif\n") + +env.Append(CPPPATH=[out_dir]) +env.Append(CCFLAGS=["-include", "wifi_secrets.h"]) diff --git a/src/AbstractUITask.h b/src/AbstractUITask.h new file mode 100644 index 00000000..80d68b26 --- /dev/null +++ b/src/AbstractUITask.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifdef PIN_BUZZER + #include +#endif + +#include "NodePrefs.h" + +// Forward decl — defined in helpers/ContactInfo.h, included by MyMesh.h users. +struct ContactInfo; + +enum class UIEventType { + none, + contactMessage, + channelMessage, + roomMessage, + newContactMessage, + ack +}; + +class AbstractUITask { +protected: + mesh::MainBoard* _board; + BaseSerialInterface* _serial; + bool _connected; + + AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) { + _connected = false; + } + +public: + void setHasConnection(bool connected) { _connected = connected; } + bool hasConnection() const { return _connected; } + uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } + bool isSerialEnabled() const { return _serial->isEnabled(); } + void enableSerial() { _serial->enable(); } + void disableSerial() { _serial->disable(); } + bool isTcpEnabled() const { return _serial->isTcpEnabled(); } + void enableTcp() { _serial->enableTcp(); } + void disableTcp() { _serial->disableTcp(); } + bool isWsStarted() const { return _serial->isWsStarted(); } + uint16_t getWsPort() const { return _serial->getWsPort(); } + int getWsConnectedCount() const { return _serial->getWsConnectedCount(); } + bool hasBleCapability() const { return _serial->hasBleCapability(); } + bool isBleEnabled() const { return _serial->isBleEnabled(); } + void enableBle() { _serial->enableBle(); } + void disableBle() { _serial->disableBle(); } + bool getBlePeerAddress(char* buf, size_t len) const { return _serial->getBlePeerAddress(buf, len); } + virtual void msgRead(int msgcount) = 0; + virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; + virtual void newMsgFromPub(uint8_t path_len, const uint8_t* from_pub, const char* from_name, const char* text, int msgcount) { + (void)from_pub; + newMsg(path_len, from_name, text, msgcount); + } + /** Same as newMsgFromPub but carries the LoRa RX metadata so the touch UI + * can surface it in the per-message Info popup. + * - path_len: hop count when is_flood is true, otherwise 0xFF (direct/routed). + * - is_flood: matches mesh::Packet::isRouteFlood() at receive time. + * - snr_q4 / rssi: same encoding the wire protocol uses (SNR × 4, RSSI dBm). + * Default delegates to newMsgFromPub so non-touch UIs ignore the metadata. */ + virtual void newMsgFromPubWithMeta(uint8_t path_len, bool is_flood, + const uint8_t* from_pub, const char* from_name, + const char* text, int msgcount, + int8_t snr_q4, int8_t rssi) { + (void)is_flood; (void)snr_q4; (void)rssi; + newMsgFromPub(path_len, from_pub, from_name, text, msgcount); + } + virtual void notify(UIEventType t = UIEventType::none) = 0; + virtual void appendDiag(const char* message) { (void)message; } + /** Notify UI of an advert reception. `is_new=true` means the contact is NOT in + * the persistent contacts[] table (filtered by auto-add rules or contacts + * full); `is_new=false` means the contact is in contacts[] and was just + * updated. Used by the touch UI to build a "Discovered" list of pending + * contacts when auto-add is off. Default impl is a no-op. */ + virtual void discoveredContact(const ContactInfo& contact, bool is_new, uint8_t path_len) { + (void)contact; + (void)is_new; + (void)path_len; + } + /** Notify UI of a status response from a repeater that was pinged from the + * UI side (e.g. via the Contacts action sheet). `data`/`len` is the raw + * reply payload (typically a JSON blob from StatsFormatHelper). Default is + * a no-op so non-touch UIs can ignore. */ + virtual void onPingReply(const ContactInfo& contact, const uint8_t* data, size_t len) { + (void)contact; + (void)data; + (void)len; + } + /** Notify UI of a TELEMETRY reply (CayenneLPP payload). Same payload format + * the companion app receives via PUSH_CODE_TELEMETRY_RESPONSE. Default + * hands off to onPingReply so an UI that doesn't differentiate (or that + * was built before the split) still sees the bytes. */ + virtual void onTelemetryReply(const ContactInfo& contact, const uint8_t* data, size_t len) { + onPingReply(contact, data, len); + } + /** Admin-login finished. `success=true` means the repeater accepted the + * password and added us to its ACL; `perms` is the granted permission + * bitmask (e.g. PERM_ACL_ADMIN | PERM_ACL_GUEST). On `success=false` the + * password was wrong or the response timed out. Default no-op so + * non-touch UIs ignore. */ + virtual void onAdminLoginResult(const ContactInfo& contact, bool success, uint8_t perms) { + (void)contact; (void)success; (void)perms; + } + /** A CLI command reply from a repeater / room server / sensor (the + * TXT_TYPE_CLI_DATA path). `text` is the decrypted command output text + * the server returned. Default no-op; the touch UI overrides to append + * the line to the open admin console. */ + virtual void onAdminCommandReply(const ContactInfo& contact, const char* text) { + (void)contact; (void)text; + } + /** Notify UI that channels[] or contacts[] changed (e.g. app added a channel + * via CMD_SET_CHANNEL). Default: no-op; touch UI overrides to bump the + * periodic thread-refresh deadline so the new entry shows up immediately. */ + virtual void onThreadsChanged() {} + /** ACK arrived for a previously-sent DM. `ack_hash` is the 4-byte + * expected-ack value the dispatcher returned from `sendMessage`. Touch + * UI flips the matching outgoing bubble to DELIVERED; default no-op for + * non-touch builds. */ + virtual void onMessageAcked(uint32_t ack_hash) { (void)ack_hash; } + /** A UI-initiated trace ping bounced back. `tag` is the 4-byte tag we + * set when creating the trace. `their_snr` is the SNR the remote node + * observed when receiving our outgoing packet (in dB, signed). `our_snr` + * is the SNR our radio observed when we picked up the remote's + * retransmission (also in dB). For multi-hop traces, `extra_hops` may + * be >0 and `extra_snrs[]` is one byte per intermediate hop (each + * reading is SNR*4 as stored on the wire). Default no-op for non-touch + * builds. */ + virtual void onTracePingResult(uint32_t tag, int8_t their_snr, + int8_t our_snr, uint8_t extra_hops, + const int8_t* extra_snrs) { + (void)tag; (void)their_snr; (void)our_snr; + (void)extra_hops; (void)extra_snrs; + } + virtual void logRxFrame(float snr, float rssi, const uint8_t* raw, int len) { + (void)snr; + (void)rssi; + (void)raw; + (void)len; + } + virtual void loop() = 0; +}; diff --git a/src/DataStore.cpp b/src/DataStore.cpp new file mode 100644 index 00000000..7ce66bee --- /dev/null +++ b/src/DataStore.cpp @@ -0,0 +1,709 @@ +#include +#include +#include "DataStore.h" +#if defined(ESP32) +#include +#endif + +#if defined(EXTRAFS) || defined(QSPIFLASH) + #define MAX_BLOBRECS 100 +#else + #define MAX_BLOBRECS 20 +#endif + +DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock), +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + identity_store(fs, "") +#elif defined(RP2040_PLATFORM) + identity_store(fs, "/identity") +#else + identity_store(fs, "/identity") +#endif +{ +} + +#if defined(EXTRAFS) || defined(QSPIFLASH) +DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock), +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + identity_store(fs, "") +#elif defined(RP2040_PLATFORM) + identity_store(fs, "/identity") +#else + identity_store(fs, "/identity") +#endif +{ +} +#endif + +const char* DataStore::_rp(const char* name) { + if (!_root[0]) return name; // default: filesystem root, zero change + snprintf(_rpbuf, sizeof(_rpbuf), "%s%s", _root, name); + return _rpbuf; +} + +File DataStore::openWrite(FILESYSTEM* fs, const char* filename) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + fs->remove(_rp(filename)); + return fs->open(_rp(filename), FILE_O_WRITE); +#elif defined(RP2040_PLATFORM) + return fs->open(_rp(filename), "w"); +#else + return fs->open(_rp(filename), "w", true); +#endif +} + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + static uint32_t _ContactsChannelsTotalBlocks = 0; +#endif + +void DataStore::begin() { +#if defined(RP2040_PLATFORM) + identity_store.begin(); +#endif + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count; + checkAdvBlobFile(); + #if defined(EXTRAFS) || defined(QSPIFLASH) + migrateToSecondaryFS(); + #endif +#else + // init 'blob store' support + _fs->mkdir("/bl"); +#endif +} + +#if defined(ESP32) + #include + #include +#elif defined(RP2040_PLATFORM) + #include +#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #if defined(QSPIFLASH) + #include + #elif defined(EXTRAFS) + #include + #else + #include + #endif +#endif + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +int _countLfsBlock(void *p, lfs_block_t block){ + if (block > _ContactsChannelsTotalBlocks) { + MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block); + return LFS_ERR_CORRUPT; // return error to abort lfs_traverse() gracefully + } + lfs_size_t *size = (lfs_size_t*) p; + *size += 1; + return 0; +} + +lfs_ssize_t _getLfsUsedBlockCount(FILESYSTEM* fs) { + lfs_size_t size = 0; + int err = lfs_traverse(fs->_getFS(), _countLfsBlock, &size); + if (err) { + MESH_DEBUG_PRINTLN("ERROR: lfs_traverse() error: %d", err); + return 0; + } + return size; +} +#endif + +uint32_t DataStore::getStorageUsedKb() const { +#if defined(ESP32) + return SPIFFS.usedBytes() / 1024; +#elif defined(RP2040_PLATFORM) + FSInfo info; + info.usedBytes = 0; + _fs->info(info); + return info.usedBytes / 1024; +#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg; + int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS()); + int usedBytes = config->block_size * usedBlockCount; + return usedBytes / 1024; +#else + return 0; +#endif +} + +uint32_t DataStore::getStorageTotalKb() const { +#if defined(ESP32) + return SPIFFS.totalBytes() / 1024; +#elif defined(RP2040_PLATFORM) + FSInfo info; + info.totalBytes = 0; + _fs->info(info); + return info.totalBytes / 1024; +#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg; + int totalBytes = config->block_size * config->block_count; + return totalBytes / 1024; +#else + return 0; +#endif +} + +File DataStore::openRead(const char* filename) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return _fs->open(_rp(filename), FILE_O_READ); +#elif defined(RP2040_PLATFORM) + return _fs->open(_rp(filename), "r"); +#else + return _fs->open(_rp(filename), "r", false); +#endif +} + +File DataStore::openRead(FILESYSTEM* fs, const char* filename) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return fs->open(_rp(filename), FILE_O_READ); +#elif defined(RP2040_PLATFORM) + return fs->open(_rp(filename), "r"); +#else + return fs->open(_rp(filename), "r", false); +#endif +} + +bool DataStore::removeFile(const char* filename) { + return _fs->remove(_rp(filename)); +} + +bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) { + return fs->remove(filename); +} + +bool DataStore::formatFileSystem() { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + if (_fsExtra == nullptr) { + return _fs->format(); + } else { + return _fs->format() && _fsExtra->format(); + } +#elif defined(RP2040_PLATFORM) + return LittleFS.format(); +#elif defined(ESP32) + bool fs_success = ((fs::SPIFFSFS *)_fs)->format(); + esp_err_t nvs_err = nvs_flash_erase(); // no need to reinit, will be done by reboot + return fs_success && (nvs_err == ESP_OK); +#else + #error "need to implement format()" +#endif +} + +bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) { + return identity_store.load("_main", identity); +} + +bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) { + return identity_store.save("_main", identity); +} + +void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) { + if (_fs->exists(_rp("/new_prefs"))) { + loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename + } else if (_fs->exists(_rp("/node_prefs"))) { + loadPrefsInt("/node_prefs", prefs, node_lat, node_lon); + savePrefs(prefs, node_lat, node_lon); // save to new filename + _fs->remove(_rp("/node_prefs")); // remove old + } +} + +namespace { + +void default_rx_boosted_gain_pref(uint8_t& out) { +#if defined(USE_SX1262) || defined(USE_SX1268) || defined(SX126X_RX_BOOSTED_GAIN) + #ifdef SX126X_RX_BOOSTED_GAIN + out = (SX126X_RX_BOOSTED_GAIN != 0) ? 1 : 0; + #else + out = 1; + #endif +#else + out = 0; +#endif +} + +// After multi_acks (byte offset 78): upstream meshcore-dev layout is 15 bytes tail; legacy Meshcomod +// used 13 bytes (path_hash, autoadd_max, ble_pin..autoadd without reserved pad or trailing rx_boost). +void load_prefs_tail_meshcomod_legacy(NodePrefs& p, const uint8_t* tail) { + p.path_hash_mode = tail[0]; + p.autoadd_max_hops = tail[1]; + memcpy(&p.ble_pin, tail + 2, 4); + p.buzzer_quiet = tail[6]; + p.gps_enabled = tail[7]; + memcpy(&p.gps_interval, tail + 8, 4); + p.autoadd_config = tail[12]; + default_rx_boosted_gain_pref(p.rx_boosted_gain); +} + +void load_prefs_tail_upstream(NodePrefs& p, const uint8_t* tail, size_t n) { + p.path_hash_mode = tail[0]; + // tail[1] reserved + memcpy(&p.ble_pin, tail + 2, 4); + p.buzzer_quiet = tail[6]; + p.gps_enabled = tail[7]; + memcpy(&p.gps_interval, tail + 8, 4); + p.autoadd_config = tail[12]; + p.autoadd_max_hops = tail[13]; + if (n >= 15) { + p.rx_boosted_gain = tail[14] ? 1 : 0; + } else { + default_rx_boosted_gain_pref(p.rx_boosted_gain); + } +} + +} // namespace + +void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) { + File file = openRead(_fs, filename); + if (file) { + uint8_t pad[8]; + + file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 + file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 + file.read(pad, 4); // 36 + file.read((uint8_t *)&node_lat, sizeof(node_lat)); // 40 + file.read((uint8_t *)&node_lon, sizeof(node_lon)); // 48 + file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 + file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 + file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 + file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 + file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 + file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 + file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 + file.read((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 + file.read((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 + file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 + file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 + file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76 + file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77 + uint8_t tail[15]; + size_t n = file.read(tail, sizeof(tail)); + bool migrated_legacy = false; + if (n == 13) { + load_prefs_tail_meshcomod_legacy(_prefs, tail); + migrated_legacy = true; + } else if (n >= 14) { + load_prefs_tail_upstream(_prefs, tail, n); + } + memset(_prefs.default_scope_name, 0, sizeof(_prefs.default_scope_name)); + memset(_prefs.default_scope_key, 0, sizeof(_prefs.default_scope_key)); + file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 + file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + + file.close(); + + if (migrated_legacy) { + savePrefs(_prefs, node_lat, node_lon); + } + } +} + +void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) { + File file = openWrite(_fs, "/new_prefs"); + if (file) { + uint8_t pad[8]; + memset(pad, 0, sizeof(pad)); + + file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 + file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 + file.write(pad, 4); // 36 + file.write((uint8_t *)&node_lat, sizeof(node_lat)); // 40 + file.write((uint8_t *)&node_lon, sizeof(node_lon)); // 48 + file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 + file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 + file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 + file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 + file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 + file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 + file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 + file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 + file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 + file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 + file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 + file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76 + file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77 + file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78 + file.write(pad, 1); // 79 reserved (upstream layout) + file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 + file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84 + file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 + // gps_interval (4B) — was MISSING here; the loader (load_prefs_tail_*) reads + // it at tail[8..11], so omitting it shifted autoadd_config/max_hops/rx_boost + // and the scope fields by 4 bytes on every load (auto-add types read garbage + // -> appeared to reset on reboot). Writing it realigns save with load. + file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 + file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 90 + file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 91 + file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 92 + file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 93 + file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 125 + + file.close(); + } +} + +void DataStore::loadContacts(DataStoreHost* host) { +File file = openRead(_getContactsChannelsFS(), "/contacts3"); + if (file) { + bool full = false; + while (!full) { + ContactInfo c; + uint8_t pub_key[32]; + uint8_t unused; + + bool success = (file.read(pub_key, 32) == 32); + success = success && (file.read((uint8_t *)&c.name, 32) == 32); + success = success && (file.read(&c.type, 1) == 1); + success = success && (file.read(&c.flags, 1) == 1); + success = success && (file.read(&unused, 1) == 1); + success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4); // was 'reserved' + success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1); + success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4); + success = success && (file.read(c.out_path, 64) == 64); + success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4); + success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4); + success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4); + + if (!success) break; // EOF + + c.id = mesh::Identity(pub_key); + if (!host->onContactLoaded(c)) full = true; + } + file.close(); + } +} + +void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) { + File file = openWrite(_getContactsChannelsFS(), "/contacts3"); + if (file) { + uint32_t idx = 0; + ContactInfo c; + uint8_t unused = 0; + + while (host->getContactForSave(idx, c)) { + if (filter && !filter(c)) { + idx++; // advance to next contact + continue; + } + bool success = (file.write(c.id.pub_key, 32) == 32); + success = success && (file.write((uint8_t *)&c.name, 32) == 32); + success = success && (file.write(&c.type, 1) == 1); + success = success && (file.write(&c.flags, 1) == 1); + success = success && (file.write(&unused, 1) == 1); + success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4); + success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1); + success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4); + success = success && (file.write(c.out_path, 64) == 64); + success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4); + success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4); + success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4); + + if (!success) break; // write failed + + idx++; // advance to next contact + } + file.close(); + } +} + +void DataStore::loadChannels(DataStoreHost* host) { + File file = openRead(_getContactsChannelsFS(), "/channels2"); + if (file) { + bool full = false; + uint8_t channel_idx = 0; + while (!full) { + ChannelDetails ch; + uint8_t unused[4]; + + bool success = (file.read(unused, 4) == 4); + success = success && (file.read((uint8_t *)ch.name, 32) == 32); + success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32); + + if (!success) break; // EOF + + if (host->onChannelLoaded(channel_idx, ch)) { + channel_idx++; + } else { + full = true; + } + } + file.close(); + } +} + +void DataStore::saveChannels(DataStoreHost* host) { + File file = openWrite(_getContactsChannelsFS(), "/channels2"); + if (file) { + uint8_t channel_idx = 0; + ChannelDetails ch; + uint8_t unused[4]; + memset(unused, 0, 4); + + while (host->getChannelForSave(channel_idx, ch)) { + bool success = (file.write(unused, 4) == 4); + success = success && (file.write((uint8_t *)ch.name, 32) == 32); + success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32); + + if (!success) break; // write failed + channel_idx++; + } + file.close(); + } +} + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + +#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE) + +struct BlobRec { + uint32_t timestamp; + uint8_t key[7]; + uint8_t len; + uint8_t data[MAX_ADVERT_PKT_LEN]; +}; + +void DataStore::checkAdvBlobFile() { + if (!_getContactsChannelsFS()->exists("/adv_blobs")) { + File file = openWrite(_getContactsChannelsFS(), "/adv_blobs"); + if (file) { + BlobRec zeroes; + memset(&zeroes, 0, sizeof(zeroes)); + for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size + file.write((uint8_t *) &zeroes, sizeof(zeroes)); + } + file.close(); + } + } +} + +void DataStore::migrateToSecondaryFS() { + // migrate old adv_blobs, contacts3 and channels2 files to secondary FS if they don't already exist + if (!_fsExtra->exists("/adv_blobs")) { + if (_fs->exists("/adv_blobs")) { + File oldAdvBlobs = openRead(_fs, "/adv_blobs"); + File newAdvBlobs = openWrite(_fsExtra, "/adv_blobs"); + + if (oldAdvBlobs && newAdvBlobs) { + BlobRec rec; + size_t count = 0; + + // Copy 20 BlobRecs from old to new + while (count < 20 && oldAdvBlobs.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) { + newAdvBlobs.seek(count * sizeof(BlobRec)); + newAdvBlobs.write((uint8_t *)&rec, sizeof(rec)); + count++; + } + } + if (oldAdvBlobs) oldAdvBlobs.close(); + if (newAdvBlobs) newAdvBlobs.close(); + _fs->remove("/adv_blobs"); + } + } + if (!_fsExtra->exists("/contacts3")) { + if (_fs->exists("/contacts3")) { + File oldFile = openRead(_fs, "/contacts3"); + File newFile = openWrite(_fsExtra, "/contacts3"); + + if (oldFile && newFile) { + uint8_t buf[64]; + int n; + while ((n = oldFile.read(buf, sizeof(buf))) > 0) { + newFile.write(buf, n); + } + } + if (oldFile) oldFile.close(); + if (newFile) newFile.close(); + _fs->remove("/contacts3"); + } + } + if (!_fsExtra->exists("/channels2")) { + if (_fs->exists("/channels2")) { + File oldFile = openRead(_fs, "/channels2"); + File newFile = openWrite(_fsExtra, "/channels2"); + + if (oldFile && newFile) { + uint8_t buf[64]; + int n; + while ((n = oldFile.read(buf, sizeof(buf))) > 0) { + newFile.write(buf, n); + } + } + if (oldFile) oldFile.close(); + if (newFile) newFile.close(); + _fs->remove("/channels2"); + } + } + // cleanup nodes which have been testing the extra fs, copy _main.id and new_prefs back to primary + if (_fsExtra->exists("/_main.id")) { + if (_fs->exists("/_main.id")) {_fs->remove("/_main.id");} + File oldFile = openRead(_fsExtra, "/_main.id"); + File newFile = openWrite(_fs, "/_main.id"); + + if (oldFile && newFile) { + uint8_t buf[64]; + int n; + while ((n = oldFile.read(buf, sizeof(buf))) > 0) { + newFile.write(buf, n); + } + } + if (oldFile) oldFile.close(); + if (newFile) newFile.close(); + _fsExtra->remove("/_main.id"); + } + if (_fsExtra->exists("/new_prefs")) { + if (_fs->exists("/new_prefs")) {_fs->remove("/new_prefs");} + File oldFile = openRead(_fsExtra, "/new_prefs"); + File newFile = openWrite(_fs, "/new_prefs"); + + if (oldFile && newFile) { + uint8_t buf[64]; + int n; + while ((n = oldFile.read(buf, sizeof(buf))) > 0) { + newFile.write(buf, n); + } + } + if (oldFile) oldFile.close(); + if (newFile) newFile.close(); + _fsExtra->remove("/new_prefs"); + } + // remove files from where they should not be anymore + if (_fs->exists("/adv_blobs")) { + _fs->remove("/adv_blobs"); + } + if (_fs->exists("/contacts3")) { + _fs->remove("/contacts3"); + } + if (_fs->exists("/channels2")) { + _fs->remove("/channels2"); + } + if (_fsExtra->exists("/_main.id")) { + _fsExtra->remove("/_main.id"); + } + if (_fsExtra->exists("/new_prefs")) { + _fsExtra->remove("/new_prefs"); + } +} + +uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { + File file = openRead(_getContactsChannelsFS(), "/adv_blobs"); + uint8_t len = 0; // 0 = not found + if (file) { + BlobRec tmp; + while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) { + if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix + len = tmp.len; + memcpy(dest_buf, tmp.data, len); + break; + } + } + file.close(); + } + return len; +} + +bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) { + if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false; + checkAdvBlobFile(); + File file = _getContactsChannelsFS()->open("/adv_blobs", FILE_O_WRITE); + if (file) { + uint32_t pos = 0, found_pos = 0; + uint32_t min_timestamp = 0xFFFFFFFF; + + // search for matching key OR evict by oldest timestmap + BlobRec tmp; + file.seek(0); + while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) { + if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix + found_pos = pos; + break; + } + if (tmp.timestamp < min_timestamp) { + min_timestamp = tmp.timestamp; + found_pos = pos; + } + + pos += sizeof(tmp); + } + + memcpy(tmp.key, key, sizeof(tmp.key)); // just record 7 byte prefix of key + memcpy(tmp.data, src_buf, len); + tmp.len = len; + tmp.timestamp = _clock->getCurrentTime(); + + file.seek(found_pos); + file.write((uint8_t *) &tmp, sizeof(tmp)); + + file.close(); + return true; + } + return false; // error +} +bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { + return true; // this is just a stub on NRF52/STM32 platforms +} +#else +inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) { + char fname[18]; + if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) + mesh::Utils::toHex(fname, key, key_len); + sprintf(path, "/bl/%s", fname); +} + +uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { + char path[64]; + makeBlobPath(key, key_len, path, sizeof(path)); + + if (_fs->exists(_rp(path))) { + File f = openRead(_fs, path); + if (f) { + int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!! + f.close(); + return len; + } + } + return 0; // not found +} + +bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) { + char path[64]; + makeBlobPath(key, key_len, path, sizeof(path)); + + File f = openWrite(_fs, path); + if (f) { + int n = f.write(src_buf, len); + f.close(); + if (n == len) return true; // success! + + _fs->remove(_rp(path)); // blob was only partially written! + } + return false; // error +} + +bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { + char path[64]; + makeBlobPath(key, key_len, path, sizeof(path)); + + _fs->remove(_rp(path)); + + return true; // return true even if file did not exist +} +#endif + +#if defined(ESP32) +// Redirect all data storage to the SD card under /meshcomod (identity, prefs, +// contacts, channels, blobs). The SD card must already be mounted by the caller. +// Creates the folders, repoints _fs + the identity store, and sets the path +// prefix so every subsequent read/write/exists/remove lands under /meshcomod. +bool DataStore::useSdStorage() { + if (!SD.exists("/meshcomod")) SD.mkdir("/meshcomod"); + if (!SD.exists("/meshcomod/bl")) SD.mkdir("/meshcomod/bl"); + if (!SD.exists("/meshcomod/identity")) SD.mkdir("/meshcomod/identity"); + strncpy(_root, "/meshcomod", sizeof(_root) - 1); + _root[sizeof(_root) - 1] = '\0'; + _fs = &SD; + _fsExtra = nullptr; + identity_store.use(SD, "/meshcomod/identity"); + return true; +} +#endif diff --git a/src/DataStore.h b/src/DataStore.h new file mode 100644 index 00000000..7d01153a --- /dev/null +++ b/src/DataStore.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include "NodePrefs.h" + +class DataStoreHost { +public: + virtual bool onContactLoaded(const ContactInfo& contact) =0; + virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) =0; + virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) =0; + virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) =0; +}; + +class DataStore { + FILESYSTEM* _fs; + FILESYSTEM* _fsExtra; + mesh::RTCClock* _clock; + IdentityStore identity_store; + // Path prefix for every data file. Empty = filesystem root (flash/SPIFFS, the + // default — zero behaviour change). Set to "/meshcomod" by useSdStorage() so + // that, when SPIFFS is unavailable (e.g. installed under Launcher) or the user + // opts in, all data lives in one tidy folder on the SD card. ESP32 only. + char _root[24] = ""; + char _rpbuf[80]; + const char* _rp(const char* name); // returns _root-prefixed path (name as-is when _root empty) + + void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + void checkAdvBlobFile(); +#endif + +public: + DataStore(FILESYSTEM& fs, mesh::RTCClock& clock); + DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock); + void begin(); + bool formatFileSystem(); + File openWrite(FILESYSTEM* fs, const char* filename); // root-aware (was a free static fn) +#if defined(ESP32) + // Redirect all data storage to the SD card under /meshcomod (identity, prefs, + // contacts, channels, blobs). Call after the SD card is mounted. Returns false + // if the dir can't be created. No-op on non-ESP32. + bool useSdStorage(); +#endif + FILESYSTEM* getPrimaryFS() const { return _fs; } + FILESYSTEM* getSecondaryFS() const { return _fsExtra; } + bool loadMainIdentity(mesh::LocalIdentity &identity); + bool saveMainIdentity(const mesh::LocalIdentity &identity); + void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon); + void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon); + void loadContacts(DataStoreHost* host); + void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL); + void loadChannels(DataStoreHost* host); + void saveChannels(DataStoreHost* host); + void migrateToSecondaryFS(); + uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); + bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); + bool deleteBlobByKey(const uint8_t key[], int key_len); + File openRead(const char* filename); + File openRead(FILESYSTEM* fs, const char* filename); + bool removeFile(const char* filename); + bool removeFile(FILESYSTEM* fs, const char* filename); + uint32_t getStorageUsedKb() const; + uint32_t getStorageTotalKb() const; + +private: + FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;}; +}; diff --git a/src/MyMesh.cpp b/src/MyMesh.cpp new file mode 100644 index 00000000..044bf513 --- /dev/null +++ b/src/MyMesh.cpp @@ -0,0 +1,4246 @@ +#include "MyMesh.h" + +#include // needed for PlatformIO +#include +#include // settings backup import (uiImportBackup) +#include +#include // derive a region's flood-scope key from its #hashtag name +#include // gmtime_r for the "clock" CLI command +#ifdef ESP32 +#include // esp_restart for the "bootloader" CLI command +#include // RTC_CNTL_OPTION1_REG / FORCE_DOWNLOAD_BOOT +#endif +#include +#include +#include +#include "WiFiConfig.h" +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) +#include +#endif +#ifdef MULTI_TRANSPORT_COMPANION +#include +#endif +#endif + +#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) +/** While `ota url` runs, pin WS/TCP reply target so OTA progress survives yield() and checkRecvFrame. */ +static int s_companion_ota_pinned_reply_target = -1; +#endif + +#define CMD_APP_START 1 +#define CMD_SEND_TXT_MSG 2 +#define CMD_SEND_CHANNEL_TXT_MSG 3 +#define CMD_GET_CONTACTS 4 // with optional 'since' (for efficient sync) +#define CMD_GET_DEVICE_TIME 5 +#define CMD_SET_DEVICE_TIME 6 +#define CMD_SEND_SELF_ADVERT 7 +#define CMD_SET_ADVERT_NAME 8 +#define CMD_ADD_UPDATE_CONTACT 9 +#define CMD_SYNC_NEXT_MESSAGE 10 +#define CMD_SET_RADIO_PARAMS 11 +#define CMD_SET_RADIO_TX_POWER 12 +#define CMD_RESET_PATH 13 +#define CMD_SET_ADVERT_LATLON 14 +#define CMD_REMOVE_CONTACT 15 +#define CMD_SHARE_CONTACT 16 +#define CMD_EXPORT_CONTACT 17 +#define CMD_IMPORT_CONTACT 18 +#define CMD_REBOOT 19 +#define CMD_GET_BATT_AND_STORAGE 20 // was CMD_GET_BATTERY_VOLTAGE +#define CMD_SET_TUNING_PARAMS 21 +#define CMD_DEVICE_QUERY 22 +#define CMD_EXPORT_PRIVATE_KEY 23 +#define CMD_IMPORT_PRIVATE_KEY 24 +#define CMD_SEND_RAW_DATA 25 +#define CMD_SEND_LOGIN 26 +#define CMD_SEND_STATUS_REQ 27 +#define CMD_HAS_CONNECTION 28 +#define CMD_LOGOUT 29 // 'Disconnect' +#define CMD_GET_CONTACT_BY_KEY 30 +#define CMD_GET_CHANNEL 31 +#define CMD_SET_CHANNEL 32 +#define CMD_SIGN_START 33 +#define CMD_SIGN_DATA 34 +#define CMD_SIGN_FINISH 35 +#define CMD_SEND_TRACE_PATH 36 +#define CMD_SET_DEVICE_PIN 37 +#define CMD_SET_OTHER_PARAMS 38 +#define CMD_SEND_TELEMETRY_REQ 39 // can deprecate this +#define CMD_GET_CUSTOM_VARS 40 +#define CMD_SET_CUSTOM_VAR 41 +#define CMD_GET_ADVERT_PATH 42 +#define CMD_GET_TUNING_PARAMS 43 +// NOTE: CMD range 44..49 parked, potentially for WiFi operations +#define CMD_SEND_BINARY_REQ 50 +#define CMD_FACTORY_RESET 51 +#define CMD_SEND_PATH_DISCOVERY_REQ 52 +#define CMD_SET_FLOOD_SCOPE 54 // v8+ +#define CMD_SEND_CONTROL_DATA 55 // v8+ +#define CMD_GET_STATS 56 // v8+, second byte is stats type +#define CMD_SEND_ANON_REQ 57 +#define CMD_SET_AUTOADD_CONFIG 58 +#define CMD_GET_AUTOADD_CONFIG 59 +#define CMD_GET_ALLOWED_REPEAT_FREQ 60 +#define CMD_SET_PATH_HASH_MODE 61 // v10+: payload [0, mode]; mode 0..2 (1/2/3-byte path hashes when sending) +#define CMD_SYNC_SINCE 62 // client sends T (4 bytes LE Unix sec); response: stream of 7/8/16/17 then 61 +#define CMD_SEND_CHANNEL_DATA 62 +#define CMD_SET_DEFAULT_FLOOD_SCOPE 63 +#define CMD_GET_DEFAULT_FLOOD_SCOPE 64 +#define CMD_SEND_RAW_PACKET 65 + +// Stats sub-types for CMD_GET_STATS +#define STATS_TYPE_CORE 0 +#define STATS_TYPE_RADIO 1 +#define STATS_TYPE_PACKETS 2 + +#define RESP_CODE_OK 0 +#define RESP_CODE_ERR 1 +#define RESP_CODE_CONTACTS_START 2 // first reply to CMD_GET_CONTACTS +#define RESP_CODE_CONTACT 3 // multiple of these (after CMD_GET_CONTACTS) +#define RESP_CODE_END_OF_CONTACTS 4 // last reply to CMD_GET_CONTACTS +#define RESP_CODE_SELF_INFO 5 // reply to CMD_APP_START +#define RESP_CODE_SENT 6 // reply to CMD_SEND_TXT_MSG +#define RESP_CODE_CONTACT_MSG_RECV 7 // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3) +#define RESP_CODE_CHANNEL_MSG_RECV 8 // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3) +#define RESP_CODE_CURR_TIME 9 // a reply to CMD_GET_DEVICE_TIME +#define RESP_CODE_NO_MORE_MESSAGES 10 // a reply to CMD_SYNC_NEXT_MESSAGE +#define RESP_CODE_EXPORT_CONTACT 11 +#define RESP_CODE_BATT_AND_STORAGE 12 // a reply to a CMD_GET_BATT_AND_STORAGE +#define RESP_CODE_DEVICE_INFO 13 // a reply to CMD_DEVICE_QUERY +#define RESP_CODE_PRIVATE_KEY 14 // a reply to CMD_EXPORT_PRIVATE_KEY +#define RESP_CODE_DISABLED 15 +#define RESP_CODE_CONTACT_MSG_RECV_V3 16 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3) +#define RESP_CODE_CHANNEL_MSG_RECV_V3 17 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3) +#define RESP_CODE_CHANNEL_INFO 18 // a reply to CMD_GET_CHANNEL +#define RESP_CODE_SIGN_START 19 +#define RESP_CODE_SIGNATURE 20 +#define RESP_CODE_CUSTOM_VARS 21 +#define RESP_CODE_ADVERT_PATH 22 +#define RESP_CODE_TUNING_PARAMS 23 +#define RESP_CODE_STATS 24 // v8+, second byte is stats type +#define RESP_CODE_AUTOADD_CONFIG 25 +#define RESP_ALLOWED_REPEAT_FREQ 26 +#define RESP_CODE_CHANNEL_DATA_RECV 27 +#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28 + +#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9) +#define RESP_CODE_SYNC_SINCE_DONE 61 // sent once after SyncSince delta stream; client sets last-sync to now + +#define SEND_TIMEOUT_BASE_MILLIS 500 +#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f +#define DIRECT_SEND_PERHOP_FACTOR 6.0f +#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 +#define LAZY_CONTACTS_WRITE_DELAY 5000 + +#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" + +// these are _pushed_ to client app at any time +#define PUSH_CODE_ADVERT 0x80 +#define PUSH_CODE_PATH_UPDATED 0x81 +#define PUSH_CODE_SEND_CONFIRMED 0x82 +#define PUSH_CODE_MSG_WAITING 0x83 +#define PUSH_CODE_RAW_DATA 0x84 +#define PUSH_CODE_LOGIN_SUCCESS 0x85 +#define PUSH_CODE_LOGIN_FAIL 0x86 +#define PUSH_CODE_STATUS_RESPONSE 0x87 +#define PUSH_CODE_LOG_RX_DATA 0x88 +#define PUSH_CODE_TRACE_DATA 0x89 +#define PUSH_CODE_NEW_ADVERT 0x8A +#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B +#define PUSH_CODE_BINARY_RESPONSE 0x8C +#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D +#define PUSH_CODE_CONTROL_DATA 0x8E // v8+ +#define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest +#define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full + +#define ERR_CODE_UNSUPPORTED_CMD 1 +#define ERR_CODE_NOT_FOUND 2 +#define ERR_CODE_TABLE_FULL 3 +#define ERR_CODE_BAD_STATE 4 +#define ERR_CODE_FILE_IO_ERROR 5 +#define ERR_CODE_ILLEGAL_ARG 6 + +#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K + +#ifndef COMPANION_SYNC_DEBUG +#define COMPANION_SYNC_DEBUG 0 +#endif + +#if COMPANION_SYNC_DEBUG +#define SYNC_DEBUG_PRINTLN(F, ...) Serial.printf("SYNCDBG: " F "\n", ##__VA_ARGS__) +struct SyncDebugCounters { + uint32_t req; + uint32_t had_frame; + uint32_t no_more; + uint32_t write_ok; + uint32_t write_fail; + uint32_t retries; +}; +static SyncDebugCounters g_sync_dbg = {0, 0, 0, 0, 0, 0}; +#else +#define SYNC_DEBUG_PRINTLN(...) do {} while (0) +#endif + +// Auto-add config bitmask +// Bit 0: If set, overwrite oldest non-favourite contact when contacts file is full +// Bits 1-4: these indicate which contact types to auto-add when manual_contact_mode = 0x01 +#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full +#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT) +#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER) +#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM) +#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR) + +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) +#define MESHCOMOD_WIFI_SCAN_MAX 12 +static char s_meshcomod_scan_ssids[MESHCOMOD_WIFI_SCAN_MAX][WIFI_CONFIG_SSID_MAX]; +static int s_meshcomod_scan_count = 0; +#endif +#endif +static const char* kMeshcomodHelpMsg = + "help\n" + "status\n" + "ota start\n" + "ota url \n" + "ota netdiag\n" + "ota status\n" + "wifi scan\n" + "wifi use \n" + "wifi set ssid \n" + "wifi set pwd \n" + "wifi status\n" + "wifi on\n" + "wifi off\n" + "wifi apply\n" + "wifi clear\n" + "tcp on\n" + "tcp off\n" + "tcp status\n" + "ble on\n" + "ble off\n" + "ble status"; + +#define MESHCOMOD_CMD_CACHE_SIZE 6 +struct MeshcomodCmdCacheEntry { + uint32_t ts; + uint32_t seen_ms; + char text[128]; +}; +static MeshcomodCmdCacheEntry s_meshcomod_cmd_cache[MESHCOMOD_CMD_CACHE_SIZE] = {}; +static int s_meshcomod_cmd_cache_next = 0; +static uint32_t s_meshcomod_last_reply_ts = 0; +static uint32_t s_last_cmd_txt_ts = 0; +static uint8_t s_last_cmd_txt_pub6[6] = {0}; +static uint32_t s_last_cmd_txt_body_crc = 0; +static uint32_t s_last_cmd_txt_ack = 0; +static uint32_t s_last_cmd_txt_est_timeout = 0; +static uint32_t s_last_cmd_txt_seen_ms = 0; + +enum MeshcomodPendingAction { + MESHCOMOD_PENDING_NONE = 0, + MESHCOMOD_PENDING_TCP_OFF = 1, + MESHCOMOD_PENDING_BLE_OFF = 2, +}; +static MeshcomodPendingAction s_meshcomod_pending_action = MESHCOMOD_PENDING_NONE; +static uint32_t s_meshcomod_pending_until_ms = 0; + + +static char* trimWsInPlace(char* s) { + if (!s) return s; + while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') s++; + int n = (int)strlen(s); + while (n > 0) { + char c = s[n - 1]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + s[n - 1] = '\0'; + n--; + } else { + break; + } + } + return s; +} + +static char* unquoteInPlace(char* s) { + s = trimWsInPlace(s); + if (!s) return s; + int n = (int)strlen(s); + if (n >= 2) { + char q = s[0]; + if ((q == '"' || q == '\'') && s[n - 1] == q) { + s[n - 1] = '\0'; + s++; + } + } + return s; +} + +static bool isMeshcomodDuplicate(uint32_t msg_ts, const char* text) { + if (!text || !*text) return false; + uint32_t now_ms = millis(); + for (int i = 0; i < MESHCOMOD_CMD_CACHE_SIZE; i++) { + const MeshcomodCmdCacheEntry& e = s_meshcomod_cmd_cache[i]; + if (e.ts == 0 || e.text[0] == '\0') continue; + // Primary dedupe key: app message timestamp + same text + if (e.ts == msg_ts && strcmp(e.text, text) == 0) return true; + // Secondary dedupe: same text repeated very quickly with a new timestamp + if (strcmp(e.text, text) == 0 && (now_ms - e.seen_ms) < 1800UL) return true; + } + return false; +} + +static void rememberMeshcomodCommand(uint32_t msg_ts, const char* text) { + MeshcomodCmdCacheEntry& e = s_meshcomod_cmd_cache[s_meshcomod_cmd_cache_next]; + e.ts = msg_ts; + e.seen_ms = millis(); + if (text) + StrHelper::strzcpy(e.text, text, sizeof(e.text)); + else + e.text[0] = '\0'; + s_meshcomod_cmd_cache_next = (s_meshcomod_cmd_cache_next + 1) % MESHCOMOD_CMD_CACHE_SIZE; +} + +void MyMesh::writeOKFrame() { + uint8_t buf[1]; + buf[0] = RESP_CODE_OK; + _serial->writeFrame(buf, 1); +} +void MyMesh::writeErrFrame(uint8_t err_code) { + uint8_t buf[2]; + buf[0] = RESP_CODE_ERR; + buf[1] = err_code; + _serial->writeFrame(buf, 2); +} + +void MyMesh::writeDisabledFrame() { + uint8_t buf[1]; + buf[0] = RESP_CODE_DISABLED; + _serial->writeFrame(buf, 1); +} + +size_t MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact, bool to_all) { + int i = 0; + out_frame[i++] = code; + memcpy(&out_frame[i], contact.id.pub_key, PUB_KEY_SIZE); + i += PUB_KEY_SIZE; + out_frame[i++] = contact.type; + out_frame[i++] = contact.flags; + out_frame[i++] = contact.out_path_len; + memcpy(&out_frame[i], contact.out_path, MAX_PATH_SIZE); + i += MAX_PATH_SIZE; + StrHelper::strzcpy((char *)&out_frame[i], contact.name, 32); + i += 32; + memcpy(&out_frame[i], &contact.last_advert_timestamp, 4); + i += 4; + memcpy(&out_frame[i], &contact.gps_lat, 4); + i += 4; + memcpy(&out_frame[i], &contact.gps_lon, 4); + i += 4; + memcpy(&out_frame[i], &contact.lastmod, 4); + i += 4; + if (to_all) + return _serial->writeFrameToAll(out_frame, i); + return _serial->writeFrame(out_frame, i); +} + +const uint8_t MyMesh::MESHCOMOD_PUB_KEY_PREFIX[6] = { 0x4D, 0x45, 0x53, 0x48, 0x43, 0x4D }; // "MESHCM" + +void MyMesh::getMeshcomodContact(ContactInfo& dest) { + memset(&dest, 0, sizeof(dest)); + memcpy(dest.id.pub_key, MESHCOMOD_PUB_KEY_PREFIX, 6); + StrHelper::strncpy(dest.name, MESHCOMOD_NAME, sizeof(dest.name) - 1); + dest.type = ADV_TYPE_CHAT; + dest.flags = 0; + dest.out_path_len = -1; + dest.last_advert_timestamp = 0; + dest.lastmod = 0; +} + +bool MyMesh::isMeshcomodRecipient(const uint8_t* pub_key_prefix_6) const { + return pub_key_prefix_6 && memcmp(pub_key_prefix_6, MESHCOMOD_PUB_KEY_PREFIX, 6) == 0; +} + +// On-device terminal sink: when the Terminal UI is open it registers a callback +// here so command replies (and async ones like "wifi scan") also land in the +// terminal log, not just the companion serial frames. +static void (*s_terminal_sink)(const char*) = nullptr; +void MyMesh::setTerminalSink(void (*cb)(const char*)) { s_terminal_sink = cb; } +void MyMesh::runLocalCli(const char* cmd) { + if (cmd && *cmd) handleMeshcomodCommand(cmd, (int)strlen(cmd)); +} + +void MyMesh::pushMeshcomodReply(const char* text, bool immediate_current) { + if (!text) return; + if (s_terminal_sink) s_terminal_sink(text); + int total_len = (int)strlen(text); + if (total_len <= 0) return; + + // Header bytes before message text: + // code(1), snr/reserved(3), sender_prefix(6), path_len(1), txt_type(1), timestamp(4) = 16 + const int header_len = 16; + const int max_text_per_frame = MAX_FRAME_SIZE - header_len; + if (max_text_per_frame <= 0) return; + + int pos = 0; + while (pos < total_len) { + int remaining = total_len - pos; + int take = remaining < max_text_per_frame ? remaining : max_text_per_frame; + + // Prefer splitting on newline so command/help lines stay intact. + if (remaining > max_text_per_frame) { + int split = -1; + for (int k = take - 1; k >= 0; k--) { + char c = text[pos + k]; + if (c == '\n') { + split = k + 1; + break; + } + } + // Always split at previous newline when available. + // If no newline exists in this window, this is a single very long line and we must hard-split. + if (split > 0) take = split; + } + + int j = 0; + out_frame[j++] = RESP_CODE_CONTACT_MSG_RECV_V3; + out_frame[j++] = 0; + out_frame[j++] = 0; + out_frame[j++] = 0; + memcpy(&out_frame[j], MESHCOMOD_PUB_KEY_PREFIX, 6); + j += 6; + out_frame[j++] = 0xFF; + out_frame[j++] = TXT_TYPE_PLAIN; + uint32_t ts = getRTCClock()->getCurrentTimeUnique(); + // Some clients coalesce messages by sender+timestamp; enforce strictly monotonic ts. + if (ts <= s_meshcomod_last_reply_ts) ts = s_meshcomod_last_reply_ts + 1; + s_meshcomod_last_reply_ts = ts; + memcpy(&out_frame[j], &ts, 4); + j += 4; + + memcpy(&out_frame[j], &text[pos], take); + j += take; + + if (immediate_current && _serial->isConnected()) { + // Immediate emit to current client connection (no waiting for sync polling). + _serial->writeFrame(out_frame, j); + } + addToHistoryRing(out_frame, j); + if (_serial->isConnected()) { + uint8_t tickle[1] = { PUSH_CODE_MSG_WAITING }; + _serial->writeFrameToAll(tickle, 1); + } + pos += take; + } +} + +bool MyMesh::handleMeshcomodCommand(const char* text, int text_len) { + if (!text || text_len <= 0) { + pushMeshcomodReply(kMeshcomodHelpMsg); + return true; + } + char buf[128]; + if ((size_t)text_len >= sizeof(buf)) text_len = (int)sizeof(buf) - 1; + memcpy(buf, text, (size_t)text_len); + buf[text_len] = '\0'; + + const char* p = buf; + while (*p == ' ' || *p == '\t') p++; + + // Safety confirmation flow for disruptive transport-off commands. + if (s_meshcomod_pending_action != MESHCOMOD_PENDING_NONE) { + uint32_t now_ms = millis(); + if ((int32_t)(s_meshcomod_pending_until_ms - now_ms) < 0) { + s_meshcomod_pending_action = MESHCOMOD_PENDING_NONE; + s_meshcomod_pending_until_ms = 0; + pushMeshcomodReply("pending confirmation expired"); + } else if (strncasecmp(p, "ok", 2) == 0 && (p[2] == '\0' || p[2] == ' ' || p[2] == '\t')) { + if (s_meshcomod_pending_action == MESHCOMOD_PENDING_TCP_OFF) { + _serial->disableTcp(); + pushMeshcomodReply("OK tcp=off"); + } else if (s_meshcomod_pending_action == MESHCOMOD_PENDING_BLE_OFF) { + _serial->disableBle(); + pushMeshcomodReply("OK ble=off"); + } + s_meshcomod_pending_action = MESHCOMOD_PENDING_NONE; + s_meshcomod_pending_until_ms = 0; + return true; + } else if (strncasecmp(p, "cancel", 6) == 0 && (p[6] == '\0' || p[6] == ' ' || p[6] == '\t')) { + s_meshcomod_pending_action = MESHCOMOD_PENDING_NONE; + s_meshcomod_pending_until_ms = 0; + pushMeshcomodReply("cancelled"); + return true; + } + } + + if (strncasecmp(p, "help", 4) == 0 && (p[4] == '\0' || p[4] == ' ' || p[4] == '\t')) { + pushMeshcomodReply(kMeshcomodHelpMsg); + return true; + } + + // ---- Native MeshCore CLI commands (on-device terminal) ---- + auto isCmd = [](const char* s, const char* name) -> bool { + size_t n = strlen(name); + return strncasecmp(s, name, n) == 0 && (s[n] == '\0' || s[n] == ' ' || s[n] == '\t'); + }; + + if (isCmd(p, "ver") || isCmd(p, "version")) { + char r[96]; + snprintf(r, sizeof r, "Meshcomod %s\nbuild %s (code %d)", + FIRMWARE_VERSION, FIRMWARE_BUILD_DATE, FIRMWARE_VER_CODE); + pushMeshcomodReply(r); + return true; + } + if (isCmd(p, "clock") || isCmd(p, "time")) { + time_t tt = (time_t)getRTCClock()->getCurrentTime(); + struct tm tmv; gmtime_r(&tt, &tmv); + char r[64]; + snprintf(r, sizeof r, "clock: %04d-%02d-%02d %02d:%02d:%02d UTC", + tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday, tmv.tm_hour, tmv.tm_min, tmv.tm_sec); + pushMeshcomodReply(r); + return true; + } + if (isCmd(p, "advert.zerohop")) { + pushMeshcomodReply(sendAdvert(false) ? "advert sent (zero-hop)" : "advert failed"); + return true; + } + if (isCmd(p, "advert")) { + pushMeshcomodReply(sendAdvert(true) ? "advert sent (flood)" : "advert failed"); + return true; + } + if (isCmd(p, "reboot")) { + pushMeshcomodReply("rebooting..."); + delay(150); + board.reboot(); + return true; // not reached + } + if (isCmd(p, "bootloader") || isCmd(p, "dfu")) { +#ifdef ESP32 + // Force the ROM into serial/USB download mode on the next reset, so the + // board can be flashed without the (flaky) trackball+reset combo. The + // FORCE_DOWNLOAD_BOOT bit lives in the RTC domain and survives the restart. + pushMeshcomodReply("rebooting into download mode (screen goes dark)..."); + delay(200); +#ifdef PIN_TFT_LEDA_CTL + if (PIN_TFT_LEDA_CTL >= 0) { // blank the backlight = clear "in download mode" signal + pinMode(PIN_TFT_LEDA_CTL, OUTPUT); + digitalWrite(PIN_TFT_LEDA_CTL, LOW); + } +#endif + // Read-modify-write ONLY the force-download bit. A full REG_WRITE zeroes the + // rest of RTC_CNTL_OPTION1 and wedges the RTC so esp_restart() hangs instead + // of resetting (that was the earlier "freeze"). board.reboot() == esp_restart, + // the proven reset path on this board; the RTC bit survives it. + uint32_t opt1 = REG_READ(RTC_CNTL_OPTION1_REG); + REG_WRITE(RTC_CNTL_OPTION1_REG, opt1 | RTC_CNTL_FORCE_DOWNLOAD_BOOT); + board.reboot(); +#else + pushMeshcomodReply("bootloader: ESP32-only"); +#endif + return true; // not reached on ESP32 + } + if (isCmd(p, "get")) { + NodePrefs* pr = getNodePrefs(); + char r[256]; + snprintf(r, sizeof r, + "name: %s\nfreq: %.3f MHz\nbw: %.1f kHz\nsf: %u\ncr: %u\ntx: %d dBm\nlat: %.5f\nlon: %.5f", + pr->node_name, pr->freq, pr->bw, (unsigned)pr->sf, (unsigned)pr->cr, + (int)pr->tx_power_dbm, sensors.node_lat, sensors.node_lon); + pushMeshcomodReply(r); + return true; + } + if (isCmd(p, "set")) { + const char* q = p + 3; + while (*q == ' ' || *q == '\t') q++; + char param[16]; int pi = 0; + while (*q && *q != ' ' && *q != '\t' && pi < (int)sizeof(param) - 1) param[pi++] = *q++; + param[pi] = '\0'; + while (*q == ' ' || *q == '\t') q++; // q -> value + NodePrefs* pr = getNodePrefs(); + bool ok = true; + if (strcasecmp(param, "name") == 0) { StrHelper::strncpy(pr->node_name, q, sizeof(pr->node_name) - 1); } + else if (strcasecmp(param, "freq") == 0) { pr->freq = (float)atof(q); } + else if (strcasecmp(param, "bw") == 0) { pr->bw = (float)atof(q); } + else if (strcasecmp(param, "sf") == 0) { pr->sf = (uint8_t)atoi(q); } + else if (strcasecmp(param, "cr") == 0) { pr->cr = (uint8_t)atoi(q); } + else if (strcasecmp(param, "tx") == 0) { pr->tx_power_dbm = (int8_t)atoi(q); } + else ok = false; + if (ok) { savePrefs(); pushMeshcomodReply("ok (radio changes apply after reboot)"); } + else { pushMeshcomodReply("set "); } + return true; + } + + if (strncasecmp(p, "status", 6) == 0 && (p[6] == '\0' || p[6] == ' ' || p[6] == '\t')) { + const char* tcp = _serial->isTcpEnabled() ? "on" : "off"; + char ble[32]; + if (_serial->hasBleCapability()) { + if (_serial->isBleEnabled()) { + char peer[24]; + if (_serial->getBlePeerAddress(peer, sizeof(peer))) + snprintf(ble, sizeof(ble), "on (%s)", peer); + else + snprintf(ble, sizeof(ble), "on"); + } else { + snprintf(ble, sizeof(ble), "off"); + } + } else { + snprintf(ble, sizeof(ble), "n/a"); + } + char ws_line[24]; + if (_serial->isWsStarted()) + snprintf(ws_line, sizeof(ws_line), "ws: %u", (unsigned)_serial->getWsPort()); + else + snprintf(ws_line, sizeof(ws_line), "ws: off"); +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + char wifi[120]; + if (WiFi.status() == WL_CONNECTED) { + IPAddress ip = WiFi.localIP(); + String ssid = WiFi.SSID(); + snprintf(wifi, sizeof(wifi), "wifi: connected\nssid: %s\nip: %d.%d.%d.%d", + ssid.length() ? ssid.c_str() : "(unknown)", ip[0], ip[1], ip[2], ip[3]); + } else { + snprintf(wifi, sizeof(wifi), "wifi: disconnected"); + } + char status[280]; + snprintf(status, sizeof(status), "companion status:\nusb: on\nble: %s\ntcp: %s\n%s\n%s", ble, tcp, ws_line, wifi); + pushMeshcomodReply(status); + return true; +#endif +#endif + char status_basic[180]; + snprintf(status_basic, sizeof(status_basic), "companion status:\nusb: on\nble: %s\ntcp: %s\n%s", ble, tcp, ws_line); + pushMeshcomodReply(status_basic); + return true; + } + + if (strncasecmp(p, "tcp", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + p += 3; + while (*p == ' ' || *p == '\t') p++; + if (strncasecmp(p, "on", 2) == 0 && (p[2] == '\0' || p[2] == ' ' || p[2] == '\t')) { + _serial->enableTcp(); + pushMeshcomodReply("OK\ntcp: on"); + return true; + } + if (strncasecmp(p, "off", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + s_meshcomod_pending_action = MESHCOMOD_PENDING_TCP_OFF; + s_meshcomod_pending_until_ms = millis() + 30000UL; + pushMeshcomodReply("warning: turning TCP off may remove wireless access to this companion."); + pushMeshcomodReply("if BLE is also off, you will need physical USB access."); + pushMeshcomodReply("reply 'ok' within 30s to confirm, or 'cancel'."); + return true; + } + if (strncasecmp(p, "status", 6) == 0 && (p[6] == '\0' || p[6] == ' ' || p[6] == '\t')) { + pushMeshcomodReply(_serial->isTcpEnabled() ? "tcp: on" : "tcp: off"); + return true; + } + pushMeshcomodReply("usage:\n- tcp on\n- tcp off\n- tcp status"); + return true; + } + + if (strncasecmp(p, "ble", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + if (!_serial->hasBleCapability()) { + pushMeshcomodReply("ble=n/a"); + return true; + } + p += 3; + while (*p == ' ' || *p == '\t') p++; + if (strncasecmp(p, "on", 2) == 0 && (p[2] == '\0' || p[2] == ' ' || p[2] == '\t')) { + _serial->enableBle(); + pushMeshcomodReply("OK\nble: on"); + return true; + } + if (strncasecmp(p, "off", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + s_meshcomod_pending_action = MESHCOMOD_PENDING_BLE_OFF; + s_meshcomod_pending_until_ms = millis() + 30000UL; + pushMeshcomodReply("warning: turning BLE off may remove wireless access to this companion."); + pushMeshcomodReply("if TCP is also off, you will need physical USB access."); + pushMeshcomodReply("reply 'ok' within 30s to confirm, or 'cancel'."); + return true; + } + if (strncasecmp(p, "status", 6) == 0 && (p[6] == '\0' || p[6] == ' ' || p[6] == '\t')) { + if (_serial->isBleEnabled()) { + char peer[24]; + if (_serial->getBlePeerAddress(peer, sizeof(peer))) { + char msg[56]; + snprintf(msg, sizeof(msg), "ble: on\npeer: %s", peer); + pushMeshcomodReply(msg); + } else { + pushMeshcomodReply("ble: on"); + } + } else { + pushMeshcomodReply("ble: off"); + } + return true; + } + pushMeshcomodReply("usage:\n- ble on\n- ble off\n- ble status"); + return true; + } + + if (strncasecmp(p, "ota", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + p += 3; + while (*p == ' ' || *p == '\t') p++; + if (strncasecmp(p, "start", 5) == 0 && (p[5] == '\0' || p[5] == ' ' || p[5] == '\t')) { + char reply[160]; + if (board.startOTAUpdate(_prefs.node_name, reply)) { + pushMeshcomodReply(reply); + } else { + pushMeshcomodReply("ERR: OTA not supported in this build"); + } + return true; + } + if (strncasecmp(p, "netdiag", 7) == 0 && (p[7] == '\0' || p[7] == ' ' || p[7] == '\t')) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + board.emitHttpOtaNetDiagnosticLines(); + pushMeshcomodReply("OK ota netdiag (see binary lines)"); +#else + pushMeshcomodReply("ERR: ota netdiag n/a"); +#endif +#else + pushMeshcomodReply("ERR: ota netdiag n/a"); +#endif + return true; + } + if (strncasecmp(p, "url", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + p += 3; + while (*p == ' ' || *p == '\t') p++; + if (*p == '\0') { + pushMeshcomodReply("ERR: missing URL"); + return true; + } +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + { + char reply[160] = {0}; +#ifdef MULTI_TRANSPORT_COMPANION + { + int rt = _serial ? _serial->getReplyTarget() : REPLY_TARGET_USB; + if (rt == REPLY_TARGET_USB || rt == REPLY_TARGET_BLE) { + meshcoreRepeaterTcpOtaEmitLine("OTA: rejected need Wi-Fi TCP/WS control session"); + pushMeshcomodReply(rt == REPLY_TARGET_BLE + ? "ERR: HTTP OTA must be started from Wi-Fi TCP or WebSocket (not BLE)" + : "ERR: HTTP OTA must be started from Wi-Fi TCP or WebSocket (not USB)"); + return true; + } + s_companion_ota_pinned_reply_target = rt; + if (_serial) _serial->prepareForHttpOta(); + bool handled = board.startHttpOtaFromUrl(p, reply); + if (_serial && (strncmp(reply, "ERR:", 4) == 0 || !handled)) _serial->restoreAfterHttpOta(); + if (!handled) { + StrHelper::strncpy(reply, "ERR: OTA URL not supported", sizeof(reply)); + } + if (_serial) _serial->setReplyTarget(rt); + pushMeshcomodReply(reply); + pushCompanionOtaProgressLine(reply); + s_companion_ota_pinned_reply_target = -1; + } +#elif defined(WIFI_SSID) + { + if (!_serial || !_serial->isHttpOtaWifiControlSession()) { + meshcoreRepeaterTcpOtaEmitLine("OTA: rejected need active Wi-Fi companion TCP session"); + pushMeshcomodReply(WiFi.status() != WL_CONNECTED + ? "ERR: WiFi not connected" + : "ERR: HTTP OTA must be started from an active Wi-Fi companion connection"); + return true; + } + bool handled = board.startHttpOtaFromUrl(p, reply); + if (!handled) { + StrHelper::strncpy(reply, "ERR: OTA URL not supported", sizeof(reply)); + } + pushMeshcomodReply(reply); + pushCompanionOtaProgressLine(reply); + } +#endif + } +#else + char reply[160]; + if (board.startHttpOtaFromUrl(p, reply)) { + pushMeshcomodReply(reply); + } else { + pushMeshcomodReply("ERR: OTA URL not supported"); + } +#endif +#else + char reply[160]; + if (board.startHttpOtaFromUrl(p, reply)) { + pushMeshcomodReply(reply); + } else { + pushMeshcomodReply("ERR: OTA URL not supported"); + } +#endif + return true; + } + if (strncasecmp(p, "status", 6) == 0 && (p[6] == '\0' || p[6] == ' ' || p[6] == '\t')) { + char line[96]; + if (g_meshcore_http_ota_display_active) { + if (g_meshcore_http_ota_display_pct == 0xFF) { + snprintf(line, sizeof(line), "ota: active\n%s", g_meshcore_http_ota_display_line[0] ? g_meshcore_http_ota_display_line : "working"); + } else { + snprintf(line, sizeof(line), "ota: %u%%\n%s", (unsigned)g_meshcore_http_ota_display_pct, + g_meshcore_http_ota_display_line[0] ? g_meshcore_http_ota_display_line : "working"); + } + } else { + snprintf(line, sizeof(line), "ota: idle"); + } + pushMeshcomodReply(line); + return true; + } + pushMeshcomodReply("usage:\n- ota start\n- ota url \n- ota netdiag\n- ota status"); + return true; + } + + if (strncasecmp(p, "wifi", 4) != 0 || (p[4] != '\0' && p[4] != ' ' && p[4] != '\t')) { + pushMeshcomodReply(kMeshcomodHelpMsg); + return true; + } + p += 4; + while (*p == ' ' || *p == '\t') p++; + +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + if (strncasecmp(p, "on", 2) == 0 && (p[2] == '\0' || p[2] == ' ' || p[2] == '\t')) { + wifiConfigSetRadioEnabled(true); + pushMeshcomodReply("OK wifi radio on"); + return true; + } + if (strncasecmp(p, "off", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + wifiConfigSetRadioEnabled(false); + pushMeshcomodReply("OK wifi radio off"); + return true; + } + if (strncasecmp(p, "set", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + p += 3; + while (*p == ' ' || *p == '\t') p++; + if (strncasecmp(p, "ssid", 4) == 0 && (p[4] == '\0' || p[4] == ' ' || p[4] == '\t')) { + p += 4; + while (*p == ' ' || *p == '\t') p++; + char* ssid = unquoteInPlace((char*)p); + if (wifiConfigSetSsid(ssid)) { + char ok[168]; + snprintf(ok, sizeof(ok), + "OK ssid=\"%s\"\nnext: wifi set pwd \"\" (or \"\" for open)\nthen: wifi apply", + ssid); + pushMeshcomodReply(ok); + } else { + pushMeshcomodReply("error: ssid too long or invalid"); + } + return true; + } + if (strncasecmp(p, "pwd", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + p += 3; + while (*p == ' ' || *p == '\t') p++; + char* pwd = unquoteInPlace((char*)p); + if (wifiConfigSetPwd(pwd)) { + pushMeshcomodReply("OK\npwd set\nnext: wifi apply"); + } else { + pushMeshcomodReply("error: password too long"); + } + return true; + } + pushMeshcomodReply("error: usage wifi set ssid|pwd \"\""); + return true; + } + if (strncasecmp(p, "scan", 4) == 0 && (p[4] == '\0' || p[4] == ' ' || p[4] == '\t')) { + if (!wifiConfigGetRadioEnabled()) { + pushMeshcomodReply("error: wifi radio off — send wifi on first"); + return true; + } + // Ensure STA is fully up before scan; disconnected STA needs a bit more settle time. + wifi_mode_t mode = WiFi.getMode(); + if ((mode & WIFI_MODE_STA) == 0) { + WiFi.mode(WIFI_STA); + delay(180); + } else { + delay(40); + } + WiFi.scanDelete(); + s_meshcomod_scan_count = 0; + int found = WiFi.scanNetworks(false, true, false, 300, 0); + // On some ESP32 states, first scan right after STA bring-up can fail when not connected. + if (found <= 0 && WiFi.status() != WL_CONNECTED) { + WiFi.disconnect(false, false); + delay(120); + found = WiFi.scanNetworks(false, true, false, 300, 0); + } + if (found <= 0) { + pushMeshcomodReply("scan: no networks (2.4GHz only)"); + WiFi.scanDelete(); + return true; + } + String scanMsg = "scan results:"; + for (int idx = 0; idx < found && s_meshcomod_scan_count < MESHCOMOD_WIFI_SCAN_MAX; idx++) { + String ssid = WiFi.SSID(idx); + if (ssid.length() <= 0) continue; + bool dup = false; + for (int k = 0; k < s_meshcomod_scan_count; k++) { + if (strcmp(s_meshcomod_scan_ssids[k], ssid.c_str()) == 0) { + dup = true; + break; + } + } + if (dup) continue; + StrHelper::strzcpy(s_meshcomod_scan_ssids[s_meshcomod_scan_count], ssid.c_str(), WIFI_CONFIG_SSID_MAX); + int ch = WiFi.channel(idx); + const char* band = (ch >= 1 && ch <= 14) ? "2.4GHz" : "5GHz"; + char line[96]; + snprintf(line, sizeof(line), "\n%d) %s [%s]", s_meshcomod_scan_count + 1, s_meshcomod_scan_ssids[s_meshcomod_scan_count], band); + scanMsg += line; + s_meshcomod_scan_count++; + } + // Fallback: if scan didn't surface any visible SSIDs, still show connected SSID. + if (s_meshcomod_scan_count == 0) { + String curr = WiFi.SSID(); + if (curr.length() > 0) { + StrHelper::strzcpy(s_meshcomod_scan_ssids[0], curr.c_str(), WIFI_CONFIG_SSID_MAX); + s_meshcomod_scan_count = 1; + int ch = WiFi.channel(); + const char* band = (ch >= 1 && ch <= 14) ? "2.4GHz" : "5GHz"; + char line[96]; + snprintf(line, sizeof(line), "\n1) %s [%s] (connected)", s_meshcomod_scan_ssids[0], band); + scanMsg += line; + } + } + if (s_meshcomod_scan_count == 0) { + pushMeshcomodReply("scan: no usable SSIDs"); + pushMeshcomodReply("tip: try wifi status or move closer to AP"); + } else { + scanMsg += "\nselect SSID: wifi use "; + scanMsg += "\nthen set password: wifi set pwd \"\" and wifi apply"; + pushMeshcomodReply(scanMsg.c_str()); + } + WiFi.scanDelete(); + return true; + } + if (strncasecmp(p, "use", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { + p += 3; + while (*p == ' ' || *p == '\t') p++; + int n = atoi(p); + if (s_meshcomod_scan_count <= 0) { + pushMeshcomodReply("error: no scan results; run wifi scan"); + return true; + } + if (n < 1 || n > s_meshcomod_scan_count) { + pushMeshcomodReply("error: invalid index"); + return true; + } + if (!wifiConfigSetSsid(s_meshcomod_scan_ssids[n - 1])) { + pushMeshcomodReply("error: ssid too long or invalid"); + return true; + } + char line[192]; + snprintf(line, sizeof(line), "OK ssid=\"%s\"\nnext: wifi set pwd \"\" (or \"\" for open)\nthen: wifi apply", s_meshcomod_scan_ssids[n - 1]); + pushMeshcomodReply(line); + return true; + } + if (strncasecmp(p, "status", 6) == 0 && (p[6] == '\0' || p[6] == ' ' || p[6] == '\t')) { + char ssid[WIFI_CONFIG_SSID_MAX]; + wifiConfigGetSsid(ssid, sizeof(ssid)); + bool has_runtime = wifiConfigHasRuntime(); + int re = wifiConfigGetRadioEnabled() ? 1 : 0; + char reply[112]; + if (!has_runtime || ssid[0] == '\0') { + snprintf(reply, sizeof(reply), "radio_enabled=%d ssid=(none) runtime=0", re); + } else { + int connected = (WiFi.status() == WL_CONNECTED) ? 1 : 0; + if (connected) { + IPAddress ip = WiFi.localIP(); + snprintf(reply, sizeof(reply), "radio_enabled=%d ssid=%s connected=1 ip=%d.%d.%d.%d", re, ssid, + ip[0], ip[1], ip[2], ip[3]); + } else { + snprintf(reply, sizeof(reply), "radio_enabled=%d ssid=%s connected=0", re, ssid); + } + } + pushMeshcomodReply(reply); + return true; + } + if (strncasecmp(p, "clear", 5) == 0 && (p[5] == '\0' || p[5] == ' ' || p[5] == '\t')) { + wifiConfigClear(); + pushMeshcomodReply("OK"); + return true; + } + if (strncasecmp(p, "apply", 5) == 0 && (p[5] == '\0' || p[5] == ' ' || p[5] == '\t')) { + if (!wifiConfigGetRadioEnabled()) { + pushMeshcomodReply("error: wifi radio off — send wifi on first"); + return true; + } + if (!wifiConfigHasRuntime()) { + pushMeshcomodReply("No runtime credentials; set ssid/pwd first"); + return true; + } + wifiConfigApply(); + pushMeshcomodReply("OK reconnecting"); + return true; + } +#endif +#endif + pushMeshcomodReply(kMeshcomodHelpMsg); + return true; +} + +void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len) { + int i = 0; + uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT + memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE); + i += PUB_KEY_SIZE; + contact.type = frame[i++]; + contact.flags = frame[i++]; + contact.out_path_len = frame[i++]; + memcpy(contact.out_path, &frame[i], MAX_PATH_SIZE); + i += MAX_PATH_SIZE; + memcpy(contact.name, &frame[i], 32); + i += 32; + memcpy(&contact.last_advert_timestamp, &frame[i], 4); + i += 4; + if (len >= i + 8) { // optional fields + memcpy(&contact.gps_lat, &frame[i], 4); + i += 4; + memcpy(&contact.gps_lon, &frame[i], 4); + i += 4; + if (len >= i + 4) { + memcpy(&last_mod, &frame[i], 4); + } + } +} + +bool MyMesh::Frame::isChannelMsg() const { + return buf[0] == RESP_CODE_CHANNEL_MSG_RECV || buf[0] == RESP_CODE_CHANNEL_MSG_RECV_V3; +} + +void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) { + if (offline_queue_len >= OFFLINE_QUEUE_SIZE) { + MESH_DEBUG_PRINTLN("WARN: offline_queue is full!"); + int pos = 0; + while (pos < offline_queue_len) { + if (offline_queue[pos].isChannelMsg()) { + for (int i = pos; i < offline_queue_len - 1; i++) { // delete oldest channel msg from queue + offline_queue[i] = offline_queue[i + 1]; + } + MESH_DEBUG_PRINTLN("INFO: removed oldest channel message from queue."); + offline_queue[offline_queue_len - 1].len = len; + memcpy(offline_queue[offline_queue_len - 1].buf, frame, len); + return; + } + pos++; + } + MESH_DEBUG_PRINTLN("INFO: no channel messages to remove from queue."); + } else { + offline_queue[offline_queue_len].len = len; + memcpy(offline_queue[offline_queue_len].buf, frame, len); + offline_queue_len++; + } +} + +int MyMesh::getFromOfflineQueue(uint8_t frame[]) { + if (offline_queue_len > 0) { // check offline queue + size_t len = offline_queue[0].len; // take from top of queue + memcpy(frame, offline_queue[0].buf, len); + + offline_queue_len--; + for (int i = 0; i < offline_queue_len; i++) { // delete top item from queue + offline_queue[i] = offline_queue[i + 1]; + } + return len; + } + return 0; // queue is empty +} + +uint32_t MyMesh::addToHistoryRing(const uint8_t frame[], int len) { + if (len <= 0 || len > MAX_FRAME_SIZE) return 0; + HistoryEntry& e = history_ring[history_head]; + e.len = (uint8_t)len; + memcpy(e.buf, frame, len); + uint32_t assigned = history_next_seq; + e.seq = history_next_seq++; + history_head = (history_head + 1) % HISTORY_RING_SIZE; + if (history_count < HISTORY_RING_SIZE) { + history_count++; + } + return assigned; +} + +static bool clientIdEqual(const char* a, const char* b) { + if (!a) a = ""; + if (!b) b = ""; + return strcmp(a, b) == 0; +} + +static bool containsIgnoreCaseAscii(const char* haystack, const char* needle) { + if (!haystack || !needle || !needle[0]) return false; + for (int i = 0; haystack[i]; i++) { + int j = 0; + while (needle[j]) { + char hc = haystack[i + j]; + if (!hc) return false; + if (hc >= 'A' && hc <= 'Z') hc = (char)(hc - 'A' + 'a'); + char nc = needle[j]; + if (nc >= 'A' && nc <= 'Z') nc = (char)(nc - 'A' + 'a'); + if (hc != nc) break; + j++; + } + if (!needle[j]) return true; + } + return false; +} + +static bool appPrefersLiveAdvance(const char* app_name) { + if (!app_name || !app_name[0]) return false; + // meshcomod/web clients explicitly identify as mccli / meshcomod-* + return containsIgnoreCaseAscii(app_name, "mccli") || + containsIgnoreCaseAscii(app_name, "meshcomod"); +} + +// Extract Unix timestamp from a message frame for SyncSince filtering. Returns 0 if not V3 or too short. +static uint32_t getMessageTimestampFromFrame(const uint8_t* buf, int len) { + if (!buf || len < 11) return 0; + uint8_t code = buf[0]; + if (code == RESP_CODE_CONTACT_MSG_RECV_V3 && len >= 16) { + uint32_t t; + memcpy(&t, &buf[12], 4); + return t; + } + if (code == RESP_CODE_CHANNEL_MSG_RECV_V3 && len >= 11) { + uint32_t t; + memcpy(&t, &buf[7], 4); + return t; + } + return 0; +} + +// Delivers history to client for sync. Includes channel messages (0x08, 0x11); clients without channel support should skip those frame types. +int MyMesh::getNextFromHistoryForClient(const char* client_id, uint8_t frame[], uint32_t* out_seq, bool do_advance) { + if (history_count <= 0) return 0; + const char* cid = (client_id && client_id[0]) ? client_id : ""; + + // Find or create client state + int slot = -1; + for (int i = 0; i < history_num_clients; i++) { + if (clientIdEqual(history_clients[i].client_id, cid)) { + slot = i; + break; + } + } + if (slot < 0) { + if (history_num_clients >= MAX_HISTORY_CLIENTS) return 0; + slot = history_num_clients++; + strncpy(history_clients[slot].client_id, cid, MAX_CLIENT_ID_LEN); + history_clients[slot].client_id[MAX_CLIENT_ID_LEN] = '\0'; + history_clients[slot].last_delivered_seq = history_next_seq > (uint32_t)history_count + ? history_next_seq - (uint32_t)history_count : 0; + } + + uint32_t last = history_clients[slot].last_delivered_seq; + int tail = (history_head - history_count + HISTORY_RING_SIZE) % HISTORY_RING_SIZE; + for (int i = 0; i < history_count; i++) { + int idx = (tail + i) % HISTORY_RING_SIZE; + const HistoryEntry& e = history_ring[idx]; + if (e.seq > last) { + // Sync stream should return only chat/channel message frames. + // Other push types (e.g. RX log 0x88) are delivered live via push and can confuse client sync parsers. + uint8_t code = e.buf[0]; + bool is_sync_message = + code == RESP_CODE_CONTACT_MSG_RECV || + code == RESP_CODE_CONTACT_MSG_RECV_V3 || + code == RESP_CODE_CHANNEL_MSG_RECV || + code == RESP_CODE_CHANNEL_MSG_RECV_V3; + if (!is_sync_message) { + if (do_advance) history_clients[slot].last_delivered_seq = e.seq; + last = e.seq; + continue; + } + memcpy(frame, e.buf, e.len); + if (out_seq) *out_seq = e.seq; + if (do_advance) history_clients[slot].last_delivered_seq = e.seq; + return e.len; + } + } + return 0; +} + +void MyMesh::commitHistoryForClient(const char* client_id, uint32_t seq) { + const char* cid = (client_id && client_id[0]) ? client_id : ""; + for (int i = 0; i < history_num_clients; i++) { + if (clientIdEqual(history_clients[i].client_id, cid)) { + if (seq > history_clients[i].last_delivered_seq) + history_clients[i].last_delivered_seq = seq; + return; + } + } +} + +void MyMesh::advanceHistoryClientsAfterV3Broadcast(uint32_t seq) { + for (int i = 0; i < history_num_clients; i++) { + if (!shouldAdvanceClientAfterV3Broadcast(history_clients[i].client_id)) + continue; + if (seq > history_clients[i].last_delivered_seq) + history_clients[i].last_delivered_seq = seq; + } +} + +void MyMesh::setClientTargetVer(const char* client_id, uint8_t target_ver) { + const char* cid = (client_id && client_id[0]) ? client_id : ""; + for (int i = 0; i < proto_num_clients; i++) { + if (clientIdEqual(proto_clients[i].client_id, cid)) { + proto_clients[i].target_ver = target_ver; + return; + } + } + if (proto_num_clients < MAX_HISTORY_CLIENTS) { + int slot = proto_num_clients++; + strncpy(proto_clients[slot].client_id, cid, MAX_CLIENT_ID_LEN); + proto_clients[slot].client_id[MAX_CLIENT_ID_LEN] = '\0'; + proto_clients[slot].target_ver = target_ver; + proto_clients[slot].prefer_live_advance = false; + } +} + +uint8_t MyMesh::getClientTargetVer(const char* client_id) const { + const char* cid = (client_id && client_id[0]) ? client_id : ""; + for (int i = 0; i < proto_num_clients; i++) { + if (clientIdEqual(proto_clients[i].client_id, cid)) { + return proto_clients[i].target_ver; + } + } + // Unknown clients default to modern format. + return 0xFF; +} + +void MyMesh::setClientAppName(const char* client_id, const char* app_name) { + const char* cid = (client_id && client_id[0]) ? client_id : ""; + bool prefer_live_advance = appPrefersLiveAdvance(app_name); + for (int i = 0; i < proto_num_clients; i++) { + if (clientIdEqual(proto_clients[i].client_id, cid)) { + proto_clients[i].prefer_live_advance = prefer_live_advance; + return; + } + } + if (proto_num_clients < MAX_HISTORY_CLIENTS) { + int slot = proto_num_clients++; + strncpy(proto_clients[slot].client_id, cid, MAX_CLIENT_ID_LEN); + proto_clients[slot].client_id[MAX_CLIENT_ID_LEN] = '\0'; + proto_clients[slot].target_ver = 0xFF; + proto_clients[slot].prefer_live_advance = prefer_live_advance; + } +} + +bool MyMesh::shouldAdvanceClientAfterV3Broadcast(const char* client_id) const { + const char* cid = (client_id && client_id[0]) ? client_id : ""; + for (int i = 0; i < proto_num_clients; i++) { + if (clientIdEqual(proto_clients[i].client_id, cid)) { + uint8_t tv = proto_clients[i].target_ver; + if (tv < 3 && tv != 0xFF) return false; // legacy sync-adapt clients must keep replay + return proto_clients[i].prefer_live_advance; + } + } + // Unknown client app: conservative default for stock compatibility. + return false; +} + +int MyMesh::adaptHistoryFrameForClient(const char* client_id, const uint8_t src[], int src_len, uint8_t dest[]) const { + if (!src || !dest || src_len <= 0 || src_len > MAX_FRAME_SIZE) return 0; + uint8_t target_ver = getClientTargetVer(client_id); + if (target_ver >= 3 || target_ver == 0xFF) { + memcpy(dest, src, src_len); + return src_len; + } + if (src[0] == RESP_CODE_CONTACT_MSG_RECV_V3 && src_len >= 4) { + dest[0] = RESP_CODE_CONTACT_MSG_RECV; + memcpy(&dest[1], &src[4], src_len - 4); + return src_len - 3; + } + if (src[0] == RESP_CODE_CHANNEL_MSG_RECV_V3 && src_len >= 4) { + dest[0] = RESP_CODE_CHANNEL_MSG_RECV; + memcpy(&dest[1], &src[4], src_len - 4); + return src_len - 3; + } + memcpy(dest, src, src_len); + return src_len; +} + +// SyncSince (CMD 62): send all message frames (7/8/16/17) with timestamp >= T from history ring, then SyncSinceDone (61). +// Client must use command 62 (not 60; 60 is CMD_GET_ALLOWED_REPEAT_FREQ). Response 61 = SyncSinceDone; client sets last-sync to now. +void MyMesh::sendSyncSinceDelta(uint32_t T) { + char client_id[MAX_CLIENT_ID_LEN + 1]; + _serial->getCurrentClientId(client_id, sizeof(client_id)); + + uint8_t send_buf[MAX_FRAME_SIZE]; + int tail = (history_head - history_count + HISTORY_RING_SIZE) % HISTORY_RING_SIZE; + + for (int i = 0; i < history_count; i++) { + int idx = (tail + i) % HISTORY_RING_SIZE; + const HistoryEntry& e = history_ring[idx]; + uint8_t code = e.buf[0]; + + if (code != RESP_CODE_CONTACT_MSG_RECV && code != RESP_CODE_CONTACT_MSG_RECV_V3 && + code != RESP_CODE_CHANNEL_MSG_RECV && code != RESP_CODE_CHANNEL_MSG_RECV_V3) + continue; + + bool include = false; + if (code == RESP_CODE_CONTACT_MSG_RECV_V3 || code == RESP_CODE_CHANNEL_MSG_RECV_V3) { + uint32_t ts = getMessageTimestampFromFrame(e.buf, e.len); + include = (ts >= T); + } else { + include = (T == 0); + } + if (!include) continue; + + const uint8_t* ptr = e.buf; + int len = e.len; + int adapted = adaptHistoryFrameForClient(client_id, e.buf, e.len, send_buf); + if (adapted > 0) { + ptr = send_buf; + len = adapted; + } + _serial->writeFrame(ptr, len); + } + + out_frame[0] = RESP_CODE_SYNC_SINCE_DONE; + _serial->writeFrame(out_frame, 1); +} + +float MyMesh::getAirtimeBudgetFactor() const { + return _prefs.airtime_factor; +} + +int MyMesh::getInterferenceThreshold() const { + return 0; // disabled for now, until currentRSSI() problem is resolved +} + +int MyMesh::calcRxDelay(float score, uint32_t air_time) const { + if (_prefs.rx_delay_base <= 0.0f) return 0; + return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); +} + +uint8_t MyMesh::getExtraAckTransmitCount() const { + return _prefs.multi_acks; +} + +uint8_t MyMesh::getAutoAddMaxHops() const { + return _prefs.autoadd_max_hops; +} + +// Lightweight payload fingerprint (FNV-1a 32) for matching our own sent flood +// against its echoes heard back from repeaters (see logRxRaw / uiTrackSentFp). +// The payload is unchanged as repeaters re-flood (only the path grows), so the +// same bytes fingerprint identically at send and on every echo. +static uint32_t fnv1a32(const uint8_t* d, int n) { + uint32_t h = 2166136261u; + for (int i = 0; i < n; i++) { h ^= d[i]; h *= 16777619u; } + return h ? h : 1; // never return 0 (our "none" sentinel) +} +// Flood message types we track for "repeats heard": DM text (TXT_MSG 0x02) AND +// group/channel text (GRP_TXT 0x05). Channel posts are NOT TXT_MSG — easy to +// miss, and the reason channel sends fingerprinted to 0 at first. +static inline bool isMsgFloodType(uint8_t t) { + return t == PAYLOAD_TYPE_TXT_MSG || t == PAYLOAD_TYPE_GRP_TXT; +} + +void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { +#if defined(DISPLAY_CLASS) + // Diagnostic: log EVERY received frame so we can prove what reaches the + // radio. Header byte layout is + // [hasXportCodes:1][reserved:1][payload_type:4][route_type:2] + // so payload_type = (raw[0]>>2)&0x0F and route_type = raw[0]&0x03. + if (_ui && len > 0) { + const uint8_t ptype = (raw[0] >> 2) & 0x0F; + const uint8_t route = raw[0] & 0x03; + const char* tname = "???"; + switch (ptype) { + case 0x00: tname = "REQ"; break; + case 0x01: tname = "RSP"; break; + case 0x02: tname = "TXT"; break; + case 0x03: tname = "ACK"; break; + case 0x04: tname = "ADV"; break; + case 0x05: tname = "GTX"; break; + case 0x06: tname = "GDT"; break; + case 0x07: tname = "ANR"; break; + case 0x08: tname = "PTH"; break; + case 0x09: tname = "TRC"; break; + case 0x0A: tname = "MUL"; break; + case 0x0B: tname = "CTL"; break; + case 0x0F: tname = "RAW"; break; + } + // Show dest_hash byte (first payload byte after the path) so we can + // see whether the packet is addressed to us. self_id.pub_key[0] is + // what isHashMatch compares against — if dest doesn't equal that, + // the dispatcher silently drops the packet at the "is this for us?" + // gate without ever calling onPeerDataRecv. + const uint8_t self_b0 = self_id.pub_key[0]; + uint8_t dest = 0xFF; + // Header is byte 0; transport codes (if present) are 1-4; path_len is + // next; then path bytes; then payload. We just want the first byte of + // the payload as dest_hash for TXT/RSP/ACK style packets. + const bool has_xport = (raw[0] & 0x80) != 0; + int payload_start = 1 + (has_xport ? 4 : 0); + if (payload_start < len) { + uint8_t path_byte = raw[payload_start]; + uint8_t path_count = path_byte & 0x3F; + uint8_t hash_size = ((path_byte >> 6) & 0x03) + 1; + int payload_off = payload_start + 1 + path_count * hash_size; + if (payload_off < len) dest = raw[payload_off]; + } + char dbg[80]; + // ADV/GTX/GDT/ACK don't have a dest_hash — show "--" instead so the + // operator isn't tricked into reading the first ack_crc byte as if + // it were addressing. + bool has_dest_hash = (ptype == 0x00 || ptype == 0x01 || ptype == 0x02 || + ptype == 0x07 || ptype == 0x08); + if (has_dest_hash) { + snprintf(dbg, sizeof(dbg), "RX %s r=%u dst=%02x me=%02x L=%d s=%d", + tname, (unsigned)route, (unsigned)dest, (unsigned)self_b0, + len, (int)rssi); + } else { + snprintf(dbg, sizeof(dbg), "RX %s r=%u me=%02x L=%d s=%d", + tname, (unsigned)route, (unsigned)self_b0, + len, (int)rssi); + } + _ui->appendDiag(dbg); + } + // "Repeats heard": our originated flood TXT comes back when repeaters + // re-broadcast it (identical payload, longer path). Fingerprint the payload + // and bump the matching sent message's count. The fp ring only holds our own + // recent sends, so other nodes' traffic can't false-match. + if (len > 0 && isMsgFloodType((raw[0] >> 2) & 0x0F)) { + const bool has_xp = (raw[0] & 0x80) != 0; + const int ps = 1 + (has_xp ? 4 : 0); + if (ps < len) { + const uint8_t pb = raw[ps]; + const int poff = ps + 1 + (pb & 0x3F) * (((pb >> 6) & 0x03) + 1); + if (poff < len) uiCountEcho(fnv1a32(raw + poff, len - poff)); + } + } +#endif + if (len + 3 <= MAX_FRAME_SIZE) { + int i = 0; + out_frame[i++] = PUSH_CODE_LOG_RX_DATA; + out_frame[i++] = (int8_t)(snr * 4); + out_frame[i++] = (int8_t)(rssi); + memcpy(&out_frame[i], raw, len); + i += len; + + // Do not add RX log to the sync history ring — it would evict chat/channel messages + // (e.g. overnight traffic causes late-connecting BLE client to miss messages). Push live only. + if (_serial->isConnected()) { + _serial->writeFrameToAll(out_frame, i); + } + } +} + +void MyMesh::uiExportBackup(Print& out, double node_lat, double node_lon) { + static const char* HX = "0123456789abcdef"; + auto hex = [&](const uint8_t* d, int n) { + for (int i = 0; i < n; ++i) { out.write(HX[d[i] >> 4]); out.write(HX[d[i] & 0xF]); } + }; + auto esc = [&](const char* s) { + for (; s && *s; ++s) { + char c = *s; + if (c == '"' || c == '\\') { out.write('\\'); out.write((uint8_t)c); } + else if (c == '\n') out.print("\\n"); + else if (c == '\r') out.print("\\r"); + else if (c == '\t') out.print("\\t"); + else if ((uint8_t)c < 0x20) { char b[8]; snprintf(b, sizeof b, "\\u%04x", (unsigned)(uint8_t)c); out.print(b); } + else out.write((uint8_t)c); + } + }; + NodePrefs* p = getNodePrefs(); + uint8_t prv[64]; self_id.writeTo(prv, 64); + + out.print("{\n \"name\": \""); esc(p ? p->node_name : ""); out.print("\",\n"); + out.print(" \"public_key\": \""); hex(self_id.pub_key, 32); out.print("\",\n"); + out.print(" \"private_key\": \""); hex(prv, 64); out.print("\",\n"); + if (p) { + char l[176]; + snprintf(l, sizeof l, + " \"radio_settings\": {\"frequency\": %.4f, \"bandwidth\": %.1f, \"spreading_factor\": %u, \"coding_rate\": %u, \"tx_power\": %d},\n", + (double)p->freq, (double)p->bw, (unsigned)p->sf, (unsigned)p->cr, (int)p->tx_power_dbm); + out.print(l); + } + { + char l[96]; + snprintf(l, sizeof l, + " \"position_settings\": {\"latitude\": %.6f, \"longitude\": %.6f},\n", + node_lat, node_lon); + out.print(l); + } + // Match the stock app's shape exactly (it always emits these two as null). + out.print(" \"other_settings\": null,\n"); + out.print(" \"auto_add_settings\": null,\n"); + out.print(" \"channels\": ["); + bool first = true; +#ifdef MAX_GROUP_CHANNELS + for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) { + ChannelDetails cd{}; + if (!getChannel(i, cd) || cd.name[0] == '\0') continue; + out.print(first ? "\n {\"name\": \"" : ",\n {\"name\": \""); + first = false; + esc(cd.name); + out.print("\", \"secret\": \""); hex(cd.channel.secret, 16); out.print("\"}"); + } +#endif + out.print(first ? "],\n" : "\n ],\n"); + out.print(" \"contacts\": ["); + first = true; + uint32_t nc = getNumContacts(); + for (uint32_t i = 0; i < nc; ++i) { + ContactInfo c; + if (!getContactByIdx(i, c)) continue; + char l[224]; + out.print(first ? "\n {\"type\": " : ",\n {\"type\": "); + first = false; + snprintf(l, sizeof l, "%u, \"name\": \"", (unsigned)c.type); out.print(l); + esc(c.name); + out.print("\", \"custom_name\": null, \"public_key\": \""); hex(c.id.pub_key, 32); + snprintf(l, sizeof l, + "\", \"flags\": %u, \"latitude\": \"%.6f\", \"longitude\": \"%.6f\", \"last_advert\": %lu, \"last_modified\": %lu, \"out_path\": null}", + (unsigned)c.flags, (double)c.gps_lat / 1e6, (double)c.gps_lon / 1e6, + (unsigned long)c.last_advert_timestamp, (unsigned long)c.lastmod); + out.print(l); + } + out.print(first ? "]\n}\n" : "\n ]\n}\n"); +} + +static uint8_t mc_hexNib(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return 0; +} +static int mc_hexToBytes(const char* hex, uint8_t* out, int max_bytes) { + int n = 0; + while (hex && hex[0] && hex[1] && n < max_bytes) { out[n++] = (mc_hexNib(hex[0]) << 4) | mc_hexNib(hex[1]); hex += 2; } + return n; +} + +bool MyMesh::uiImportBackup(Stream& in, uint8_t sections, + bool replace_channels, bool replace_contacts, + int* out_channels, int* out_contacts) { + (void)replace_channels; + if (out_channels) *out_channels = 0; + if (out_contacts) *out_contacts = 0; + // PSRAM-backed doc so a big backup (60 KB+) doesn't exhaust internal RAM. + struct PsAlloc : ArduinoJson::Allocator { + void* allocate(size_t n) override { void* p = heap_caps_malloc(n, MALLOC_CAP_SPIRAM); return p ? p : malloc(n); } + void deallocate(void* p) override { heap_caps_free(p); } + void* reallocate(void* p, size_t n) override { void* q = heap_caps_realloc(p, n, MALLOC_CAP_SPIRAM); return q ? q : realloc(p, n); } + } alloc; + JsonDocument doc(&alloc); + if (deserializeJson(doc, in)) return false; + JsonObjectConst root = doc.as(); + if (root.isNull()) return false; + + bool prefs_dirty = false; + if (sections & 0x01) { // identity: name + private key + const char* nm = root["name"].as(); + if (nm && nm[0]) { strncpy(_prefs.node_name, nm, sizeof(_prefs.node_name) - 1); _prefs.node_name[sizeof(_prefs.node_name) - 1] = 0; prefs_dirty = true; } + const char* pk = root["private_key"].as(); + if (pk && strlen(pk) >= 128) { uint8_t prv[64]; mc_hexToBytes(pk, prv, 64); uiImportPrivKey(prv); } + } + if (sections & 0x02) { // radio + JsonObjectConst r = root["radio_settings"].as(); + if (!r.isNull()) { + if (!r["frequency"].isNull()) { _prefs.freq = r["frequency"].as(); prefs_dirty = true; } + if (!r["bandwidth"].isNull()) { _prefs.bw = r["bandwidth"].as(); prefs_dirty = true; } + if (!r["spreading_factor"].isNull()) { _prefs.sf = (uint8_t)r["spreading_factor"].as(); prefs_dirty = true; } + if (!r["coding_rate"].isNull()) { _prefs.cr = (uint8_t)r["coding_rate"].as(); prefs_dirty = true; } + if (!r["tx_power"].isNull()) { _prefs.tx_power_dbm = (int8_t)r["tx_power"].as(); prefs_dirty = true; } + } + } + if (sections & 0x04) { // position + JsonObjectConst ps = root["position_settings"].as(); + if (!ps.isNull()) { + if (!ps["latitude"].isNull()) { sensors.node_lat = ps["latitude"].as(); prefs_dirty = true; } + if (!ps["longitude"].isNull()) { sensors.node_lon = ps["longitude"].as(); prefs_dirty = true; } + } + } + if (prefs_dirty) savePrefs(); + + int nch = 0; + if (sections & 0x08) { // channels (overwrite from slot 0) + JsonArrayConst ch = root["channels"].as(); + if (!ch.isNull()) { + int idx = 0; + for (JsonVariantConst ev : ch) { + JsonObjectConst e = ev.as(); + const char* nm = e["name"].as(); + const char* sec = e["secret"].as(); + if (!nm || !sec || strlen(sec) < 32) { idx++; continue; } +#ifdef MAX_GROUP_CHANNELS + if (idx >= MAX_GROUP_CHANNELS) break; +#endif + ChannelDetails cd{}; + strncpy(cd.name, nm, sizeof(cd.name) - 1); + mc_hexToBytes(sec, cd.channel.secret, 16); + if (setChannel((uint8_t)idx, cd)) nch++; + idx++; + } + saveChannels(); + } + } + int nco = 0; + if (sections & 0x10) { // contacts + JsonArrayConst co = root["contacts"].as(); + if (!co.isNull()) { + if (replace_contacts) resetContacts(); + for (JsonVariantConst ev : co) { + JsonObjectConst e = ev.as(); + const char* pk = e["public_key"].as(); + if (!pk || strlen(pk) < 64) continue; + uint8_t pub[32]; mc_hexToBytes(pk, pub, 32); + const char* cn = e["custom_name"].as(); + const char* nm = (cn && cn[0]) ? cn : e["name"].as(); + double lat = 0, lon = 0; + const char* la = e["latitude"].as(); if (la) lat = atof(la); + const char* lo = e["longitude"].as(); if (lo) lon = atof(lo); + if (uiAddContactFromBackup(pub, nm, e["type"].as(), e["flags"].as(), + (int32_t)(lat * 1e6), (int32_t)(lon * 1e6), + e["last_advert"].as(), e["last_modified"].as())) nco++; + } + saveContacts(); + } + } + if (out_channels) *out_channels = nch; + if (out_contacts) *out_contacts = nco; + return true; +} + +bool MyMesh::isAutoAddEnabled() const { + return (_prefs.manual_add_contacts & 1) == 0; +} + +bool MyMesh::shouldAutoAddContactType(uint8_t contact_type) const { + // Manual-add OFF: auto-add every type. + if ((_prefs.manual_add_contacts & 1) == 0) { + return true; + } + + // Manual-add ON: honor each type's autoadd_config bit. Chat/person peers are + // included now — they used to be force-added here unconditionally, which made + // the UI's "auto-add chats" toggle a no-op and surprised users who'd turned + // auto-add off (person adverts still landed straight in Contacts). With the + // chat bit off they now go to the Discovered/"Found" list to be added by hand. + // + // Trade-off: decoding a sender's DM needs their pub key, which only arrives in + // their advert — so a brand-new sender's *first* DM can't be decoded until + // they're added. Add them from Found first, or turn the "auto-add chats" + // toggle back on if you want strangers' messages to land automatically. + // (A future enhancement can auto-add a sender the moment a DM from them + // actually decodes, keeping Found clean while not dropping cold DMs.) + uint8_t type_bit = 0; + switch (contact_type) { + case ADV_TYPE_CHAT: + type_bit = AUTO_ADD_CHAT; + break; + case ADV_TYPE_REPEATER: + type_bit = AUTO_ADD_REPEATER; + break; + case ADV_TYPE_ROOM: + type_bit = AUTO_ADD_ROOM_SERVER; + break; + case ADV_TYPE_SENSOR: + type_bit = AUTO_ADD_SENSOR; + break; + default: + return false; // Unknown type, don't auto-add + } + + return (_prefs.autoadd_config & type_bit) != 0; +} + +bool MyMesh::shouldOverwriteWhenFull() const { + return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0; +} + +void MyMesh::onContactOverwrite(const uint8_t* pub_key) { + _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage + if (_serial->isConnected()) { + out_frame[0] = PUSH_CODE_CONTACT_DELETED; + memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE); + _serial->writeFrameToAll(out_frame, 1 + PUB_KEY_SIZE); + } +} + +void MyMesh::onContactsFull() { + if (_serial->isConnected()) { + out_frame[0] = PUSH_CODE_CONTACTS_FULL; + _serial->writeFrameToAll(out_frame, 1); + } +} + +void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (_serial->isConnected()) { + if (is_new) { + writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact, true); + } else { + out_frame[0] = PUSH_CODE_ADVERT; + memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); + _serial->writeFrameToAll(out_frame, 1 + PUB_KEY_SIZE); + } + } else { +#ifdef DISPLAY_CLASS + if (_ui) _ui->notify(UIEventType::newContactMessage); +#endif + } +#ifdef DISPLAY_CLASS + // Mirror to the touch UI's Discovered store so the user can browse pending + // adverts and manually add nodes to contacts[] (used when auto-add is off + // or contacts[] is full). `is_new=true` here means the contact is NOT yet + // in contacts[]; `is_new=false` means it's a refresh of an existing one. + if (_ui) _ui->discoveredContact(contact, is_new, path_len); +#endif + + // add inbound-path to mem cache + if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid + AdvertPath* p = advert_paths; + uint32_t oldest = 0xFFFFFFFF; + for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest + if (memcmp(advert_paths[i].pubkey_prefix, contact.id.pub_key, sizeof(AdvertPath::pubkey_prefix)) == 0) { + p = &advert_paths[i]; // found + break; + } + if (advert_paths[i].recv_timestamp < oldest) { + oldest = advert_paths[i].recv_timestamp; + p = &advert_paths[i]; + } + } + + memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix)); + strcpy(p->name, contact.name); + p->recv_timestamp = getRTCClock()->getCurrentTime(); + p->path_len = path_len; + memcpy(p->path, path, p->path_len); + } + + if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[] +} + +static int sort_by_recent(const void *a, const void *b) { + return ((AdvertPath *) b)->recv_timestamp - ((AdvertPath *) a)->recv_timestamp; +} + +int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { + if (max_num > ADVERT_PATH_TABLE_SIZE) max_num = ADVERT_PATH_TABLE_SIZE; + qsort(advert_paths, ADVERT_PATH_TABLE_SIZE, sizeof(advert_paths[0]), sort_by_recent); + + for (int i = 0; i < max_num; i++) { + dest[i] = advert_paths[i]; + } + return max_num; +} + +void MyMesh::onContactPathUpdated(const ContactInfo &contact) { + out_frame[0] = PUSH_CODE_PATH_UPDATED; + memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); + _serial->writeFrameToAll(out_frame, 1 + PUB_KEY_SIZE); // NOTE: app may not be connected + + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); +} + +ContactInfo* MyMesh::processAck(const uint8_t *data) { +#if defined(DISPLAY_CLASS) + // Diag: log every processAck call so we can see whether the ACK matching + // pipeline gets reached after the radio surface dispatches an ACK frame. + if (_ui) { + uint32_t in_ack = 0; + memcpy(&in_ack, data, 4); + char dbg[64]; + snprintf(dbg, sizeof(dbg), "procACK %08lx", (unsigned long)in_ack); + _ui->appendDiag(dbg); + } +#endif + // see if matches any in a table + for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { + if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient + out_frame[0] = PUSH_CODE_SEND_CONFIRMED; + memcpy(&out_frame[1], data, 4); + uint32_t trip_time = _ms->getMillis() - expected_ack_table[i].msg_sent; + memcpy(&out_frame[5], &trip_time, 4); + _serial->writeFrameToAll(out_frame, 9); + +#ifdef DISPLAY_CLASS + // Tell the touch UI so the outgoing bubble flips to DELIVERED. + if (_ui) { + uint32_t ack4 = 0; + memcpy(&ack4, data, 4); + _ui->onMessageAcked(ack4); + } +#endif + + // NOTE: the same ACK can be received multiple times! + expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK + return expected_ack_table[i].contact; + } + } + return checkConnectionsAck(data); +} + +void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, + uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text) { + int i = 0; + out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV_V3; + out_frame[i++] = (int8_t)(pkt->getSNR() * 4); + out_frame[i++] = 0; // reserved1 + out_frame[i++] = 0; // reserved2 + memcpy(&out_frame[i], from.id.pub_key, 6); + i += 6; // just 6-byte prefix + uint8_t path_len = out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; + out_frame[i++] = txt_type; + memcpy(&out_frame[i], &sender_timestamp, 4); + i += 4; + if (extra_len > 0) { + memcpy(&out_frame[i], extra, extra_len); + i += extra_len; + } + int tlen = strlen(text); // TODO: UTF-8 ?? + if (i + tlen > MAX_FRAME_SIZE) { + tlen = MAX_FRAME_SIZE - i; + } + memcpy(&out_frame[i], text, tlen); + i += tlen; + uint32_t hist_seq = addToHistoryRing(out_frame, i); + + if (_serial->isConnected()) { + if (_serial->writeFrameToAll(out_frame, i) == (size_t)i && hist_seq != 0 && + _serial->companionUnsolicitedPushesBroadcastToAll()) { + advanceHistoryClientsAfterV3Broadcast(hist_seq); + } + uint8_t frame[1]; + frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' + _serial->writeFrameToAll(frame, 1); + } + +#ifdef DISPLAY_CLASS + // we only want to show text messages on display, not cli data + bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; + if (should_display && _ui) { + /* notify BEFORE newMsgFromPub: UITask::newMsg keys on g_last_event to + * decide channel-thread vs DM-thread. Previously this fired only when + * serial was disconnected, which meant after a channel message arrived + * over TCP/BLE g_last_event stayed at `channelMessage` and the next DM + * was routed into the channel thread (or vice versa). */ + _ui->notify(UIEventType::contactMessage); + // Pass RX metadata so the touch UI can surface it via the bubble's + // long-press Info sheet. SNR comes off the packet itself (most accurate + // per-message); RSSI is the radio's last-RSSI, which is current since + // the packet handler runs inline with reception. + const int8_t snr_q4 = (int8_t)(pkt->getSNR() * 4); + const int8_t rssi = (int8_t)(_radio->getLastRSSI()); + const bool is_flood = pkt->isRouteFlood(); + uiStashRxMeta(pkt); // capture route + scope for the per-message Info popup + _ui->newMsgFromPubWithMeta(path_len, is_flood, from.id.pub_key, from.name, + text, history_count, snr_q4, rssi); + } + // CLI command replies don't belong in the chat thread but the touch UI + // *does* want them — they're the response to whatever was typed into the + // admin console. Surfaces via a dedicated hook so the console can append + // the line without polluting the chat history. + if (txt_type == TXT_TYPE_CLI_DATA && _ui) { + _ui->onAdminCommandReply(from, text); + } +#endif +} + +bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { + // REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses + // if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender + return false; +} + +bool MyMesh::allowPacketForward(const mesh::Packet* packet) { + return _prefs.client_repeat != 0; +} + +// Fingerprint + track an originated flood, but only for chat messages — DM text +// and group/channel text (the things the UI shows "repeats heard" for); +// adverts/acks/traces are ignored. fnv1a32/isMsgFloodType are defined above. +static inline uint32_t txtFloodFp(mesh::Packet* pkt) { + if (!pkt || !isMsgFloodType(pkt->getPayloadType())) return 0; + return fnv1a32(pkt->payload, pkt->payload_len); +} + +void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) { + uiTrackSentFp(txtFloodFp(pkt)); + uint8_t phs = (uint8_t)(_prefs.path_hash_mode + 1); + if (scope.isNull()) { + sendFlood(pkt, delay_millis, phs); + } else { + uint16_t codes[2]; + codes[0] = scope.calcTransportCode(pkt); + codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? + sendFlood(pkt, codes, delay_millis, phs); + } +} + +void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { + uiTrackSentFp(txtFloodFp(pkt)); + // UNICAST floods (login, DM when out_path is unknown, status/telemetry reqs, + // acks, path-returns) must be able to reach their specific target regardless of + // our 'home' Region, so they are NOT tagged with the default region scope — + // doing so region-locked logins/DMs and broke cross-region repeater/room login + // (the old "TODO: dynamic send_scope depending on recipient/Region"). Only an + // EXPLICIT per-send override (send_scope, set by the app's CMD_SET_FLOOD_SCOPE) + // is honoured here; otherwise unscoped. Channel/group floods keep default_scope + // — see the GroupChannel overload below — so public-channel containment is intact. + if (send_unscoped || send_scope.isNull()) { + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + } else { + sendFloodScoped(send_scope, pkt, delay_millis); // explicit override only + } +} +void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { + uiTrackSentFp(txtFloodFp(pkt)); + // TODO: have per-channel send_scope + if (send_unscoped) { + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped + } else { + TransportKey default_scope; + memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key)); + + auto scope = send_scope.isNull() ? &default_scope : &send_scope; + sendFloodScoped(*scope, pkt, delay_millis); // the lower overload applies path_hash_mode + } +} + +void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text) { + markConnectionActive(from); // in case this is from a server, and we have a connection + queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); +} + +void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text) { + markConnectionActive(from); // in case this is from a server, and we have a connection + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); +} + +void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const uint8_t *sender_prefix, const char *text) { + markConnectionActive(from); + // from.sync_since change needs to be persisted + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text); +} + +void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, + const char *text) { + int i = 0; + out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3; + out_frame[i++] = (int8_t)(pkt->getSNR() * 4); + out_frame[i++] = 0; // reserved1 + out_frame[i++] = 0; // reserved2 + + int channel_idx_i = findChannelIdx(channel); + if (channel_idx_i < 0 || channel_idx_i >= MAX_GROUP_CHANNELS) { + // Keep on-wire channel index stable for clients that assume 0..MAX_GROUP_CHANNELS-1. + channel_idx_i = 0; + } + uint8_t channel_idx = (uint8_t)channel_idx_i; + out_frame[i++] = channel_idx; + uint8_t path_len = out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; + + out_frame[i++] = TXT_TYPE_PLAIN; + memcpy(&out_frame[i], ×tamp, 4); + i += 4; + int tlen = strlen(text); // TODO: UTF-8 ?? + if (i + tlen > MAX_FRAME_SIZE) { + tlen = MAX_FRAME_SIZE - i; + } + memcpy(&out_frame[i], text, tlen); + i += tlen; + uint32_t hist_seq = addToHistoryRing(out_frame, i); + if (_serial->isConnected()) { + if (_serial->writeFrameToAll(out_frame, i) == (size_t)i && hist_seq != 0 && + _serial->companionUnsolicitedPushesBroadcastToAll()) { + advanceHistoryClientsAfterV3Broadcast(hist_seq); + } + uint8_t frame[1]; + frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' + _serial->writeFrameToAll(frame, 1); + } +#ifdef DISPLAY_CLASS + // Get the channel name from the channel index + const char *channel_name = "Unknown"; + ChannelDetails channel_details; + if (getChannel(channel_idx, channel_details)) { + channel_name = channel_details.name; + } + /* notify BEFORE newMsgFromPub: UITask::newMsg keys on the last UIEventType + * to decide whether the message lands in a channel thread or a DM thread. + * Used to only fire when the serial client was disconnected, which meant + * channel messages got appended as DMs whenever TCP/BLE was up. */ + if (_ui) _ui->notify(UIEventType::channelMessage); + if (_ui) { + const int8_t snr_q4 = (int8_t)(pkt->getSNR() * 4); + const int8_t rssi = (int8_t)(_radio->getLastRSSI()); + const bool is_flood = pkt->isRouteFlood(); + uiStashRxMeta(pkt); // capture route + scope for the per-message Info popup + _ui->newMsgFromPubWithMeta(path_len, is_flood, nullptr, channel_name, + text, history_count, snr_q4, rssi); + } +#endif +} + +uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, + uint8_t len, uint8_t *reply) { + if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) { + uint8_t permissions = 0; + uint8_t cp = contact.flags >> 1; // LSB used as 'favourite' bit (so only use upper bits) + + if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_ALL) { + permissions = TELEM_PERM_BASE; + } else if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_FLAGS) { + permissions = cp & TELEM_PERM_BASE; + } + + if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_ALL) { + permissions |= TELEM_PERM_LOCATION; + } else if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_FLAGS) { + permissions |= cp & TELEM_PERM_LOCATION; + } + + if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_ALL) { + permissions |= TELEM_PERM_ENVIRONMENT; + } else if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_FLAGS) { + permissions |= cp & TELEM_PERM_ENVIRONMENT; + } + + uint8_t perm_mask = ~(data[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions + permissions &= perm_mask; + + if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(permissions, telemetry); + + memcpy(reply, &sender_timestamp, + 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply[4], telemetry.getBuffer(), tlen); + return 4 + tlen; + } + } + return 0; // unknown +} + +void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) { + uint32_t tag; + memcpy(&tag, data, 4); + + /* Touch-UI ping match: when the touch UI fired a status request via + * sendStatusPingForUI, deliver the reply payload to it. Done before the + * companion-serial pending_status branch so a UI-only ping doesn't write + * a STATUS_RESPONSE frame to a serial client that didn't ask for it. */ +#ifdef DISPLAY_CLASS + // Match by *tag* (the 4-byte timestamp the responder echoes from our + // request), not just by pubkey. The chained guest-login response uses + // the repeater's own clock at data[0..3], so without a tag check it + // would race ahead of the real STATUS/TELEMETRY reply and get + // misrouted as if it were the answer. Tag is set when we send the REQ. + if (_ui && _ui_pending_status && _ui_pending_tag != 0 && len > 4 && + memcmp(&_ui_pending_status, contact.id.pub_key, 4) == 0 && + tag == _ui_pending_tag) { + UiReqKind kind = _ui_pending_kind; + _ui_pending_status = 0; + _ui_pending_kind = UiReqKind::None; + _ui_pending_tag = 0; + if (kind == UiReqKind::Telemetry) { + // CayenneLPP payload — let the UI decode the LPP channels. + _ui->onTelemetryReply(contact, &data[4], (size_t)(len - 4)); + } else { + // STATUS or unknown — let the UI fall through its RepeaterStats / + // JSON parser path. + _ui->onPingReply(contact, &data[4], (size_t)(len - 4)); + } + return; + } +#endif + + if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response + // yes, is response to pending sendLogin() + pending_login = 0; + + int i = 0; + bool ok = false; + uint8_t perms = 0; + if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response + out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; + out_frame[i++] = 0; // legacy: is_admin = false + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + ok = true; + } else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response + uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16; + if (keep_alive_secs > 0) { + startConnection(contact, keep_alive_secs); + } + out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; + out_frame[i++] = data[6]; // permissions (eg. is_admin) + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + memcpy(&out_frame[i], &tag, 4); + i += 4; // NEW: include server timestamp + out_frame[i++] = data[7]; // NEW (v7): ACL permissions + out_frame[i++] = data[12]; // FIRMWARE_VER_LEVEL + ok = true; + perms = data[6]; + } else { + out_frame[i++] = PUSH_CODE_LOGIN_FAIL; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + } + _serial->writeFrame(out_frame, i); + // Diagnostic (room-server login trace): the login response we matched + + // whether we accepted it. resp byte: 0x4F 'O' = legacy "OK"; otherwise a + // RESP_SERVER_LOGIN_OK / fail code. Pair with "[ROOM] login send". + Serial.printf("[ROOM] login resp '%s' resp=0x%02x ok=%d perms=%d\n", + contact.name, data[4], (int)ok, (int)perms); +#ifdef DISPLAY_CLASS + // Notify the touch UI so the admin console can flip from "logging in…" + // to the prompt (success) or show "wrong password" (fail). Same data + // the companion app sees via PUSH_CODE_LOGIN_SUCCESS/_FAIL. + if (_ui) _ui->onAdminLoginResult(contact, ok, perms); +#endif + } else if (len > 4 && // check for status response + pending_status && + memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme + // FUTURE: tag == pending_status + ) { + pending_status = 0; + + int i = 0; + out_frame[i++] = PUSH_CODE_STATUS_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + memcpy(&out_frame[i], &data[4], len - 4); + i += (len - 4); + _serial->writeFrame(out_frame, i); + } else if (len > 4 && tag == pending_telemetry) { // check for matching response tag + pending_telemetry = 0; + + int i = 0; + out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + memcpy(&out_frame[i], &data[4], len - 4); + i += (len - 4); + _serial->writeFrame(out_frame, i); + } else if (len > 4 && tag == pending_req) { // check for matching response tag + pending_req = 0; + + int i = 0; + out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], &tag, 4); // app needs to match this to RESP_CODE_SENT.tag + i += 4; + memcpy(&out_frame[i], &data[4], len - 4); + i += (len - 4); + _serial->writeFrame(out_frame, i); + } +} + +bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { + if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) { + uint32_t tag; + memcpy(&tag, extra, 4); + + if (tag == pending_discovery) { // check for matching response tag) + pending_discovery = 0; + + if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) { + MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len); + } else { + int i = 0; + out_frame[i++] = PUSH_CODE_PATH_DISCOVERY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + out_frame[i++] = out_path_len; + memcpy(&out_frame[i], out_path, out_path_len); + i += out_path_len; + out_frame[i++] = in_path_len; + memcpy(&out_frame[i], in_path, in_path_len); + i += in_path_len; + // NOTE: telemetry data in 'extra' is discarded at present + + _serial->writeFrame(out_frame, i); + } + return false; // DON'T send reciprocal path! + } + } + // let base class handle received path and data + return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len); +} + +void MyMesh::onControlDataRecv(mesh::Packet *packet) { + if (packet->payload_len + 4 > sizeof(out_frame)) { + MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); + return; + } + int i = 0; + out_frame[i++] = PUSH_CODE_CONTROL_DATA; + out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); + out_frame[i++] = (int8_t)(_radio->getLastRSSI()); + out_frame[i++] = packet->path_len; + memcpy(&out_frame[i], packet->payload, packet->payload_len); + i += packet->payload_len; + + if (_serial->isConnected()) { + _serial->writeFrame(out_frame, i); + } else { + MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline"); + } +} + +void MyMesh::onRawDataRecv(mesh::Packet *packet) { + if (packet->payload_len + 4 > sizeof(out_frame)) { + MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len); + return; + } + int i = 0; + out_frame[i++] = PUSH_CODE_RAW_DATA; + out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); + out_frame[i++] = (int8_t)(_radio->getLastRSSI()); + out_frame[i++] = 0xFF; // reserved (possibly path_len in future) + memcpy(&out_frame[i], packet->payload, packet->payload_len); + i += packet->payload_len; + + if (_serial->isConnected()) { + _serial->writeFrame(out_frame, i); + } else { + MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline"); + } +} + +void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { + uint8_t path_sz = flags & 0x03; // NEW v1.11+ +#if defined(DISPLAY_CLASS) + // If this trace's tag matches the most recent UI-initiated ping, surface + // the bidirectional SNRs directly to the touch UI instead of (or in + // addition to) sending the companion-protocol push frame. path_len is + // the *byte* length of path_hashes; the number of SNR readings is + // (path_len >> path_sz). For a 0-hop ping the path has a single entry + // (the neighbor's hash) and we get a single SNR: their RX of us. Our RX + // of their retransmission comes from packet->getSNR(). + if (_ui && tag != 0 && tag == _ui_trace_ping_tag) { + _ui_trace_ping_tag = 0; + const uint8_t snr_count = (path_sz >= 4) ? 0 : (uint8_t)(path_len >> path_sz); + int8_t their_snr = (snr_count > 0) ? (int8_t)path_snrs[0] : (int8_t)0; + int8_t our_snr = (int8_t)(packet->getSNR() * 4); + const int8_t* extra = (snr_count > 1) ? (const int8_t*)&path_snrs[1] : nullptr; + uint8_t extra_hops = (snr_count > 1) ? (uint8_t)(snr_count - 1) : 0; + _ui->onTracePingResult(tag, their_snr, our_snr, extra_hops, extra); + // Don't also push to companion: a UI ping shouldn't leak as if the + // companion app asked for it. (Companion-initiated traces use a tag + // that won't collide because we generate _ui_trace_ping_tag with the + // RNG and clear it after one match.) + return; + } +#endif + if (12 + path_len + (path_len >> path_sz) + 1 > sizeof(out_frame)) { + MESH_DEBUG_PRINTLN("onTraceRecv(), path_len is too long: %d", (uint32_t)path_len); + return; + } + int i = 0; + out_frame[i++] = PUSH_CODE_TRACE_DATA; + out_frame[i++] = 0; // reserved + out_frame[i++] = path_len; + out_frame[i++] = flags; + memcpy(&out_frame[i], &tag, 4); + i += 4; + memcpy(&out_frame[i], &auth_code, 4); + i += 4; + memcpy(&out_frame[i], path_hashes, path_len); + i += path_len; + + memcpy(&out_frame[i], path_snrs, path_len >> path_sz); + i += path_len >> path_sz; + out_frame[i++] = (int8_t)(packet->getSNR() * 4); // extra/final SNR (to this node) + + if (_serial->isConnected()) { + _serial->writeFrame(out_frame, i); + } else { + MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); + } +} + +uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { + return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); +} +uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const { + return SEND_TIMEOUT_BASE_MILLIS + + ((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * + (path_len + 1)); +} + +void MyMesh::onSendTimeout() {} + +MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) + : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), + _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { + _iter_started = false; + _contact_send_index = 0; + _contact_list_reply_target = -1; + _cli_rescue = false; + offline_queue_len = 0; + history_count = 0; + history_head = 0; + history_next_seq = 0; + history_num_clients = 0; + proto_num_clients = 0; + app_target_ver = 0; + clearPendingReqs(); + _ui_pending_status = 0; + next_ack_idx = 0; + sign_data = NULL; + dirty_contacts_expiry = 0; + memset(advert_paths, 0, sizeof(advert_paths)); + memset(send_scope.key, 0, sizeof(send_scope.key)); + send_unscoped = false; + + // defaults + memset(&_prefs, 0, sizeof(_prefs)); + _prefs.airtime_factor = 1.0; + strcpy(_prefs.node_name, "NONAME"); + _prefs.freq = LORA_FREQ; + _prefs.sf = LORA_SF; + _prefs.bw = LORA_BW; + _prefs.cr = LORA_CR; + _prefs.tx_power_dbm = LORA_TX_POWER; + _prefs.gps_enabled = 0; // GPS disabled by default + _prefs.gps_interval = 0; // No automatic GPS updates by default + //_prefs.rx_delay_base = 10.0f; enable once new algo fixed +#if defined(USE_SX1262) || defined(USE_SX1268) +#ifdef SX126X_RX_BOOSTED_GAIN + _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN ? 1 : 0; +#else + _prefs.rx_boosted_gain = 1; // enabled by default +#endif +#endif +} + +void MyMesh::applyRadioFromPrefs() { + radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_driver.setTxPower(_prefs.tx_power_dbm); +#if defined(USE_SX1262) || defined(USE_SX1268) + _prefs.rx_boosted_gain = _prefs.rx_boosted_gain ? 1 : 0; + radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain != 0); + MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", + radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); +#endif +} + +void MyMesh::setDefaultFloodScope(const char* region_name) { + // Trim leading/trailing whitespace — a stray space changes the hash and would + // derive the wrong scope key (the node would never match the region). + char tag[40] = {0}; + if (region_name) { + while (*region_name == ' ' || *region_name == '\t') region_name++; + size_t n = strlen(region_name); + while (n && (region_name[n-1] == ' ' || region_name[n-1] == '\t' || + region_name[n-1] == '\n' || region_name[n-1] == '\r')) n--; + // Normalise to a leading '#': a public hashtag region's key is SHA256("#name"). + size_t o = 0; + if (n && region_name[0] != '#' && o < sizeof(tag)-1) tag[o++] = '#'; + for (size_t i = 0; i < n && o < sizeof(tag)-1; ++i) tag[o++] = region_name[i]; + tag[o] = '\0'; + } + if (tag[0] == '\0' || (tag[0] == '#' && tag[1] == '\0')) { + memset(_prefs.default_scope_key, 0, sizeof(_prefs.default_scope_key)); // unscoped + } else { + SHA256 sha; + sha.update(tag, strlen(tag)); + sha.finalize(_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); + send_unscoped = false; // make sure the scope is actually applied on send + } + savePrefs(); +} + +bool MyMesh::pushChannelScope(const char* region_name) { + // Mirror setDefaultFloodScope's trim + "#name" normalisation, but derive into + // the transient send_scope so it applies only to the next channel send. + char tag[40] = {0}; + if (region_name) { + while (*region_name == ' ' || *region_name == '\t') region_name++; + size_t n = strlen(region_name); + while (n && (region_name[n-1] == ' ' || region_name[n-1] == '\t' || + region_name[n-1] == '\n' || region_name[n-1] == '\r')) n--; + size_t o = 0; + if (n && region_name[0] != '#' && o < sizeof(tag)-1) tag[o++] = '#'; + for (size_t i = 0; i < n && o < sizeof(tag)-1; ++i) tag[o++] = region_name[i]; + tag[o] = '\0'; + } + if (tag[0] == '\0' || (tag[0] == '#' && tag[1] == '\0')) return false; // no override + memcpy(&_chan_scope_saved, &send_scope, sizeof(send_scope)); + _chan_scope_saved_unscoped = send_unscoped; + _chan_scope_pushed = true; + SHA256 sha; + sha.update(tag, strlen(tag)); + sha.finalize(send_scope.key, sizeof(send_scope.key)); + send_unscoped = false; + return true; +} + +void MyMesh::popChannelScope() { + if (!_chan_scope_pushed) return; + memcpy(&send_scope, &_chan_scope_saved, sizeof(send_scope)); + send_unscoped = _chan_scope_saved_unscoped; + _chan_scope_pushed = false; +} + +void MyMesh::begin(bool has_display) { + BaseChatMesh::begin(); + + if (!_store->loadMainIdentity(self_id)) { + self_id = radio_new_identity(); // create new random identity + int count = 0; + while (count < 10 && (self_id.pub_key[0] == 0x00 || self_id.pub_key[0] == 0xFF)) { // reserved id hashes + self_id = radio_new_identity(); + count++; + } + _store->saveMainIdentity(self_id); + } + +// if name is provided as a build flag, use that as default node name instead +#ifdef ADVERT_NAME + strcpy(_prefs.node_name, ADVERT_NAME); +#else + // use hex of first 4 bytes of identity public key as default node name + char pub_key_hex[10]; + mesh::Utils::toHex(pub_key_hex, self_id.pub_key, 4); + strcpy(_prefs.node_name, pub_key_hex); +#endif + + // load persisted prefs + _store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon); + + // sanitise bad pref values + _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); + _prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f); + _prefs.freq = constrain(_prefs.freq, 400.0f, 2500.0f); + _prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f); + _prefs.sf = constrain(_prefs.sf, 5, 12); + _prefs.cr = constrain(_prefs.cr, 5, 8); + _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); + _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 + _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours + _prefs.path_hash_mode = constrain(_prefs.path_hash_mode, 0, 2); + if (_prefs.autoadd_max_hops > 64) { + _prefs.autoadd_max_hops = 64; + } + +#ifdef BLE_PIN_CODE // 123456 by default + if (_prefs.ble_pin == 0) { +#ifdef DISPLAY_CLASS + if (has_display && BLE_PIN_CODE == 123456) { + StdRNG rng; + _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session + } else { + _active_ble_pin = BLE_PIN_CODE; // otherwise static pin + } +#else + _active_ble_pin = BLE_PIN_CODE; // otherwise static pin +#endif + } else { + _active_ble_pin = _prefs.ble_pin; + } +#else + _active_ble_pin = 0; +#endif + + resetContacts(); + _store->loadContacts(this); + bootstrapRTCfromContacts(); + addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel + _store->loadChannels(this); + + applyRadioFromPrefs(); // freq/bw/sf/cr + TX power + RX-boost (shared with the live UI apply) +#if defined(DISPLAY_CLASS) + // Boot diag: identity prefix + radio config so we can confirm the touch + // firmware's pubkey is stable across flashes (replies are addressed to + // the first byte of our pub_key, so any drift = no inbound) and that + // freq/SF/BW match the stock firmware we're comparing against. + if (_ui) { + char dbg[80]; + snprintf(dbg, sizeof(dbg), + "ID %02x%02x%02x%02x %.3fMHz sf%u bw%.1f", + (unsigned)self_id.pub_key[0], (unsigned)self_id.pub_key[1], + (unsigned)self_id.pub_key[2], (unsigned)self_id.pub_key[3], + (double)_prefs.freq, (unsigned)_prefs.sf, (double)_prefs.bw); + _ui->appendDiag(dbg); + } +#endif + +#if defined(ENABLE_ADVERT_ON_BOOT) && ENABLE_ADVERT_ON_BOOT == 1 + // Schedule a flood advert ~6s after boot so peers with auto-add ON learn + // our current pubkey. Critical for touch firmware where SPIFFS may have + // been wiped during a flash, leaving prior contacts with a stale pubkey + // and silently dropping our DMs. + _boot_advert_due_ms = _ms->getMillis() + 6000UL; + _boot_advert_done = false; +#endif +} + +const char *MyMesh::getNodeName() { + return _prefs.node_name; +} +NodePrefs *MyMesh::getNodePrefs() { + return &_prefs; +} +uint32_t MyMesh::getBLEPin() { + return _active_ble_pin; +} + +struct FreqRange { + uint32_t lower_freq, upper_freq; +}; + +static FreqRange repeat_freq_ranges[] = { + #ifdef ALLOWED_REPEAT_FREQ_RANGE + ALLOWED_REPEAT_FREQ_RANGE + #else + { 433000, 433000 }, + { 869495, 869495 }, + { 918000, 918000 } + #endif +}; + +bool MyMesh::isValidClientRepeatFreq(uint32_t f) const { + for (int i = 0; i < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]); i++) { + auto r = &repeat_freq_ranges[i]; + if (f >= r->lower_freq && f <= r->upper_freq) return true; + } + return false; +} + +void MyMesh::startInterface(BaseSerialInterface &serial) { + _serial = &serial; + serial.enable(); +} + +void MyMesh::handleCmdFrame(size_t len) { + if (cmd_frame[0] == CMD_DEVICE_QUERY && len >= 2) { // sent when app establishes connection + app_target_ver = cmd_frame[1]; // which version of protocol does app understand + char client_id[MAX_CLIENT_ID_LEN + 1]; + _serial->getCurrentClientId(client_id, sizeof(client_id)); + setClientTargetVer(client_id, cmd_frame[1]); // version per connected client + + int i = 0; + out_frame[i++] = RESP_CODE_DEVICE_INFO; + out_frame[i++] = FIRMWARE_VER_CODE; + out_frame[i++] = MAX_CONTACTS / 2; // v3+ + out_frame[i++] = MAX_GROUP_CHANNELS; // v3+ + memcpy(&out_frame[i], &_prefs.ble_pin, 4); + i += 4; + memset(&out_frame[i], 0, 12); + StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_BUILD_DATE, 12); + i += 12; + StrHelper::strzcpy((char *)&out_frame[i], board.getManufacturerName(), 40); + i += 40; + StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); + i += 20; + out_frame[i++] = _prefs.client_repeat; // v9+ + out_frame[i++] = _prefs.path_hash_mode; // v10+ (matches upstream 1.14 companion protocol) + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_APP_START && + len >= 8) { // sent when app establishes connection, respond with node ID + // Optional client_id: byte 1 = length (0 = none), bytes 2..1+len = client_id. Then app_name. + char* app_name; + if (len >= 2 && cmd_frame[1] > 0 && len >= 2 + (size_t)cmd_frame[1]) { + uint8_t cid_len = cmd_frame[1]; + if (cid_len > MAX_CLIENT_ID_LEN) cid_len = MAX_CLIENT_ID_LEN; + char cid_buf[MAX_CLIENT_ID_LEN + 1]; + memcpy(cid_buf, &cmd_frame[2], cid_len); + cid_buf[cid_len] = '\0'; + _serial->setCurrentClientId(cid_buf); + app_name = (char*)&cmd_frame[2 + cmd_frame[1]]; + } else { + app_name = (char*)&cmd_frame[8]; + } + cmd_frame[len] = 0; // make app_name null terminated + MESH_DEBUG_PRINTLN("App %s connected", app_name); + char client_id[MAX_CLIENT_ID_LEN + 1]; + _serial->getCurrentClientId(client_id, sizeof(client_id)); + setClientAppName(client_id, app_name); + + _iter_started = false; // stop any left-over ContactsIterator + int i = 0; + out_frame[i++] = RESP_CODE_SELF_INFO; + out_frame[i++] = ADV_TYPE_CHAT; // what this node Advert identifies as (maybe node's pronouns too?? :-) + out_frame[i++] = _prefs.tx_power_dbm; + out_frame[i++] = MAX_LORA_TX_POWER; + memcpy(&out_frame[i], self_id.pub_key, PUB_KEY_SIZE); + i += PUB_KEY_SIZE; + + int32_t lat, lon; + lat = (sensors.node_lat * 1000000.0); + lon = (sensors.node_lon * 1000000.0); + memcpy(&out_frame[i], &lat, 4); + i += 4; + memcpy(&out_frame[i], &lon, 4); + i += 4; + out_frame[i++] = _prefs.multi_acks; // new v7+ + out_frame[i++] = _prefs.advert_loc_policy; + out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) | + (_prefs.telemetry_mode_base); // v5+ + out_frame[i++] = _prefs.manual_add_contacts; + + uint32_t freq = _prefs.freq * 1000; + memcpy(&out_frame[i], &freq, 4); + i += 4; + uint32_t bw = _prefs.bw * 1000; + memcpy(&out_frame[i], &bw, 4); + i += 4; + out_frame[i++] = _prefs.sf; + out_frame[i++] = _prefs.cr; + + int tlen = strlen(_prefs.node_name); // revisit: UTF_8 ?? + memcpy(&out_frame[i], _prefs.node_name, tlen); + i += tlen; + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { + int i = 1; + uint8_t txt_type = cmd_frame[i++]; + uint8_t attempt = cmd_frame[i++]; + uint32_t msg_timestamp; + memcpy(&msg_timestamp, &cmd_frame[i], 4); + i += 4; + uint8_t *pub_key_prefix = &cmd_frame[i]; + i += 6; + if (isMeshcomodRecipient(pub_key_prefix)) { + char *text = (char *)&cmd_frame[i]; + int tlen = len - i; + uint32_t expected_ack = msg_timestamp ? msg_timestamp : getRTCClock()->getCurrentTimeUnique(); + uint32_t est_timeout = 1; + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = 0; // local handling, not flood + memcpy(&out_frame[2], &expected_ack, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + // Immediately confirm local command "delivery" so companion UI doesn't keep retrying. + uint8_t confirmed[9]; + confirmed[0] = PUSH_CODE_SEND_CONFIRMED; + uint32_t trip_time = 0; + memcpy(&confirmed[1], &expected_ack, 4); + memcpy(&confirmed[5], &trip_time, 4); + _serial->writeFrame(confirmed, sizeof(confirmed)); + if (tlen > 0) { + text[tlen] = 0; + if (!isMeshcomodDuplicate(msg_timestamp, text)) { + rememberMeshcomodCommand(msg_timestamp, text); + handleMeshcomodCommand(text, tlen); + } + } else { + pushMeshcomodReply("usage: wifi help"); + } + } else { + ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6); + if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) { + char *text = (char *)&cmd_frame[i]; + int tlen = len - i; + uint32_t est_timeout; + text[tlen] = 0; // ensure null + int result; + uint32_t expected_ack; + bool skip_radio_send = false; + if (txt_type == TXT_TYPE_PLAIN) { + uint32_t body_crc = 0; + mesh::Utils::sha256((uint8_t*)&body_crc, 4, (const uint8_t*)text, tlen); + uint32_t now_ms = millis(); + if (msg_timestamp != 0 && + msg_timestamp == s_last_cmd_txt_ts && + memcmp(pub_key_prefix, s_last_cmd_txt_pub6, sizeof(s_last_cmd_txt_pub6)) == 0 && + body_crc == s_last_cmd_txt_body_crc && + (uint32_t)(now_ms - s_last_cmd_txt_seen_ms) < 30000UL) { + // Transport/client retry of same command frame: ack locally but avoid re-transmitting stale packet. + skip_radio_send = true; + expected_ack = s_last_cmd_txt_ack; + est_timeout = s_last_cmd_txt_est_timeout; + result = MSG_SEND_SENT_FLOOD; + } else { + s_last_cmd_txt_ts = msg_timestamp; + memcpy(s_last_cmd_txt_pub6, pub_key_prefix, sizeof(s_last_cmd_txt_pub6)); + s_last_cmd_txt_body_crc = body_crc; + s_last_cmd_txt_seen_ms = now_ms; + } + } + if (txt_type == TXT_TYPE_CLI_DATA) { + msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection + TxtTxDebugInfo dbg{}; + result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout, nullptr, &dbg); + expected_ack = 0; // no Ack expected + if (_ui) { + char line[160]; + snprintf(line, sizeof(line), "TX CMD src=CMD_SEND_TXT_MSG kind=CLI ts=%lu att=%u r=%d h=%08lX core_ts=%lu core_att=%u", + static_cast(msg_timestamp), + static_cast(attempt), + result, + static_cast(dbg.packet_hash4), + static_cast(dbg.uniq_ts), + static_cast(dbg.uniq_attempt)); + _ui->appendDiag(line); + } + } else if (!skip_radio_send) { + // Force node-side unique timestamp for plain sends as well. + // Some clients may resend/reuse app timestamps, which can cause repeated packet hashes + // and replay-like suppression on receivers. + msg_timestamp = getRTCClock()->getCurrentTimeUnique(); + uint32_t tx_hash4 = 0; + TxtTxDebugInfo dbg{}; + result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout, &tx_hash4, &dbg); + if (_ui) { + char line[192]; + snprintf(line, sizeof(line), "TX CMD src=CMD_SEND_TXT_MSG kind=PLAIN ts=%lu att=%u r=%d ack=%lu h=%08lX core_ts=%lu core_att=%u n=%u", + static_cast(msg_timestamp), static_cast(attempt), result, + static_cast(expected_ack), + static_cast(tx_hash4), + static_cast(dbg.uniq_ts), + static_cast(dbg.uniq_attempt), + static_cast(dbg.nonce)); + _ui->appendDiag(line); + } + } + // TODO: add expected ACK to table + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + if (txt_type == TXT_TYPE_PLAIN && !skip_radio_send) { + s_last_cmd_txt_ack = expected_ack; + s_last_cmd_txt_est_timeout = est_timeout; + } + if (expected_ack) { + expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); // add to circular table + expected_ack_table[next_ack_idx].ack = expected_ack; + expected_ack_table[next_ack_idx].contact = recipient; + next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE; + } + + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &expected_ack, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + + // Do not synthesize a private-message "recv" frame from self. That frame has no recipient + // context and can be rendered by clients as a chat with self. + } + } else { + writeErrFrame(recipient == NULL + ? ERR_CODE_NOT_FOUND + : ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsupported TXT_TYPE_* + } + } + } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel msg + int i = 1; + uint8_t txt_type = cmd_frame[i++]; // should be TXT_TYPE_PLAIN + uint8_t channel_idx = cmd_frame[i++]; + uint32_t msg_timestamp; + memcpy(&msg_timestamp, &cmd_frame[i], 4); + i += 4; + const char *text = (char *)&cmd_frame[i]; + + if (txt_type != TXT_TYPE_PLAIN) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else { + ChannelDetails channel; + bool success = getChannel(channel_idx, channel); + if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) { + writeOKFrame(); + // Sent channel messages are not added to shared history / broadcast: channel_idx and + // frame format are device-specific and clients (e.g. HA) without channel support can + // misparse or show "text as sender"; also avoids failed-to-sync when versions differ. + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx + } + } + } else if (cmd_frame[0] == CMD_SEND_CHANNEL_DATA && len > 5) { // send GroupChannel datagram + if (len < 4) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + return; + } + int i = 1; + uint8_t channel_idx = cmd_frame[i++]; + uint8_t path_len = cmd_frame[i++]; + + // validate path len, allowing 0xFF for flood + if (!mesh::Packet::isValidPathLen(path_len) && path_len != OUT_PATH_UNKNOWN) { + MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA invalid path size: %d", path_len); + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + return; + } + + // parse provided path if not flood + uint8_t path[MAX_PATH_SIZE]; + if (path_len != OUT_PATH_UNKNOWN) { + i += mesh::Packet::writePath(path, &cmd_frame[i], path_len); + } + + uint16_t data_type = ((uint16_t)cmd_frame[i]) | (((uint16_t)cmd_frame[i + 1]) << 8); + i += 2; + const uint8_t *payload = &cmd_frame[i]; + int payload_len = (len > (size_t)i) ? (int)(len - i) : 0; + + ChannelDetails channel; + if (!getChannel(channel_idx, channel)) { + writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx + } else if (data_type == DATA_TYPE_RESERVED) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else if (payload_len > MAX_CHANNEL_DATA_LENGTH) { + MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH); + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else if (cmd_frame[0] == CMD_GET_CONTACTS) { // get Contact list + if (_iter_started) { + writeErrFrame(ERR_CODE_BAD_STATE); // iterator is currently busy + } else { + if (len >= 5) { // has optional 'since' param + memcpy(&_iter_filter_since, &cmd_frame[1], 4); + } else { + _iter_filter_since = 0; + } + + uint8_t reply[5]; + reply[0] = RESP_CODE_CONTACTS_START; + uint32_t count = getNumContacts(); // total, NOT filtered count + memcpy(&reply[1], &count, 4); + // Save reply target so CONTACT/END go to same client (WS/TCP) even if next checkRecvFrame overwrites it + _contact_list_reply_target = _serial->getReplyTarget(); + size_t start_ret = _serial->writeFrame(reply, 5); + _contact_send_index = 0; + // No debug prints here: companion stream must be binary-only (no ASCII in same transport as protocol frames) + + // start iterator + _iter = startContactsIterator(); + _iter_started = true; + _most_recent_lastmod = 0; + } + } else if (cmd_frame[0] == CMD_SET_ADVERT_NAME && len >= 2) { + int nlen = len - 1; + if (nlen > sizeof(_prefs.node_name) - 1) nlen = sizeof(_prefs.node_name) - 1; // max len + memcpy(_prefs.node_name, &cmd_frame[1], nlen); + _prefs.node_name[nlen] = 0; // null terminator + savePrefs(); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_SET_ADVERT_LATLON && len >= 9) { + int32_t lat, lon, alt = 0; + memcpy(&lat, &cmd_frame[1], 4); + memcpy(&lon, &cmd_frame[5], 4); + if (len >= 13) { + memcpy(&alt, &cmd_frame[9], 4); // for FUTURE support + } + if (lat <= 90 * 1E6 && lat >= -90 * 1E6 && lon <= 180 * 1E6 && lon >= -180 * 1E6) { + sensors.node_lat = ((double)lat) / 1000000.0; + sensors.node_lon = ((double)lon) / 1000000.0; + savePrefs(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid geo coordinate + } + } else if (cmd_frame[0] == CMD_GET_DEVICE_TIME) { + uint8_t reply[5]; + reply[0] = RESP_CODE_CURR_TIME; + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply[1], &now, 4); + _serial->writeFrame(reply, 5); + } else if (cmd_frame[0] == CMD_SET_DEVICE_TIME && len >= 5) { + uint32_t secs; + memcpy(&secs, &cmd_frame[1], 4); + uint32_t curr = getRTCClock()->getCurrentTime(); + if (secs >= curr) { + getRTCClock()->setCurrentTime(secs); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { + mesh::Packet* pkt; + if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { + pkt = createSelfAdvert(_prefs.node_name); + } else { + pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); + } + if (pkt) { + if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) + TransportKey default_scope; + memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key)); + sendFloodScoped(default_scope, pkt, 0); + } else { + sendZeroHop(pkt); + } + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else if (cmd_frame[0] == CMD_RESET_PATH && len >= 1 + 32) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + recipient->out_path_len = -1; + // recipient->lastmod = ?? shouldn't be needed, app already has this version of contact + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // unknown contact + } + } else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1 + 32 + 2 + 1) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame + if (recipient) { + updateContactFromFrame(*recipient, last_mod, cmd_frame, len); + recipient->lastmod = last_mod; + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + writeOKFrame(); + } else { + ContactInfo contact; + updateContactFromFrame(contact, last_mod, cmd_frame, len); + contact.lastmod = last_mod; + contact.sync_since = 0; + if (addContact(contact)) { + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); +#ifdef DISPLAY_CLASS + // Tell the touch UI to refresh its thread list immediately so the new + // contact's DM shows up in Chats without waiting for the periodic poll. + if (_ui) _ui->onThreadsChanged(); +#endif + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } + } else if (cmd_frame[0] == CMD_REMOVE_CONTACT) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient && removeContact(*recipient)) { + _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); +#ifdef DISPLAY_CLASS + // Removing a contact drops its DM thread from the Chats list — ping + // the UI so it picks the change up immediately. + if (_ui) _ui->onThreadsChanged(); +#endif + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // not found, or unable to remove + } + } else if (cmd_frame[0] == CMD_SHARE_CONTACT) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + if (shareContactZeroHop(*recipient)) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); // unable to send + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); + } + } else if (cmd_frame[0] == CMD_GET_CONTACT_BY_KEY) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (contact) { + writeContactRespFrame(RESP_CODE_CONTACT, *contact); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // not found + } + } else if (cmd_frame[0] == CMD_EXPORT_CONTACT) { + if (len < 1 + PUB_KEY_SIZE) { + // export SELF + mesh::Packet* pkt; + if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { + pkt = createSelfAdvert(_prefs.node_name); + } else { + pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); + } + if (pkt) { + pkt->header |= ROUTE_TYPE_FLOOD; // would normally be sent in this mode + + out_frame[0] = RESP_CODE_EXPORT_CONTACT; + uint8_t out_len = pkt->writeTo(&out_frame[1]); + releasePacket(pkt); // undo the obtainNewPacket() + _serial->writeFrame(out_frame, out_len + 1); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); // Error + } + } else { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + uint8_t out_len; + if (recipient && (out_len = exportContact(*recipient, &out_frame[1])) > 0) { + out_frame[0] = RESP_CODE_EXPORT_CONTACT; + _serial->writeFrame(out_frame, out_len + 1); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // not found + } + } + } else if (cmd_frame[0] == CMD_IMPORT_CONTACT && len > 2 + 32 + 64) { + if (importContact(&cmd_frame[1], len - 1)) { +#ifdef DISPLAY_CLASS + // Imported contact = a new DM thread. Ping the UI so it picks it up + // immediately instead of after the next 4 s mesh-refresh tick. + if (_ui) _ui->onThreadsChanged(); +#endif + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_SYNC_SINCE) { + if (len >= 5) { + uint32_t T; + memcpy(&T, &cmd_frame[1], 4); + sendSyncSinceDelta(T); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) { + char client_id[MAX_CLIENT_ID_LEN + 1]; + _serial->getCurrentClientId(client_id, sizeof(client_id)); +#if COMPANION_SYNC_DEBUG + g_sync_dbg.req++; +#endif + uint32_t sent_seq = 0; + int out_len = getNextFromHistoryForClient(client_id, out_frame, &sent_seq, false); + const bool had_history_frame = (out_len > 0); +#if COMPANION_SYNC_DEBUG + if (had_history_frame) g_sync_dbg.had_frame++; + else g_sync_dbg.no_more++; +#endif + uint8_t send_buf[MAX_FRAME_SIZE]; + const uint8_t* send_ptr = out_frame; + if (out_len <= 0) { + out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; + out_len = 1; + } else { + int adapted_len = adaptHistoryFrameForClient(client_id, out_frame, out_len, send_buf); + if (adapted_len > 0) { + send_ptr = send_buf; + out_len = adapted_len; + } + } + size_t to_send = (size_t)out_len; + // Retry write so transient full buffers (TCP/BLE) don't cause client timeout + const int max_retries = 10; + bool sent_ok = false; + for (int r = 0; r < max_retries; r++) { + if (_serial->writeFrame(send_ptr, to_send) == to_send) { + sent_ok = true; +#if COMPANION_SYNC_DEBUG + g_sync_dbg.write_ok++; + g_sync_dbg.retries += (uint32_t)r; + SYNC_DEBUG_PRINTLN("resp client=%s had=%d code=%u len=%u seq=%lu retry=%d", + client_id, had_history_frame ? 1 : 0, + (unsigned)send_ptr[0], (unsigned)to_send, + (unsigned long)sent_seq, r); + if (r > 0) { + SYNC_DEBUG_PRINTLN("retry_ok client=%s retries=%d had=%d len=%u seq=%lu", + client_id, r, had_history_frame ? 1 : 0, (unsigned)to_send, + (unsigned long)sent_seq); + } + if ((g_sync_dbg.req % 100u) == 0u) { + SYNC_DEBUG_PRINTLN("summary req=%lu had=%lu no_more=%lu ok=%lu fail=%lu retries=%lu", + (unsigned long)g_sync_dbg.req, (unsigned long)g_sync_dbg.had_frame, + (unsigned long)g_sync_dbg.no_more, (unsigned long)g_sync_dbg.write_ok, + (unsigned long)g_sync_dbg.write_fail, (unsigned long)g_sync_dbg.retries); + } +#endif + if (had_history_frame) commitHistoryForClient(client_id, sent_seq); +#ifdef DISPLAY_CLASS + if (_ui && had_history_frame) _ui->msgRead(history_count); +#endif + break; + } + if (r < max_retries - 1) delay(25); + } +#if COMPANION_SYNC_DEBUG + if (!sent_ok) { + g_sync_dbg.write_fail++; + SYNC_DEBUG_PRINTLN("write_fail client=%s had=%d len=%u seq=%lu req=%lu", + client_id, had_history_frame ? 1 : 0, (unsigned)to_send, (unsigned long)sent_seq, + (unsigned long)g_sync_dbg.req); + } +#endif + } else if (cmd_frame[0] == CMD_SET_RADIO_PARAMS) { + int i = 1; + uint32_t freq; + memcpy(&freq, &cmd_frame[i], 4); + i += 4; + uint32_t bw; + memcpy(&bw, &cmd_frame[i], 4); + i += 4; + uint8_t sf = cmd_frame[i++]; + uint8_t cr = cmd_frame[i++]; + uint8_t repeat = 0; // default - false + if (len > i) { + repeat = cmd_frame[i++]; // FIRMWARE_VER_CODE 9+ + } + + if (repeat && !isValidClientRepeatFreq(freq)) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 && + bw <= 500000) { + _prefs.sf = sf; + _prefs.cr = cr; + _prefs.freq = (float)freq / 1000.0; + _prefs.bw = (float)bw / 1000.0; + _prefs.client_repeat = repeat; + savePrefs(); + + radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); +#if defined(USE_SX1262) || defined(USE_SX1268) || defined(SX126X_RX_BOOSTED_GAIN) + radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain != 0); +#endif + MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, + (uint32_t)cr); + + writeOKFrame(); + } else { + MESH_DEBUG_PRINTLN("Error: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, + (uint32_t)cr); + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) { + int8_t power = (int8_t)cmd_frame[1]; + if (power < -9 || power > MAX_LORA_TX_POWER) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else { + _prefs.tx_power_dbm = power; + savePrefs(); + radio_driver.setTxPower(_prefs.tx_power_dbm); + writeOKFrame(); + } + } else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) { + int i = 1; + uint32_t rx, af; + memcpy(&rx, &cmd_frame[i], 4); + i += 4; + memcpy(&af, &cmd_frame[i], 4); + i += 4; + _prefs.rx_delay_base = ((float)rx) / 1000.0f; + _prefs.airtime_factor = ((float)af) / 1000.0f; + savePrefs(); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) { + uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000; + int i = 0; + out_frame[i++] = RESP_CODE_TUNING_PARAMS; + memcpy(&out_frame[i], &rx, 4); i += 4; + memcpy(&out_frame[i], &af, 4); i += 4; + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) { + _prefs.manual_add_contacts = cmd_frame[1]; + if (len >= 3) { + _prefs.telemetry_mode_base = cmd_frame[2] & 0x03; // v5+ + _prefs.telemetry_mode_loc = (cmd_frame[2] >> 2) & 0x03; + _prefs.telemetry_mode_env = (cmd_frame[2] >> 4) & 0x03; + + if (len >= 4) { + _prefs.advert_loc_policy = cmd_frame[3]; + if (len >= 5) { + _prefs.multi_acks = cmd_frame[4]; + } + } + } + savePrefs(); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && cmd_frame[1] == 0 && len >= 3) { + if (cmd_frame[2] >= 3) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else { + _prefs.path_hash_mode = cmd_frame[2]; + savePrefs(); + writeOKFrame(); + } + } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { + if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed? + saveContacts(); + } + board.reboot(); + } else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) { + uint8_t reply[11]; + int i = 0; + reply[i++] = RESP_CODE_BATT_AND_STORAGE; + uint16_t battery_millivolts = board.getBattMilliVolts(); + uint32_t used = _store->getStorageUsedKb(); + uint32_t total = _store->getStorageTotalKb(); + memcpy(&reply[i], &battery_millivolts, 2); i += 2; + memcpy(&reply[i], &used, 4); i += 4; + memcpy(&reply[i], &total, 4); i += 4; + _serial->writeFrame(reply, i); + } else if (cmd_frame[0] == CMD_EXPORT_PRIVATE_KEY) { +#if ENABLE_PRIVATE_KEY_EXPORT + uint8_t reply[65]; + reply[0] = RESP_CODE_PRIVATE_KEY; + self_id.writeTo(&reply[1], 64); + _serial->writeFrame(reply, 65); +#else + writeDisabledFrame(); +#endif + } else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) { +#if ENABLE_PRIVATE_KEY_IMPORT + if (!mesh::LocalIdentity::validatePrivateKey(&cmd_frame[1])) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid key + } else { + mesh::LocalIdentity identity; + identity.readFrom(&cmd_frame[1], 64); + if (_store->saveMainIdentity(identity)) { + self_id = identity; + writeOKFrame(); + // re-load contacts, to invalidate ecdh shared_secrets + resetContacts(); + _store->loadContacts(this); + } else { + writeErrFrame(ERR_CODE_FILE_IO_ERROR); + } + } +#else + writeDisabledFrame(); +#endif + } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { + int i = 1; + int8_t path_len = cmd_frame[i++]; + if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload + uint8_t *path = &cmd_frame[i]; + i += path_len; + // Companion OTA panel currently sends `ota url ...` using CMD_SEND_RAW_DATA with empty path. + // Treat zero-path ASCII `ota ...` as a local meshcomod command. + if (path_len == 0 && i < (int)len) { + char local_cmd[220]; + int payload_len = (int)len - i; + if (payload_len >= (int)sizeof(local_cmd)) payload_len = (int)sizeof(local_cmd) - 1; + memcpy(local_cmd, &cmd_frame[i], (size_t)payload_len); + local_cmd[payload_len] = '\0'; + // Drop trailing NULs/whitespace from transport payload. + while (payload_len > 0 && + (local_cmd[payload_len - 1] == '\0' || local_cmd[payload_len - 1] == '\r' || + local_cmd[payload_len - 1] == '\n' || local_cmd[payload_len - 1] == ' ' || + local_cmd[payload_len - 1] == '\t')) { + local_cmd[--payload_len] = '\0'; + } + const char* cp = local_cmd; + while (*cp == ' ' || *cp == '\t') cp++; + if (strncasecmp(cp, "ota", 3) == 0 && (cp[3] == '\0' || cp[3] == ' ' || cp[3] == '\t')) { + // Acknowledge before synchronous OTA (same pattern as meshcomod TXT: SENT + confirmed first). + int j = 0; + out_frame[j++] = PUSH_CODE_BINARY_RESPONSE; + out_frame[j++] = 0; + uint32_t tag = 0; + memcpy(&out_frame[j], &tag, 4); + j += 4; + const char *line = "OTA command accepted"; + int ll = (int)strlen(line); + if (j + ll > MAX_FRAME_SIZE) ll = MAX_FRAME_SIZE - j; + memcpy(&out_frame[j], line, (size_t)ll); + j += ll; + _serial->writeFrame(out_frame, j); + writeOKFrame(); + handleMeshcomodCommand(cp, (int)strlen(cp)); + return; + } + } + auto pkt = createRawData(&cmd_frame[i], len - i); + if (pkt) { + sendDirect(pkt, path, path_len); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet) + } + } else if (cmd_frame[0] == CMD_SEND_LOGIN && len >= 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + char *password = (char *)&cmd_frame[1 + PUB_KEY_SIZE]; + cmd_frame[len] = 0; // ensure null terminator in password + if (recipient) { + uint32_t est_timeout; + int result = sendLogin(*recipient, password, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + clearPendingReqs(); + memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &pending_login, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_SEND_ANON_REQ && len > 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + ContactInfo anon; + if (recipient == NULL) { // FIRMWARE_VER_CODE 13+, allow non-contact requests + memset(&anon, 0, sizeof(anon)); + memcpy(anon.id.pub_key, pub_key, PUB_KEY_SIZE); + anon.out_path_len = 0; // default to zero-hop direct + anon.type = ADV_TYPE_NONE; // unknown + + if (addContact(anon)) recipient = &anon; + } + uint8_t *data = &cmd_frame[1 + PUB_KEY_SIZE]; + if (recipient) { + uint32_t tag, est_timeout; + int result = sendAnonReq(*recipient, data, len - (1 + PUB_KEY_SIZE), tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + clearPendingReqs(); + pending_req = tag; // match this to onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); // contacts full + } + } else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, REQ_TYPE_GET_STATUS, tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + clearPendingReqs(); + // FUTURE: pending_status = tag; // match this in onContactResponse() + memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && cmd_frame[1] == 0 && len >= 2 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[2]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint32_t tag, est_timeout; + // 'Path Discovery' is just a special case of flood + Telemetry req + uint8_t req_data[9]; + req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA; + req_data[1] = ~(TELEM_PERM_BASE); // NEW: inverse permissions mask (ie. we only want BASE telemetry) + memset(&req_data[2], 0, 3); // reserved + getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique + auto save = recipient->out_path_len; // temporarily force sendRequest() to flood + recipient->out_path_len = -1; + int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout); + recipient->out_path_len = save; + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + clearPendingReqs(); + pending_discovery = tag; // match this in onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { // can deprecate, in favour of CMD_SEND_BINARY_REQ + uint8_t *pub_key = &cmd_frame[4]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, REQ_TYPE_GET_TELEMETRY_DATA, tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + clearPendingReqs(); + pending_telemetry = tag; // match this in onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(0xFF, telemetry); + + int i = 0; + out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], self_id.pub_key, 6); + i += 6; // pub_key_prefix + uint8_t tlen = telemetry.getSize(); + memcpy(&out_frame[i], telemetry.getBuffer(), tlen); + i += tlen; + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_SEND_BINARY_REQ && len >= 2 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint8_t *req_data = &cmd_frame[1 + PUB_KEY_SIZE]; + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, req_data, len - (1 + PUB_KEY_SIZE), tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + clearPendingReqs(); + pending_req = tag; // match this in onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + if (hasConnectionTo(pub_key)) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); + } + } else if (cmd_frame[0] == CMD_LOGOUT && len >= 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + stopConnection(pub_key); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2) { + uint8_t channel_idx = cmd_frame[1]; + ChannelDetails channel; + if (getChannel(channel_idx, channel)) { + int i = 0; + out_frame[i++] = RESP_CODE_CHANNEL_INFO; + out_frame[i++] = channel_idx; + StrHelper::strzcpy((char *)&out_frame[i], channel.name, 32); + i += 32; + memcpy(&out_frame[i], channel.channel.secret, 16); + i += 16; // NOTE: only 128-bit supported + _serial->writeFrame(out_frame, i); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); + } + } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // not supported (yet) + } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) { + uint8_t channel_idx = cmd_frame[1]; + ChannelDetails channel; + StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32); + memset(channel.channel.secret, 0, sizeof(channel.channel.secret)); + memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // NOTE: only 128-bit supported + if (setChannel(channel_idx, channel)) { + saveChannels(); +#ifdef DISPLAY_CLASS + /* Tell the touch UI to refresh its thread list immediately so the new + * channel shows up without waiting for the periodic refresh. */ + if (_ui) _ui->onThreadsChanged(); +#endif + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx + } + } else if (cmd_frame[0] == CMD_SIGN_START) { + out_frame[0] = RESP_CODE_SIGN_START; + out_frame[1] = 0; // reserved + uint32_t len = MAX_SIGN_DATA_LEN; + memcpy(&out_frame[2], &len, 4); + _serial->writeFrame(out_frame, 6); + + if (sign_data) { + free(sign_data); + } + sign_data = (uint8_t *)malloc(MAX_SIGN_DATA_LEN); + sign_data_len = 0; + } else if (cmd_frame[0] == CMD_SIGN_DATA && len > 1) { + if (sign_data == NULL || sign_data_len + (len - 1) > MAX_SIGN_DATA_LEN) { + writeErrFrame(sign_data == NULL ? ERR_CODE_BAD_STATE : ERR_CODE_TABLE_FULL); // error: too long + } else { + memcpy(&sign_data[sign_data_len], &cmd_frame[1], len - 1); + sign_data_len += (len - 1); + writeOKFrame(); + } + } else if (cmd_frame[0] == CMD_SIGN_FINISH) { + if (sign_data) { + self_id.sign(&out_frame[1], sign_data, sign_data_len); + + free(sign_data); // don't need sign_data now + sign_data = NULL; + + out_frame[0] = RESP_CODE_SIGNATURE; + _serial->writeFrame(out_frame, 1 + SIGNATURE_SIZE); + } else { + writeErrFrame(ERR_CODE_BAD_STATE); + } + } else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 < MAX_PACKET_PAYLOAD-5) { + uint8_t path_len = len - 10; + uint8_t flags = cmd_frame[9]; + uint8_t path_sz = flags & 0x03; // NEW v1.11+ + if ((path_len >> path_sz) > MAX_PATH_SIZE || (path_len % (1 << path_sz)) != 0) { // make sure is multiple of path_sz + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else { + uint32_t tag, auth; + memcpy(&tag, &cmd_frame[1], 4); + memcpy(&auth, &cmd_frame[5], 4); + auto pkt = createTrace(tag, auth, flags); + if (pkt) { + sendDirect(pkt, &cmd_frame[10], path_len); + + uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); + uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len >> path_sz); + + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } + } else if (cmd_frame[0] == CMD_SET_DEVICE_PIN && len >= 5) { + + // get pin from command frame + uint32_t pin; + memcpy(&pin, &cmd_frame[1], 4); + + // ensure pin is zero, or a valid 6 digit pin + if (pin == 0 || (pin >= 100000 && pin <= 999999)) { + _prefs.ble_pin = pin; + savePrefs(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { + out_frame[0] = RESP_CODE_CUSTOM_VARS; + char *dp = (char *)&out_frame[1]; + for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < 140; i++) { + if (i > 0) { + *dp++ = ','; + } + strcpy(dp, sensors.getSettingName(i)); + dp = strchr(dp, 0); + *dp++ = ':'; + strcpy(dp, sensors.getSettingValue(i)); + dp = strchr(dp, 0); + } + _serial->writeFrame(out_frame, dp - (char *)out_frame); + } else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { + cmd_frame[len] = 0; + char *sp = (char *)&cmd_frame[1]; + char *np = strchr(sp, ':'); // look for separator char + if (np) { + *np++ = 0; // modify 'cmd_frame', replace ':' with null + bool success = sensors.setSettingValue(sp, np); + if (success) { + #if ENV_INCLUDE_GPS == 1 + // Update node preferences for GPS settings + if (strcmp(sp, "gps") == 0) { + _prefs.gps_enabled = (np[0] == '1') ? 1 : 0; + savePrefs(); + } else if (strcmp(sp, "gps_interval") == 0) { + uint32_t interval_seconds = atoi(np); + _prefs.gps_interval = constrain(interval_seconds, 0, 86400); + savePrefs(); + } + #endif + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { + // FUTURE use: uint8_t reserved = cmd_frame[1]; + uint8_t *pub_key = &cmd_frame[2]; + AdvertPath* found = NULL; + for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { + auto p = &advert_paths[i]; + if (memcmp(p->pubkey_prefix, pub_key, sizeof(p->pubkey_prefix)) == 0) { + found = p; + break; + } + } + if (found) { + out_frame[0] = RESP_CODE_ADVERT_PATH; + memcpy(&out_frame[1], &found->recv_timestamp, 4); + out_frame[5] = found->path_len; + memcpy(&out_frame[6], found->path, found->path_len); + _serial->writeFrame(out_frame, 6 + found->path_len); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); + } + } else if (cmd_frame[0] == CMD_GET_STATS && len >= 2) { + uint8_t stats_type = cmd_frame[1]; + if (stats_type == STATS_TYPE_CORE) { + int i = 0; + out_frame[i++] = RESP_CODE_STATS; + out_frame[i++] = STATS_TYPE_CORE; + uint16_t battery_mv = board.getBattMilliVolts(); + uint32_t uptime_secs = _ms->getMillis() / 1000; + uint8_t queue_len = (uint8_t)_mgr->getOutboundTotal(); + memcpy(&out_frame[i], &battery_mv, 2); i += 2; + memcpy(&out_frame[i], &uptime_secs, 4); i += 4; + memcpy(&out_frame[i], &_err_flags, 2); i += 2; + out_frame[i++] = queue_len; + _serial->writeFrame(out_frame, i); + } else if (stats_type == STATS_TYPE_RADIO) { + int i = 0; + out_frame[i++] = RESP_CODE_STATS; + out_frame[i++] = STATS_TYPE_RADIO; + int16_t noise_floor = (int16_t)_radio->getNoiseFloor(); + int8_t last_rssi = (int8_t)radio_driver.getLastRSSI(); + int8_t last_snr = (int8_t)(radio_driver.getLastSNR() * 4); // scaled by 4 for 0.25 dB precision + uint32_t tx_air_secs = getTotalAirTime() / 1000; + uint32_t rx_air_secs = getReceiveAirTime() / 1000; + memcpy(&out_frame[i], &noise_floor, 2); i += 2; + out_frame[i++] = last_rssi; + out_frame[i++] = last_snr; + memcpy(&out_frame[i], &tx_air_secs, 4); i += 4; + memcpy(&out_frame[i], &rx_air_secs, 4); i += 4; + _serial->writeFrame(out_frame, i); + } else if (stats_type == STATS_TYPE_PACKETS) { + int i = 0; + out_frame[i++] = RESP_CODE_STATS; + out_frame[i++] = STATS_TYPE_PACKETS; + uint32_t recv = radio_driver.getPacketsRecv(); + uint32_t sent = radio_driver.getPacketsSent(); + uint32_t n_sent_flood = getNumSentFlood(); + uint32_t n_sent_direct = getNumSentDirect(); + uint32_t n_recv_flood = getNumRecvFlood(); + uint32_t n_recv_direct = getNumRecvDirect(); + uint32_t n_recv_errors = radio_driver.getPacketsRecvErrors(); + memcpy(&out_frame[i], &recv, 4); i += 4; + memcpy(&out_frame[i], &sent, 4); i += 4; + memcpy(&out_frame[i], &n_sent_flood, 4); i += 4; + memcpy(&out_frame[i], &n_sent_direct, 4); i += 4; + memcpy(&out_frame[i], &n_recv_flood, 4); i += 4; + memcpy(&out_frame[i], &n_recv_direct, 4); i += 4; + memcpy(&out_frame[i], &n_recv_errors, 4); i += 4; + _serial->writeFrame(out_frame, i); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type + } + } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) { + if (_serial) { + MESH_DEBUG_PRINTLN("Factory reset: disabling serial interface to prevent reconnects (BLE/WiFi)"); + _serial->disable(); // Phone app disconnects before we can send OK frame so it's safe here + } + bool success = _store->formatFileSystem(); + if (success) { + writeOKFrame(); + delay(1000); + board.reboot(); // doesn't return + } else { + writeErrFrame(ERR_CODE_FILE_IO_ERROR); + } + } else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 0) { + if (len >= 2 + 16) { + memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set scope override TransportKey + } else { + memset(send_scope.key, 0, sizeof(send_scope.key)); // reset scope override + } + send_unscoped = false; + writeOKFrame(); + } else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 1) { // ver 12+ (1.16 send_unscoped; fork keeps its cmd name/number 54) + send_unscoped = true; + writeOKFrame(); + } else if (cmd_frame[0] == CMD_SEND_CONTROL_DATA && len >= 2 && (cmd_frame[1] & 0x80) != 0) { + auto resp = createControlData(&cmd_frame[1], len - 1); + if (resp) { + sendZeroHop(resp); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) { + _prefs.autoadd_config = cmd_frame[1]; + if (len >= 3) { + uint8_t mh = cmd_frame[2]; + _prefs.autoadd_max_hops = mh > 64 ? 64 : mh; + } + savePrefs(); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) { + int i = 0; + out_frame[i++] = RESP_CODE_AUTOADD_CONFIG; + out_frame[i++] = _prefs.autoadd_config; + out_frame[i++] = _prefs.autoadd_max_hops; + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) { + int i = 0; + out_frame[i++] = RESP_ALLOWED_REPEAT_FREQ; + for (int k = 0; k < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]) && i + 8 < sizeof(out_frame); k++) { + auto r = &repeat_freq_ranges[k]; + memcpy(&out_frame[i], &r->lower_freq, 4); i += 4; + memcpy(&out_frame[i], &r->upper_freq, 4); i += 4; + } + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_SEND_RAW_PACKET && len >= 4) { + auto pkt = obtainNewPacket(); + if (pkt) { + uint8_t priority = cmd_frame[1]; + if (tryParsePacket(pkt, &cmd_frame[2], len - 2)) { + sendPacket(pkt, priority, 0); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); + } +} + +static bool save_filter(const ContactInfo& c) { + return c.type != ADV_TYPE_NONE; // don't save the transient/anon entries +} + +void MyMesh::saveContacts() { + _store->saveContacts(this, save_filter); +} + +void MyMesh::enterCLIRescue() { + _cli_rescue = true; + cli_command[0] = 0; + Serial.println("========= CLI Rescue ========="); +} + +void MyMesh::checkCLIRescueCmd() { + int len = strlen(cli_command); + bool line_complete = false; + while (Serial.available() && len < sizeof(cli_command)-1) { + if (Serial.peek() == '<') { + cli_command[0] = 0; + return; + } + char c = Serial.read(); + if (c == '\r' || c == '\n') { + line_complete = true; + Serial.print(c); // echo + break; + } else { + cli_command[len++] = c; + cli_command[len] = 0; + Serial.print(c); // echo + } + } + if (len == sizeof(cli_command)-1) { // command buffer full + line_complete = true; + } + + if (line_complete && len > 0) { // received complete line + + if (memcmp(cli_command, "set ", 4) == 0) { + const char* config = &cli_command[4]; + if (memcmp(config, "pin ", 4) == 0) { + _prefs.ble_pin = atoi(&config[4]); + savePrefs(); + Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); + } else if (memcmp(config, "wifi.ssid ", 10) == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + const char* ssid = &config[10]; + if (wifiConfigSetSsid((char*)ssid)) { + Serial.println(" > OK: wifi ssid set"); + } else { + Serial.println(" Error: invalid/too long SSID"); + } +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (memcmp(config, "wifi.pwd ", 9) == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + const char* pwd = &config[9]; + if (wifiConfigSetPwd((char*)pwd)) { + Serial.println(" > OK: wifi password set"); + } else { + Serial.println(" Error: password too long"); + } +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (memcmp(config, "wifi.radio ", 11) == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + int v = atoi(&config[11]); + wifiConfigSetRadioEnabled(v != 0); + Serial.println(v ? " > OK: wifi radio on" : " > OK: wifi radio off"); +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (strcmp(config, "wifi.apply") == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + if (!wifiConfigGetRadioEnabled()) { + Serial.println(" Error: wifi radio off; set wifi.radio 1 first"); + } else if (!wifiConfigHasRuntime()) { + Serial.println(" Error: no runtime credentials set"); + } else { + wifiConfigApply(); + Serial.println(" > OK: reconnecting WiFi"); + } +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (strcmp(config, "wifi.clear") == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + wifiConfigClear(); + Serial.println(" > OK: wifi credentials cleared"); +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else { + Serial.printf(" Error: unknown config: %s\n", config); + } + } else if (strcmp(cli_command, "get wifi.ssid") == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + char ssid[WIFI_CONFIG_SSID_MAX]; + wifiConfigGetSsid(ssid, sizeof(ssid)); + Serial.printf(" > %s\n", ssid[0] ? ssid : "(none)"); +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (strcmp(cli_command, "get wifi.status") == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + char ssid[WIFI_CONFIG_SSID_MAX]; + wifiConfigGetSsid(ssid, sizeof(ssid)); + bool has_runtime = wifiConfigHasRuntime(); + int re = wifiConfigGetRadioEnabled() ? 1 : 0; + Serial.printf(" > runtime=%d radio_enabled=%d ssid=%s\n", has_runtime ? 1 : 0, re, + (ssid[0] ? ssid : "(none)")); + if (WiFi.status() == WL_CONNECTED) { + IPAddress ip = WiFi.localIP(); + Serial.printf(" > connected=1 ip=%d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); + } else { + Serial.println(" > connected=0"); + } +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (strcmp(cli_command, "wifi.status") == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + char ssid[WIFI_CONFIG_SSID_MAX]; + wifiConfigGetSsid(ssid, sizeof(ssid)); + bool has_runtime = wifiConfigHasRuntime(); + int re = wifiConfigGetRadioEnabled() ? 1 : 0; + Serial.printf(" > runtime=%d radio_enabled=%d ssid=%s\n", has_runtime ? 1 : 0, re, + (ssid[0] ? ssid : "(none)")); + if (WiFi.status() == WL_CONNECTED) { + IPAddress ip = WiFi.localIP(); + Serial.printf(" > connected=1 ip=%d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); + } else { + Serial.println(" > connected=0"); + } +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (strcmp(cli_command, "wifi.apply") == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + if (!wifiConfigGetRadioEnabled()) { + Serial.println(" Error: wifi radio off; set wifi.radio 1 first"); + } else if (!wifiConfigHasRuntime()) { + Serial.println(" Error: no runtime credentials set"); + } else { + wifiConfigApply(); + Serial.println(" > OK: reconnecting WiFi"); + } +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (strcmp(cli_command, "wifi.clear") == 0) { +#ifdef ESP32 +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + wifiConfigClear(); + Serial.println(" > OK: wifi credentials cleared"); +#else + Serial.println(" Error: WiFi config not enabled in this build"); +#endif +#else + Serial.println(" Error: WiFi config only supported on ESP32 builds"); +#endif + } else if (strcmp(cli_command, "rebuild") == 0) { + bool success = _store->formatFileSystem(); + if (success) { + _store->saveMainIdentity(self_id); + savePrefs(); + saveContacts(); + saveChannels(); + Serial.println(" > erase and rebuild done"); + } else { + Serial.println(" Error: erase failed"); + } + } else if (strcmp(cli_command, "erase") == 0) { + bool success = _store->formatFileSystem(); + if (success) { + Serial.println(" > erase done"); + } else { + Serial.println(" Error: erase failed"); + } + } else if (memcmp(cli_command, "ls", 2) == 0) { + + // get path from command e.g: "ls /adafruit" + const char *path = &cli_command[3]; + + bool is_fs2 = false; + if (memcmp(path, "UserData/", 9) == 0) { + path += 8; // skip "UserData" + } else if (memcmp(path, "ExtraFS/", 8) == 0) { + path += 7; // skip "ExtraFS" + is_fs2 = true; + } + Serial.printf("Listing files in %s\n", path); + + // log each file and directory + File root = _store->openRead(path); + if (is_fs2 == false) { + if (root) { + File file = root.openNextFile(); + while (file) { + if (file.isDirectory()) { + Serial.printf("[dir] UserData%s/%s\n", path, file.name()); + } else { + Serial.printf("[file] UserData%s/%s (%d bytes)\n", path, file.name(), file.size()); + } + // move to next file + file = root.openNextFile(); + } + root.close(); + } + } + + if (is_fs2 == true || strlen(path) == 0 || strcmp(path, "/") == 0) { + if (_store->getSecondaryFS() != nullptr) { + File root2 = _store->openRead(_store->getSecondaryFS(), path); + File file = root2.openNextFile(); + while (file) { + if (file.isDirectory()) { + Serial.printf("[dir] ExtraFS%s/%s\n", path, file.name()); + } else { + Serial.printf("[file] ExtraFS%s/%s (%d bytes)\n", path, file.name(), file.size()); + } + // move to next file + file = root2.openNextFile(); + } + root2.close(); + } + } + } else if (memcmp(cli_command, "cat", 3) == 0) { + + // get path from command e.g: "cat /contacts3" + const char *path = &cli_command[4]; + + bool is_fs2 = false; + if (memcmp(path, "UserData/", 9) == 0) { + path += 8; // skip "UserData" + } else if (memcmp(path, "ExtraFS/", 8) == 0) { + path += 7; // skip "ExtraFS" + is_fs2 = true; + } else { + Serial.println("Invalid path provided, must start with UserData/ or ExtraFS/"); + cli_command[0] = 0; + return; + } + + // log file content as hex + File file = _store->openRead(path); + if (is_fs2 == true) { + file = _store->openRead(_store->getSecondaryFS(), path); + } + if(file){ + + // get file content + int file_size = file.available(); + uint8_t buffer[file_size]; + file.read(buffer, file_size); + + // print hex + mesh::Utils::printHex(Serial, buffer, file_size); + Serial.print("\n"); + + file.close(); + + } + + } else if (memcmp(cli_command, "rm ", 3) == 0) { + // get path from command e.g: "rm /adv_blobs" + const char *path = &cli_command[3]; + MESH_DEBUG_PRINTLN("Removing file: %s", path); + // ensure path is not empty, or root dir + if(!path || strlen(path) == 0 || strcmp(path, "/") == 0){ + Serial.println("Invalid path provided"); + } else { + bool is_fs2 = false; + if (memcmp(path, "UserData/", 9) == 0) { + path += 8; // skip "UserData" + } else if (memcmp(path, "ExtraFS/", 8) == 0) { + path += 7; // skip "ExtraFS" + is_fs2 = true; + } + + // remove file + bool removed; + if (is_fs2) { + MESH_DEBUG_PRINTLN("Removing file from ExtraFS: %s", path); + removed = _store->removeFile(_store->getSecondaryFS(), path); + } else { + MESH_DEBUG_PRINTLN("Removing file from UserData: %s", path); + removed = _store->removeFile(path); + } + if(removed){ + Serial.println("File removed"); + } else { + Serial.println("Failed to remove file"); + } + + } + + } else if (strcmp(cli_command, "reboot") == 0) { + board.reboot(); // doesn't return + } else { + Serial.println(" Error: unknown command"); + } + + cli_command[0] = 0; // reset command buffer + } +} + +void MyMesh::checkSerialInterface() { + bool handled_cmd = false; + // Drain a small burst of inbound frames each loop to reduce sync latency under load. + for (int n = 0; n < 4; n++) { + size_t len = _serial->checkRecvFrame(cmd_frame); + if (len == 0) break; + handled_cmd = true; + handleCmdFrame(len); + } + if (!handled_cmd && _iter_started // check if our ContactsIterator is 'running' + && !_serial->isWriteBusy() // don't spam the Serial Interface too quickly! + ) { + // Restore reply target so CONTACT/END go to the client that got START (fixes WS/TCP when USB is polled first) + _serial->setReplyTarget(_contact_list_reply_target); + ContactInfo contact; + bool found = false; + while (_iter.hasNext(this, contact)) { + if (contact.type != ADV_TYPE_NONE) { + found = true; + break; + } + } + + if (found) { + if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter + // Retry so transient full buffers (TCP/WebSocket) don't drop CONTACT frames + const int max_retries = 10; + size_t sent = 0; + for (int r = 0; r < max_retries && sent == 0; r++) { + if (r > 0) delay(5); + sent = writeContactRespFrame(RESP_CODE_CONTACT, contact); + } + _contact_send_index++; + if (contact.lastmod > _most_recent_lastmod) { + _most_recent_lastmod = contact.lastmod; // save for the RESP_CODE_END_OF_CONTACTS frame + } + } + } else { // EOF + ContactInfo meshcomod; + getMeshcomodContact(meshcomod); + // Retry meshcomod CONTACT and END so WiFi/WebSocket get full sequence + const int max_retries = 10; + size_t sent = 0; + for (int r = 0; r < max_retries && sent == 0; r++) { + if (r > 0) delay(5); + sent = writeContactRespFrame(RESP_CODE_CONTACT, meshcomod); + } + out_frame[0] = RESP_CODE_END_OF_CONTACTS; + memcpy(&out_frame[1], &_most_recent_lastmod, + 4); // include the most recent lastmod, so app can update their 'since' + sent = 0; + for (int r = 0; r < max_retries && sent != 5; r++) { + if (r > 0) delay(5); + sent = _serial->writeFrame(out_frame, 5); + } + _iter_started = false; + } + //} else if (!_serial->isWriteBusy()) { + // checkConnections(); // TODO - deprecate the 'Connections' stuff + } +} + +void MyMesh::loop() { + BaseChatMesh::loop(); + + if (_cli_rescue) { + checkCLIRescueCmd(); + } else { + // Prefer plain-text console commands (e.g. flasher Console) before binary + // companion parsing — but only when the first byte looks like a command + // letter (a-z, A-Z). App binary frames use command bytes 1–62; 32–62 are + // printable, so we must not treat them as console or we break USB app connection. +#if defined(ESP32) + if (Serial.available() > 0) { + int first = Serial.peek(); + // Web consoles often send CRLF. Drop a leading line-ending byte so a + // stale '\r'/'\n' cannot block the next plain-text command. + if (first == '\r' || first == '\n') { + Serial.read(); + if (Serial.available() > 0) { + first = Serial.peek(); + } + } + if (first == '<') { + // Companion frame marker: handled below by checkSerialInterface(). + } else if ((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')) { + checkCLIRescueCmd(); + } + } +#endif + // Always process companion frames in the same loop so TCP/BLE clients cannot + // be starved by plain-text console traffic on USB Serial. + checkSerialInterface(); + } + + // is there are pending dirty contacts write needed? + if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { + saveContacts(); + dirty_contacts_expiry = 0; + } + +#if defined(ENABLE_ADVERT_ON_BOOT) && ENABLE_ADVERT_ON_BOOT == 1 + // Fire the one-shot boot advert when the scheduled time passes. Flood so + // it reaches peers across repeaters, refreshing any stale pubkey for us + // in their contact lists (with auto-add on). Without this, DMs from us + // silently MAC-fail at peers that still have a prior identity. + if (!_boot_advert_done && _boot_advert_due_ms != 0 && + _ms->getMillis() >= _boot_advert_due_ms) { + _boot_advert_done = true; + bool ok = sendAdvert(true); +#ifdef DISPLAY_CLASS + if (_ui) { + char dbg[64]; + snprintf(dbg, sizeof(dbg), "TX self-advert flood %s", + ok ? "ok" : "FAIL"); + _ui->appendDiag(dbg); + } +#else + (void)ok; +#endif + } +#endif + +#ifdef DISPLAY_CLASS + if (_ui) _ui->setHasConnection(_serial->isConnected()); +#endif +} + +#if defined(ESP32) && (defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION)) +void MyMesh::pushCompanionOtaProgressLine(const char* line) { + if (!line || !_serial) return; +#if defined(MULTI_TRANSPORT_COMPANION) + if (s_companion_ota_pinned_reply_target >= 0) + _serial->setReplyTarget(s_companion_ota_pinned_reply_target); +#endif + if (!_serial->isConnected()) return; + int j = 0; + out_frame[j++] = PUSH_CODE_BINARY_RESPONSE; + out_frame[j++] = 0; + uint32_t tag = 0; + memcpy(&out_frame[j], &tag, 4); + j += 4; + int ll = (int)strlen(line); + if (j + ll > MAX_FRAME_SIZE) ll = MAX_FRAME_SIZE - j; + memcpy(&out_frame[j], line, (size_t)ll); + j += ll; + _serial->writeFrame(out_frame, j); +} + +void meshcoreRepeaterTcpOtaEmitLine(const char* line) { + the_mesh.pushCompanionOtaProgressLine(line); +#ifdef MULTI_TRANSPORT_COMPANION + if (line && line[0]) + Serial.printf("%s\n", line); +#endif +} +#endif + +bool MyMesh::advert() { + return sendAdvert(false); // backward-compat: original advert() was zero-hop +} + +bool MyMesh::sendAdvert(bool flood) { + mesh::Packet* pkt; + if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { + pkt = createSelfAdvert(_prefs.node_name); + } else { + pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); + } + if (!pkt) return false; + if (flood) { + // path_hash_size = path_hash_mode + 1 (matches the CMD_SEND_ADVERT path). + sendFlood(pkt, 0, (uint8_t)(_prefs.path_hash_mode + 1)); + } else { + sendZeroHop(pkt); + } + return true; +} + +// To check if there is pending work +bool MyMesh::hasPendingWork() const { + return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0; +} diff --git a/src/MyMesh.h b/src/MyMesh.h new file mode 100644 index 00000000..67ce04a2 --- /dev/null +++ b/src/MyMesh.h @@ -0,0 +1,811 @@ +#pragma once + +#include +#include +#include +#include +#include "AbstractUITask.h" + +/*------------ Frame Protocol --------------*/ +// Keep the fork's companion-protocol lineage (27 — a superset of upstream's 13) +// so the existing meshcomod web/phone client still matches. 1.16's new companion +// commands (raw packet, un-scoped flood, anon req) are added as handlers below; +// advertising them by bumping this is a coordinated client change for later. +#define FIRMWARE_VER_CODE 27 + +#ifndef FIRMWARE_BUILD_DATE +#define FIRMWARE_BUILD_DATE "7 Jun 2026" +#endif + +#ifndef FIRMWARE_VERSION +#define FIRMWARE_VERSION "v1.16.0-touch" +#endif + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#include +#elif defined(RP2040_PLATFORM) +#include +#elif defined(ESP32) +#include +#endif + +#include "DataStore.h" +#include "NodePrefs.h" + +/* Synthetic local command contact: appears in contact list, messages to it are intercepted and never sent over mesh. */ +#define MESHCOMOD_NAME "Meshcomod" + +#include +#include +#include +#include +#include +#include +#include + +/* ---------------------------------- CONFIGURATION ------------------------------------- */ + +#ifndef LORA_FREQ +#define LORA_FREQ 915.0 +#endif +#ifndef LORA_BW +#define LORA_BW 250 +#endif +#ifndef LORA_SF +#define LORA_SF 10 +#endif +#ifndef LORA_CR +#define LORA_CR 5 +#endif +#ifndef LORA_TX_POWER +#define LORA_TX_POWER 20 +#endif +#ifndef MAX_LORA_TX_POWER +#define MAX_LORA_TX_POWER LORA_TX_POWER +#endif + +#ifndef MAX_CONTACTS +#define MAX_CONTACTS 100 +#endif + +#ifndef OFFLINE_QUEUE_SIZE +#define OFFLINE_QUEUE_SIZE 16 +#endif + +#ifndef HISTORY_RING_SIZE +#define HISTORY_RING_SIZE 128 +#endif +#ifndef MAX_HISTORY_CLIENTS +#define MAX_HISTORY_CLIENTS 8 +#endif +#ifndef MAX_CLIENT_ID_LEN +#define MAX_CLIENT_ID_LEN 31 +#endif + +#ifndef BLE_NAME_PREFIX +#define BLE_NAME_PREFIX "MeshCore-" +#endif + +#include +#include + +/* -------------------------------------------------------------------------------------- */ + +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 + +struct AdvertPath { + uint8_t pubkey_prefix[7]; + uint8_t path_len; + char name[32]; + uint32_t recv_timestamp; + uint8_t path[MAX_PATH_SIZE]; +}; + +class MyMesh : public BaseChatMesh, public DataStoreHost { +public: + MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); + + void begin(bool has_display); + void startInterface(BaseSerialInterface &serial); + + const char *getNodeName(); + NodePrefs *getNodePrefs(); + uint32_t getBLEPin(); + + // Live device info accessors (used by the touch Settings → Device modal to + // mirror the web client's "Device (live)" panel — public key prefix, channel + // count, etc.). self_id and num_channels live in `protected:` of the base + // classes, so we expose narrow getters here. + const uint8_t *getSelfPubKey() const { return self_id.pub_key; } + /** Export this node's 64-byte private key (settings backup). */ + size_t uiExportPrivKey(uint8_t dest[64]) { return self_id.writeTo(dest, 64); } + /** Import a 64-byte private key from a settings backup: validate, persist, + * swap the active identity, reload contacts (invalidate shared secrets). + * Returns false on an invalid key or storage failure. */ + bool uiImportPrivKey(const uint8_t src[64]) { + if (!mesh::LocalIdentity::validatePrivateKey(src)) return false; + mesh::LocalIdentity identity; + identity.readFrom(src, 64); + if (!_store->saveMainIdentity(identity)) return false; + self_id = identity; + resetContacts(); + _store->loadContacts(this); + return true; + } + /** Add/merge a contact from a settings backup (name + pubkey + flags + GPS). + * Returns false if the slot table is full. Mirrors how addContact works. */ + bool uiAddContactFromBackup(const uint8_t pub_key[32], const char* name, uint8_t type, + uint8_t flags, int32_t lat, int32_t lon, + uint32_t last_advert, uint32_t lastmod) { + ContactInfo c{}; + memcpy(c.id.pub_key, pub_key, 32); + c.type = type; c.flags = flags; + c.gps_lat = lat; c.gps_lon = lon; + c.last_advert_timestamp = last_advert; c.lastmod = lastmod; + c.out_path_len = OUT_PATH_UNKNOWN; + strncpy(c.name, name ? name : "", sizeof(c.name) - 1); + return addContact(c); + } + // Channel count by iterating slots — `num_channels` is private in the base. + // A channel slot is in-use when its name is non-empty (matches the lookup + // pattern used elsewhere in the firmware). + int getNumChannels() { +#ifdef MAX_GROUP_CHANNELS + int n = 0; + for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) { + ChannelDetails cd{}; + if (getChannel(i, cd) && cd.name[0] != '\0') ++n; + } + return n; +#else + return 0; +#endif + } + + void loop(); + void handleCmdFrame(size_t len); + + // ---- Settings backup (MeshCore-app-compatible JSON) ---- + /** Write the full backup JSON (name, public/private key, radio + position + * settings, channels, contacts) to `out`. Matches the stock MeshCore app + * export shape so the file opens in the app / web client. */ + void uiExportBackup(Print& out, double node_lat, double node_lon); + /** Apply a parsed backup. `sections` bit flags choose what to apply: + * bit0=identity(name+key) bit1=radio bit2=position bit3=channels bit4=contacts. + * `replace_*` clears existing channels/contacts first. Returns false on a + * malformed document. Fills counts (channels/contacts applied) if non-null. */ + bool uiImportBackup(Stream& in, uint8_t sections, + bool replace_channels, bool replace_contacts, + int* out_channels, int* out_contacts); + /** Factory reset: wipe ALL persistent data — formats the internal filesystem + * (identity, contacts, channels, saved messages, exported backups) and erases + * NVS (Wi-Fi credentials + every setting). The caller MUST reboot immediately + * after; a fresh identity is generated on the next boot. Returns false if the + * format/erase failed. */ + bool uiFactoryReset() { return _store->formatFileSystem(); } + bool advert(); + /** Send advert with explicit routing: true = flood (multi-hop), false = zero-hop. + * `advert()` (zero-hop) is kept for backward compatibility. */ + bool sendAdvert(bool flood); + void enterCLIRescue(); + + int getRecentlyHeard(AdvertPath dest[], int max_num); + + // On-device terminal: register an output sink (nullptr = off) and run a local + // CLI command. Replies flow through pushMeshcomodReply -> the sink. + static void setTerminalSink(void (*cb)(const char* line)); + void runLocalCli(const char* cmd); + +#if defined(ESP32) && (defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION)) + /** HTTP OTA: one UTF-8 line as PUSH_CODE_BINARY_RESPONSE (ESP32Board → meshcoreRepeaterTcpOtaEmitLine). */ + void pushCompanionOtaProgressLine(const char* line); +#endif + +protected: + float getAirtimeBudgetFactor() const override; + int getInterferenceThreshold() const override; + int calcRxDelay(float score, uint32_t air_time) const override; + uint8_t getExtraAckTransmitCount() const override; + uint8_t getAutoAddMaxHops() const override; + bool filterRecvFloodPacket(mesh::Packet* packet) override; + bool allowPacketForward(const mesh::Packet* packet) override; + + void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis); + // Meshcomod: synthetic local command contact (no RF) + static const uint8_t MESHCOMOD_PUB_KEY_PREFIX[6]; + void getMeshcomodContact(ContactInfo& dest); + bool isMeshcomodRecipient(const uint8_t* pub_key_prefix_6) const; + bool handleMeshcomodCommand(const char* text, int text_len); + void pushMeshcomodReply(const char* text, bool immediate_current = false); + + void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; + void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override; + + void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; + bool isAutoAddEnabled() const override; + bool shouldAutoAddContactType(uint8_t type) const override; + bool shouldOverwriteWhenFull() const override; + void onContactsFull() override; + void onContactOverwrite(const uint8_t* pub_key) override; + bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onContactPathUpdated(const ContactInfo &contact) override; + ContactInfo* processAck(const uint8_t *data) override; + void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp, + const uint8_t *extra, int extra_len, const char *text); + + void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text) override; + void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text) override; + void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const uint8_t *sender_prefix, const char *text) override; + void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, + const char *text) override; + + uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, + uint8_t len, uint8_t *reply) override; + void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override; + void onControlDataRecv(mesh::Packet *packet) override; + void onRawDataRecv(mesh::Packet *packet) override; + void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override; + + uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override; + uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override; + void onSendTimeout() override; + + // DataStoreHost methods + bool onContactLoaded(const ContactInfo& contact) override { return addContact(contact); } + bool getContactForSave(uint32_t idx, ContactInfo& contact) override { return getContactByIdx(idx, contact); } + bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) override { return setChannel(channel_idx, ch); } + bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); } + + void clearPendingReqs() { + pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0; + } + +public: + /** Which kind of touch-UI request is currently in flight, so the response + * matcher in onContactResponse routes to the right callback. */ + enum class UiReqKind : uint8_t { None = 0, Status = 1, Telemetry = 2 }; + + /** Fire a REQ_TYPE_GET_STATUS from the touch UI side. Result is delivered + * via AbstractUITask::onPingReply when the reply arrives. + * Returns the MSG_SEND_* result code. */ + int sendStatusPingForUI(ContactInfo& recipient) { + uint32_t tag = 0, est = 0; + /* Clear any stale serial-side status pending so the legacy match doesn't + * eat our reply before the UI hook sees it. */ + pending_status = 0; + int r = sendRequest(recipient, REQ_TYPE_GET_STATUS, tag, est); + if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { + memcpy(&_ui_pending_status, recipient.id.pub_key, 4); + _ui_pending_kind = UiReqKind::Status; + _ui_pending_tag = tag; // request tag, reflected by the repeater + } + return r; + } + + /** Same as sendStatusPingForUI, but sends an unauthenticated guest LOGIN + * first. Repeaters need the sender to be in their ACL before they will + * decrypt a PAYLOAD_TYPE_REQ; the ACL is only populated by handleLoginReq. + * An empty-password sendLogin matches repeaters whose guest_password is + * blank (the typical out-of-box default) and adds us as a guest. The + * dispatcher serializes outbound TX, so the LOGIN ANON_REQ reaches the + * destination before the STATUS REQ that follows. + * No-op if the LOGIN packet pool is empty; falls through to send the + * STATUS REQ anyway so a repeater that already knows us still replies. */ + int sendStatusPingWithGuestLoginForUI(ContactInfo& recipient) { + uint32_t login_est = 0; + sendLogin(recipient, "", login_est); + return sendStatusPingForUI(recipient); + } + + /** Same chained-login flavour for telemetry — repeaters and sensors also + * require ACL membership for REQ_TYPE_GET_TELEMETRY_DATA. */ + int sendTelemetryRequestWithGuestLoginForUI(ContactInfo& recipient) { + uint32_t login_est = 0; + sendLogin(recipient, "", login_est); + return sendTelemetryRequestForUI(recipient); + } + + /** Admin login for the touch UI repeater admin console. + * Sends a sendLogin with the given password (empty = guest), records + * pending_login so onContactResponse's existing login branch can route + * the response. The same branch now also fires + * AbstractUITask::onAdminLoginResult so the UI can flip from "logging + * in…" to "logged in" (or "failed"). */ + int uiSendAdminLogin(ContactInfo& recipient, const char* password) { + uint32_t est = 0; + int r = sendLogin(recipient, password ? password : "", est); + if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { + memcpy(&pending_login, recipient.id.pub_key, 4); + } + // Diagnostic (room-server login trace): which contact/type we sent the login + // to and the send result. Pair with the "[ROOM] login resp" line in + // onContactResponse to see exactly where a room login breaks. + Serial.printf("[ROOM] login send '%s' type=%u pwlen=%u sync_since=%lu -> r=%d\n", + recipient.name, (unsigned)recipient.type, + (unsigned)(password ? strlen(password) : 0), + (unsigned long)recipient.sync_since, r); + return r; + } + + /** Send a CLI command line to a previously-logged-in repeater / room + * server. The reply lands in AbstractUITask::onAdminCommandReply via + * queueMessage when the server returns a TXT_TYPE_CLI_DATA frame. + * No-op if recipient isn't in our contacts; returns MSG_SEND_FAILED. */ + int uiSendAdminCommand(ContactInfo& recipient, const char* text) { + if (!text || !text[0]) return MSG_SEND_FAILED; + uint32_t est = 0; + uint32_t ts = getRTCClock()->getCurrentTimeUnique(); + return sendCommandData(recipient, ts, 0, text, est); + } + /** Cancel UI ping pending state (used by timeout). */ + void cancelUIPingPending() { + _ui_pending_status = 0; + _ui_pending_kind = UiReqKind::None; + _ui_pending_tag = 0; + } + /** True if a UI ping is still waiting on a reply. */ + bool hasUIPingPending() const { return _ui_pending_status != 0; } + + /** Register an expected ACK hash that came out of a touch-UI sendMessage + * call, so MyMesh::processAck can match the inbound ACK and dispatch + * onMessageAcked back to the UI. The companion-serial CMD_SEND_TXT_MSG + * handler already does this for app-originated messages; the touch UI + * reaches sendMessage directly and skipped this until now. */ + void uiRegisterExpectedAck(uint32_t expected_ack, const uint8_t pub_key[32]) { + if (expected_ack == 0) return; + ContactInfo* c = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (!c) return; + expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); + expected_ack_table[next_ack_idx].ack = expected_ack; + expected_ack_table[next_ack_idx].contact = c; + // EXPECTED_ACK_TABLE_SIZE is #defined further down in this header next + // to the table itself; hard-code 8 here so this inline helper compiles + // wherever it's used. + next_ack_idx = (next_ack_idx + 1) % 8; + } + + // ---- "Repeats heard" for sent floods + route of the last received flood ---- + // When we originate a flood TXT, repeaters re-broadcast it and our own radio + // hears the echoes (same payload, longer path). We fingerprint the payload at + // send time (sendFloodScoped) and match echoes in logRxRaw, counting repeats + // per recent send. The touch UI stamps its outgoing bubble with the + // fingerprint (uiLastSentFp) and later reads the count (uiRepeatsForFp). + // _last_rx_path holds the path hashes of the just-received flood so the UI's + // newMsg* handler (called synchronously next) can stash the inbound route. + static const int UI_ECHO_SLOTS = 12; + uint32_t _echo_fp[UI_ECHO_SLOTS] = {0}; + uint8_t _echo_rep[UI_ECHO_SLOTS] = {0}; + uint8_t _echo_idx = 0; + uint32_t _last_sent_fp = 0; + uint8_t _last_rx_path[32] = {0}; + uint8_t _last_rx_path_n = 0; + uint16_t _last_rx_scope = 0; // transport_codes[0] of the last RX flood ("scope") + bool _last_rx_has_scope = false; // false if the packet carried no transport codes + volatile bool _echo_dirty = false; // a repeat was counted -> UI should refresh + + /** Fingerprint of the most-recently originated flood TXT payload (0 if none). */ + uint32_t uiLastSentFp() const { return _last_sent_fp; } + + /** Echoes (repeater re-broadcasts) heard of the flood TXT with this payload + * fingerprint. 0 if unknown / evicted from the ring. */ + uint8_t uiRepeatsForFp(uint32_t fp) const { + if (fp == 0) return 0; + for (int i = 0; i < UI_ECHO_SLOTS; i++) if (_echo_fp[i] == fp) return _echo_rep[i]; + return 0; + } + + /** Path (repeater hashes) of the most-recently received flood; copies up to + * `max` bytes into buf, returns the count. Read synchronously from the + * newMsg* handler that follows reception. */ + uint8_t lastRxPath(uint8_t* buf, uint8_t max) const { + uint8_t n = _last_rx_path_n < max ? _last_rx_path_n : max; + if (buf && n) memcpy(buf, _last_rx_path, n); + return n; + } + /** Scope (transport_codes[0]) of the last received flood; *has = false when + * the packet carried no transport codes. */ + uint16_t lastRxScope(bool* has) const { if (has) *has = _last_rx_has_scope; return _last_rx_scope; } + /** Capture route + scope of a just-received flood for the Info popup. Call + * synchronously right before the newMsg* notification. */ + void uiStashRxMeta(mesh::Packet* pkt) { + _last_rx_path_n = 0; + if (pkt && pkt->isRouteFlood()) { + int nb = (int)pkt->getPathHashCount() * (int)pkt->getPathHashSize(); + if (nb > (int)sizeof(_last_rx_path)) nb = (int)sizeof(_last_rx_path); + if (nb > 0) { memcpy(_last_rx_path, pkt->path, nb); _last_rx_path_n = (uint8_t)nb; } + } + const uint8_t rt = pkt ? pkt->getRouteType() : 0xFF; + _last_rx_has_scope = (rt == ROUTE_TYPE_TRANSPORT_FLOOD || rt == ROUTE_TYPE_TRANSPORT_DIRECT); + _last_rx_scope = (_last_rx_has_scope && pkt) ? pkt->transport_codes[0] : 0; + } + + /** Track a freshly-sent flood TXT fingerprint (called from sendFloodScoped). */ + void uiTrackSentFp(uint32_t fp) { + if (fp == 0) return; + _last_sent_fp = fp; + for (int i = 0; i < UI_ECHO_SLOTS; i++) if (_echo_fp[i] == fp) { _echo_rep[i] = 0; return; } + _echo_fp[_echo_idx] = fp; _echo_rep[_echo_idx] = 0; + _echo_idx = (uint8_t)((_echo_idx + 1) % UI_ECHO_SLOTS); + } + + /** Count one echo of fingerprint `fp` (called from logRxRaw on a match). */ + void uiCountEcho(uint32_t fp) { + for (int i = 0; i < UI_ECHO_SLOTS; i++) + if (_echo_fp[i] == fp) { if (_echo_rep[i] < 255) _echo_rep[i]++; _echo_dirty = true; return; } + } + /** True once if a repeat was counted since the last call — the UI uses this + * to refresh the chat so the bubble's repeat tag updates live. */ + bool takeEchoDirty() { bool d = _echo_dirty; _echo_dirty = false; return d; } + + /** Send a 0-hop trace ping to a single neighbor (typically a repeater). + * Returns the trace tag we chose (non-zero on success) so the UI can + * match the onTracePingResult callback. The trace path is just the + * neighbor's hash; when they retransmit, our radio picks it up and the + * dispatcher fires onTraceRecv at us with the SNRs in both directions. + * Returns 0 on failure. */ + uint32_t uiSendTracePing(const uint8_t pub_key[32]) { + if (!pub_key) return 0; + uint8_t hash_sz = (uint8_t)(_prefs.path_hash_mode + 1); + if (hash_sz == 0 || hash_sz > 4) hash_sz = 1; + uint8_t flags = (uint8_t)(_prefs.path_hash_mode & 0x03); + uint32_t tag = 0; + getRNG()->random((uint8_t*)&tag, sizeof(tag)); + if (tag == 0) tag = 1; + // Stash so onTraceRecv can route the right callback to the UI. + _ui_trace_ping_tag = tag; + mesh::Packet* pkt = createTrace(tag, 0, flags); + if (!pkt) { _ui_trace_ping_tag = 0; return 0; } + sendDirect(pkt, pub_key, hash_sz); + return tag; + } + + /** Send a FULL-ROUTE trace toward `c` along its entire known path, so every + * repeater on the way appends its RX-SNR (vs uiSendTracePing, which only + * probes the immediate neighbour). The result arrives via the same + * onTracePingResult callback, but with one SNR reading per hop. Falls back + * to a single-hop trace when the path to `c` is unknown (flood-routed + * contact — there's no fixed path to walk). Returns the tag, 0 on failure. */ + uint32_t uiSendTraceRoute(const ContactInfo& c) { + uint8_t flags = (uint8_t)(_prefs.path_hash_mode & 0x03); + uint32_t tag = 0; + getRNG()->random((uint8_t*)&tag, sizeof(tag)); + if (tag == 0) tag = 1; + _ui_trace_ping_tag = tag; + mesh::Packet* pkt = createTrace(tag, 0, flags); + if (!pkt) { _ui_trace_ping_tag = 0; return 0; } + if (c.out_path_len != OUT_PATH_UNKNOWN && c.out_path_len > 0) { + sendDirect(pkt, c.out_path, c.out_path_len); // walk the full route + } else { + uint8_t hash_sz = (uint8_t)(_prefs.path_hash_mode + 1); + if (hash_sz == 0 || hash_sz > 4) hash_sz = 1; + sendDirect(pkt, c.id.pub_key, hash_sz); // unknown path → single hop + } + return tag; + } + + /** Request CayenneLPP telemetry from a remote contact. Reply is delivered + * via AbstractUITask::onTelemetryReply with the raw LPP payload after the + * 4-byte timestamp header. Falls back to onPingReply if the UI didn't + * override the telemetry hook. + * Returns the MSG_SEND_* result code. */ + int sendTelemetryRequestForUI(ContactInfo& recipient) { + uint32_t tag = 0, est = 0; + pending_telemetry = 0; + int r = sendRequest(recipient, REQ_TYPE_GET_TELEMETRY_DATA, tag, est); + if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { + memcpy(&_ui_pending_status, recipient.id.pub_key, 4); + _ui_pending_kind = UiReqKind::Telemetry; + _ui_pending_tag = tag; // request tag, reflected by the repeater + } + return r; + } + +private: + +public: + void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } + + // Set the default flood scope (region) used to tag outgoing flood packets, so + // repeaters on region-scoped networks ("flood only for their region") will + // re-broadcast them. A public "#hashtag" region's scope key is SHA256("#name"); + // a blank/NULL name clears it (unscoped, the default). Persists immediately. + void setDefaultFloodScope(const char* region_name); + + // Per-channel flood-scope override. pushChannelScope() derives a transient scope + // from "#region" (SHA256) for the NEXT channel send and returns true (the caller + // must popChannelScope() right after that send to restore); a blank/NULL name is + // a no-op returning false. Lets a channel override the default flood scope. + bool pushChannelScope(const char* region_name); + void popChannelScope(); + + // Re-apply the persisted radio settings (freq/bw/sf/cr, TX power, RX-boost) to + // the live radio — the same calls begin() makes at boot. Lets the UI change + // region / radio params and have them take effect immediately, no reboot. + void applyRadioFromPrefs(); + + /** Find the first unused channel slot, or -1 if the table is full. A slot + * is considered free when its name is empty (matches getNumChannels). */ + int findFirstEmptyChannelSlot() { +#ifdef MAX_GROUP_CHANNELS + for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) { + ChannelDetails cd{}; + if (!getChannel(i, cd) || cd.name[0] == '\0') return i; + } +#endif + return -1; + } + + /** UI-side equivalent of the CMD_SET_CHANNEL serial handler in handleCmdFrame. + * Writes channel `idx` with `name` (up to 31 chars) and a 16-byte secret, + * persists it to NVS and pokes the UI to refresh its thread list. + * Returns false if `idx` is out of range or setChannel rejects the slot. */ + bool uiAddOrUpdateChannel(int idx, const char* name, const uint8_t secret16[16]) { + if (idx < 0) return false; + ChannelDetails channel{}; + StrHelper::strncpy(channel.name, name, sizeof(channel.name)); + memset(channel.channel.secret, 0, sizeof(channel.channel.secret)); + memcpy(channel.channel.secret, secret16, 16); + if (!setChannel(idx, channel)) return false; + saveChannels(); + if (_ui) _ui->onThreadsChanged(); + return true; + } + + /** Wipe the cached return path for a contact so the next outgoing message + * re-floods instead of routing through a stale hop list. Returns false + * when the contact isn't in the table any more. */ + bool uiResetContactPath(const uint8_t pub_key[32]) { + ContactInfo* slot = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (!slot) return false; + slot->out_path_len = OUT_PATH_UNKNOWN; + memset(slot->out_path, 0, sizeof(slot->out_path)); + saveContacts(); + return true; + } + + /** Update a contact's stored GPS position (microdegrees, *1e6) — e.g. from a + * telemetry reply that carried a CayenneLPP GPS field. Lets contacts that + * don't flood position adverts (but do answer telemetry) appear on the map + * (issue #27). Returns false if the contact isn't in the table. */ + bool uiSetContactGps(const uint8_t pub_key[32], int32_t lat_e6, int32_t lon_e6) { + ContactInfo* slot = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (!slot) return false; + slot->gps_lat = lat_e6; + slot->gps_lon = lon_e6; + slot->lastmod = getRTCClock()->getCurrentTime(); + saveContacts(); + return true; + } + + /** Manually add a chat-type peer to contacts[] from a raw 32-byte pubkey + * + display name. Used by the Contacts tab "+" → "Add by pubkey" flow + * when we want to talk to someone whose advert we haven't received yet. + * Returns false if the pubkey is already a contact, the name is empty, + * or the contact table is full. */ + bool uiAddManualContact(const uint8_t pub_key[32], const char* name) { + if (!name || !name[0]) return false; + if (lookupContactByPubKey(pub_key, PUB_KEY_SIZE) != nullptr) return false; + ContactInfo ci{}; + memcpy(ci.id.pub_key, pub_key, PUB_KEY_SIZE); + ci.type = ADV_TYPE_CHAT; + ci.out_path_len = OUT_PATH_UNKNOWN; + ci.last_advert_timestamp = 0; // unknown — we never heard them + ci.lastmod = getRTCClock()->getCurrentTime(); + StrHelper::strncpy(ci.name, name, sizeof(ci.name)); + if (!addContact(ci)) return false; + saveContacts(); + if (_ui) _ui->onThreadsChanged(); + return true; + } + + /** Persist the in-RAM contact table to flash (/contacts3). Public wrapper so + * UI paths that insert via the base addContact() — e.g. the Discovered-list + * "Add to contacts" button — can persist; otherwise that contact is RAM-only + * and lost on reboot. */ + bool uiPersistContacts() { saveContacts(); return true; } + + /** Remove a contact from a device-UI action and PERSIST it (mirrors the + * companion app's CMD_REMOVE_CONTACT). The base removeContact() only drops it + * from RAM, so without rewriting /contacts3 the contact reappears on the next + * reboot. Also deletes its stored blob and pings the chats list. */ + bool uiRemoveContact(const ContactInfo& c) { + ContactInfo* slot = lookupContactByPubKey(c.id.pub_key, PUB_KEY_SIZE); + if (!slot || !removeContact(*slot)) return false; + _store->deleteBlobByKey(c.id.pub_key, PUB_KEY_SIZE); + saveContacts(); + if (_ui) _ui->onThreadsChanged(); + return true; + } + + /** Clear channel slot `idx` (zero name + secret), persist, and ping the UI + * so the chats list drops the entry. Returns false if the index is out of + * range. Used by the long-press → Delete action on the chats list. */ + bool uiDeleteChannel(int idx) { +#ifdef MAX_GROUP_CHANNELS + if (idx < 0 || idx >= MAX_GROUP_CHANNELS) return false; + ChannelDetails empty{}; + if (!setChannel(idx, empty)) return false; + saveChannels(); + if (_ui) _ui->onThreadsChanged(); + return true; +#else + (void)idx; + return false; +#endif + } + + /** Idempotently re-add the default "Public" channel (PSK matches + * MyMesh.cpp's PUBLIC_GROUP_PSK). Returns true if the channel is present + * after the call, false if no slot was available. */ + bool uiJoinPublicChannel() { +#ifdef MAX_GROUP_CHANNELS + for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) { + ChannelDetails cd{}; + if (getChannel(i, cd) && cd.name[0] != '\0' && + strncasecmp(cd.name, "Public", 6) == 0) return true; + } +#endif + if (addChannel("Public", "izOH6cXN6mrJ5e26oRXNcg==") == nullptr) return false; + saveChannels(); + if (_ui) _ui->onThreadsChanged(); + return true; + } + + // To check if there is pending work + bool hasPendingWork() const; + +private: + void writeOKFrame(); + void writeErrFrame(uint8_t err_code); + void writeDisabledFrame(); + /** Returns bytes written, or 0 on failure. Caller can retry on 0. */ + size_t writeContactRespFrame(uint8_t code, const ContactInfo &contact, bool to_all = false); + void updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len); + void addToOfflineQueue(const uint8_t frame[], int len); + int getFromOfflineQueue(uint8_t frame[]); + /** Appends frame to ring; returns assigned seq, or 0 if rejected. */ + uint32_t addToHistoryRing(const uint8_t frame[], int len); + /** Get next history frame for client. If do_advance is false, does not advance last_delivered_seq (call commitHistoryForClient after successful write). */ + int getNextFromHistoryForClient(const char* client_id, uint8_t frame[], uint32_t* out_seq = nullptr, bool do_advance = true); + void commitHistoryForClient(const char* client_id, uint32_t seq); + /** After a successful V3 live broadcast, bump sync watermarks for clients that understand V3 on the wire (target_ver >= 3 or unknown 0xFF). + * Legacy apps (CMD_DEVICE_QUERY second byte < 3) are skipped so CMD_SYNC_NEXT_MESSAGE can still deliver adapted 7/8 they never got from live 16/17. */ + void advanceHistoryClientsAfterV3Broadcast(uint32_t seq); + void setClientTargetVer(const char* client_id, uint8_t target_ver); + uint8_t getClientTargetVer(const char* client_id) const; + void setClientAppName(const char* client_id, const char* app_name); + bool shouldAdvanceClientAfterV3Broadcast(const char* client_id) const; + int adaptHistoryFrameForClient(const char* client_id, const uint8_t src[], int src_len, uint8_t dest[]) const; + void sendSyncSinceDelta(uint32_t T); + int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override { + return _store->getBlobByKey(key, key_len, dest_buf); + } + bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) override { + return _store->putBlobByKey(key, key_len, src_buf, len); + } + + void checkCLIRescueCmd(); + void checkSerialInterface(); + bool isValidClientRepeatFreq(uint32_t f) const; + + // helpers, short-cuts + void saveChannels() { _store->saveChannels(this); } + void saveContacts(); + + DataStore* _store; + NodePrefs _prefs; + uint32_t pending_login; + uint32_t pending_status; + uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ + uint32_t pending_req; // pending _BINARY_REQ + /** UI-side ping tracker: holds first 4 bytes of recipient pub_key when the + * touch UI fired a status request. Independent from `pending_status` so it + * doesn't collide with the companion-serial workflow. */ + uint32_t _ui_pending_status; + /** Which UI-side request is in flight against `_ui_pending_status` — + * STATUS replies should route to onPingReply, TELEMETRY replies to + * onTelemetryReply (CayenneLPP-aware). */ + UiReqKind _ui_pending_kind = UiReqKind::None; + /** Reflected-tag match for the UI request. STATUS/TELEMETRY responses + * echo our request's 4-byte timestamp at data[0..3], but a chained + * guest LOGIN response carries the *repeater's* own clock there — so + * matching by pubkey alone misroutes the login OK as the REQ reply. + * Compare tag in onContactResponse to keep the two streams separate. */ + uint32_t _ui_pending_tag = 0; + BaseSerialInterface *_serial; + AbstractUITask* _ui; + + ContactsIterator _iter; + uint32_t _iter_filter_since; + uint32_t _most_recent_lastmod; + uint32_t _contact_send_index; // temporary diagnostic: index of CONTACT sent + int _contact_list_reply_target; // reply target for this contact list (so CONTACT/END go to same client as START) + uint32_t _active_ble_pin; + bool _iter_started; + bool _cli_rescue; + bool send_unscoped; // force un-scoped flood (instead of using send_scope) + char cli_command[80]; + uint8_t app_target_ver; + uint8_t *sign_data; + uint32_t sign_data_len; + unsigned long dirty_contacts_expiry; + + TransportKey send_scope; + TransportKey _chan_scope_saved; // push/popChannelScope stash + bool _chan_scope_pushed = false; + bool _chan_scope_saved_unscoped = false; + + uint8_t cmd_frame[MAX_FRAME_SIZE + 1]; + uint8_t out_frame[MAX_FRAME_SIZE + 1]; + CayenneLPP telemetry; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + + bool isChannelMsg() const; + }; + int offline_queue_len; + Frame offline_queue[OFFLINE_QUEUE_SIZE]; + + // Per-client history: ring buffer of frames + per-client read position + struct HistoryEntry { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + uint32_t seq; + }; + struct ClientHistoryState { + char client_id[MAX_CLIENT_ID_LEN + 1]; + uint32_t last_delivered_seq; + }; + struct ClientProtoState { + char client_id[MAX_CLIENT_ID_LEN + 1]; + uint8_t target_ver; + bool prefer_live_advance; + }; + HistoryEntry history_ring[HISTORY_RING_SIZE]; + int history_count; + int history_head; + uint32_t history_next_seq; + ClientHistoryState history_clients[MAX_HISTORY_CLIENTS]; + int history_num_clients; + ClientProtoState proto_clients[MAX_HISTORY_CLIENTS]; + int proto_num_clients; + + struct AckTableEntry { + unsigned long msg_sent; + uint32_t ack; + ContactInfo* contact; + }; + #define EXPECTED_ACK_TABLE_SIZE 8 + AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table + int next_ack_idx; + + #define ADVERT_PATH_TABLE_SIZE 16 + AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table + + // One-shot auto-advert on boot. Recipients with auto-add ON pick up our + // current pubkey, which is critical when the touch firmware regenerates + // identity after a SPIFFS wipe (otherwise old contacts have stale pubkey + // and silently drop our DMs because shared-secret no longer matches). + // 0 = already fired or disabled. + uint32_t _boot_advert_due_ms = 0; + bool _boot_advert_done = false; + + // Most recently sent UI-initiated trace-ping tag. onTraceRecv compares + // this against the trace's tag field: if it matches, the trace was ours + // (a Ping from the action sheet) and we forward the SNRs to the UI; if + // not, it's an app-originated trace and we route to the companion app + // path. Single-slot is enough because the UI gates a new ping until the + // last one resolves or times out. + uint32_t _ui_trace_ping_tag = 0; +}; + +extern MyMesh the_mesh; diff --git a/src/NodePrefs.h b/src/NodePrefs.h new file mode 100644 index 00000000..7fbfe49c --- /dev/null +++ b/src/NodePrefs.h @@ -0,0 +1,37 @@ +#pragma once +#include // For uint8_t, uint32_t + +#define TELEM_MODE_DENY 0 +#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags +#define TELEM_MODE_ALLOW_ALL 2 + +#define ADVERT_LOC_NONE 0 +#define ADVERT_LOC_SHARE 1 + +struct NodePrefs { // persisted to file + float airtime_factor; + char node_name[32]; + float freq; + uint8_t sf; + uint8_t cr; + uint8_t multi_acks; + uint8_t manual_add_contacts; + float bw; + int8_t tx_power_dbm; + uint8_t telemetry_mode_base; + uint8_t telemetry_mode_loc; + uint8_t telemetry_mode_env; + float rx_delay_base; + uint32_t ble_pin; + uint8_t advert_loc_policy; + uint8_t buzzer_quiet; + uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled) + uint32_t gps_interval; // GPS read interval in seconds + uint8_t autoadd_config; // bitmask for auto-add contacts config + uint8_t rx_boosted_gain; // SX126x RX boosted gain (0=power saving, 1=boosted) + uint8_t client_repeat; + uint8_t path_hash_mode; // which path mode to use when sending (upstream 1.14) + uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) + char default_scope_name[31]; + uint8_t default_scope_key[16]; +}; \ No newline at end of file diff --git a/src/WiFiConfig.h b/src/WiFiConfig.h new file mode 100644 index 00000000..34a89cfc --- /dev/null +++ b/src/WiFiConfig.h @@ -0,0 +1,8 @@ +#pragma once + +/* Implementation lives in src/helpers/esp32/WifiRuntimeStore (NVS namespace meshcomod). */ +#if defined(ESP32) +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) +#include +#endif +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 00000000..c0f7ae6b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,607 @@ +#include // needed for PlatformIO +#include +#include "MyMesh.h" +#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI) +#include +#include +#include // recovery-first boot: running slot + reset the boot pointer to factory +#include // find/erase otadata so the bootloader returns to the recovery +#include +#include +#include // touchPrefsGetUiRotation for the boot wordmark +#endif + +// Believe it or not, this std C function is busted on some platforms! +static uint32_t _atoi(const char* sp) { + uint32_t n = 0; + while (*sp && *sp >= '0' && *sp <= '9') { + n *= 10; + n += (*sp++ - '0'); + } + return n; +} + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #include + #if defined(QSPIFLASH) + #include + DataStore store(InternalFS, QSPIFlash, rtc_clock); + #else + #if defined(EXTRAFS) + #include + CustomLFS ExtraFS(0xD4000, 0x19000, 128); + DataStore store(InternalFS, ExtraFS, rtc_clock); + #else + DataStore store(InternalFS, rtc_clock); + #endif + #endif +#elif defined(RP2040_PLATFORM) + #include + DataStore store(LittleFS, rtc_clock); +#elif defined(ESP32) + #include + #if defined(HAS_TDECK_GT911) + #include + #include + #ifndef PIN_SD_CS + #define PIN_SD_CS 39 // T-Deck microSD chip-select + #endif + #endif + extern "C" void set_boot_phase(int phase); + namespace { struct MainBootTrace { MainBootTrace() { set_boot_phase(2); } } _main_boot_trace; } + DataStore store(SPIFFS, rtc_clock); + #if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + #include "WiFiConfig.h" + #endif +#endif + +#ifdef ESP32 + #ifdef MULTI_TRANSPORT_COMPANION + #include + MultiTransportCompanionInterface serial_interface; + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #ifndef WS_PORT + #define WS_PORT 8765 + #endif + #elif defined(WIFI_SSID) + #include + SerialWifiInterface serial_interface; + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #elif defined(BLE_PIN_CODE) + #include + SerialBLEInterface serial_interface; + #elif defined(SERIAL_RX) + #include + ArduinoSerialInterface serial_interface; + HardwareSerial companion_serial(1); + #else + #include + ArduinoSerialInterface serial_interface; + #endif +#elif defined(RP2040_PLATFORM) + //#ifdef WIFI_SSID + // #include + // SerialWifiInterface serial_interface; + // #ifndef TCP_PORT + // #define TCP_PORT 5000 + // #endif + // #elif defined(BLE_PIN_CODE) + // #include + // SerialBLEInterface serial_interface; + #if defined(SERIAL_RX) + #include + ArduinoSerialInterface serial_interface; + HardwareSerial companion_serial(1); + #else + #include + ArduinoSerialInterface serial_interface; + #endif +#elif defined(NRF52_PLATFORM) + #ifdef BLE_PIN_CODE + #include + SerialBLEInterface serial_interface; + #else + #include + ArduinoSerialInterface serial_interface; + #endif +#elif defined(STM32_PLATFORM) + #include + ArduinoSerialInterface serial_interface; +#else + #error "need to define a serial interface" +#endif + +/* GLOBAL OBJECTS */ +#ifdef DISPLAY_CLASS + #include "UITask.h" + UITask ui_task(&board, &serial_interface); +#endif + +StdRNG fast_rng; +SimpleMeshTables tables; +MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store + #ifdef DISPLAY_CLASS + , &ui_task + #endif +); + +/* END GLOBAL OBJECTS */ + +#if defined(ESP32) +volatile int g_boot_phase = 0; +extern "C" void set_boot_phase(int phase) { g_boot_phase = phase; } +#endif + + +void halt() { + while (1) ; +} + +/* WIFI RECONNECT TRACKERS */ +#if defined(ESP32) && defined(WIFI_SSID) + bool wifi_needs_reconnect = false; + unsigned long last_wifi_reconnect_attempt = 0; +#endif + +void setup() { + Serial.begin(115200); + delay(200); + Serial.println("[BOOT] setup start"); + +#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI) + // Record which slot we booted from so the recovery's "Boot firmware" can return + // here. Recovery-first itself is enforced by the CUSTOM bootloader (it boots + // factory by default and an ota slot only on its one-shot flag), so we must NOT + // touch otadata here — otadata just tracks which A/B slot is current, and the + // bootloader's default-to-factory is what makes recovery survive ANY app + // (Meshtastic included). Skipped where there's no factory partition (V4 / + // standalone dual-OTA T-Deck). + { + const esp_partition_t* fac = + esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL); + if (fac) { + const esp_partition_t* run = esp_ota_get_running_partition(); + Preferences pslot; + if (pslot.begin("mcboot", false)) { + pslot.putString("slot", (run && run->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_1) ? "ota_1" : "ota_0"); + pslot.end(); + } + } + } +#endif + { + bool aes_ok = mesh::Utils::selfTestAES(); + Serial.printf("[BOOT] AES self-test: %s\n", aes_ok ? "PASS" : "FAIL"); + #if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI) + mesh_touch_tx_tracef("AES_SELFTEST: %s", aes_ok ? "PASS" : "FAIL"); + #endif + } + + board.begin(); + Serial.println("[BOOT] board ok"); + +#ifdef DISPLAY_CLASS + DisplayDriver* disp = NULL; + if (display.begin()) { + disp = &display; +#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI) + // Rotate the panel to the saved UI orientation BEFORE painting the boot + // wordmark, so it's upright in landscape too (UITask applies the same + // hardware rotation later for the LVGL UI). ROT_90->1, ROT_270->3. + { + uint8_t r = touchPrefsGetUiRotation(); + if (r == 1) display.setDisplayRotation(1); + else if (r == 3) display.setDisplayRotation(3); + } +#endif + disp->startFrame(); + // Centered MESHCOMOD title so the pre-LVGL boot window looks like the + // product, not a debug screen. Size 2 fits comfortably in 240 px (size 3 + // wraps the trailing "D" because the Adafruit GFX font has no kerning). + disp->setTextSize(2); + disp->drawTextCentered(disp->width() / 2, disp->height() / 2 - 8, "MESHCOMOD"); + disp->endFrame(); + } +#endif + + if (!radio_init()) { halt(); } + Serial.println("[BOOT] radio ok"); + + fast_rng.begin(radio_driver.getRngSeed()); + +#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI) + { + Preferences prefs; + if (prefs.begin("mcboot", false)) { + uint32_t bn = prefs.getUInt("n", 0); + ++bn; + prefs.putUInt("n", bn); + prefs.end(); + meshcomod_touch_set_boot_stats(bn, static_cast(esp_reset_reason())); + Serial.printf("[BOOT] touch_boot_n=%lu reason=%u\n", + static_cast(bn), + static_cast(esp_reset_reason())); + } + the_mesh.initTxtTxUniquenessFromRng(); + } +#endif + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + InternalFS.begin(); + #if defined(QSPIFLASH) + if (!QSPIFlash.begin()) { + // debug output might not be available at this point, might be too early. maybe should fall back to InternalFS here? + MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: failed to initialize"); + } else { + MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: initialized successfully"); + } + #else + #if defined(EXTRAFS) + ExtraFS.begin(); + #endif + #endif + store.begin(); + the_mesh.begin( + #ifdef DISPLAY_CLASS + disp != NULL + #else + false + #endif + ); + +#ifdef BLE_PIN_CODE + serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); +#else + serial_interface.begin(Serial); +#endif + the_mesh.startInterface(serial_interface); +#elif defined(RP2040_PLATFORM) + LittleFS.begin(); + store.begin(); + the_mesh.begin( + #ifdef DISPLAY_CLASS + disp != NULL + #else + false + #endif + ); + + //#ifdef WIFI_SSID + // WiFi.begin(WIFI_SSID, WIFI_PWD); + // serial_interface.begin(TCP_PORT); + // #elif defined(BLE_PIN_CODE) + // char dev_name[32+16]; + // sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName()); + // serial_interface.begin(dev_name, the_mesh.getBLEPin()); + #if defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + serial_interface.begin(companion_serial); + #else + serial_interface.begin(Serial); + #endif + the_mesh.startInterface(serial_interface); +#elif defined(ESP32) + // Storage selection. SPIFFS by default; use the SD card under /meshcomod when + // SPIFFS is unavailable (e.g. installed under Launcher) OR the user opted in + // ("Store data on SD"). The SD shares the LoRa SPI bus, already brought up by + // radio_init() above, so SD.begin's spi.begin is a no-op. Graceful: any SD + // failure falls back to SPIFFS so the device always boots. + bool spiffs_ok = SPIFFS.begin(false); // try first WITHOUT auto-format + bool sd_storage = false; +#if defined(HAS_TDECK_GT911) + { + extern SPIClass* tdeckSharedSPI(); + bool use_sd_pref = false, setup_done = false; + { Preferences _p; if (_p.begin("touch", true)) { + use_sd_pref = _p.getBool("use_sd", false); // explicit user choice + setup_done = _p.getBool("setup_ok", false); // finished first-run setup + _p.end(); + } } + + // First-run SD default: the very first time meshcomod boots on a brand-new + // device — the user hasn't finished setup yet AND nothing is stored on + // SPIFFS — prefer the SD card when one is present. Keeps internal flash free + // and is Launcher-friendly. The "no SPIFFS data" guard is what makes this + // safe: a device that already holds data on internal flash (e.g. one updated + // from an earlier build) is never silently migrated onto an empty card. + bool spiffs_has_data = spiffs_ok && + (SPIFFS.exists("/new_prefs") || SPIFFS.exists("/node_prefs") || + SPIFFS.exists("/identity/_main.id")); + bool fresh_install = !use_sd_pref && !setup_done && !spiffs_has_data; + + bool want_sd = !spiffs_ok // no usable SPIFFS partition -> must use SD + || use_sd_pref // user opted in + || fresh_install; // brand-new device: try SD first + SPIClass* _spi = tdeckSharedSPI(); + if (want_sd && _spi) { + for (int a = 0; a < 4 && !sd_storage; ++a) { // short mount ladder (cold cards) + SD.end(); + delay(a == 0 ? 40 : 220); + if (SD.begin(PIN_SD_CS, *_spi, 4000000, "/sd", 3) && SD.cardType() != CARD_NONE) { + sd_storage = store.useSdStorage(); + } + } + // On a genuine first run, persist the auto-pick so the "Store data on SD" + // toggle reflects it and the choice sticks on every later boot. + if (fresh_install && sd_storage && !use_sd_pref) { + Preferences _p; if (_p.begin("touch", false)) { _p.putBool("use_sd", true); _p.end(); } + Serial.println("[BOOT] first run + SD card present -> data defaults to SD"); + } + } + } +#endif + if (!sd_storage && !spiffs_ok) SPIFFS.begin(true); // last resort: format SPIFFS + Serial.printf("[BOOT] storage: %s\n", sd_storage ? "SD /meshcomod" : "SPIFFS"); + store.begin(); + the_mesh.begin( + #ifdef DISPLAY_CLASS + disp != NULL + #else + false + #endif + ); + Serial.println("[BOOT] mesh ok"); + +#if defined(WIFI_SSID) || defined(MULTI_TRANSPORT_COMPANION) + wifiConfigBegin(); + Serial.println("[BOOT] wifiConfig ok"); +#endif + +#ifdef MULTI_TRANSPORT_COMPANION + board.setInhibitSleep(true); + serial_interface.begin(Serial, TCP_PORT, WS_PORT); + Serial.println("[BOOT] serial_interface ok"); + serial_interface.setBroadcastResponses(true); // RX log, channel messages, etc. go to all clients (USB + TCP + WS [+ BLE]), not only last sender + /* Pick BLE vs WiFi at boot. The ESP32-S3 doesn't have enough internal heap + * (esp_wifi_init needs ~50KB for DMA buffers) to run Bluedroid BLE + + * LVGL/TFT + WiFi all at once — esp_wifi_init silently returns ESP_ERR_NO_MEM, + * leaving WiFi.getMode() at WIFI_MODE_NULL. So we mutex them: if the user + * has saved WiFi credentials AND the radio is enabled, skip BLE init and + * use WiFi exclusively. Otherwise init BLE. Toggle by saving/clearing creds + * + reboot (saveWifiCb auto-restarts). On the touch build the user can also + * pick Wi-Fi with no creds yet (to scan/configure on-device) — wantsWifi() + * returns true for that case so the radio comes up scannable. */ + bool want_wifi = wifiConfigWantsWifi(); + /* Wi-Fi + BLE now COEXIST (NimBLE host is light enough — the old Bluedroid + * heap clash is gone). Bring Wi-Fi up FIRST: esp_wifi_init grabs a big + * contiguous DMA block, so let it claim memory before BLE. (Association + * happens later in loop(); this just inits the stack.) */ + if (want_wifi) { + WiFi.mode(WIFI_STA); + WiFi.setAutoReconnect(false); + WiFi.persistent(false); + } +#if defined(BLE_PIN_CODE) + /* Always stash the BLE params so the toggle can bring BLE up live later, even + * if we defer it now. Then co-init BLE if the user has it enabled AND there's + * comfortable internal heap left after Wi-Fi — otherwise defer to Wi-Fi-only + * this boot rather than risk an OOM at NimBLE init (recoverable via the live + * toggle once memory frees). */ + // Defensive: force node_name NUL-terminated before it builds the BLE device + // name. Under Launcher (degraded storage) it can load non-terminated, which + // is what overran the BLE name buffer; the snprintf there now bounds the write, + // and this bounds the read so the name is the first <=31 chars, not garbage. + { NodePrefs* _np = the_mesh.getNodePrefs(); + _np->node_name[sizeof(_np->node_name) - 1] = '\0'; } + serial_interface.prepareBle(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); + if (wifiConfigGetBleEnabled()) { + const size_t BLE_COEXIST_MIN_FREE = 50 * 1024; // free heap after Wi-Fi to also start BLE + const size_t BLE_COEXIST_MIN_BLOCK = 20 * 1024; // largest contiguous block (NimBLE controller/host) + const size_t freeh = ESP.getFreeHeap(); + const size_t maxblk = ESP.getMaxAllocHeap(); + if (!want_wifi || (freeh >= BLE_COEXIST_MIN_FREE && maxblk >= BLE_COEXIST_MIN_BLOCK)) { + serial_interface.beginBle(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); + Serial.printf("[boot] BLE co-init OK (wifi=%d free=%u maxblk=%u)\n", (int)want_wifi, (unsigned)freeh, (unsigned)maxblk); + } else { + Serial.printf("[boot] BLE deferred: low heap (free=%u maxblk=%u) — Wi-Fi only\n", (unsigned)freeh, (unsigned)maxblk); + } + } +#endif +#elif defined(WIFI_SSID) + board.setInhibitSleep(true); // prevent sleep when WiFi is active + WiFi.setAutoReconnect(true); + + WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ + if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { + WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); + wifi_needs_reconnect = true; + } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { + WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); + wifi_needs_reconnect = false; + } + }); + + if (wifiConfigHasRuntime()) { + char ssid[WIFI_CONFIG_SSID_MAX]; + char pwd[WIFI_CONFIG_PWD_MAX]; + wifiConfigGetSsid(ssid, sizeof(ssid)); + wifiConfigGetPwd(pwd, sizeof(pwd)); + WiFi.begin(ssid, pwd[0] ? pwd : nullptr); + } else { + WiFi.begin(WIFI_SSID, WIFI_PWD); + } + serial_interface.begin(TCP_PORT); +#elif defined(BLE_PIN_CODE) + serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); +#elif defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + serial_interface.begin(companion_serial); +#else + serial_interface.begin(Serial); +#endif + the_mesh.startInterface(serial_interface); +#else + #error "need to define filesystem" +#endif + + sensors.begin(); + +#ifdef DISPLAY_CLASS + ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved +#endif + + board.onBootComplete(); +} + +void loop() { + // Run UI first every iteration so splash can dismiss at 3s even if mesh/serial blocks later (was stuck on version screen when the_mesh.loop() ran before ui_task.loop()). +#ifdef DISPLAY_CLASS + ui_task.loop(); +#endif +#ifdef MULTI_TRANSPORT_COMPANION + static bool wifi_started = false; + static uint32_t last_wifi_retry_ms = 0; + static const uint32_t WIFI_RETRY_INTERVAL_MS = 10000; + static bool wifi_radio_prev = true; + static bool wifi_radio_inited = false; + /* BLE-vs-WiFi mutex (chosen at setup based on saved creds + radio_en pref): + * if BLE was initialized, do NOT attempt to bring WiFi up here — esp_wifi_init + * would fail with ESP_ERR_NO_MEM after Bluedroid grabbed the internal heap, + * and the resulting OOM cascade freezes LVGL. Only run the WiFi state + * machine if creds are saved AND the radio pref is on, mirroring `want_wifi` + * in setup(). (Touch may also want Wi-Fi up with no creds, to scan.) */ + bool wifi_radio_en = wifiConfigWantsWifi(); + if (!wifi_radio_inited) { + wifi_radio_inited = true; + wifi_radio_prev = wifi_radio_en; + } else if (wifi_radio_en != wifi_radio_prev) { + wifi_radio_prev = wifi_radio_en; + if (!wifi_radio_en) { + WiFi.disconnect(true); + delay(50); + WiFi.mode(WIFI_OFF); + } + wifi_started = false; + } + /* UI may have changed SSID/PWD and asked for a re-apply. Trigger re-begin + * by forcing wifi_started=false; on next iter the block below will WiFi.begin + * with the freshly-saved credentials. Also handles toggling radio_en off + * from the UI (the transition above already covered the on case). */ + if (wifiConfigConsumeApplyRequest()) { + /* Only touch WiFi state if it was actually started this session. When + * BLE is the active transport (no creds saved), WiFi was never inited + * and calling WiFi.disconnect()/mode(WIFI_OFF) would trigger esp_wifi_init + * under low heap → crash. Setting wifi_started=false here is harmless; + * setup() will re-pick BLE-vs-WiFi on the auto-reboot from saveWifiCb. */ + if (wifi_started) { + if (!wifi_radio_en) { + WiFi.disconnect(true); + delay(50); + WiFi.mode(WIFI_OFF); + } else { + WiFi.disconnect(false, false); + delay(50); + } + } + wifi_started = false; + last_wifi_retry_ms = 0; + } + if (wifi_radio_en) { + if (!wifi_started) { + wifi_started = true; + WiFi.mode(WIFI_STA); + if (wifiConfigHasRuntime()) { + char ssid[WIFI_CONFIG_SSID_MAX]; + char pwd[WIFI_CONFIG_PWD_MAX]; + wifiConfigGetSsid(ssid, sizeof(ssid)); + wifiConfigGetPwd(pwd, sizeof(pwd)); + if (strlen(ssid) > 0) { + WiFi.begin(ssid, pwd[0] ? pwd : nullptr); + last_wifi_retry_ms = millis(); + } + } + } + // Automatic WiFi recovery for TCP mode: retry connection periodically if link drops. + if (wifiConfigHasRuntime() && WiFi.status() != WL_CONNECTED) { + uint32_t now = millis(); + if ((uint32_t)(now - last_wifi_retry_ms) >= WIFI_RETRY_INTERVAL_MS) { + last_wifi_retry_ms = now; + char ssid[WIFI_CONFIG_SSID_MAX]; + char pwd[WIFI_CONFIG_PWD_MAX]; + wifiConfigGetSsid(ssid, sizeof(ssid)); + wifiConfigGetPwd(pwd, sizeof(pwd)); + if (strlen(ssid) > 0) { + WiFi.begin(ssid, pwd[0] ? pwd : nullptr); + } + } + } + /* SNTP: kick off when Wi-Fi associates; once system time syncs, push it + * into the mesh RTC so timestamps on messages are accurate. */ + static bool sntp_kicked = false; + static bool sntp_pushed = false; + static uint32_t sntp_kick_ms = 0; + if (WiFi.status() == WL_CONNECTED) { + if (!sntp_kicked) { + /* Brussels timezone with DST rules baked in (POSIX "CET-1CEST,..."). + * On touch builds the base is shifted by the user's manual hour offset + * (Settings -> Device -> Time offset) so localtime() matches what they + * set. configTzTime only affects localtime() display; the mesh RTC + * still stores UTC seconds (protocol-facing). */ + char _tz[48]; +#if defined(HAS_TOUCH_UI) + touchPrefsBuildLocalTz(_tz, sizeof _tz); +#else + strncpy(_tz, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof _tz); + _tz[sizeof _tz - 1] = '\0'; +#endif + configTzTime(_tz, "pool.ntp.org", "time.google.com"); + sntp_kicked = true; + sntp_kick_ms = millis(); + } else if (!sntp_pushed && (uint32_t)(millis() - sntp_kick_ms) >= 1500) { + time_t t = time(nullptr); + if (t > 1700000000) { + /* Mesh RTC stores UTC seconds (protocol-facing); display layer + * converts to local via localtime_r() using the TZ from configTzTime. */ + rtc_clock.setCurrentTime((uint32_t)t); + sntp_pushed = true; + } + } + } else { + // Link dropped: allow re-sync on next reconnect. + if (sntp_kicked && !sntp_pushed) sntp_kicked = false; + } + } + // Defer TCP and WebSocket until after splash dismisses so the_mesh.loop() never blocks on accept() before ui_task.loop() runs. + static const uint32_t TCP_DEFER_MS = 5000; // 5 s: don't start TCP/WS until version screen has dismissed + /* Only start TCP / WS when WiFi was actually brought up. In BLE-only mode + * (no saved creds) the lwIP stack is never initialized — calling + * WiFiServer::begin() crashes with a tcpip_adapter assert. */ + if (millis() > TCP_DEFER_MS && wifi_started) { + serial_interface.startTcpServer(WiFi.status() == WL_CONNECTED); + serial_interface.tickWebSocketHandshake(); + } +#endif + the_mesh.loop(); + sensors.loop(); + rtc_clock.tick(); + + // (1.16) sleep when there's no pending work — nRF power saving + if (!the_mesh.hasPendingWork()) { +#if defined(NRF52_PLATFORM) + board.sleep(0); // nrf ignores seconds param, sleeps whenever possible +#endif + } + + // (1.16) non-blocking WiFi auto-reconnect (event-flagged in setup). Touch / + // multi-transport builds run their own WiFi reconnect state machine above and + // don't include SerialWifiInterface's WIFI_DEBUG_PRINTLN, so skip it there. +#if defined(ESP32) && defined(WIFI_SSID) && !defined(MULTI_TRANSPORT_COMPANION) + if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { + WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); + WiFi.disconnect(); + WiFi.reconnect(); + last_wifi_reconnect_attempt = millis(); + } +#endif + + // (fork) drive the in-firmware OTA staged-reboot +#if defined(ESP32_PLATFORM) + board.pollHttpOtaReboot(); +#endif +} diff --git a/src/ui-touch/KeyboardLayouts.cpp b/src/ui-touch/KeyboardLayouts.cpp new file mode 100644 index 00000000..8cd1b5e2 --- /dev/null +++ b/src/ui-touch/KeyboardLayouts.cpp @@ -0,0 +1,524 @@ +/******************************************************************************* + * KeyboardLayouts.cpp — keyboard map data for on-screen + T-Deck physical input + * + * To add a new language later: + * 1. Add an entry to KeyboardLayoutId in KeyboardLayouts.h + * 2. Define OsKeyboardLayout and HwKeyboardLayout structs below + * 3. Append them to k_os_layouts[] and k_hw_layouts[] + * 4. Update KEYBOARD_LAYOUT_COUNT in KeyboardLayouts.h + ******************************************************************************/ +#include "KeyboardLayouts.h" + +#if 1 +#include "lvgl.h" +#else +#include "lvgl/lvgl.h" +#endif + +/* ================================================================ + * On-screen keyboard (LVGL lv_keyboard) maps + * ================================================================ */ + +/* Helper: build a control entry for a special key with a given width multiplier. */ +static constexpr lv_btnmatrix_ctrl_t KC(uint8_t width_mul) { + return LV_BTNMATRIX_CTRL_NO_REPEAT | (width_mul << 4); +} + +/* ---------- Bulgarian on-screen keyboard ---------- */ + +/* Lower-case Cyrillic, 3 letter rows + 1 control row. */ +static const char* kb_bg_lower[] = { + "я","в","е","р","т","ъ","у","и","о","п","\n", + "а","с","д","ф","г","х","й","к","л","ь","\n", + "з","ц","ч","ж","б","н","м","ш","щ","ю","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; + +static const lv_btnmatrix_ctrl_t kb_bg_lower_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; + +/* Upper-case Cyrillic. */ +static const char* kb_bg_upper[] = { + "Я","В","Е","Р","Т","Ъ","У","И","О","П","\n", + "А","С","Д","Ф","Г","Х","Й","К","Л","Ь","\n", + "З","Ц","Ч","Ж","Б","Н","М","Ш","Щ","Ю","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; + +static const lv_btnmatrix_ctrl_t kb_bg_upper_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; + +/* ---------- Russian (ЙЦУКЕН) on-screen keyboard ---------- */ +/* Standard Russian layout, 3 rows of 11 = all 33 letters. */ +static const char* kb_ru_lower[] = { + "й","ц","у","к","е","н","г","ш","щ","з","х","\n", + "ф","ы","в","а","п","р","о","л","д","ж","э","\n", + "я","ч","с","м","и","т","ь","б","ю","ъ","ё","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; + +static const lv_btnmatrix_ctrl_t kb_ru_lower_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; + +static const char* kb_ru_upper[] = { + "Й","Ц","У","К","Е","Н","Г","Ш","Щ","З","Х","\n", + "Ф","Ы","В","А","П","Р","О","Л","Д","Ж","Э","\n", + "Я","Ч","С","М","И","Т","Ь","Б","Ю","Ъ","Ё","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; + +static const lv_btnmatrix_ctrl_t kb_ru_upper_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; + +/* ---------- Ukrainian on-screen keyboard ---------- */ +/* Standard Ukrainian layout, 3 rows of 11 = all 33 letters. */ +static const char* kb_uk_lower[] = { + "й","ц","у","к","е","н","г","ш","щ","з","х","\n", + "ф","і","в","а","п","р","о","л","д","ж","є","\n", + "я","ч","с","м","и","т","ь","б","ю","ї","ґ","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; +static const lv_btnmatrix_ctrl_t kb_uk_lower_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; +static const char* kb_uk_upper[] = { + "Й","Ц","У","К","Е","Н","Г","Ш","Щ","З","Х","\n", + "Ф","І","В","А","П","Р","О","Л","Д","Ж","Є","\n", + "Я","Ч","С","М","И","Т","Ь","Б","Ю","Ї","Ґ","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; +static const lv_btnmatrix_ctrl_t kb_uk_upper_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; + +/* ---------- Serbian (Cyrillic) on-screen keyboard ---------- */ +/* Serbian azbuka, 3 rows of 10 = all 30 letters. */ +static const char* kb_sr_lower[] = { + "љ","њ","е","р","т","з","у","и","о","п","\n", + "а","с","д","ф","г","х","ј","к","л","ч","\n", + "ж","џ","ц","в","б","н","м","ђ","ћ","ш","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; +static const lv_btnmatrix_ctrl_t kb_sr_lower_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; +static const char* kb_sr_upper[] = { + "Љ","Њ","Е","Р","Т","З","У","И","О","П","\n", + "А","С","Д","Ф","Г","Х","Ј","К","Л","Ч","\n", + "Ж","Џ","Ц","В","Б","Н","М","Ђ","Ћ","Ш","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; +static const lv_btnmatrix_ctrl_t kb_sr_upper_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; + +/* ---------- Greek on-screen keyboard ---------- */ +/* ΕΛΟΤ-style positions; 9+9+7 = all 24 letters (+ final sigma ς). */ +static const char* kb_el_lower[] = { + "ς","ε","ρ","τ","υ","θ","ι","ο","π","\n", + "α","σ","δ","φ","γ","η","ξ","κ","λ","\n", + "ζ","χ","ψ","ω","β","ν","μ","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; +static const lv_btnmatrix_ctrl_t kb_el_lower_ctrl[] = { + 0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; +static const char* kb_el_upper[] = { + "Σ","Ε","Ρ","Τ","Υ","Θ","Ι","Ο","Π","\n", + "Α","Σ","Δ","Φ","Γ","Η","Ξ","Κ","Λ","\n", + "Ζ","Χ","Ψ","Ω","Β","Ν","Μ","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; +static const lv_btnmatrix_ctrl_t kb_el_upper_ctrl[] = { + 0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; + +/* ---------- Arabic on-screen keyboard (RTL) ---------- */ +/* Arabic-101 letter positions, 3 rows of 11 = all 28 letters + ة ى ء ئ ؤ و. + * Arabic is unicameral, so the same map serves LOWER and UPPER. The text + * field renders RTL + shaped via LV_USE_BIDI / LV_USE_ARABIC_PERSIAN_CHARS. */ +static const char* kb_ar_lower[] = { + "ض","ص","ث","ق","ف","غ","ع","ه","خ","ح","ج","\n", + "ش","س","ي","ب","ل","ا","ت","ن","م","ك","ط","\n", + "ئ","ء","ؤ","ر","ى","ة","و","ز","ظ","ذ","د","\n", + LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL +}; +static const lv_btnmatrix_ctrl_t kb_ar_lower_ctrl[] = { + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + KC(2), KC(1), KC(4), KC(2), KC(1) +}; + +/* ---------- English on-screen keyboard ---------- */ +/* EXACT verbatim copy of LVGL's built-in default keyboard — the layout the alpha + * builds shipped before the multi-language maps existed (working "ABC" shift, the + * 1# symbols key, full punctuation row, magnified key-press preview). lv_keyboard + * keeps the maps in a GLOBAL kb_map[] that lv_keyboard_set_map overwrites in place, + * so once a non-Latin layout is applied the stock map is gone — we re-apply this + * copy to restore it when the user cycles back to English. EN_KB_BTN mirrors the + * library's private LV_KB_BTN (popover preview + width). */ +#define EN_KB_BTN(w) (LV_BTNMATRIX_CTRL_POPOVER | (w)) +static const char* kb_en_lower[] = { + "1#", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n", + "ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", LV_SYMBOL_NEW_LINE, "\n", + "_", "-", "z", "x", "c", "v", "b", "n", "m", ".", ",", ":", "\n", + LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, "" +}; + +static const lv_btnmatrix_ctrl_t kb_en_lower_ctrl[] = { + LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7, + LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7, + LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), + LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2 +}; + +static const char* kb_en_upper[] = { + "1#", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n", + "abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", LV_SYMBOL_NEW_LINE, "\n", + "_", "-", "Z", "X", "C", "V", "B", "N", "M", ".", ",", ":", "\n", + LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, "" +}; + +static const lv_btnmatrix_ctrl_t kb_en_upper_ctrl[] = { + LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7, + LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7, + LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), + LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2 +}; + +struct OsKeyboardLayout { + KeyboardLayoutId id; + const char* name; + const char** lower_map; + const lv_btnmatrix_ctrl_t* lower_ctrl; + const char** upper_map; + const lv_btnmatrix_ctrl_t* upper_ctrl; +}; + +static const OsKeyboardLayout k_os_layouts[] = { + { KeyboardLayoutId::EN, "EN", + kb_en_lower, kb_en_lower_ctrl, + kb_en_upper, kb_en_upper_ctrl }, + { KeyboardLayoutId::BG, "BG", + kb_bg_lower, kb_bg_lower_ctrl, + kb_bg_upper, kb_bg_upper_ctrl }, + { KeyboardLayoutId::RU, "RU", + kb_ru_lower, kb_ru_lower_ctrl, + kb_ru_upper, kb_ru_upper_ctrl }, + { KeyboardLayoutId::UK, "UK", + kb_uk_lower, kb_uk_lower_ctrl, + kb_uk_upper, kb_uk_upper_ctrl }, + { KeyboardLayoutId::SR, "SR", + kb_sr_lower, kb_sr_lower_ctrl, + kb_sr_upper, kb_sr_upper_ctrl }, + { KeyboardLayoutId::EL, "EL", + kb_el_lower, kb_el_lower_ctrl, + kb_el_upper, kb_el_upper_ctrl }, + { KeyboardLayoutId::AR, "AR", + kb_ar_lower, kb_ar_lower_ctrl, + kb_ar_lower, kb_ar_lower_ctrl }, // unicameral: same map for both +}; + +/* ================================================================ + * T-Deck physical keyboard (phonetic transliteration) + * ================================================================ */ + +struct HwKeyboardLayout { + KeyboardLayoutId id; + const char* name; +}; + +static const HwKeyboardLayout k_hw_layouts[] = { + { KeyboardLayoutId::EN, "EN" }, + { KeyboardLayoutId::BG, "BG" }, + { KeyboardLayoutId::RU, "RU" }, + { KeyboardLayoutId::UK, "UK" }, + { KeyboardLayoutId::SR, "SR" }, + { KeyboardLayoutId::EL, "EL" }, + { KeyboardLayoutId::AR, "AR" }, +}; + +/* Bulgarian phonetic mapping for T-Deck. + * Index 0 = 'a' / 'A', 1 = 'b' / 'B', etc. + * nullptr = pass through unchanged (not a mapped letter). */ +static const char* hw_bg_lower[26] = { + "а", "б", "ц", "д", "е", "ф", "г", "х", "и", "й", + "к", "л", "м", "н", "о", "п", "я", "р", "с", "т", + "у", "ж", "в", "ь", "ъ", "з" +}; + +static const char* hw_bg_upper[26] = { + "А", "Б", "Ц", "Д", "Е", "Ф", "Г", "Х", "И", "Й", + "К", "Л", "М", "Н", "О", "П", "Я", "Р", "С", "Т", + "У", "Ж", "В", "Ь", "Ъ", "З" +}; + +/* Digits 1-4 map to Cyrillic letters; 5-9,0 pass through as numbers. + * Note: 'ю' replaces the earlier Russian 'э' so the Bulgarian alphabet + * is fully covered (30 letters). */ +static const char* hw_bg_digits[10] = { + /* 0 */ nullptr, + /* 1 */ "ш", + /* 2 */ "щ", + /* 3 */ "ч", + /* 4 */ "ю", + /* 5 */ nullptr, + /* 6 */ nullptr, + /* 7 */ nullptr, + /* 8 */ nullptr, + /* 9 */ nullptr +}; + +static const char* hw_bg_digits_shift[10] = { + /* 0 */ nullptr, + /* 1 */ "Ш", + /* 2 */ "Щ", + /* 3 */ "Ч", + /* 4 */ "Ю", + /* 5 */ nullptr, + /* 6 */ nullptr, + /* 7 */ nullptr, + /* 8 */ nullptr, + /* 9 */ nullptr +}; + +/* Russian phonetic mapping for T-Deck (homophonic transliteration). + * a..z -> nearest-sounding Cyrillic; the 7 letters with no Latin homophone + * (ч щ ъ ь э ю ё) ride on digit keys 1-7. All 33 letters reachable. */ +static const char* hw_ru_lower[26] = { + "а", "б", "ц", "д", "е", "ф", "г", "х", "и", "й", + "к", "л", "м", "н", "о", "п", "я", "р", "с", "т", + "у", "в", "ш", "ж", "ы", "з" +}; + +static const char* hw_ru_upper[26] = { + "А", "Б", "Ц", "Д", "Е", "Ф", "Г", "Х", "И", "Й", + "К", "Л", "М", "Н", "О", "П", "Я", "Р", "С", "Т", + "У", "В", "Ш", "Ж", "Ы", "З" +}; + +static const char* hw_ru_digits[10] = { + /* 0 */ nullptr, + /* 1 */ "ч", + /* 2 */ "щ", + /* 3 */ "ъ", + /* 4 */ "ь", + /* 5 */ "э", + /* 6 */ "ю", + /* 7 */ "ё", + /* 8 */ nullptr, + /* 9 */ nullptr +}; + +static const char* hw_ru_digits_shift[10] = { + /* 0 */ nullptr, + /* 1 */ "Ч", + /* 2 */ "Щ", + /* 3 */ "Ъ", + /* 4 */ "Ь", + /* 5 */ "Э", + /* 6 */ "Ю", + /* 7 */ "Ё", + /* 8 */ nullptr, + /* 9 */ nullptr +}; + +/* Ukrainian phonetic for T-Deck (best-effort; g=ґ, h=г, x=х, y=и, i=і). + * є ж ї ч ш щ ь ю ride digits 1-8. */ +static const char* hw_uk_lower[26] = { + "а", "б", "ц", "д", "е", "ф", "ґ", "г", "і", "й", + "к", "л", "м", "н", "о", "п", "я", "р", "с", "т", + "у", "в", "в", "х", "и", "з" +}; +static const char* hw_uk_upper[26] = { + "А", "Б", "Ц", "Д", "Е", "Ф", "Ґ", "Г", "І", "Й", + "К", "Л", "М", "Н", "О", "П", "Я", "Р", "С", "Т", + "У", "В", "В", "Х", "И", "З" +}; +static const char* hw_uk_digits[10] = { + nullptr, "ж", "ч", "ш", "щ", "є", "ї", "ь", "ю", nullptr +}; +static const char* hw_uk_digits_shift[10] = { + nullptr, "Ж", "Ч", "Ш", "Щ", "Є", "Ї", "Ь", "Ю", nullptr +}; + +/* Serbian phonetic for T-Deck (best-effort; Gaj's-Latin homophones, single key: + * q=љ w=ш x=џ y=њ). ч ћ ђ ж ride digits 1-4. */ +static const char* hw_sr_lower[26] = { + "а", "б", "ц", "д", "е", "ф", "г", "х", "и", "ј", + "к", "л", "м", "н", "о", "п", "љ", "р", "с", "т", + "у", "в", "ш", "џ", "њ", "з" +}; +static const char* hw_sr_upper[26] = { + "А", "Б", "Ц", "Д", "Е", "Ф", "Г", "Х", "И", "Ј", + "К", "Л", "М", "Н", "О", "П", "Љ", "Р", "С", "Т", + "У", "В", "Ш", "Џ", "Њ", "З" +}; +static const char* hw_sr_digits[10] = { + nullptr, "ч", "ћ", "ђ", "ж", nullptr, nullptr, nullptr, nullptr, nullptr +}; +static const char* hw_sr_digits_shift[10] = { + nullptr, "Ч", "Ћ", "Ђ", "Ж", nullptr, nullptr, nullptr, nullptr, nullptr +}; + +/* Greek phonetic for T-Deck (ΕΛΟΤ Latin positions). All 24 letters fit a..z; + * 'q' has no Greek letter (pass-through), 'w' = final sigma ς. */ +static const char* hw_el_lower[26] = { + "α", "β", "ψ", "δ", "ε", "φ", "γ", "η", "ι", "ξ", + "κ", "λ", "μ", "ν", "ο", "π", nullptr, "ρ", "σ", "τ", + "θ", "ω", "ς", "χ", "υ", "ζ" +}; +static const char* hw_el_upper[26] = { + "Α", "Β", "Ψ", "Δ", "Ε", "Φ", "Γ", "Η", "Ι", "Ξ", + "Κ", "Λ", "Μ", "Ν", "Ο", "Π", nullptr, "Ρ", "Σ", "Τ", + "Θ", "Ω", "Σ", "Χ", "Υ", "Ζ" +}; +static const char* hw_el_digits[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr }; +static const char* hw_el_digits_shift[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr }; + +/* Arabic-101 physical positions for T-Deck (unicameral -> upper == lower). + * a..z follow the standard Arabic keyboard; the 7 letters that live on + * punctuation keys there (ج د ذ ز ط ظ و) ride digits 1-7 so they stay + * reachable on the T-Deck's compact keyboard. EXPERIMENTAL. */ +static const char* hw_ar_lower[26] = { + "ش", "لا", "ؤ", "ي", "ث", "ب", "ل", "ا", "ه", "ت", + "ن", "م", "ة", "ى", "خ", "ح", "ض", "ق", "س", "ف", + "ع", "ر", "ص", "ء", "غ", "ئ" +}; +static const char* hw_ar_digits[10] = { + nullptr, "ج", "د", "ذ", "ز", "ط", "ظ", "و", nullptr, nullptr +}; + +/* ================================================================ + * Runtime state + * ================================================================ */ +static KeyboardLayoutId s_current_layout = KeyboardLayoutId::EN; +static uint32_t s_enabled_mask = 0; // bit i = KeyboardLayoutId i is in the space-cycle (EN implicit) + +/* ================================================================ + * Public API + * ================================================================ */ + +const char* keyboardLayoutName(KeyboardLayoutId id) { + if (static_cast(id) >= KEYBOARD_LAYOUT_COUNT) id = KeyboardLayoutId::EN; + return k_os_layouts[static_cast(id)].name; +} + +void keyboardLayoutsApply(lv_obj_t* keyboard, KeyboardLayoutId id) { + if (!keyboard) return; + if (static_cast(id) >= KEYBOARD_LAYOUT_COUNT) id = KeyboardLayoutId::EN; + s_current_layout = id; + + const OsKeyboardLayout& lo = k_os_layouts[static_cast(id)]; + lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_TEXT_LOWER, lo.lower_map, lo.lower_ctrl); + lv_keyboard_set_map(keyboard, LV_KEYBOARD_MODE_TEXT_UPPER, lo.upper_map, lo.upper_ctrl); +} + +KeyboardLayoutId keyboardLayoutsGetCurrent() { + return s_current_layout; +} + +KeyboardLayoutId keyboardLayoutsCycle(lv_obj_t* keyboard) { + uint32_t mask = s_enabled_mask | 1u; // English is always part of the cycle + int cur = static_cast(s_current_layout); + // Walk forward from the current layout to the next enabled one, wrapping. + for (int step = 1; step <= KEYBOARD_LAYOUT_COUNT; ++step) { + int n = (cur + step) % KEYBOARD_LAYOUT_COUNT; + if (mask & (1u << n)) { + keyboardLayoutsApply(keyboard, static_cast(n)); + return s_current_layout; + } + } + return s_current_layout; // nothing enabled but EN — stay put +} + +uint32_t keyboardLayoutsGetEnabledMask() { return s_enabled_mask; } + +bool keyboardLayoutsAnySecondary() { + return (s_enabled_mask & ~1u) != 0u; // any non-English layout switched on +} + +void keyboardLayoutsSetEnabledMask(uint32_t mask) { + s_enabled_mask = mask & ((1u << KEYBOARD_LAYOUT_COUNT) - 1u); + // If the active layout was just switched off, fall back to English. + if (s_current_layout != KeyboardLayoutId::EN && + !(s_enabled_mask & (1u << static_cast(s_current_layout)))) { + s_current_layout = KeyboardLayoutId::EN; + } +} + +/* Per-layout physical-key tables. Index by KeyboardLayoutId. A null `lower` + * means pass-through (English). Adding a language = add one row here plus its + * four arrays above — no code changes. Keep this in enum order. */ +struct HwPhoneticMap { + const char* const* lower; // 26, a..z + const char* const* upper; // 26, A..Z + const char* const* digits; // 10, '0'..'9' + const char* const* digits_shift; // 10, shifted number row +}; +static const HwPhoneticMap k_hw_maps[KEYBOARD_LAYOUT_COUNT] = { + /* EN */ { nullptr, nullptr, nullptr, nullptr }, + /* BG */ { hw_bg_lower, hw_bg_upper, hw_bg_digits, hw_bg_digits_shift }, + /* RU */ { hw_ru_lower, hw_ru_upper, hw_ru_digits, hw_ru_digits_shift }, + /* UK */ { hw_uk_lower, hw_uk_upper, hw_uk_digits, hw_uk_digits_shift }, + /* SR */ { hw_sr_lower, hw_sr_upper, hw_sr_digits, hw_sr_digits_shift }, + /* EL */ { hw_el_lower, hw_el_upper, hw_el_digits, hw_el_digits_shift }, + /* AR */ { hw_ar_lower, hw_ar_lower, hw_ar_digits, hw_ar_digits }, // unicameral +}; + +const char* keyboardLayoutMapHwKey(KeyboardLayoutId id, int key, bool shifted) { + uint8_t idx = static_cast(id); + if (idx >= KEYBOARD_LAYOUT_COUNT) return nullptr; + const HwPhoneticMap& m = k_hw_maps[idx]; + if (!m.lower) return nullptr; // English / pass-through + + if (key >= 'a' && key <= 'z') return m.lower[key - 'a']; + if (key >= 'A' && key <= 'Z') return m.upper[key - 'A']; + if (key >= '0' && key <= '9') { + int d = key - '0'; + return shifted ? m.digits_shift[d] : m.digits[d]; + } + /* A keyboard that sends the shifted symbol ('!' for Shift+1, etc.) instead + * of the digit+shift flag: map the US-QWERTY top-row symbol to its digit + * index and reuse the layout's shifted-digit glyph. */ + static const char sym[] = ")!@#$%^&*("; // sym[d] == Shift+d + for (int d = 0; d <= 9; ++d) { + if (key == sym[d]) return m.digits_shift[d]; + } + return nullptr; /* pass through unchanged (space, punctuation, etc.) */ +} diff --git a/src/ui-touch/KeyboardLayouts.h b/src/ui-touch/KeyboardLayouts.h new file mode 100644 index 00000000..e3e24423 --- /dev/null +++ b/src/ui-touch/KeyboardLayouts.h @@ -0,0 +1,60 @@ +/******************************************************************************* + * KeyboardLayouts.h — extensible on-screen + physical keyboard layout registry + * + * Adding a new language later only requires editing KeyboardLayouts.cpp. + ******************************************************************************/ +#pragma once + +#include + +/* Forward-declare LVGL types so we don't pull lvgl.h into every translation unit. */ +struct _lv_obj_t; +typedef struct _lv_obj_t lv_obj_t; + +enum class KeyboardLayoutId : uint8_t { + EN = 0, + BG = 1, // Bulgarian (phonetic) + RU = 2, // Russian (ЙЦУКЕН on-screen, phonetic physical) + UK = 3, // Ukrainian (phonetic) — best-effort, validate with a native speaker + SR = 4, // Serbian (phonetic) — best-effort, validate with a native speaker + EL = 5, // Greek (ΕΛΟΤ-style phonetic) + AR = 6, // Arabic (RTL, Arabic-101 positions) — EXPERIMENTAL, needs on-device validation + Count +}; + +constexpr int KEYBOARD_LAYOUT_COUNT = static_cast(KeyboardLayoutId::Count); + +/** Human-readable label for a layout (e.g. "EN", "BG"). */ +const char* keyboardLayoutName(KeyboardLayoutId id); + +/** Apply an on-screen keyboard layout to an LVGL keyboard widget. + * Replaces the TEXT_LOWER and TEXT_UPPER maps; keeps the default SPECIAL map. + */ +void keyboardLayoutsApply(lv_obj_t* keyboard, KeyboardLayoutId id); + +/** Currently active layout (cached in RAM). */ +KeyboardLayoutId keyboardLayoutsGetCurrent(); + +/** Cycle to the next ENABLED layout (the double-tap-space action). The cycle is + * English plus whichever layouts are switched on in the enabled mask, walked in + * KeyboardLayoutId order and wrapping back to English. Returns the new active + * layout. With nothing enabled it stays on English. */ +KeyboardLayoutId keyboardLayoutsCycle(lv_obj_t* keyboard); + +/** Enabled-layout set. Bit (1 << KeyboardLayoutId) marks a layout as part of the + * space-cycle; English is always implicitly in the cycle. Setting a mask that + * no longer contains the active layout snaps the active layout back to English. + * Cached in RAM — persistence is the caller's job (TouchPrefsStore). */ +void keyboardLayoutsSetEnabledMask(uint32_t mask); +uint32_t keyboardLayoutsGetEnabledMask(); + +/** True if any non-English layout is enabled (i.e. the space-cycle does + * something and the status indicator should show). */ +bool keyboardLayoutsAnySecondary(); + +/** Map a physical T-Deck key to a UTF-8 string in the given layout. + * @param key ASCII code from tdeckKeyboardReadKey() (e.g. 'a', 'A', '1', '!') + * @param shifted true if Shift was active (key >= 'A' && key <= 'Z', or symbol) + * @return UTF-8 string to insert, or nullptr to pass the key through unchanged. + */ +const char* keyboardLayoutMapHwKey(KeyboardLayoutId id, int key, bool shifted); diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp new file mode 100644 index 00000000..a48e3c06 --- /dev/null +++ b/src/ui-touch/UITask.cpp @@ -0,0 +1,21886 @@ +#include "UITask.h" + +#include "../MyMesh.h" + +#include +#include +#include +#include +#if defined(ESP32) + #include + #include + // Dedicated LittleFS instance for the map tile pack (separate + // partition — see variants/heltec_v4/partitions_tft_touch.csv). Keeps + // tiles out of the SPIFFS partition where /new_prefs + /contacts3 + + // chat history live, so refreshing the tile pack no longer wipes the + // operator's Profile / Radio settings. + #include + #include + #include + #include // esp_deep_sleep_start / ext0 wakeup for the power-off menu + #include // rtc_gpio_pullup_en — hold the wake pin's level in deep sleep + #include "assets/lockscreen_placeholder_jpg.h" // seeded to SPIFFS /lock/placeholder.jpg on first boot (PNG decode is broken on this board) + #if defined(HAS_TDECK_GT911) + #include "assets/lockscreen_wallpaper_rgb565.h" // crisp pre-dithered default lock-screen wallpaper (T-Deck only; no JPEG banding) + #endif + #include + #include + #include + #include // A/B slot info + reboot-to-recovery (esp_ota_get_running_partition) + #include // find/erase otadata to fall back to the factory(recovery) slot +#endif +#if defined(HAS_TDECK_GT911) + #include // microSD (CS=39) on the shared LoRa SPI bus + #include "sd_diskio.h" // internal Arduino-SD drive helpers (sdcard_init / sd_*_raw) + extern SPIClass* tdeckSharedSPI(); + // FatFs mkfs (the prebuilt ESP-IDF compiles f_setlabel OUT — FF_USE_LABEL=0 — + // so the "MESHCOMOD" volume label is written by hand via sd_*_raw, below). + extern "C" int f_mkfs(const char* path, uint8_t opt, uint32_t au, void* work, unsigned len); + #ifndef MC_FM_FAT32 + #define MC_FM_FAT32 0x02 // FatFs f_mkfs option: force FAT32 + #endif + #ifndef PIN_SD_CS + #define PIN_SD_CS 39 // T-Deck microSD chip-select + #endif + #include // T-Deck MAX98357A speaker amp (notification tones) + // T-Deck I2S audio amp pins (MAX98357A, no MCLK). Overridable via build flags. + #ifndef PIN_I2S_BCK + #define PIN_I2S_BCK 7 + #endif + #ifndef PIN_I2S_WS + #define PIN_I2S_WS 5 + #endif + #ifndef PIN_I2S_DOUT + #define PIN_I2S_DOUT 6 + #endif +#endif +#include +#include // PSRAM-preferred alloc helpers for the map tile cache + +#if defined(HAS_TOUCH_UI) + #include + #include + #if defined(HAS_TDECK_TRACKBALL) + #include + #endif + #if defined(HAS_TDECK_KEYBOARD) + #include + #endif + #include "KeyboardLayouts.h" + #include "i18n.h" + #include "emoji_data.h" // baked Noto colour-emoji glyphs (emojiGlyphLookup) + #include + #include + #include + #include + #if defined(ESP32) + #include + #include // NVS-or-SD prefs backend (Launcher-safe) + #include + #if defined(MULTI_TRANSPORT_COMPANION) + #include + #include + #include + #endif + #endif + extern ST7789LCDDisplay display; +#endif + +constexpr unsigned long UI_REFRESH_MS = 250; +constexpr int UI_SORT_SCRATCH = UITask::MAX_UI_THREADS; +UIEventType g_last_event = UIEventType::none; + +#if defined(ESP32) +namespace { +constexpr const char* k_ui_history_path = "/ui_chat_history_v1.bin"; +constexpr uint32_t k_ui_history_magic = 0x55494348; // "UICH" +// Bump when the on-disk record layout changes. From v5 on, the header +// self-describes its record sizes (see loadHistoryFromStorage), so you can +// APPEND fields to the END of UiHistoryThread/UiHistoryMsg and OLD blobs still +// load — the appended tail reads back zero instead of the whole history being +// discarded. v4 added meta_flags/path_len/snr_q4/rssi per message. +constexpr uint16_t k_ui_history_version = 6; // v6: MAX_MSG_TEXT 96 -> 160 (record size changed) +// Oldest on-disk version we still load. v4's record layout matches the current +// build, so it reads fine via the sizeof() fallback below; anything older +// predates that layout and is discarded. +constexpr uint16_t k_ui_history_min_version = 6; // v4/v5 used 96-char records; reject (don't mis-read) + +struct __attribute__((packed)) UiHistoryHeader { + uint32_t magic; + uint16_t version; + uint16_t reserved; + uint16_t ui_msg_count; + uint16_t ui_msg_head; + uint32_t msgcount; + int16_t active_thread_idx; + uint8_t active_thread_is_channel; + // v5+: on-disk record sizes at write time, so the loader can read a blob + // whose records were SHORTER (fields appended since) and zero-fill the new + // tail. Zero in a v4 blob -> loader falls back to sizeof(). Carved out of the + // old _pad, so the header size is unchanged from v4. + uint16_t thread_rec_size; + uint16_t msg_rec_size; + uint8_t _pad[1]; +}; + +struct __attribute__((packed)) UiHistoryThread { + uint8_t used; + uint8_t channel; + uint16_t unread; + uint32_t last_ts; + int16_t mesh_contact_idx; + uint8_t mesh_contact_pub[32]; + uint8_t mesh_contact_key6[6]; + int16_t mesh_channel_slot; + char name[UITask::MAX_THREAD_NAME + 1]; +}; + +struct __attribute__((packed)) UiHistoryMsg { + uint32_t ts; + uint8_t channel; + uint8_t outgoing; + // v4 additions — RX metadata for the per-bubble Info popup. All zero when + // the message was either outgoing or loaded from a pre-v4 blob (which + // gets discarded outright on the version-mismatch check, so the latter + // shouldn't happen in practice, but the zero defaults keep the Info + // popup honest if it ever does). + uint8_t meta_flags; + uint8_t path_len; + int8_t snr_q4; + int8_t rssi; + char thread[UITask::MAX_THREAD_NAME + 1]; + char sender[UITask::MAX_SENDER_NAME + 1]; + char text[UITask::MAX_MSG_TEXT + 1]; + // Append any FUTURE fields HERE (after text) and bump k_ui_history_version. + // The v5+ loader zero-fills appended fields for older blobs, so appending + // never wipes chat history. Inserting in the middle (as v4 did) breaks that + // and must instead raise k_ui_history_min_version. +}; +} // namespace +#endif + +// ---- Shared by all companion_radio + ui-new builds (thread/mesh sync, prefs bits, loop counters) ---- +constexpr uint8_t AUTO_ADD_OVERWRITE_OLDEST = (1u << 0); +constexpr uint8_t AUTO_ADD_CHAT = (1u << 1); +constexpr uint8_t AUTO_ADD_REPEATER = (1u << 2); +constexpr uint8_t AUTO_ADD_ROOM_SERVER = (1u << 3); +constexpr uint8_t AUTO_ADD_SENSOR = (1u << 4); + +static bool hasContactPub(const uint8_t pub[32]) { + static const uint8_t z[32] = {0}; + return memcmp(pub, z, sizeof(z)) != 0; +} +static bool hasContactKey6(const uint8_t key6[6]) { + static const uint8_t z[6] = {0, 0, 0, 0, 0, 0}; + return memcmp(key6, z, sizeof(z)) != 0; +} + +static uint32_t s_live_diag_loops = 0; + +#if !defined(HAS_TOUCH_UI) +static void pushDiagLine(const char* message) { (void)message; } +#else +static void pushDiagLine(const char* message); // real def near the diag log +#endif + +// ============================================================ +// LVGL TOUCH UI — Heltec V4 TFT Cap-Touch only +// ============================================================ +#if defined(HAS_TOUCH_UI) + +// ---- colour palette: TACTICAL (sober) ---- +// Aimed at how *actual* fielded military comms equipment looks — Harris +// PRC handhelds, Codan terminals, Nett Warrior / MFOCS dashboards — +// rather than the Hollywood "amber + OD-green camo" look. Real military +// UIs are deliberately boring: dark neutral gray, clean white text, and +// colour reserved strictly for FUNCTIONAL status (amber = caution, green +// = OK, red = danger). The base palette here is all desaturated grays +// and one cool slate accent; the warm/saturated colours (the status +// amber + OD green + alert red) live in the status-color constants +// below and are used only where state really matters. +constexpr uint32_t COLOR_BG = 0x000000; // pure black, OLED-style +constexpr uint32_t COLOR_PANEL = 0x040506; // essentially black panel +// Earlier accent was 0x4E5C66 — RGB (78,92,102), cool blue-leaning. Even +// at low opacity that tinted every chip and settings row blue. Switched +// to a true neutral medium gray (very slight warm) so chip fills read as +// "darker gray" rather than "blue". +// Runtime-themeable accent (Settings -> Theme colour). NOT constexpr: the picker +// rewrites these live and they're reloaded from the saved pref at boot. Every +// accent site reads them through lv_color_hex(), so one write re-themes the UI. +uint32_t COLOR_ACCENT = 0x57585A; // neutral gray (default) +uint32_t COLOR_ACCENT_PRESS = 0x3A3B3D; // darker neutral gray (default) + +// Perceived luminance 0..255 (keep the accent dark enough for off-white text). +static inline uint32_t accentLuma(uint32_t rgb) { + return (299u*((rgb>>16)&0xFF) + 587u*((rgb>>8)&0xFF) + 114u*(rgb&0xFF)) / 1000u; +} +// Scale brightness to `pct` percent, preserving hue. +static inline uint32_t accentDarken(uint32_t rgb, int pct) { + uint32_t r=((rgb>>16)&0xFF)*pct/100, g=((rgb>>8)&0xFF)*pct/100, b=(rgb&0xFF)*pct/100; + return (r<<16)|(g<<8)|b; +} +// Clamp a picked accent dark enough that off-white button text stays readable. +static inline uint32_t accentClampReadable(uint32_t rgb) { + const uint32_t kMaxLuma = 140; + uint32_t L = accentLuma(rgb); + if (L > kMaxLuma) return accentDarken(rgb, (int)(kMaxLuma * 100 / L)); + return rgb & 0xFFFFFFu; +} +constexpr uint32_t COLOR_TEXT = 0xE0E3E6; // clean off-white +constexpr uint32_t COLOR_SUB = 0x828891; // medium neutral gray +// Chat-bubble palette: kept near-monochrome (military comms terminals +// don't colour-code direction). Slight luminance + hue lean keeps L/R +// readable but neither side gets a "fun" tint. +constexpr uint32_t COLOR_SENT_BG = 0x1D2226; // very subtle steel +constexpr uint32_t COLOR_RECV_BG = 0x1B1D1F; // warmer neutral gray +constexpr uint32_t COLOR_MENTION = 0x4FA3FF; // blue — @mentions of me +constexpr uint32_t COLOR_MENTION_BG = 0x16324F; // blue-tinted bubble for a message that @mentions me +// Functional status colours — use sparingly. These are the ONLY warm/ +// saturated colours in the palette, so when they appear the operator +// instantly reads them as state, not decoration. +constexpr uint32_t COLOR_STATUS_OK = 0x4A8E4A; // muted go-green +constexpr uint32_t COLOR_STATUS_WARN = 0xC8A030; // dim amber +constexpr uint32_t COLOR_STATUS_DANGER= 0xA04040; // muted red + +// LVGL 8.3 / Montserrat doesn't ship a STAR glyph. We carry a small custom +// font subset (one glyph: U+2605 BLACK STAR at size 28) in +// star_font_28.c — see &star_font_28 below. Pair the BIG macro with that +// font; the small ASCII macro stays as a plain '*' for places that still +// render in Montserrat (e.g. the action-sheet menu rows at font 12). +#define TOUCH_SYM_STAR "*" /* render in Montserrat at any size */ +#define TOUCH_SYM_STAR_BIG "\xE2\x98\x85" /* render ONLY with star_font_28 */ +extern "C" const lv_font_t star_font_28; +extern "C" const lv_font_t star_font_14; +// FontAwesome "user" glyph (U+F007) for the Contacts tab icon. Spliced into the +// g_font_14 fallback chain (the tab bar's font) in initTouchFontFallbacks, so the +// single PUA codepoint renders a person and still follows the tab's active colour. +extern "C" const lv_font_t person_font; +#define TOUCH_SYM_PERSON "\xEF\x80\x87" /* U+F007 */ + +// Extras fallback fonts — em-dash (U+2014), ellipsis (U+2026), middle dot +// (U+00B7). LVGL's stock Montserrat subset doesn't include these, so any +// string carrying them renders missing-glyph rectangles (visible in the +// settings page, toasts, etc.). We carry tiny custom subsets at the three +// label sizes we use (12, 14, 16) and chain them as fallback fonts via +// initTouchFontFallbacks() — runs once at the top of UITask::begin after +// lv_init. Anywhere the touch UI references a Montserrat font, swap to +// &g_font_NN to pick up the fallback chain. +extern "C" const lv_font_t extras_12; +extern "C" const lv_font_t extras_14; +extern "C" const lv_font_t extras_16; +static lv_font_t g_font_12; +static lv_font_t g_font_14; +static lv_font_t g_font_16; +#if LV_USE_IMGFONT +// lv_imgfont path callback: hand back the baked colour image for an emoji +// codepoint (copied into the imgfont's scratch buffer as an lv_img_dsc_t), or +// false so LVGL keeps walking the fallback chain for everything else. +static bool emojiImgfontPathCb(const lv_font_t* /*font*/, void* img_src, uint16_t /*len*/, + uint32_t unicode, uint32_t /*unicode_next*/) { + const lv_img_dsc_t* d = emojiGlyphLookup(unicode); + if (!d) return false; + lv_memcpy(img_src, d, sizeof(lv_img_dsc_t)); + // The imgfont reuses ONE scratch buffer as the image source for every glyph, so + // LVGL's image cache (keyed on the src pointer) hands back the first-decoded + // emoji for all of them — every emoji renders identical. Drop the stale entry so + // the just-copied dsc is decoded fresh. (True-colour "decode" is a no-op copy, so + // this is cheap, and other images — map tiles — use distinct src ptrs, untouched.) + lv_img_cache_invalidate_src(img_src); + return true; +} +static lv_font_t* s_emoji_font[3] = { nullptr, nullptr, nullptr }; // one per text size +#endif + +static void initTouchFontFallbacks() { + g_font_12 = lv_font_montserrat_12; + g_font_14 = lv_font_montserrat_14; + g_font_16 = lv_font_montserrat_16; +#if LV_USE_IMGFONT + // Insert the colour-emoji image font as the tail of each chain: + // g_font_NN (montserrat, Latin) -> emoji (colour images) -> extras_NN + // (Cyrillic/Greek/Arabic). The emoji font returns false for non-emoji + // codepoints, so they fall straight through to the size-matched extras. + const lv_font_t* extras[3] = { &extras_12, &extras_14, &extras_16 }; + lv_font_t* prim[3] = { &g_font_12, &g_font_14, &g_font_16 }; + for (int i = 0; i < 3; ++i) { + s_emoji_font[i] = lv_imgfont_create(16, emojiImgfontPathCb); // 16 px baked glyphs (~15% larger; sit on the text baseline) + if (s_emoji_font[i]) { s_emoji_font[i]->fallback = extras[i]; prim[i]->fallback = s_emoji_font[i]; } + else { prim[i]->fallback = extras[i]; } // OOM: plain chain + } +#else + g_font_12.fallback = &extras_12; + g_font_14.fallback = &extras_14; + g_font_16.fallback = &extras_16; +#endif + // Contacts-tab person glyph: splice onto the HEAD of g_font_16's chain — the + // tab bar's btnmatrix renders its icons in g_font_16 (g_font_16 -> person -> + // [emoji ->] extras). One PUA codepoint (U+F007); it follows the tab's + // active/inactive text colour like the other monochrome tab symbols. + static lv_font_t s_person_font; + s_person_font = person_font; + s_person_font.fallback = g_font_16.fallback; + g_font_16.fallback = &s_person_font; +} + +// ---- T-Deck notification tones (I2S → MAX98357A speaker amp) ---- +// Simple synthesized beeps for UI feedback (message arrived, etc). NOT file +// playback — the T-Deck's amp is driven over I2S; we generate a short sine +// burst on the fly. The legacy genericBuzzer (RTTTL on a digital pin) doesn't +// apply here — that's for boards with a piezo on a GPIO, which the touch boards +// don't have. Gated to the T-Deck; the V4 has no speaker at all. +#if defined(HAS_TDECK_GT911) +static constexpr int kI2sSampleRate = 16000; +static constexpr i2s_port_t kI2sPort = I2S_NUM_0; +// The tile fetcher's in-flight counter (defined later in the file). We skip +// beeping while tiles are downloading — the I2S DMA buffers + Wi-Fi RX DMA + a +// tile decode all contend for the scarce internal DMA RAM, and this build is +// already tight enough that tile downloads can OOM-reboot on their own. +extern volatile uint16_t s_tile_fetch_pending; + +// I2S is installed ON DEMAND for the duration of a tone and uninstalled after. +// Holding the driver resident permanently kept ~2 KB of internal DMA RAM, which +// shrank the margin the tile-fetch worker relies on and made tile downloads +// OOM-reboot. Transient install keeps steady-state internal RAM untouched. +static bool tdeckAudioInstall() { + i2s_config_t cfg = {}; + cfg.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX); + cfg.sample_rate = kI2sSampleRate; + cfg.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT; + cfg.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; // MAX98357A is mono + cfg.communication_format = I2S_COMM_FORMAT_STAND_I2S; + cfg.intr_alloc_flags = 0; + cfg.dma_buf_count = 4; + cfg.dma_buf_len = 256; + cfg.use_apll = false; + cfg.tx_desc_auto_clear = true; + if (i2s_driver_install(kI2sPort, &cfg, 0, nullptr) != ESP_OK) return false; + i2s_pin_config_t pins = {}; + pins.mck_io_num = I2S_PIN_NO_CHANGE; + pins.bck_io_num = PIN_I2S_BCK; + pins.ws_io_num = PIN_I2S_WS; + pins.data_out_num = PIN_I2S_DOUT; + pins.data_in_num = I2S_PIN_NO_CHANGE; + if (i2s_set_pin(kI2sPort, &pins) != ESP_OK) { i2s_driver_uninstall(kI2sPort); return false; } + return true; +} + +// Render `freq` Hz for `ms` ms into the already-installed I2S as a 16-bit sine +// with a short fade-in/out so it doesn't click. +static void tdeckPlayToneRaw(int freq, int ms, int vol = 9000) { + const int total = (kI2sSampleRate * ms) / 1000; + const int fade = total / 8 > 0 ? total / 8 : 1; + int16_t buf[128]; + int written_total = 0; + double phase = 0.0; + const double step = 2.0 * M_PI * (double)freq / (double)kI2sSampleRate; + while (written_total < total) { + int n = 0; + for (; n < 128 && written_total < total; ++n, ++written_total) { + double amp = (double)vol; + if (written_total < fade) amp *= (double)written_total / fade; + else if (written_total > total-fade) amp *= (double)(total - written_total)/fade; + buf[n] = (int16_t)(sin(phase) * amp); + phase += step; + if (phase > 2.0 * M_PI) phase -= 2.0 * M_PI; + } + size_t bw = 0; + i2s_write(kI2sPort, buf, n * sizeof(int16_t), &bw, pdMS_TO_TICKS(200)); + } + i2s_zero_dma_buffer(kI2sPort); +} + +static volatile bool s_notify_playing = false; +// The chime body: install I2S, play the two notes, uninstall. ~300 ms of blocking +// i2s_write + driver setup/teardown — run on its own throwaway task (below). +static void tdeckNotifyTaskFn(void* arg) { + (void)arg; + if (tdeckAudioInstall()) { + tdeckPlayToneRaw(880, 90); // A5 + tdeckPlayToneRaw(1318, 110); // E6 + i2s_driver_uninstall(kI2sPort); + } + s_notify_playing = false; + vTaskDelete(nullptr); +} + +// Notification chime. Spawned on a short-lived task so the ~300 ms of I2S work +// does NOT freeze the UI — newMsgImpl (the caller) runs on the UI/mesh thread, so +// playing synchronously locked the screen for the whole chime. Skips while tiles +// download (DMA-RAM contention → reboot) and won't stack itself. Caller checks the +// sound pref. +static void tdeckPlayNotify() { + if (s_tile_fetch_pending > 0) return; // don't fight the Wi-Fi/tile DMA buffers + if (s_notify_playing) return; // already chiming — don't stack tasks/I2S + s_notify_playing = true; + if (xTaskCreate(tdeckNotifyTaskFn, "notify", 4096, nullptr, 3, nullptr) != pdPASS) { + s_notify_playing = false; // couldn't spawn (low DRAM) — skip the chime + } +} +#endif // HAS_TDECK_GT911 + +// ---- Unified UI notification sound (T-Deck I2S speaker OR Heltec V4 piezo) ---- +#if defined(HAS_TDECK_GT911) || defined(HELTEC_V4_BUZZER_PIN) + #define HAS_UI_SOUND 1 +#endif + +#if defined(HELTEC_V4_BUZZER_PIN) +// Heltec V4 expansion-kit piezo buzzer on GPIO6 (PWM). Plays a short two-note +// chime via LEDC on a throwaway task so the ~210 ms doesn't stall the UI thread. +// LEDC ch 5 (the TFT backlight uses ch 0); the pin is detached + parked high-Z +// afterwards so the piezo is silent at idle. +static volatile bool s_v4_beep_playing = false; +static void v4BeepTaskFn(void* arg) { + (void)arg; + // Drive the GPIO6 piezo with Arduino tone() — the exact mechanism Meshtastic's + // RTTTL buzzer uses on this board (heltec-v4-tft). Raw LEDC (ledcWriteTone on a + // hand-picked channel) produced no sound, likely colliding with the TFT + // backlight's LEDC channel; tone() auto-manages its own channel. ~520 ms chime. + tone(HELTEC_V4_BUZZER_PIN, 1000); vTaskDelay(pdMS_TO_TICKS(160)); + tone(HELTEC_V4_BUZZER_PIN, 1500); vTaskDelay(pdMS_TO_TICKS(160)); + tone(HELTEC_V4_BUZZER_PIN, 2000); vTaskDelay(pdMS_TO_TICKS(200)); + noTone(HELTEC_V4_BUZZER_PIN); + pinMode(HELTEC_V4_BUZZER_PIN, INPUT); // high-Z → no idle current / no buzz + s_v4_beep_playing = false; + vTaskDelete(nullptr); +} +static void v4BuzzerBeep() { + if (s_v4_beep_playing) return; + s_v4_beep_playing = true; + if (xTaskCreate(v4BeepTaskFn, "v4beep", 2048, nullptr, 3, nullptr) != pdPASS) + s_v4_beep_playing = false; +} +#endif + +// Play the platform's notification chime. Caller checks the buzzer/sound pref. +static inline void uiPlayNotify() { +#if defined(HAS_TDECK_GT911) + tdeckPlayNotify(); +#elif defined(HELTEC_V4_BUZZER_PIN) + v4BuzzerBeep(); +#endif +} + +// ---- misc UI constants ---- +constexpr int SWIPE_SCROLL_STEP = 90; + +// ---- Global status bar (lv_layer_sys → drawn above everything) ---- +// Always-visible 240×22 strip across the top of the display. Left zone is +// dynamic (MESHCOMOD on Home tab, mail-icon + total-unread count on the +// other tabs). Right zone is a fixed stack: connection icon (WiFi or +// BLE, whichever is up), clock, battery percent, battery icon. The bar +// sits on lv_layer_sys so modals on lv_layer_top can't cover it, and the +// tabview / chat overlay are explicitly sized to leave the top 22 px +// clear. +constexpr lv_coord_t STATUSBAR_H = 22; +struct GlobalStatusBar { + lv_obj_t* root; + lv_obj_t* left_label; + lv_obj_t* conn_icon; + lv_obj_t* clock; + lv_obj_t* batt_pct; + lv_obj_t* batt_icon; + lv_obj_t* layout_label; // EN / BG indicator, right side + lv_obj_t* chan_gear; // channel-settings gear, left of the thread name (channels only) +}; +static GlobalStatusBar g_statusbar = {}; +static void updateGlobalStatusBar(); // fwd decl, called from refresh tick + +// Active chat thread name, surfaced in the status bar's left zone. The in-chat +// header bar (back button + name) was removed to give the conversation the full +// height — like the Terminal/Files views, the chat name lives in the status bar +// and a small floating HOME button handles exit. Empty = no chat open. +static char s_chat_title[40] = {0}; +static void setChatStatusTitle(const char* sanitized) { // already glyph-sanitized name, or nullptr to clear + if (sanitized && sanitized[0]) { + strncpy(s_chat_title, sanitized, sizeof(s_chat_title) - 1); + s_chat_title[sizeof(s_chat_title) - 1] = '\0'; + } else { + s_chat_title[0] = '\0'; + } + updateGlobalStatusBar(); +} +/** Bottom tab indices. Tabs: Home=0, Chats=1, Contacts=2, Map=3, Set=4. */ +constexpr int CHAT_INBOX_TAB_INDEX = 1; +constexpr int CONTACTS_TAB_INDEX = 2; +constexpr int MAP_TAB_INDEX = 3; +constexpr int SETTINGS_TAB_INDEX = 4; +constexpr int TAB_LAST = 4; + +// ---- Chat overlay layout ---- +constexpr int CHAT_HDR_H = 0; // in-chat header bar removed; thread name shows in the status bar +constexpr int CHAT_COMP_H = 34; // composer row, single line (slimmed 50 → 40 → 34; hugs the 30px textbox) +constexpr int CHAT_COMP_MAX_LINES = 4; // composer grows up to this many wrapped lines, then scrolls vertically +// Current composer-row height. The composer wraps long text to multiple lines +// and grows UPWARD (its bottom stays pinned, the message list above shrinks) +// instead of horizontally scrolling a single line. The chat layout helpers +// below derive the message-area height + composer Y from this, so a taller +// composer automatically reclaims space from the message list. Reset to +// CHAT_COMP_H on each chat-panel (re)build; updated by chatComposerAutoGrow(). +static lv_coord_t s_comp_h = CHAT_COMP_H; +constexpr int CHAT_KB_H = 130; // on-screen keyboard (portrait) +// Bottom tab-bar height (matches lv_tabview_create in buildUiTree). A tab +// page's usable content area is the screen minus the status bar and tab bar — +// queried live so it tracks the current rotation (240×260 portrait / +// 320×180 landscape). +constexpr int TABBAR_H = 30; // bottom nav bar (trimmed from 38; icons stay g_font_16) +static inline lv_coord_t tabContentW() { return lv_disp_get_hor_res(nullptr); } +static inline lv_coord_t tabContentH() { return lv_disp_get_ver_res(nullptr) - STATUSBAR_H - TABBAR_H; } +// Usable area for a centered modal below the global status bar (small margin). +// Popups were sized for the 320-tall portrait screen; these let them shrink to +// fit the 240-tall landscape one. Backdrops should use the full screen so the +// dark overlay covers everything and the card centers on-screen. +static inline lv_coord_t modalAvailW() { return lv_disp_get_hor_res(nullptr) - 12; } +static inline lv_coord_t modalAvailH() { return lv_disp_get_ver_res(nullptr) - STATUSBAR_H - 12; } +// Chat-detail geometry — runtime so the conversation view fills the screen and +// tracks rotation. The keyboard is half the screen tall in landscape (matches +// kbApplyLayoutForRotation), full CHAT_KB_H in portrait. +static inline bool chatLandscape() { return lv_disp_get_hor_res(nullptr) > lv_disp_get_ver_res(nullptr); } +static inline lv_coord_t chatScreenW() { return lv_disp_get_hor_res(nullptr); } +static inline lv_coord_t chatScreenH() { return lv_disp_get_ver_res(nullptr) - STATUSBAR_H; } +static inline lv_coord_t chatKbH() { return chatLandscape() ? (lv_disp_get_ver_res(nullptr) / 2) : CHAT_KB_H; } +// without keyboard (s_comp_h tracks the composer's current, possibly grown, height): +static inline lv_coord_t chatMsgHOpen() { return chatScreenH() - CHAT_HDR_H - s_comp_h; } +static inline lv_coord_t chatCompYOpen() { return chatScreenH() - s_comp_h; } +// with keyboard shown: +static inline lv_coord_t chatMsgHKb() { return chatScreenH() - CHAT_HDR_H - s_comp_h - chatKbH(); } +static inline lv_coord_t chatCompYKb() { return chatScreenH() - s_comp_h - chatKbH(); } + +// ---- Thread list button context ---- +struct LvThreadButtonCtx { int idx; bool channel; }; + +// ---- Per-panel state (DM chats and channels share this shape) ---- +struct LvChatPanel { + lv_obj_t* list_cont; // full-page lv_list in the tab (thread chooser) + lv_obj_t* overlay; // full-screen overlay on lv_scr_act (chat detail) + lv_obj_t* header_name; // label: thread name in the overlay header + lv_obj_t* msgs; // read-only textarea: message history + lv_obj_t* jump_btn; // floating "jump to latest" button (Discord-style) + lv_obj_t* composer_row; // container: input + send button + lv_obj_t* composer_ta; // textarea: user types here + LvThreadButtonCtx ctx_store[UITask::MAX_UI_THREADS]; + bool channel_mode; + /** When true, `list_cont` shows channels + DMs with history (Chats tab only). */ + bool inbox_combined; + bool detail_open; + uint32_t list_sig = 0; // change-signature of the rendered thread list (skip no-op rebuilds) +}; + +struct LvContactButtonCtx { + uint32_t mesh_idx; + bool is_repeater; +}; + +static LvContactButtonCtx s_contacts_ctx[128]; + +// ---- Contact list sort modes (cycled via short-press on the active filter) ---- +enum ContactsSortMode : uint8_t { + CONTACTS_SORT_AZ = 0, // alphabetical + CONTACTS_SORT_LAST_HEARD = 1, // most recent advert first (default) + CONTACTS_SORT_LAST_MSG = 2, // most recent message first (falls back to last-heard) + CONTACTS_SORT_COUNT = 3, +}; +// Default to most-recently-heard at the top — operators care who's on the +// air now far more than alphabetical order. Favorites still pin above all +// (see the qsort comparator in refreshContactsList). +static uint8_t g_contacts_sort = CONTACTS_SORT_LAST_HEARD; + +static const char* contactsSortLabel(uint8_t m) { + switch (m) { + case CONTACTS_SORT_AZ: return "A-Z"; + case CONTACTS_SORT_LAST_HEARD: return "RECENT"; + case CONTACTS_SORT_LAST_MSG: return "MSG"; + } + return "?"; +} +/** Last tab index before `tabChangedCb`; used to close chat overlays only when leaving Chats. */ +static int s_lv_tab_prev = 0; + +// ---- Top-level LVGL state ---- +struct LvUiState { + bool ready; + bool touch_inited; + bool dirty_threads; + bool dirty_timeline; + bool defer_heavy_refresh; + unsigned long heavy_refresh_at_ms; + uint32_t lvgl_tick_prev_us; + lv_disp_draw_buf_t draw_buf; + lv_disp_drv_t disp_drv; + lv_indev_drv_t indev_drv; + lv_obj_t* tabview; + lv_obj_t* home_state; + lv_obj_t* home_stats; + // Duty-cycle meter (label + bar) on the Home tab. Surfaces the live TX + // budget remaining so the operator notices regulatory throttling before + // a ten-message-burst stalls. Created in makeHome iff the user pref is on. + lv_obj_t* home_dc_label; + lv_obj_t* home_dc_bar; + lv_obj_t* settings_status; + lv_obj_t* diag_id_label; // pinned ID/freq/sf line above the diag ring + lv_obj_t* diag_label; + lv_obj_t* keyboard; // shared LVGL on-screen keyboard + LvChatPanel dm; // Chats tab list + DM overlay + LvChatPanel ch; // channel thread overlay only (no tab list) + lv_obj_t* contacts_list; + uint8_t contacts_filter; // 0=all 1=repeaters 2=peers (non-repeater) 3=favorites + // Per-session text-search filter applied on top of the category filter. + // Empty = no text filter. Set/cleared by the magnifier sheet. + char contacts_search[24]; + lv_obj_t* contacts_search_indicator; // tiny chip shown when search is active + UITask* task; +}; + +// ---- Diagnostics ring buffer ---- +constexpr int DIAG_LINES = 16; // bumped from 7 so dispatcher TX traces survive the UI flow +constexpr int DIAG_COLS = 44; +static char s_diag_ring[DIAG_LINES][DIAG_COLS]; +static int s_diag_line = 0; +// Sticky copy of the latest "ID …" identity/radio diag line. MyMesh emits +// this during begin() — before the Diag tab's labels exist — so we cache +// the most recent value and back-fill the pinned label when the tab is +// constructed. +static char s_diag_id_pinned[DIAG_COLS] = { 0 }; +constexpr int RXLOG_LINES = 28; +constexpr int RXLOG_COLS = 80; +static char s_rxlog_ring[RXLOG_LINES][RXLOG_COLS]; +static int s_rxlog_line = 0; +static char s_rawlog_ring[RXLOG_LINES][RXLOG_COLS]; +static int s_rawlog_line = 0; +static lv_obj_t* s_live_diag_label = nullptr; +static uint32_t s_live_diag_reads = 0; +static uint32_t s_live_diag_pressed = 0; +static uint32_t s_live_diag_tap_edges = 0; +static unsigned long s_live_diag_next_ms = 0; +/** Keep live diag overlay available but hidden by default; flip true for field debugging. */ +constexpr bool k_show_live_diag_overlay = false; + +#if defined(HAS_TDECK_TRACKBALL) +// ---- T-Deck trackball cursor ---- +// A soft pointer the trackball drives; auto-hides after inactivity, and the +// centre click injects a touch press at the cursor (see lvglTouchRead / loop). +static lv_obj_t* s_tb_cursor = nullptr; +static int s_tb_cursor_x = 160; // rendered position (landscape 320x240) +static int s_tb_cursor_y = 120; +static float s_tb_target_x = 160.0f, s_tb_target_y = 120.0f; // full-sensitivity target +static float s_tb_render_x = 160.0f, s_tb_render_y = 120.0f; // eased render position +static unsigned long s_tb_prev_ms = 0; +static unsigned long s_tb_last_active_ms = 0; +static bool s_tb_click_press = false; // centre button held this frame +constexpr int kTbCursorDiameter = 16; // px +constexpr int kTbCursorStepPx = 12; // px per encoder step +constexpr float kTbCursorSmoothMs = 60.0f; // ease time-constant (smaller = snappier) +constexpr unsigned long kTbCursorHideMs = 800; // auto-hide after idle + +// True while the trackball cursor is visible AND screen-Y is within the bottom +// tab bar. Used to swallow stray FINGER touches on the tab bar while the user is +// driving the cursor — a trackball CLICK on a tab still works (it arrives via +// the cursor path, not as a raw touch). +static bool tbFingerTouchOnTabBarBlocked(uint16_t y) { + if (!s_tb_cursor) return false; + if ((millis() - s_tb_last_active_ms) >= kTbCursorHideMs) return false; // cursor hidden + return (int)y >= (lv_disp_get_ver_res(nullptr) - TABBAR_H); +} +#endif + +// ---- Panel currently linked to the keyboard (or nullptr) ---- +static LvChatPanel* s_kb_panel = nullptr; +/** Full-width strip above the on-screen keyboard: mirrors the focused textarea so edits stay visible. */ +static lv_obj_t* s_kb_mirror_root = nullptr; +static lv_obj_t* s_kb_mirror_ta = nullptr; +/** Real textarea whose text is synced with `s_kb_mirror_ta` while the keyboard is open. */ +static lv_obj_t* s_kb_bind_ta = nullptr; + +// ---- Chats "+" add-channel modal pointers (see lower in file for impl) ---- +static lv_obj_t* s_addch_sheet = nullptr; +static lv_obj_t* s_addch_name_ta = nullptr; +static lv_obj_t* s_addch_secret_ta = nullptr; +static lv_obj_t* s_addch_hashtag_ta = nullptr; +static lv_obj_t* s_addch_error_l = nullptr; +// ---- Contacts → "Add" manual contact modal pointers ---- +static lv_obj_t* s_addct_pub_ta = nullptr; +static lv_obj_t* s_addct_name_ta = nullptr; +static lv_obj_t* s_addct_error_l = nullptr; + +// ---- Keyboard rotation (landscape typing) ---- +// Stored as the lv_disp_rot_t value: 0 = portrait, 1 = ROT_90, 3 = ROT_270. +// Applied to the whole display while the keyboard is shown; reverts to +// portrait on hideKb(). Persisted in NVS under "kbrot" so the user's choice +// survives reboots. +static uint8_t s_kb_rotation = 0; +static lv_obj_t* s_kb_rot_left_btn = nullptr; +static lv_obj_t* s_kb_rot_right_btn = nullptr; +static lv_obj_t* s_kb_lang_btn = nullptr; // on-screen language-cycle key (boards w/o a physical keyboard) +static lv_obj_t* s_kb_lang_lbl = nullptr; // its label — the active layout's 2-letter code +static lv_obj_t* s_kb_alt_btn = nullptr; // on-screen "Alt" key: cycles the last letter's accent (issue #22) +// Accent-popup picker on/off (default on). Loaded from NVS at boot; gates +// accentBoxMaybeShow() so the tap-to-pick accent box can be turned off in +// the keyboard settings (applies to the on-screen + physical keyboards). +static bool s_accent_popups = true; + +// ---- Global UI orientation (persistent) ---- +// The base orientation of the whole UI, loaded from NVS ("uirot") and applied +// at boot BEFORE the screens are built so the layout reflows to the rotated +// resolution. The keyboard-landscape toggle above is transient and layers on +// top of this; hideKb() reverts to s_ui_rotation (not hard portrait). +// Stored as the lv_disp_rot_t value: 0 = portrait, 1 = ROT_90, 3 = ROT_270. +static uint8_t s_ui_rotation = 0; + +// The rotation the keyboard should use when shown: if the UI is already in a +// landscape base orientation, keep it; otherwise honour the transient +// keyboard-landscape toggle. Lets the rotate arrows still work in portrait. +static inline uint8_t effectiveKbRotation() { + const bool ui_landscape = + (s_ui_rotation == LV_DISP_ROT_90 || s_ui_rotation == LV_DISP_ROT_270); + return ui_landscape ? s_ui_rotation : s_kb_rotation; +} + +// ---- Discovered adverts ring buffer ---- +// Filled by UITask::discoveredContact() each time an advert comes in for a +// node that's not in contacts[]. Browsable + addable via the Discovered +// modal opened from the Contacts tab "Found" button. +constexpr int DISCOVERED_MAX = 24; +struct LvDiscoveredEntry { + bool used; + bool in_contacts; + uint8_t path_len; + uint32_t last_advert_ts; + uint32_t recv_ms; + ContactInfo ci; +}; +// PSRAM-first allocator (falls back to internal RAM) for moving large static UI buffers off +// the scarce internal SRAM. Safe as a static initializer: PSRAM is up before C++ ctors run, +// and the fallback keeps the original internal behaviour if PSRAM is ever unavailable. +static void* psAlloc(size_t n) { + void* p = heap_caps_malloc(n, MALLOC_CAP_SPIRAM); + if (!p) p = heap_caps_malloc(n, MALLOC_CAP_8BIT); + // Zero-init: these buffers replace zero-initialized static UI arrays, and + // their callers rely on that — e.g. s_discovered[].used is the validity flag + // for the "Found" list. Without this, fresh-boot PSRAM garbage makes random + // entries look "used" and the Found list shows corrupt contacts. + if (p) memset(p, 0, n); + return p; +} +static LvDiscoveredEntry* s_discovered = (LvDiscoveredEntry*)psAlloc(sizeof(LvDiscoveredEntry) * DISCOVERED_MAX); +static uint32_t s_discovered_seq = 0; + +// ---- Touch-init deferred flag ---- +static bool g_cap_touch_hw_started = false; + +// ---- LVGL draw buffer ---- +// 240x24 RGB565 = 11,520 bytes. Allocated in PSRAM at UITask::begin() so the +// internal DRAM stays free for WiFi DMA buffers (esp_wifi_init needs ~50 KB +// of DRAM; with the buffer + LVGL widgets in DRAM the device was OOM'ing as +// soon as WiFi came up). PSRAM is slower than DRAM but the Adafruit ST7789 +// SPI driver reads the buffer linearly which the cache handles well. +// +// 1.5x the original 240x16 size — modest bump to reduce setAddrWindow +// round-trips without giving LVGL more headroom to over-invalidate. +// Double-buffering and 240x40 were tried and starved the CHSC6X poll cadence +// (touch I2C is read from inside lv_timer_handler at LV_INDEV_DEF_READ_PERIOD +// = 30 ms, and bigger render windows pushed effective poll rate to ~11 Hz). +// 48 lines × 240 × 2 B = 23 KB. Lives in internal DMA-capable DRAM (see +// allocator below) which reads ~3× faster than PSRAM during SPI push. +// Doubled from 24 to amortize the per-flush setAddrWindow + startWrite/ +// endWrite overhead. Tried double-buffering (two 23 KB internal blocks) +// — bootlooped after the top bar showed, because the second 23 KB block +// starved Bluedroid's DMA-DRAM pool at init time. Stayed single-buffer. +// Was 48 — dropped to 24 to claw back ~12 KB of internal DMA-DRAM for +// the Wi-Fi tile-fetch task stack (which has to live in internal RAM +// because mbedTLS/lwIP crash on PSRAM stacks under this Arduino-ESP32 +// build). 24 × 240 × 2 B = 11.5 KB. Flush time is bound by SPI clock +// (80 MHz), not memcpy speed, so the throughput delta is small — +// LVGL just calls flush_cb more often. +static constexpr int LV_DRAW_BUF_LINES = 24; +static lv_color_t* g_draw_buffer = nullptr; + +// ---- Global UI state instance ---- +LvUiState g_lv = {}; + +/** If `cp` has no glyph in `font`, show '*' instead of LVGL's missing-glyph box. Preserves \\n \\r \\t. */ +static bool uiFontHasGlyph(const lv_font_t* font, uint32_t cp) { + if (!font) return true; + if (cp == '\n' || cp == '\r' || cp == '\t') return true; + if (cp < 0x20u) return true; + lv_font_glyph_dsc_t dsc{}; + return lv_font_get_glyph_dsc(font, &dsc, cp, 0); +} + +/** + * Copy UTF-8 `in` → `out` (NUL-terminated, capped). Codepoints not in `font` become ASCII '*'. + * Invalid UTF-8 bytes become '*'. + */ +static void copyUtf8ReplacingMissingGlyphs(const lv_font_t* font, char* out, size_t out_cap, const char* in) { + if (!out || out_cap == 0) return; + out[0] = '\0'; + if (!in) return; + size_t w = 0; + const char* p = in; + const char* end = in + strlen(in); + + while (*p && w + 1 < out_cap) { + const char* seq_beg = p; + uint32_t cp = 0; + bool ok = false; + + const unsigned char c0 = static_cast(*p); + if (c0 < 0x80u) { + cp = c0; + p += 1; + ok = true; + } else if ((c0 & 0xE0u) == 0xC0u && end - p >= 2) { + const unsigned char c1 = static_cast(p[1]); + if ((c1 & 0xC0u) == 0x80u) { + cp = (static_cast(c0 & 0x1Fu) << 6) | (c1 & 0x3Fu); + if (cp >= 0x80u) { + p += 2; + ok = true; + } + } + } else if ((c0 & 0xF0u) == 0xE0u && end - p >= 3) { + const unsigned char c1 = static_cast(p[1]); + const unsigned char c2 = static_cast(p[2]); + if ((c1 & 0xC0u) == 0x80u && (c2 & 0xC0u) == 0x80u) { + cp = (static_cast(c0 & 0x0Fu) << 12) | + (static_cast(c1 & 0x3Fu) << 6) | (c2 & 0x3Fu); + if (cp >= 0x800u && !(cp >= 0xD800u && cp <= 0xDFFFu)) { + p += 3; + ok = true; + } + } + } else if ((c0 & 0xF8u) == 0xF0u && end - p >= 4) { + const unsigned char c1 = static_cast(p[1]); + const unsigned char c2 = static_cast(p[2]); + const unsigned char c3 = static_cast(p[3]); + if ((c1 & 0xC0u) == 0x80u && (c2 & 0xC0u) == 0x80u && (c3 & 0xC0u) == 0x80u) { + cp = (static_cast(c0 & 0x07u) << 18) | + (static_cast(c1 & 0x3Fu) << 12) | + (static_cast(c2 & 0x3Fu) << 6) | (c3 & 0x3Fu); + if (cp >= 0x10000u && cp <= 0x10FFFFu) { + p += 4; + ok = true; + } + } + } + + if (!ok) { + out[w++] = '*'; + p = seq_beg + 1; + continue; + } + + if (uiFontHasGlyph(font, cp)) { + for (const char* q = seq_beg; q < p && w + 1 < out_cap; ++q) out[w++] = *q; + } else { + out[w++] = '*'; + } + } + out[w] = '\0'; +} + +enum class SettingsModalKind : uint8_t { + None = 0, + Profile, + Radio, + AutoAdd, + Experimental, + Bluetooth, // was Transport — replaced with a dedicated BLE page + Device, + Wifi, + Log, + Discovered, // discovered adverts list (manual-add view) + Advert, // "send advert" picker (flood vs zero-hop) + ChCreatePrv, // Chats "+": create a private channel (name + secret) + ChJoinPrv, // Chats "+": join a private channel by secret + ChJoinTag, // Chats "+": join a hashtag channel by name + SystemInfo, // Read-only system / firmware diagnostic page + AddContact, // Manual add-contact (pubkey + name) modal + QuickReply, // Edit the 6 quick-reply preset macros +}; + +struct SettingsModalState { + lv_obj_t* root; + SettingsModalKind kind; + lv_obj_t* name_ta; + lv_obj_t* lat_ta; + lv_obj_t* lon_ta; + lv_obj_t* freq_ta; + lv_obj_t* bw_ta; + lv_obj_t* sf_ta; + lv_obj_t* cr_ta; + lv_obj_t* tx_ta; + lv_obj_t* airtime_ta; + lv_obj_t* region_ta; + lv_obj_t* max_hops_ta; + lv_obj_t* auto_chat_sw; + lv_obj_t* auto_rep_sw; + lv_obj_t* auto_room_sw; + lv_obj_t* auto_sensor_sw; + lv_obj_t* auto_overwrite_sw; + lv_obj_t* manual_add_sw; + lv_obj_t* share_loc_sw; + lv_obj_t* path_mode_dd; + lv_obj_t* exp_multi_sw; + lv_obj_t* exp_repeat_sw; + lv_obj_t* exp_boost_sw; + lv_obj_t* exp_dc_sw; + lv_obj_t* wifi_sw; + lv_obj_t* wifi_ssid_ta; + lv_obj_t* wifi_pwd_ta; + /** Transports modal: live STA line (IP when connected, else Arduino WiFi status). */ + lv_obj_t* wifi_sta_status_l; + lv_obj_t* radio_preset_dd; + lv_obj_t* log_view_ta; + lv_obj_t* log_rx_btn; + lv_obj_t* log_raw_btn; + uint8_t log_mode; // 0 = rx summary, 1 = raw + /** Device modal: screen-timeout (seconds, 0=never) textarea. */ + lv_obj_t* screen_to_ta; + /** Device modal: live GPS fix-status line under the GPS toggle. */ + lv_obj_t* gps_status; + /** Quick replies modal: 6 textareas, one per slot. Empty save clears + * the slot (so the picker shows "(empty)" and a save with text repopulates). + * Allocated only while the modal is open. */ + lv_obj_t* qr_tas[TOUCH_QUICK_REPLY_COUNT]; +}; +static SettingsModalState g_set_modal = {}; + +/** Mirrors `Meshcomod-client/shared/radio_presets.ts` (community presets); keep in sync manually. */ +struct MeshRadioPreset { + const char* label; + float freq_mhz; + float bw_khz; + uint8_t sf; + uint8_t cr; + int8_t tx_dbm; + uint8_t airtime_limit_pct; // web % → NodePrefs.airtime_factor = pct / 100 +}; + +static constexpr size_t k_mesh_radio_preset_count = 18; +static const MeshRadioPreset k_mesh_radio_presets[k_mesh_radio_preset_count] = { + {"Australia", 915.8f, 250.f, 10, 5, 22, 10}, + {"Australia (Narrow)", 916.575f, 62.5f, 7, 8, 22, 10}, + {"Australia: SA, WA", 923.125f, 62.5f, 8, 8, 22, 10}, + {"Australia: QLD", 923.125f, 62.5f, 8, 5, 22, 10}, + {"Australia: NSW (SF11)", 915.8f, 250.f, 11, 5, 22, 10}, + {"New Zealand", 917.375f, 250.f, 11, 5, 22, 10}, + {"New Zealand (Narrow)", 917.375f, 62.5f, 7, 5, 22, 10}, + {"EU/UK (Narrow)", 869.618f, 62.5f, 8, 8, 22, 10}, + {"EU/UK (Deprecated)", 869.525f, 250.f, 11, 5, 22, 10}, + {"Switzerland (Narrow)", 869.618f, 62.5f, 8, 8, 22, 10}, + {"Czech Republic (Narrow)", 869.432f, 62.5f, 7, 5, 22, 10}, + {"Slovakia (Narrow)", 869.618f, 62.5f, 8, 8, 22, 10}, + {"EU 433MHz (Long Range)", 433.65f, 250.f, 11, 5, 22, 10}, + {"US915", 915.f, 250.f, 11, 5, 22, 10}, + {"USA: SoCal (Community)", 927.875f, 62.5f, 7, 5, 22, 10}, + {"USA: San Francisco (Community)", 910.525f, 62.5f, 7, 5, 22, 10}, + {"USA: Sacramento (Community)", 909.875f, 62.5f, 9, 5, 22, 10}, + {"USA: Pacific NW (Community)", 910.525f, 62.5f, 7, 5, 22, 10}, +}; + +static bool g_radio_preset_cb_silent = false; + +static void kbMirrorSyncToReal(); // keyboard mirror strip; defined below + +static int findMatchingMeshRadioPreset(const NodePrefs* prefs) { + if (!prefs) return -1; + for (size_t i = 0; i < k_mesh_radio_preset_count; ++i) { + const MeshRadioPreset& p = k_mesh_radio_presets[i]; + if (std::fabs(prefs->freq - p.freq_mhz) > 0.002) continue; + if (std::fabs(prefs->bw - p.bw_khz) > 0.02) continue; + if (prefs->sf != p.sf) continue; + if (prefs->cr != p.cr) continue; + if (prefs->tx_power_dbm != p.tx_dbm) continue; + const double exp_af = static_cast(p.airtime_limit_pct) / 100.0; + if (std::fabs(static_cast(prefs->airtime_factor) - exp_af) > 0.05) continue; + return static_cast(i); + } + return -1; +} + +static void applyMeshRadioPresetFields(unsigned preset_idx) { + if (preset_idx >= k_mesh_radio_preset_count || !g_set_modal.freq_ta) return; + kbMirrorSyncToReal(); + const MeshRadioPreset& p = k_mesh_radio_presets[preset_idx]; + char buf[28]; + snprintf(buf, sizeof(buf), "%.3f", static_cast(p.freq_mhz)); + lv_textarea_set_text(g_set_modal.freq_ta, buf); + snprintf(buf, sizeof(buf), "%.1f", static_cast(p.bw_khz)); + lv_textarea_set_text(g_set_modal.bw_ta, buf); + snprintf(buf, sizeof(buf), "%u", static_cast(p.sf)); + lv_textarea_set_text(g_set_modal.sf_ta, buf); + snprintf(buf, sizeof(buf), "%u", static_cast(p.cr)); + lv_textarea_set_text(g_set_modal.cr_ta, buf); + snprintf(buf, sizeof(buf), "%d", static_cast(p.tx_dbm)); + lv_textarea_set_text(g_set_modal.tx_ta, buf); + const float af = static_cast(p.airtime_limit_pct) / 100.0f; + snprintf(buf, sizeof(buf), "%.2f", static_cast(af)); + lv_textarea_set_text(g_set_modal.airtime_ta, buf); +} + +static void radioPresetChangedCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED || g_radio_preset_cb_silent) return; + if (!g_set_modal.radio_preset_dd || !g_set_modal.freq_ta) return; + const uint16_t sel = lv_dropdown_get_selected(g_set_modal.radio_preset_dd); + if (sel == 0) return; + const unsigned idx = static_cast(sel - 1u); + if (idx >= k_mesh_radio_preset_count) return; + applyMeshRadioPresetFields(idx); +} + +/** Subtitle labels on Set tab section rows (titles are static; see `refreshSettingsSectionSubtitles`). */ +enum : int { + SEC_PROFILE = 0, + SEC_RADIO, + SEC_AUTOADD, + SEC_BLUETOOTH, // was SEC_TRANSPORT — replaced with a dedicated BLE page + SEC_DEVICE, + SEC_EXPERIMENTAL, + SEC_WIFI, + SEC_LOG, + SEC_SYSTEM, + SEC_QUICKREPLY, // user-editable composer macro presets + SEC_COUNT +}; +static lv_obj_t* g_set_sec_sub[SEC_COUNT]; + +/** Settings list rows: full width inside tab padding (240 − 16). */ +static constexpr lv_coord_t k_settings_row_w = 224; +static constexpr lv_coord_t k_settings_row_h = 54; +static constexpr lv_coord_t k_settings_row_gap = 6; + +static bool settingsModalIsOpen() { return g_set_modal.root != nullptr; } + +// Compact one-line GPS status for the Device settings panel + control center. +// TR("GPS: off") / "GPS: searching · N sats" / "GPS: fix · N sats , ". +static const char* gpsStatusStr() { + static char s[72]; + if (!g_lv.task || !g_lv.task->getGPSState()) { snprintf(s, sizeof s, TR("GPS: off")); return s; } + // satellitesCount() is satellites USED IN THE FIX (0 until a lock), so during + // cold acquisition it stays 0 — show "acquiring…" rather than a misleading + // "0 sats". On lock, show the sat count + the (auto-populated) coordinates. + if (!g_lv.task->getGpsFix()) { snprintf(s, sizeof s, TR("GPS: acquiring...")); return s; } + const int sats = g_lv.task->getGpsSats(); + int n = snprintf(s, sizeof s, TR("GPS: fix")); + if (sats >= 0 && n < (int)sizeof s) n += snprintf(s + n, sizeof s - n, " · %d sats", sats); + if (n < (int)sizeof s) + snprintf(s + n, sizeof s - n, " %.5f, %.5f", + g_lv.task->getNodeLat(), g_lv.task->getNodeLon()); + return s; +} + +// Control-center popup state — declared up here so the periodic settings refresh +// (which is defined above the control-center code) can update the live GPS line. +static lv_obj_t* s_cc_root = nullptr; +static lv_obj_t* s_cc_gps_label = nullptr; +static lv_obj_t* s_cc_sys_label = nullptr; // CPU/RAM/PSRAM/IP line; refreshed live while CC open +static void ccBuildSysInfo(char* buf, size_t n); // fwd-decl; defined with the CC helpers below +static void closeControlCenter(); // defined in the control-center section below +static lv_obj_t* s_power_menu = nullptr; // power off / reboot menu (control center) +static void closePowerMenu(); // defined in the control-center section below +#if defined(HAS_TDECK_KEYBOARD) +// Keyboard backlight: mode 0=off, 1=on, 2=auto (on while typing, off after idle). +static uint8_t s_kb_bl_mode = 2; +static unsigned long s_kb_last_key_ms = 0; +constexpr unsigned long kKbBacklightIdleMs = 3000; // auto: off 3 s after last key +// Register input activity (keypress / field focus / tap) for the auto backlight. +static inline void noteKbActivity() { s_kb_last_key_ms = millis(); } +#else +static inline void noteKbActivity() {} +#endif + +/** List-style settings launcher (not theme-default `lv_btn` chrome). */ +static void styleSettingsRow(lv_obj_t* row) { + // Matches the same low-opacity neutral-slate look as styleButton so the + // settings rows don't read as a different visual language than the + // chips / sheet buttons elsewhere. Earlier hardcoded blue-slate hexes + // (0x141C27 / 0x2A3848) predated the tactical palette overhaul. + lv_obj_remove_style_all(row); + lv_obj_set_style_bg_color(row, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_bg_opa(row, LV_OPA_0, LV_PART_MAIN); // fully transparent at rest + lv_obj_set_style_bg_color(row, lv_color_hex(COLOR_ACCENT_PRESS), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_bg_opa(row, LV_OPA_30, LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_radius(row, 6, LV_PART_MAIN); // sharper than 14 + lv_obj_set_style_border_width(row, 1, LV_PART_MAIN); + lv_obj_set_style_border_color(row, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_border_opa(row, LV_OPA_30, LV_PART_MAIN); + lv_obj_set_style_pad_all(row, 0, LV_PART_MAIN); + lv_obj_set_style_shadow_width(row, 0, LV_PART_MAIN); + lv_obj_set_style_shadow_ofs_y(row, 0, LV_PART_MAIN); +} + +// ============================================================ +// Style helpers +// ============================================================ +static void styleSurface(lv_obj_t* obj, uint32_t bg, lv_coord_t radius = 10) { + lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_bg_color(obj, lv_color_hex(bg), LV_PART_MAIN); + lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN); + lv_obj_set_style_radius(obj, radius, LV_PART_MAIN); +} + +static void styleCard(lv_obj_t* obj) { + styleSurface(obj, COLOR_PANEL, 10); + lv_obj_set_style_border_width(obj, 1, LV_PART_MAIN); + lv_obj_set_style_border_color(obj, lv_color_hex(0x18191A), LV_PART_MAIN); +} + +static void styleButton(lv_obj_t* obj) { + // Subdued slate chip. Background sits at the panel colour with a low + // overall opacity so the pure-black BG bleeds through — chips read as + // "etched out of the bezel" rather than glowing. Border is a faint + // 1-px line of COLOR_ACCENT at low opacity, text is clean white-ish. + // Press state flashes a brighter slate fill so taps still register. + // Primary action buttons (Send / Save / Login / Apply / Add) override + // the bg to COLOR_STATUS_OK so they remain visually distinct. + lv_obj_set_style_bg_color(obj, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_bg_opa(obj, LV_OPA_10, LV_PART_MAIN); + lv_obj_set_style_bg_color(obj, lv_color_hex(COLOR_ACCENT_PRESS), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_bg_opa(obj, LV_OPA_50, LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_border_color(obj, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_border_width(obj, 1, LV_PART_MAIN); + lv_obj_set_style_border_opa(obj, LV_OPA_40, LV_PART_MAIN); + lv_obj_set_style_text_color(obj, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_radius(obj, 4, LV_PART_MAIN); + lv_obj_set_style_shadow_width(obj, 0, LV_PART_MAIN); +} + +// "X" close affordance for popup cards. A bare 16-px glyph with an +// invisible 32×32 tap target so it reads as just a symbol but is forgiving +// to hit on a 240-px touch screen. Sits flush inside the card's top-right +// corner — no border / bg, so it doesn't compete visually with the card's +// own buttons. +// IMPORTANT: each caller is responsible for keeping the top-right ~32×32 +// of the card content-free (or for placing only short titles there) so +// the X doesn't sit on top of a real button. +static void addCloseXBadge(lv_obj_t* card, lv_event_cb_t cb, void* user_data = nullptr) { + lv_obj_t* x = lv_obj_create(card); + lv_obj_remove_style_all(x); + lv_obj_set_size(x, 32, 32); + // Flush to top-right of the card. No outward offset — keeps the badge + // entirely on top of the card so it can't clip behind the modal backdrop. + lv_obj_align(x, LV_ALIGN_TOP_RIGHT, 0, 0); + // No fill / border — bare glyph. Pressed state nudges a faint dim so + // there's *some* visual feedback on tap. + lv_obj_set_style_bg_opa(x, LV_OPA_TRANSP, LV_PART_MAIN); + lv_obj_set_style_bg_color(x, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_bg_opa(x, LV_OPA_20, LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_radius(x, 16, LV_PART_MAIN); + lv_obj_set_style_border_width(x, 0, LV_PART_MAIN); + lv_obj_set_style_pad_all(x, 0, LV_PART_MAIN); + lv_obj_add_flag(x, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(x, LV_OBJ_FLAG_FLOATING); + lv_obj_add_flag(x, LV_OBJ_FLAG_IGNORE_LAYOUT); + lv_obj_clear_flag(x, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_move_foreground(x); + lv_obj_add_event_cb(x, cb, LV_EVENT_CLICKED, user_data); + lv_obj_t* lbl = lv_label_create(x); + lv_label_set_text(lbl, LV_SYMBOL_CLOSE); + lv_obj_set_style_text_font(lbl, &g_font_16, LV_PART_MAIN); + lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + // Anchor the glyph itself to the TOP-RIGHT of the (32×32) tap area — + // centering it made the X look like it was floating in the middle of + // nothing. The 32×32 hit zone still extends down/left for an easy tap, + // but visually the glyph occupies the corner of the popup card. + lv_obj_align(lbl, LV_ALIGN_TOP_RIGHT, -6, 4); +} + +// ============================================================ +// Display / input drivers +// ============================================================ +static void lvglFlush(lv_disp_drv_t* disp_drv, const lv_area_t* area, lv_color_t* color_p) { + (void)disp_drv; + int32_t w = area->x2 - area->x1 + 1; + int32_t h = area->y2 - area->y1 + 1; + display.writePixelsRGB565(area->x1, area->y1, w, h, reinterpret_cast(color_p)); + lv_disp_flush_ready(disp_drv); +} + +// Rotate the ST7789 panel in hardware for the landscape orientations. Uses the +// global `display` (ST7789LCDDisplay) — a free function so it isn't shadowed by +// UITask::begin's DisplayDriver* parameter, also called `display`. ROT_90 maps +// to panel rotation 1, ROT_270 to 3. Portrait leaves the panel as inited. +static void applyHardwarePanelRotation(uint8_t lvgl_rot) { + if (lvgl_rot == LV_DISP_ROT_90) display.setDisplayRotation(1); + else if (lvgl_rot == LV_DISP_ROT_270) display.setDisplayRotation(3); +} + +static void lvglTouchRead(lv_indev_drv_t* indev, lv_indev_data_t* data) { + (void)indev; + static lv_point_t p = {0, 0}; + ++s_live_diag_reads; + // When the background polling task is running it is the sole owner of the + // chsc6x driver's state machine — calling heltecV4CapTouchCheck() from here + // too would race the state machine and double-dispatch tap/swipe events. + if (!heltecV4CapTouchIsAsyncPolling()) { + (void)heltecV4CapTouchCheck(); + } + // If the gesture has turned into a swipe, tell LVGL to abandon the press + // it started on the originally-touched widget. Without this, swiping + // sideways across a settings row both switches tabs (our applySwipeGesture) + // and clicks the row (LVGL's default press-release-to-click). + // + // EXCEPTION: on the Map tab, pan IS the primary gesture. The swipe abort + // was cutting off horizontal drags after the 36-px swipe threshold, so + // the user could only move the map a tiny amount before LVGL released + // the finger. Skip the abort there and keep delivering live touch. + if (heltecV4CapTouchIsSwiping()) { + const bool on_map = g_lv.tabview && + (int)lv_tabview_get_tab_act(g_lv.tabview) == MAP_TAB_INDEX; + // Chat detail overlay needs full touch too — its composer / send / + // back / bubble interactions are all taps, not swipes. Bypass the + // abort there as well, otherwise opening a chat from a map marker + // (which leaves the swipe-detector latched briefly) freezes the + // detail until the next manual tab change. + const bool detail_open = g_lv.dm.detail_open || g_lv.ch.detail_open; + // Control center open: its brightness slider needs the full horizontal drag, + // so don't abort the press there either. + if (!on_map && !detail_open && !s_cc_root) { + lv_indev_t* act = lv_indev_get_act(); + if (act) lv_indev_wait_release(act); + data->state = LV_INDEV_STATE_RELEASED; + data->point = p; + return; + } + } + uint16_t x = 0, y = 0; + bool raw_press = false; + if (heltecV4CapTouchGetLive(&x, &y)) { + ++s_live_diag_pressed; + raw_press = true; + } else if (heltecV4CapTouchPopTap(&x, &y)) { + // Fallback: if live state is missed, emit one press edge from finalized tap. + ++s_live_diag_tap_edges; + raw_press = true; + } + if (raw_press +#if defined(HAS_TDECK_TRACKBALL) + // Ignore a stray finger on the tab bar while the cursor is up. + && !tbFingerTouchOnTabBarBlocked(y) +#endif + ) { + p.x = static_cast(x); + p.y = static_cast(y); + data->point = p; +#if defined(HAS_TDECK_GT911) + // A finger press must NOT reach the hidden/locked UI. When merely + // idle-dimmed, noteUserInput() wakes it; on a manual lock the touch is + // inert (only a trackball *hold* unlocks), whether the lock screen is dark + // or lit. Either way the press is absorbed, not delivered. + if (g_lv.task && (g_lv.task->isScreenOff() || g_lv.task->isManualLock())) { + if (!g_lv.task->isManualLock()) g_lv.task->noteUserInput(); + data->state = LV_INDEV_STATE_RELEASED; + return; + } +#endif + data->state = LV_INDEV_STATE_PRESSED; + if (g_lv.task) g_lv.task->noteUserInput(); + return; + } +#if defined(HAS_TDECK_TRACKBALL) + // Trackball centre click acts as a touch at the cursor (lower priority than a + // real finger). Holding it = a held press, so taps, long-press and drag all + // work; releasing fires the click on whatever is under the cursor. + if (s_tb_click_press) { + p.x = static_cast(s_tb_cursor_x); + p.y = static_cast(s_tb_cursor_y); + data->point = p; + data->state = LV_INDEV_STATE_PRESSED; + if (g_lv.task) g_lv.task->noteUserInput(); + return; + } +#endif + data->point = p; + data->state = LV_INDEV_STATE_RELEASED; +} + +// ============================================================ +// Forward declarations +// ============================================================ +static void refreshChatDetail(LvChatPanel& p); +static void refreshChatList(LvChatPanel& p); +static void applyAccent(uint32_t rgb); // theme accent (Settings -> Theme colour) +static void openAccentPicker(); +static void openAccentPickerCb(lv_event_t* e); +static void openChannelScopeModal(int slot, const char* name); // per-channel region scope +static void channelGearCb(lv_event_t* e); +static void openBlockedUsersModal(); // ignore-list manager (unblock) +static bool overlayBlocksTabSwipe(); // theme/channel-scope pickers swallow tab swipes +static void refreshContactsList(); +static void refreshThreadLists(); +static void refreshStatusLabels(); +static void refreshLiveDiag(unsigned long now); +static void refreshSettingsSectionSubtitles(); +static void refreshLogModalView(); +static void hideKb(); +static void accentExit(); // long-press accent picker (issue #22, dead on touch) +static void accentBoxHide(); // tap-to-pick accent box (issue #22) +static void txtMenuHide(); // cut/copy/paste/select-all edit menu +static void showKb(LvChatPanel* p); +static void closeSettingsModal(); +static void contactSelectCb(lv_event_t* e); +static double contactDistanceKm(double lat1, double lon1, double lat2, double lon2); +static void openLogModalCb(lv_event_t* e); +static void logModeRxCb(lv_event_t* e); +static void logModeRawCb(lv_event_t* e); +static void showConfirm(const char* msg, const char* ok_label, void (*on_confirm)()); +static void clipboardSet(const char* text, const char* tag); +static void copyLabelLongPressCb(lv_event_t* e); +static lv_obj_t* createSettingsModal(const char* title, SettingsModalKind kind); +static void chatDeleteApply(); +static void shareMyContactBtnCb(lv_event_t* e); +static void openShareMyContactPopup(); +static void refreshMapInfoLabel(); +static void renderMapTiles(); +static void freeMapTiles(); +// Single entry point used by tabChangedCb. Recenters on self GPS and +// rebuilds the tile grid. Defined alongside the map state below. +static void onMapTabActivated(); +static void applyMapChrome(bool on); // map-tab immersive chrome (transparent bars); defined near makeMapTab +static void formatAgeBadge(char* buf, size_t cap, uint32_t age_secs); // defined with the contacts list +static void formatDistanceBadge(char* out, size_t out_cap, double self_lat, double self_lon, + int32_t c_lat_e6, int32_t c_lon_e6); // defined with the contacts list +static void openChannelLongPressActionSheet(int thread_idx, const char* name); +// ============================================================ +// Helpers +// ============================================================ +static const char* onOff(bool v) { return v ? "On" : "Off"; } + +static void scheduleHeavyRefresh(unsigned long delay_ms) { + g_lv.defer_heavy_refresh = true; + g_lv.heavy_refresh_at_ms = millis() + delay_ms; +} + +static bool hasChatDetailOpen() { + return (g_lv.dm.detail_open && g_lv.dm.overlay) || + (g_lv.ch.detail_open && g_lv.ch.overlay); +} +static void closeChatPanel(LvChatPanel* p); // fwd: HOME button + swipe-back both close an open chat + +static void resetSettingsModalState() { + g_set_modal = {}; + g_set_modal.kind = SettingsModalKind::None; +} + +static bool parseFloatField(lv_obj_t* ta, float& out) { + if (!ta) return false; + const char* txt = lv_textarea_get_text(ta); + if (!txt || !txt[0]) return false; + // Tolerate a European decimal comma ("50,8466") and any trailing whitespace — + // strict strtof stops at the comma and rejected such coordinates as invalid. + char buf[40]; size_t j = 0; + for (const char* p = txt; *p && j < sizeof(buf) - 1; ++p) + buf[j++] = (*p == ',') ? '.' : *p; + buf[j] = '\0'; + char* endptr = nullptr; + float v = strtof(buf, &endptr); + if (!endptr || endptr == buf) return false; + while (*endptr == ' ' || *endptr == '\t' || *endptr == '\r' || *endptr == '\n') ++endptr; + if (*endptr != '\0') return false; + out = v; + return true; +} + +static bool parseIntField(lv_obj_t* ta, int& out) { + if (!ta) return false; + const char* txt = lv_textarea_get_text(ta); + if (!txt || !txt[0]) return false; + char* endptr = nullptr; + long v = strtol(txt, &endptr, 10); + if (!endptr || endptr == txt || *endptr != '\0') return false; + out = static_cast(v); + return true; +} + +// ============================================================ +// Tab / swipe navigation +// ============================================================ +static void goToTab(int idx); // fwd-declared; defined after tabChangedCb + +static int getActiveTab() { + return g_lv.tabview ? static_cast(lv_tabview_get_tab_act(g_lv.tabview)) : 0; +} + +static lv_obj_t* getActiveTabPage() { + if (!g_lv.tabview) return nullptr; + lv_obj_t* content = lv_tabview_get_content(g_lv.tabview); + if (!content) return nullptr; + return lv_obj_get_child(content, getActiveTab()); +} + +// Settings is a category landing (a scrollable list in portrait, a 2-column grid +// in landscape) that opens a focused detail "sheet" per category — replacing the +// old nested sub-tabview. Stage 1 maps the categories onto the existing inline +// builders; Stage 2 will split the "Device" catch-all further. +enum { + CAT_PROFILE = 0, // identity / name / advert location / QR / export-import + CAT_RADIO, // radio params + auto-add + experimental + CAT_WIFI, // Wi-Fi config + saved slots + CAT_BLUETOOTH, // BLE enable + pairing code + CAT_DISPLAY, // screen timeout, units, bubbles, theme, orientation + CAT_KEYBOARD, // secondary layouts + accent popups + CAT_QUICKREPLIES, // quick-reply macros + CAT_SOUND, // notification sound + CAT_LOCK, // lock-screen wallpaper + text colour (T-Deck only) + CAT_SYSTEM, // GPS, clock, advert, storage, time offset, battery, reboot, setup, live info + CAT_BACKUPS, // list/delete .json backups + factory reset + CAT_LANGUAGE, // UI language picker + CAT_ABOUT, // firmware / update / system info / diagnostics + CAT_COUNT +}; +struct SettingsCatDef { const char* label; const char* icon; }; +static const SettingsCatDef kSettingsCats[CAT_COUNT] = { + { "Profile", LV_SYMBOL_EDIT }, + { "Radio & Mesh", LV_SYMBOL_GPS }, + { "Wi-Fi", LV_SYMBOL_WIFI }, + { "Bluetooth", LV_SYMBOL_BLUETOOTH }, + { "Display", LV_SYMBOL_IMAGE }, + { "Keyboard", LV_SYMBOL_KEYBOARD }, + { "Quick replies", LV_SYMBOL_ENVELOPE }, + { "Sound", LV_SYMBOL_AUDIO }, + { "Lock screen", LV_SYMBOL_EYE_CLOSE }, + { "System", LV_SYMBOL_SETTINGS }, + { "Backups", LV_SYMBOL_SAVE }, + { "Language", LV_SYMBOL_BARS }, + { "About", LV_SYMBOL_LIST }, +}; +// Which slice of the (formerly monolithic) Device settings a builder emits. One +// detail page per section; buildDeviceSettings(sec) emits only that section's +// blocks (the skipped blocks don't advance the y-cursor, so each page lays out +// from the top). +enum { DSEC_DISPLAY = 0, DSEC_KEYBOARD, DSEC_SOUND, DSEC_LOCK, DSEC_SYSTEM }; +static lv_obj_t* s_settings_landing = nullptr; // the category landing container +static lv_obj_t* s_settings_sheet = nullptr; // open detail sheet (layer_top); null = on landing +static int s_settings_open_cat = -1; // which category is open (for About-label teardown) +static void closeSettingsCategory(); // fwd: tab-change + key-dismiss close the sheet + +// ---- Firmware update check (red badge on the Settings gear + About-tab line) ---- +// Compares our embedded release tag against the latest pre-alpha_N published to +// ALLFATHER-BV/meshcomod (the same listing the web flasher reads), fetched over +// plain HTTP via the meshcomod api-github proxy. FIRMWARE_RELEASE_TAG is set at +// release time; an empty/non-release tag disables the check (dev build). +#ifndef FIRMWARE_RELEASE_TAG +#define FIRMWARE_RELEASE_TAG "" +#endif +static lv_obj_t* s_update_badge = nullptr; // red "!" over the bottom-bar gear +static lv_obj_t* s_update_subtab_badge = nullptr;// red dot over the "About" sub-tab button +static lv_obj_t* s_update_about_lbl = nullptr; // status line on the About sub-tab +static lv_obj_t* s_sysinfo_lbl = nullptr; // System-info text (refreshed live on the About tab) +static bool s_update_available = false; +static bool s_verchk_ran = false; // a check has completed (ok or failed) +static volatile bool s_verchk_request = false; // UI -> core-0 worker: please check +static volatile bool s_verchk_done = false; // worker -> UI: result ready +static volatile int s_verchk_latest_n = -1; // highest published pre-alpha_N (-1 = failed) + +// ---- Wi-Fi scan (serviced by the core-0 fetch worker; results drawn in the +// Network tab + the setup wizard) ---- +static constexpr int kWifiScanMax = 14; +static char s_wifiscan_ssids[kWifiScanMax][WIFI_CONFIG_SSID_MAX]; +static volatile int s_wifiscan_count = 0; +static volatile bool s_wifiscan_request = false; // UI -> worker: scan now +static volatile bool s_wifiscan_done = false; // worker -> UI: results ready +static lv_obj_t* s_wifi_scan_list = nullptr; // results list (inside the scan popup) +static lv_obj_t* s_wifi_scan_popup = nullptr; // full-screen scan overlay (covers bar + sub-tabs) +static bool ensureTileFetchTaskRunning(); // fwd: respawn the core-0 worker if it idled out + +// ---- First-boot setup wizard (welcome -> name -> region -> Wi-Fi). Shown once +// on a fresh flash; gated by touchPrefsGetSetupDone(). s_setup_root is declared +// here (not with the wizard functions further down) so the hardware-key handler +// can swallow stray keystrokes while the wizard owns the screen. ---- +static lv_obj_t* s_setup_root = nullptr; // full-screen overlay; non-null = wizard active +static int s_setup_step = 0; // 0 welcome / 1 name / 2 region / 3 Wi-Fi +static lv_obj_t* s_setup_name_ta = nullptr; +static lv_obj_t* s_setup_region_list = nullptr; +static int s_setup_region_sel = -1; // selected preset index, -1 = keep default +static lv_obj_t* s_setup_ssid_ta = nullptr; +static lv_obj_t* s_setup_pwd_ta = nullptr; +static bool s_setup_wifi_autoscan_done = false; // auto-open the picker once per wizard run +static void setupWizardOpen(); // fwd: re-trigger the flow (Device settings button) +static void setupRerunCb(lv_event_t* e); // fwd: "Run setup again" button callback + +// Our release number (the N in "beta_N"); -1 if this isn't a tagged build. +static int firmwareReleaseN() { + const char* t = FIRMWARE_RELEASE_TAG; + const char* pfx = "beta_"; + const size_t pl = strlen(pfx); + if (strncmp(t, pfx, pl) != 0) return -1; + if (t[pl] < '0' || t[pl] > '9') return -1; + return atoi(t + pl); +} + +// Reflect the latest check result in the gear badge + the About status line. +static void otaButtonRefreshState(); // defined below; greys the Install button when current + +static void versionCheckUpdateUi() { + // Two badges: the bottom-bar gear (get the user into Settings) and the About + // sub-tab (the actual update info lives there). + lv_obj_t* badges[2] = { s_update_badge, s_update_subtab_badge }; + for (lv_obj_t* bd : badges) { + if (!bd) continue; + if (s_update_available) lv_obj_clear_flag(bd, LV_OBJ_FLAG_HIDDEN); + else lv_obj_add_flag(bd, LV_OBJ_FLAG_HIDDEN); + } + otaButtonRefreshState(); // enable/grey the OTA button to match the result + if (!s_update_about_lbl) return; + const int my_n = firmwareReleaseN(); + char b[140]; + if (my_n < 0) { + snprintf(b, sizeof b, "Firmware %s\nDevelopment build — update check off", FIRMWARE_VERSION); + lv_obj_set_style_text_color(s_update_about_lbl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + } else if (s_update_available && s_verchk_latest_n >= 0) { + snprintf(b, sizeof b, LV_SYMBOL_DOWNLOAD " Update available: beta_%d\nYou have beta_%d — update manually at flasher.meshcomod.com", + s_verchk_latest_n, my_n); + lv_obj_set_style_text_color(s_update_about_lbl, lv_color_hex(0xE2A23A), LV_PART_MAIN); + } else if (s_verchk_latest_n >= 0) { + snprintf(b, sizeof b, LV_SYMBOL_OK " Up to date (beta_%d)", my_n); + lv_obj_set_style_text_color(s_update_about_lbl, lv_color_hex(0x6FCF6F), LV_PART_MAIN); + } else if (s_verchk_ran) { + snprintf(b, sizeof b, TR("Firmware beta_%d\nCouldn't reach the update server"), my_n); + lv_obj_set_style_text_color(s_update_about_lbl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + } else { + snprintf(b, sizeof b, "Firmware beta_%d\nChecking for updates over Wi-Fi…", my_n); + lv_obj_set_style_text_color(s_update_about_lbl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + } + lv_label_set_text(s_update_about_lbl, b); +} + +// ---- OTA: install the latest published release over the air ---- +// The download goes through the meshcomod flasher proxy over PLAIN HTTP (the +// device can't do HTTPS after Wi-Fi associates — see the tile/version-check +// notes), which startHttpOtaFromUrl + its allow-list already support. The +// proxy maps /firmware-download/* → ALLFATHER-BV/meshcomod main/* on GitHub, +// so the app-only .bin under prebuilt/releases/TOUCH//.bin is what we +// fetch. Requires the dual-slot partition table (app1 ≥ image size). +#ifndef FIRMWARE_OTA_ENV +#define FIRMWARE_OTA_ENV "" +#endif +static lv_obj_t* s_ota_status_lbl = nullptr; // live OTA status line (About page) +static lv_obj_t* s_ota_btn = nullptr; // "Install update" button (greyed when up to date) +static lv_obj_t* s_ota_btn_lbl = nullptr; // its caption (recoloured when disabled) + +// Enable the OTA button only when an update is actually available — or when the +// check hasn't completed yet (offline / still checking), so the user isn't +// blocked by a stale "up to date". Grey + non-clickable when current. +static void otaButtonRefreshState() { + if (!s_ota_btn) return; + // Known-current = a successful check (latest_n >= 0) that isn't newer than us. + const bool known_current = (s_verchk_latest_n >= 0) && !s_update_available; + const bool enabled = !known_current; + if (enabled) { + lv_obj_add_flag(s_ota_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_state(s_ota_btn, LV_STATE_DISABLED); + lv_obj_set_style_bg_color(s_ota_btn, lv_color_hex(COLOR_STATUS_OK), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_ota_btn, LV_OPA_COVER, LV_PART_MAIN); + if (s_ota_btn_lbl) lv_obj_set_style_text_color(s_ota_btn_lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + } else { + lv_obj_clear_flag(s_ota_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_state(s_ota_btn, LV_STATE_DISABLED); + lv_obj_set_style_bg_color(s_ota_btn, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_ota_btn, LV_OPA_20, LV_PART_MAIN); + if (s_ota_btn_lbl) lv_obj_set_style_text_color(s_ota_btn_lbl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + } +} + +static void otaInstallLatestCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + // Wi-Fi (OTA) self-update is temporarily disabled while we sort out update + // issues (OTA partition-slot sizing on V4 / T-Deck). The version check and the + // "update available" badge stay on — this button now points the user at the + // manual flasher instead of running the (currently unreliable) OTA path. + if (s_ota_status_lbl) { + lv_label_set_text(s_ota_status_lbl, + "Wi-Fi update is paused while we fix some issues.\n" + "Please update manually at flasher.meshcomod.com\n" + "(T-Deck under Launcher: reinstall the new bin there)."); + lv_obj_set_style_text_color(s_ota_status_lbl, lv_color_hex(0xE2A23A), LV_PART_MAIN); + } + if (g_lv.task) + g_lv.task->showAlert(TR("Update manually at flasher.meshcomod.com\n(Wi-Fi update is paused for now)"), 3500); +} + +// Trigger a check once Wi-Fi is up (then every 6 h); apply the result when ready. +static void versionCheckService(unsigned long now) { +#if defined(MULTI_TRANSPORT_COMPANION) + if (firmwareReleaseN() < 0) return; // dev build: nothing to compare against + static bool started = false; + static unsigned long next_ms = 0; + if (WiFi.status() == WL_CONNECTED && !s_verchk_request && !s_verchk_done && + (!started || (long)(now - next_ms) >= 0)) { + started = true; + next_ms = now + 6UL * 60UL * 60UL * 1000UL; // re-check every 6 h + ensureTileFetchTaskRunning(); // worker idles out — make sure it's up + s_verchk_request = true; + } + if (s_verchk_done) { + s_verchk_done = false; + s_verchk_ran = true; + s_update_available = (s_verchk_latest_n > firmwareReleaseN()); + versionCheckUpdateUi(); + } +#else + (void)now; +#endif +} +static void applySwipeGesture(int8_t swipe_x, int8_t swipe_y) { + // An open chat/channel: a left→right (rightward) swipe closes it (iOS-style + // back). Any other swipe is swallowed so the conversation keeps the screen. + if (hasChatDetailOpen()) { + if (swipe_x > 0) { + if (g_lv.dm.detail_open) closeChatPanel(&g_lv.dm); + else if (g_lv.ch.detail_open) closeChatPanel(&g_lv.ch); + } + return; + } + if (s_wifi_scan_popup) return; // scan popup is modal; don't switch tabs underneath + if (overlayBlocksTabSwipe()) return; // theme / channel-scope pickers: don't swipe the menu beneath + // The control center drops down from the top bar, so an up-swipe dismisses + // it (natural "push it back up" gesture). Any other swipe is swallowed so + // that e.g. dragging its brightness slider can't also switch the tab + // underneath the card. swipe_y < 0 == upward (see TDeckTouch swipe sign). + if (s_cc_root) { + if (swipe_y < 0) closeControlCenter(); + return; + } + if (!g_lv.tabview) return; + if (swipe_x != 0) { + // An open settings detail sheet: a left→right (rightward) swipe goes Back, + // like iOS edge-back. Any other swipe is swallowed so the tab underneath the + // sheet never switches. + if (s_settings_sheet) { + if (swipe_x > 0) closeSettingsCategory(); + return; + } + // A settings modal overlays the tab — never switch under it. + if (settingsModalIsOpen()) return; + // Block horizontal tab-swipes on the Map tab — that gesture is "pan" + // there, not "switch tab". The +/− and recenter buttons + the tabbar + // icons remain the only way off the tab. + if (getActiveTab() == MAP_TAB_INDEX) return; + int idx = getActiveTab(); + int next = idx + (swipe_x < 0 ? 1 : -1); + if (next < 0) next = 0; + if (next > TAB_LAST) next = TAB_LAST; + if (next != idx) goToTab(next); + return; + } + // Vertical swipes intentionally fall through: LVGL's native drag-scroll on + // the underlying list (chats / contacts / settings tab) handles them + // continuously during the gesture, which feels naturally finger-locked. + // The old fixed-step jump on release was the source of "jittery / jumpy" + // vertical scroll feel. + (void)swipe_y; +} + +/** After elastic/rubber-band overscroll, snap back to the furthest valid top or bottom. */ +static void scrollClampOnEndCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_SCROLL_END) return; + lv_obj_t* obj = lv_event_get_target(e); + if (!obj || !lv_obj_has_flag(obj, LV_OBJ_FLAG_SCROLLABLE)) return; + lv_dir_t d = lv_obj_get_scroll_dir(obj); + if (!(d & LV_DIR_VER)) return; + + lv_obj_update_layout(obj); + lv_coord_t sy = lv_obj_get_scroll_y(obj); + lv_coord_t sm = lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj); + if (sm < 0) sm = 0; + + if (sy < 0) lv_obj_scroll_to_y(obj, 0, LV_ANIM_ON); + else if (sy > sm) lv_obj_scroll_to_y(obj, sm, LV_ANIM_ON); +} + +// ============================================================ +// Keyboard show / hide +// Chat: keyboard binds to composer (layout already lifts composer above keys). +// Settings modals: optional top mirror strip so low fields stay readable. +// ============================================================ +constexpr int KB_MIRROR_STRIP_H = 52; + +// Keyboard rotation helpers (defined here so showKb/hideKb/kbMirrorBind can use them). +// Layout adjusts keyboard + mirror + rotate arrows for the current rotation. +static void kbApplyLayoutForRotation(uint8_t rot) { + lv_disp_t* disp = lv_disp_get_default(); + if (!disp) return; + const lv_coord_t scr_w = lv_disp_get_hor_res(disp); + const lv_coord_t scr_h = lv_disp_get_ver_res(disp); + const bool landscape = (rot == LV_DISP_ROT_90 || rot == LV_DISP_ROT_270); + // Cap keyboard to half the screen so the textarea above stays visible. + const lv_coord_t kb_h = landscape ? (scr_h / 2) : CHAT_KB_H; + + if (g_lv.keyboard) { + lv_obj_set_size(g_lv.keyboard, scr_w, kb_h); + lv_obj_align(g_lv.keyboard, LV_ALIGN_BOTTOM_MID, 0, 0); + } + if (s_kb_mirror_root) { + lv_obj_set_width(s_kb_mirror_root, scr_w); + if (s_kb_mirror_ta) lv_obj_set_width(s_kb_mirror_ta, scr_w - 16); + } + // Re-flow the active chat panel for the new orientation. Its children were + // sized/positioned for whatever rotation was current when the panel was built + // or last shown — so after a rotate the message area + composer keep a stale + // (e.g. portrait) width and Y: the composer ends up narrow and parked off the + // visible area until something nudges it (the auto-grow on the 2nd line was the + // only thing fixing the Y). The display is already rotated here, so the chat*() + // helpers return the new geometry. + if (s_kb_panel) { + // Resize the overlay container FIRST — it's sized to the screen at build time + // and otherwise stays portrait-narrow in landscape, clipping everything + // inside it (the composer looked "not full width" because its parent wasn't). + if (s_kb_panel->overlay) + lv_obj_set_size(s_kb_panel->overlay, chatScreenW(), chatScreenH()); + if (s_kb_panel->msgs) + lv_obj_set_size(s_kb_panel->msgs, chatScreenW(), chatMsgHKb()); + if (s_kb_panel->composer_row) { + lv_obj_set_size(s_kb_panel->composer_row, chatScreenW(), s_comp_h); + lv_obj_set_y(s_kb_panel->composer_row, chatCompYKb()); + } + if (s_kb_panel->composer_ta) + lv_obj_set_width(s_kb_panel->composer_ta, chatScreenW() - 120); + } + // Rotation arrows. Default: just above the keyboard's top edge. In the + // chat panel that area is occupied by the composer row (QR + textarea + + // Send), so the arrows would land on top of the buttons. When the chat + // composer is active, bump the arrows up to sit just above the COMPOSER + // instead of just above the keyboard. + lv_coord_t arrow_bottom_y_from_screen_top = scr_h - (kb_h + 2); + if (s_kb_panel && s_kb_panel->composer_row) { + // Sit just above the chat composer, which itself sits just above the + // keyboard (composer bottom == keyboard top). COMPUTE that from this + // rotation's keyboard height (kb_h) + composer height (s_comp_h) rather than + // sampling the composer's live coords: on a rotate the composer hasn't been + // re-laid-out for the new orientation yet, so sampling raced it and dropped + // these buttons on top of the landscape keyboard. 2-px gap above the composer. + arrow_bottom_y_from_screen_top = scr_h - kb_h - s_comp_h - 2; + } + // Convert "absolute y of arrow bottom" → offset usable by LV_ALIGN_BOTTOM_*. + const lv_coord_t y_off = -(scr_h - arrow_bottom_y_from_screen_top); + if (s_kb_rot_left_btn) lv_obj_align(s_kb_rot_left_btn, LV_ALIGN_BOTTOM_LEFT, 4, y_off); + if (s_kb_rot_right_btn) lv_obj_align(s_kb_rot_right_btn, LV_ALIGN_BOTTOM_RIGHT, -4, y_off); + // Language key sits just right of the left rotate arrow. + if (s_kb_lang_btn) lv_obj_align(s_kb_lang_btn, LV_ALIGN_BOTTOM_LEFT, 40, y_off); + // Accent ("Alt") key sits just right of the language key. + if (s_kb_alt_btn) lv_obj_align(s_kb_alt_btn, LV_ALIGN_BOTTOM_LEFT, 76, y_off); +} + +static void kbApplyRotation(uint8_t rot) { + lv_disp_t* disp = lv_disp_get_default(); + if (!disp) return; + // In hardware-landscape the panel is already rotated; applying LVGL software + // rotation on top would double-rotate. Software rotation is used ONLY for the + // transient keyboard-landscape trick when the base orientation is portrait. + const bool hw_landscape = (s_ui_rotation == LV_DISP_ROT_90 || + s_ui_rotation == LV_DISP_ROT_270); + if (!hw_landscape) { + lv_disp_set_rotation(disp, static_cast(rot)); + } + // Keep the touch driver's swipe-axis transform in sync with the visible + // orientation so horizontal swipes are detected correctly in landscape. + heltecV4CapTouchSetRotation(rot); + kbApplyLayoutForRotation(rot); + // Force a full redraw — without this the previous frame's contents linger + // briefly at the new orientation's coordinates. + lv_obj_invalidate(lv_scr_act()); +} + +static void kbSaveRotationPref() { +#if defined(ESP32) + SdNvsPrefs pr; + if (pr.begin("meshTouch", false)) { + pr.putUChar("kbrot", s_kb_rotation); + pr.end(); + } +#endif +} + +static void kbSetRotateArrowsOpa(lv_opa_t opa) { + if (s_kb_rot_left_btn) lv_obj_set_style_opa(s_kb_rot_left_btn, opa, LV_PART_MAIN); + if (s_kb_rot_right_btn) lv_obj_set_style_opa(s_kb_rot_right_btn, opa, LV_PART_MAIN); + if (s_kb_lang_btn) lv_obj_set_style_opa(s_kb_lang_btn, opa, LV_PART_MAIN); + if (s_kb_alt_btn) lv_obj_set_style_opa(s_kb_alt_btn, opa, LV_PART_MAIN); +} + +// ---- On-screen language-cycle key ---------------------------------------- +// Boards without a physical keyboard (e.g. Heltec V4 TFT) can't double-tap a +// hardware SPACE to switch keyboard languages, so the on-screen keyboard carries +// a little key showing the active layout's 2-letter code; tapping it cycles +// English -> each enabled secondary layout -> back, like the T-Deck's SPACE. +static const char* kbLayoutCode(KeyboardLayoutId id) { + static const char* k_codes[] = { "EN", "BG", "RU", "UK", "SR", "EL", "AR" }; + const uint8_t i = static_cast(id); + return (i < (sizeof k_codes / sizeof k_codes[0])) ? k_codes[i] : "EN"; +} +static void kbLangBtnRefresh() { + if (s_kb_lang_lbl) lv_label_set_text(s_kb_lang_lbl, kbLayoutCode(keyboardLayoutsGetCurrent())); +} +static void kbLangCycleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !keyboardLayoutsAnySecondary()) return; + KeyboardLayoutId next = keyboardLayoutsCycle(g_lv.keyboard); +#if defined(ESP32) + touchPrefsSetKeyboardLayout(static_cast(next)); +#endif + kbLangBtnRefresh(); + if (g_lv.task) g_lv.task->showAlert(keyboardLayoutName(next), 800); +} + +static void kbShowRotateArrows(bool show) { + if (s_kb_rot_left_btn) show ? lv_obj_clear_flag(s_kb_rot_left_btn, LV_OBJ_FLAG_HIDDEN) + : lv_obj_add_flag (s_kb_rot_left_btn, LV_OBJ_FLAG_HIDDEN); + if (s_kb_rot_right_btn) show ? lv_obj_clear_flag(s_kb_rot_right_btn, LV_OBJ_FLAG_HIDDEN) + : lv_obj_add_flag (s_kb_rot_right_btn, LV_OBJ_FLAG_HIDDEN); + // Language key: only when ≥1 secondary layout is enabled (else nothing to cycle). + const bool lang_show = show && keyboardLayoutsAnySecondary(); + if (s_kb_lang_btn) lang_show ? lv_obj_clear_flag(s_kb_lang_btn, LV_OBJ_FLAG_HIDDEN) + : lv_obj_add_flag (s_kb_lang_btn, LV_OBJ_FLAG_HIDDEN); + // Accent ("Alt") key: shown whenever the keyboard is up. + if (s_kb_alt_btn) show ? lv_obj_clear_flag(s_kb_alt_btn, LV_OBJ_FLAG_HIDDEN) + : lv_obj_add_flag (s_kb_alt_btn, LV_OBJ_FLAG_HIDDEN); + if (show) { + // Each new show starts at full opacity; the first keystroke fades arrows + // to ~20% so they're out of the way while typing but still tappable. + kbSetRotateArrowsOpa(LV_OPA_COVER); + if (s_kb_rot_left_btn) lv_obj_move_foreground(s_kb_rot_left_btn); + if (s_kb_rot_right_btn) lv_obj_move_foreground(s_kb_rot_right_btn); + if (lang_show) { kbLangBtnRefresh(); lv_obj_move_foreground(s_kb_lang_btn); } + if (s_kb_alt_btn) lv_obj_move_foreground(s_kb_alt_btn); + } +} + +static void kbRotLeftCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + // Tap the left arrow: rotate landscape that way; tap again returns to portrait. + s_kb_rotation = (s_kb_rotation == LV_DISP_ROT_270) ? LV_DISP_ROT_NONE : LV_DISP_ROT_270; + kbApplyRotation(s_kb_rotation); + kbSaveRotationPref(); +} + +static void kbRotRightCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + s_kb_rotation = (s_kb_rotation == LV_DISP_ROT_90) ? LV_DISP_ROT_NONE : LV_DISP_ROT_90; + kbApplyRotation(s_kb_rotation); + kbSaveRotationPref(); +} + +static void kbMirrorEnsureCreated() { + if (s_kb_mirror_root) return; + s_kb_mirror_root = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_kb_mirror_root); + lv_obj_set_size(s_kb_mirror_root, 240, KB_MIRROR_STRIP_H); + // Sit just below the global status bar so the time/battery are still + // visible while typing. + lv_obj_align(s_kb_mirror_root, LV_ALIGN_TOP_MID, 0, STATUSBAR_H + 2); + styleSurface(s_kb_mirror_root, COLOR_PANEL, 8); + lv_obj_set_style_pad_hor(s_kb_mirror_root, 8, LV_PART_MAIN); + lv_obj_set_style_pad_ver(s_kb_mirror_root, 6, LV_PART_MAIN); + lv_obj_clear_flag(s_kb_mirror_root, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(s_kb_mirror_root, LV_OBJ_FLAG_HIDDEN); + + lv_obj_t* hint = lv_label_create(s_kb_mirror_root); + lv_label_set_text(hint, TR("Editing")); + lv_obj_set_style_text_font(hint, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_align(hint, LV_ALIGN_TOP_LEFT, 0, 0); + + s_kb_mirror_ta = lv_textarea_create(s_kb_mirror_root); + lv_obj_set_size(s_kb_mirror_ta, 224, 30); + lv_obj_align(s_kb_mirror_ta, LV_ALIGN_BOTTOM_LEFT, 0, 0); + lv_textarea_set_one_line(s_kb_mirror_ta, true); + lv_obj_set_style_bg_color(s_kb_mirror_ta, lv_color_hex(0x0A0B0C), LV_PART_MAIN); + lv_obj_set_style_text_color(s_kb_mirror_ta, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_border_color(s_kb_mirror_ta, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_set_style_border_width(s_kb_mirror_ta, 1, LV_PART_MAIN); + lv_obj_set_style_text_font(s_kb_mirror_ta, &g_font_14, LV_PART_MAIN); + // Live-sync the mirror's text into the bound real textarea on every + // keystroke instead of waiting for hideKb. Lets the underlying field + // reflect what the user is typing in real time (useful when the field is + // partially visible behind the keyboard, e.g. a chat composer). + lv_obj_add_event_cb(s_kb_mirror_ta, [](lv_event_t* e) { + (void)e; + kbMirrorSyncToReal(); + }, LV_EVENT_VALUE_CHANGED, nullptr); +} + +static void kbMirrorSyncToReal() { + if (!s_kb_bind_ta || !s_kb_mirror_ta) return; + lv_textarea_set_text(s_kb_bind_ta, lv_textarea_get_text(s_kb_mirror_ta)); +} + +// Set a textarea's text from code (not via keystrokes) while keeping the on-screen +// keyboard mirror consistent. If the mirror is currently bound to this field, a +// later kbMirrorSyncToReal() (on Save, or when focus moves to another field) would +// otherwise copy the STALE mirror contents back over what we just set — which is +// exactly what made the Wi-Fi scan picker "not stick" and save the OLD SSID. +// Updating the mirror too keeps both in agreement. +static void setTextareaSynced(lv_obj_t* ta, const char* text) { + if (!ta) return; + lv_textarea_set_text(ta, text ? text : ""); + if (s_kb_bind_ta == ta && s_kb_mirror_ta) { + lv_textarea_set_text(s_kb_mirror_ta, text ? text : ""); + } +} + +// Cleared automatically when the bound textarea is destroyed. kbMirrorSyncToReal +// (and the click/keystroke paths) write into s_kb_bind_ta; if its modal is torn +// down by a path that skips hideKb() — e.g. a hardware-key action or screen lock +// firing mid-flow — the pointer would dangle and the next sync writes into freed +// memory (the 'loopTask' LoadProhibited use-after-free seen in the coredump). An +// LV_EVENT_DELETE hook nulls it the moment LVGL frees the field, so it can never +// point at freed memory regardless of which close path runs. +static void kbBoundTaDeletedCb(lv_event_t* e) { + if (lv_event_get_target(e) == s_kb_bind_ta) s_kb_bind_ta = nullptr; +} + +/** Show keyboard bound to a top mirror of `real_ta` so typed text stays visible above the keyboard. */ +static void kbMirrorBind(lv_obj_t* real_ta) { + if (!real_ta || !g_lv.keyboard) return; + kbMirrorEnsureCreated(); + if (s_kb_bind_ta && s_kb_bind_ta != real_ta) kbMirrorSyncToReal(); + // Detach the bind while we mutate the mirror's properties below. + // set_one_line / set_text emit VALUE_CHANGED on the mirror synchronously, + // and the live-sync callback would otherwise copy the mirror's stale/empty + // state into the freshly-bound real textarea before we've loaded it. + s_kb_bind_ta = nullptr; + + const uint32_t max_len = lv_textarea_get_max_length(real_ta); + lv_textarea_set_max_length(s_kb_mirror_ta, max_len); + lv_textarea_set_password_mode(s_kb_mirror_ta, lv_textarea_get_password_mode(real_ta)); + lv_textarea_set_one_line(s_kb_mirror_ta, true); + lv_textarea_set_text(s_kb_mirror_ta, lv_textarea_get_text(real_ta)); + + // Mirror now holds the real_ta's current text; arm the bind so subsequent + // user keystrokes get mirrored live. + s_kb_bind_ta = real_ta; + // Auto-clear the bind if this field is ever deleted (use-after-free guard). + // Dedup so re-binding a long-lived field (chat composer) doesn't stack hooks. + lv_obj_remove_event_cb(real_ta, kbBoundTaDeletedCb); + lv_obj_add_event_cb(real_ta, kbBoundTaDeletedCb, LV_EVENT_DELETE, nullptr); + + lv_keyboard_set_textarea(g_lv.keyboard, s_kb_mirror_ta); +#if !defined(HAS_TDECK_KEYBOARD) + // T-Deck has a physical keyboard — the mirror/keys never show; the typed text + // syncs straight into the (visible) real field. Other boards show both. + lv_obj_clear_flag(s_kb_mirror_root, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(s_kb_mirror_root); + // Slide the mirror BELOW the settings modal header (when one is open) + // so it doesn't sit on top of the modal's Close button. The mirror is on + // lv_layer_top and gets move_foreground'd above the modal — so any tap + // in the mirror's area was being consumed by the mirror's textarea + // instead of reaching the Close button just behind it. + const lv_coord_t mirror_y = settingsModalIsOpen() + ? (STATUSBAR_H + 46 + 2) // below the 46-px modal header + : (STATUSBAR_H + 2); // top of screen for chat / non-modal contexts + lv_obj_align(s_kb_mirror_root, LV_ALIGN_TOP_MID, 0, mirror_y); + lv_obj_clear_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(g_lv.keyboard); + kbApplyRotation(effectiveKbRotation()); + kbShowRotateArrows(true); +#endif +} + +static void hideKb() { + accentExit(); // tear down any open accent picker + accentBoxHide(); + txtMenuHide(); // tear down any open edit menu + kbMirrorSyncToReal(); + s_kb_bind_ta = nullptr; + if (s_kb_mirror_root) lv_obj_add_flag(s_kb_mirror_root, LV_OBJ_FLAG_HIDDEN); + if (g_lv.keyboard) { + lv_keyboard_set_textarea(g_lv.keyboard, nullptr); + lv_obj_add_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN); + } + kbShowRotateArrows(false); + // Revert the display to the global UI orientation once the keyboard is + // dismissed (portrait unless the user set a landscape base in Settings). + // s_kb_rotation is preserved so the next showKb restores the transient + // keyboard-landscape choice when the base is portrait. + const bool kb_was_landscape = + (effectiveKbRotation() == LV_DISP_ROT_90 || effectiveKbRotation() == LV_DISP_ROT_270); + const bool base_is_landscape = + (s_ui_rotation == LV_DISP_ROT_90 || s_ui_rotation == LV_DISP_ROT_270); + const bool was_landscape = kb_was_landscape && !base_is_landscape; + kbApplyRotation(s_ui_rotation); + // Landscape forced the modal/chat scroll containers down to keep the field + // visible above the rotated keyboard; reset them to the top so the user + // doesn't land mid-scroll when the screen comes back to portrait. + if (was_landscape) { + if (g_set_modal.root) lv_obj_scroll_to(g_set_modal.root, 0, 0, LV_ANIM_OFF); + lv_obj_scroll_to(lv_scr_act(), 0, 0, LV_ANIM_OFF); + } + if (s_kb_panel) { + if (s_kb_panel->composer_ta) lv_obj_clear_state(s_kb_panel->composer_ta, LV_STATE_FOCUSED); + if (s_kb_panel->msgs) lv_obj_set_height(s_kb_panel->msgs, chatMsgHOpen()); + if (s_kb_panel->composer_row) lv_obj_set_y(s_kb_panel->composer_row, chatCompYOpen()); + s_kb_panel = nullptr; + } +} + +static void showKb(LvChatPanel* p) { + if (!g_lv.keyboard || !p || !p->composer_ta || !p->msgs || !p->composer_row) return; + kbMirrorSyncToReal(); + s_kb_bind_ta = nullptr; + if (s_kb_mirror_root) lv_obj_add_flag(s_kb_mirror_root, LV_OBJ_FLAG_HIDDEN); + s_kb_panel = p; + lv_keyboard_set_textarea(g_lv.keyboard, p->composer_ta); + // lv_keyboard_set_textarea focuses the *keyboard*, not the field, so the + // composer shows no cursor and reads as "not targeted". Focus it explicitly — + // on the T-Deck this is the auto-target when a chat opens (typing already + // routes here via handleHwKey; this just makes the cursor visible so the user + // can see the field is ready). + lv_obj_add_state(p->composer_ta, LV_STATE_FOCUSED); +#if !defined(HAS_TDECK_KEYBOARD) + // No on-screen keyboard on the T-Deck — the physical keyboard types straight + // into the composer (already visible), so skip showing the keys + the lift. + lv_obj_clear_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(g_lv.keyboard); + // Shrink message area to keep composer visible above keyboard. + lv_obj_set_height(p->msgs, chatMsgHKb()); + lv_obj_set_y(p->composer_row, chatCompYKb()); + kbApplyRotation(effectiveKbRotation()); + kbShowRotateArrows(true); +#endif +} + +// The composer wraps long messages to multiple lines and grows UPWARD instead +// of scrolling a single line sideways. Recompute the row height from how many +// lines the current text wraps to (capped at CHAT_COMP_MAX_LINES), then re-apply +// the chat layout so the message list above shrinks to match. Fires on every +// composer text change (physical-keyboard insert, on-screen mirror sync, the +// post-send clear, emoji/quick-reply inserts). Past the cap it scrolls vertically. +static void chatComposerAutoGrow(LvChatPanel* p) { + if (!p || !p->composer_ta || !p->composer_row || !p->msgs) return; + const lv_coord_t lh = lv_font_get_line_height(&g_font_14); + if (lh <= 0) return; + const char* txt = lv_textarea_get_text(p->composer_ta); + // Wrap width = the textarea's content box, less a few px for the cursor so the + // box grows a hair before the field would actually need to scroll sideways. + lv_coord_t maxw = lv_obj_get_content_width(p->composer_ta) - 4; + if (maxw < 16) maxw = 16; + lv_point_t sz; + lv_txt_get_size(&sz, (txt && txt[0]) ? txt : " ", &g_font_14, 0, 0, maxw, LV_TEXT_FLAG_NONE); + int lines = (int)((sz.y + lh - 1) / lh); // ceil to whole lines + if (lines < 1) lines = 1; + if (lines > CHAT_COMP_MAX_LINES) lines = CHAT_COMP_MAX_LINES; + const lv_coord_t want = CHAT_COMP_H + (lv_coord_t)(lines - 1) * lh; + if (want == s_comp_h) return; // height unchanged → nothing to relayout + s_comp_h = want; + // Keyboard shown (V4) lifts the composer above the keys; otherwise (T-Deck, or + // V4 with the keyboard down) it sits at the screen bottom. + const bool kb = g_lv.keyboard && !lv_obj_has_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_height(p->composer_row, s_comp_h); + lv_obj_set_height(p->composer_ta, s_comp_h - 4); + lv_obj_set_y(p->composer_row, kb ? chatCompYKb() : chatCompYOpen()); + lv_obj_set_height(p->msgs, kb ? chatMsgHKb() : chatMsgHOpen()); +} + +static void composerAutoGrowCb(lv_event_t* e) { + chatComposerAutoGrow(static_cast(lv_event_get_user_data(e))); +} + +// ============================================================ +// Callbacks +// ============================================================ +// ===== Long-press accent picker (issue #22) ================================= +// Hold a Latin letter key -> a popup shows its accented variants; each further +// tap of that key cycles the selection and replaces the just-typed char in +// place; a short pause (or tapping a different key) commits the highlighted one. +// On-screen, so it works on both V4 + T-Deck. The accent glyphs already live in +// the extras_* fallback fonts (Latin-1 + Latin-Extended-A), so they render in +// the textarea and chat. Keys carry LV_BTNMATRIX_CTRL_NO_REPEAT (KeyboardLayouts +// .cpp) so a hold doesn't auto-repeat — it cleanly long-presses instead. +static const char* const kAccA[] = {"à","á","â","ä","ã","å"}; +static const char* const kAccA_u[] = {"À","Á","Â","Ä","Ã","Å"}; +static const char* const kAccE[] = {"è","é","ê","ë"}; +static const char* const kAccE_u[] = {"È","É","Ê","Ë"}; +static const char* const kAccI[] = {"ì","í","î","ï"}; +static const char* const kAccI_u[] = {"Ì","Í","Î","Ï"}; +static const char* const kAccO[] = {"ò","ó","ô","ö","õ","ø"}; +static const char* const kAccO_u[] = {"Ò","Ó","Ô","Ö","Õ","Ø"}; +static const char* const kAccU[] = {"ù","ú","û","ü"}; +static const char* const kAccU_u[] = {"Ù","Ú","Û","Ü"}; +static const char* const kAccN[] = {"ñ"}; +static const char* const kAccN_u[] = {"Ñ"}; +static const char* const kAccC[] = {"ç"}; +static const char* const kAccC_u[] = {"Ç"}; +static const char* const kAccS[] = {"ß","ś","š"}; +static const char* const kAccY[] = {"ý","ÿ"}; +struct AccentSet { char key; const char* const* v; uint8_t n; }; +static const AccentSet kAccentSets[] = { + {'a',kAccA,6},{'A',kAccA_u,6},{'e',kAccE,4},{'E',kAccE_u,4}, + {'i',kAccI,4},{'I',kAccI_u,4},{'o',kAccO,6},{'O',kAccO_u,6}, + {'u',kAccU,4},{'U',kAccU_u,4},{'n',kAccN,1},{'N',kAccN_u,1}, + {'c',kAccC,1},{'C',kAccC_u,1},{'s',kAccS,3},{'y',kAccY,2}, +}; +static const AccentSet* accentLookup(const char* key) { + if (!key || !key[0] || key[1]) return nullptr; // single ASCII-char keys only + for (const auto& s : kAccentSets) if (s.key == key[0]) return &s; + return nullptr; +} + +static constexpr uint32_t kAccentCommitMs = 900; // pause that commits the choice +static bool s_acc_active = false; +static bool s_acc_first = false; // next VALUE_CHANGED is the base insert +static char s_acc_base[6] = {0}; +static const char* s_acc_opts[12]; +static int s_acc_n = 0; +static int s_acc_idx = 0; +static char s_acc_anchor[256] = {0}; // text before the managed accent char +static lv_obj_t* s_acc_popup = nullptr; +static lv_obj_t* s_acc_cells[12] = {nullptr}; +static lv_timer_t* s_acc_timer = nullptr; + +static void accentPopupHighlight() { + for (int i = 0; i < s_acc_n; ++i) { + if (!s_acc_cells[i]) continue; + const bool sel = (i == s_acc_idx); + lv_obj_set_style_bg_color(s_acc_cells[i], lv_color_hex(sel ? COLOR_ACCENT : 0x10202E), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_acc_cells[i], LV_OPA_COVER, LV_PART_MAIN); + } +} +static void accentPopupHide() { + if (s_acc_popup) { lv_obj_del(s_acc_popup); s_acc_popup = nullptr; } + for (int i = 0; i < 12; ++i) s_acc_cells[i] = nullptr; +} +static void accentExit() { + if (s_acc_timer) { lv_timer_del(s_acc_timer); s_acc_timer = nullptr; } + accentPopupHide(); + s_acc_active = false; + s_acc_first = false; +} +static void accentCommitTimerCb(lv_timer_t* t) { (void)t; accentExit(); } + +static void accentPopupShow() { + accentPopupHide(); + const int cw = 30, ch = 30, gap = 4, pad = 6; + s_acc_popup = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_acc_popup); + lv_obj_set_style_bg_color(s_acc_popup, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_acc_popup, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(s_acc_popup, 8, LV_PART_MAIN); + lv_obj_set_style_border_color(s_acc_popup, lv_color_hex(0x2A3D52), LV_PART_MAIN); + lv_obj_set_style_border_width(s_acc_popup, 1, LV_PART_MAIN); + lv_obj_set_style_pad_all(s_acc_popup, pad, LV_PART_MAIN); + lv_obj_set_style_pad_column(s_acc_popup, gap, LV_PART_MAIN); + lv_obj_set_flex_flow(s_acc_popup, LV_FLEX_FLOW_ROW); + lv_obj_set_size(s_acc_popup, s_acc_n * cw + (s_acc_n - 1) * gap + pad * 2, ch + pad * 2); + lv_obj_clear_flag(s_acc_popup, LV_OBJ_FLAG_SCROLLABLE); + for (int i = 0; i < s_acc_n; ++i) { + lv_obj_t* c = lv_obj_create(s_acc_popup); + lv_obj_remove_style_all(c); + lv_obj_set_size(c, cw, ch); + lv_obj_set_style_radius(c, 5, LV_PART_MAIN); + lv_obj_clear_flag(c, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_t* l = lv_label_create(c); + lv_label_set_text(l, s_acc_opts[i]); + lv_obj_set_style_text_font(l, &g_font_16, LV_PART_MAIN); + lv_obj_set_style_text_color(l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_center(l); + s_acc_cells[i] = c; + } + lv_obj_update_layout(s_acc_popup); + const lv_coord_t kb_y = g_lv.keyboard ? lv_obj_get_y(g_lv.keyboard) : 0; + lv_obj_align(s_acc_popup, LV_ALIGN_TOP_MID, 0, kb_y - lv_obj_get_height(s_acc_popup) - 6); + accentPopupHighlight(); +} + +// Re-assert the text as anchor + the selected option AFTER the keyboard's own +// insert has run for this event, so we don't depend on cb-dispatch ordering and +// fast taps still converge on the final choice (each call sets the whole tail). +static void accentApplyAsync(void* unused) { + (void)unused; + if (!s_acc_active || !s_kb_mirror_ta) return; + char buf[300]; + snprintf(buf, sizeof buf, "%s%s", s_acc_anchor, s_acc_opts[s_acc_idx]); + lv_textarea_set_text(s_kb_mirror_ta, buf); // fires mirror VALUE_CHANGED -> kbMirrorSyncToReal +} + +static const char* kbSelectedKeyText() { + if (!g_lv.keyboard) return nullptr; + uint32_t id = lv_btnmatrix_get_selected_btn(g_lv.keyboard); + if (id == LV_BTNMATRIX_BTN_NONE) return nullptr; + return lv_btnmatrix_get_btn_text(g_lv.keyboard, id); +} + +static void accentLongPressCb(lv_event_t* e) { // (dead on touch: LONG_PRESSED never reaches the keyboard) + if (lv_event_get_code(e) != LV_EVENT_LONG_PRESSED || s_acc_active) return; + const char* key = kbSelectedKeyText(); + const AccentSet* set = accentLookup(key); + if (!set) return; + strncpy(s_acc_base, key, sizeof(s_acc_base) - 1); + s_acc_base[sizeof(s_acc_base) - 1] = 0; + s_acc_opts[0] = s_acc_base; s_acc_n = 1; + for (uint8_t i = 0; i < set->n && s_acc_n < 12; ++i) s_acc_opts[s_acc_n++] = set->v[i]; + s_acc_idx = 0; + s_acc_active = true; + s_acc_first = true; // the imminent release inserts the base (= option 0) + accentPopupShow(); + s_acc_timer = lv_timer_create(accentCommitTimerCb, kAccentCommitMs, nullptr); +} + +// Called from keyboardCb on VALUE_CHANGED while a picker is open. +static void accentHandleValueChanged() { + if (!s_acc_active) return; + const char* key = kbSelectedKeyText(); + const bool is_base = key && s_acc_base[0] && strcmp(key, s_acc_base) == 0; + if (!is_base) { accentExit(); return; } // a different key commits the choice + if (s_acc_first) { + // The base char was just inserted (on release); capture the text before it. + s_acc_first = false; + const char* t = s_kb_mirror_ta ? lv_textarea_get_text(s_kb_mirror_ta) : ""; + size_t len = strlen(t); + if (len > 0) len -= 1; // drop the trailing base (ASCII, 1 byte) + if (len >= sizeof(s_acc_anchor)) len = sizeof(s_acc_anchor) - 1; + memcpy(s_acc_anchor, t, len); s_acc_anchor[len] = 0; + s_acc_idx = 0; + } else { + s_acc_idx = (s_acc_idx + 1) % s_acc_n; // cycle to the next variant + lv_async_call(accentApplyAsync, nullptr); + } + accentPopupHighlight(); + if (s_acc_timer) lv_timer_reset(s_acc_timer); +} +// =========================================================================== + +// ---- "Alt" accent key (issue #22) ---------------------------------------- +// The touch keyboard can't do long-press (the cap-touch driver finalizes taps +// before LVGL's long-press fires), so accents are entered with a dedicated +// floating "Alt" key: type a letter, then tap Alt to cycle the character before +// the cursor to its next accent variant in place (à->á->â->...->base). Glyphs +// already live in the extras_* fallback fonts. +static const char* lastUtf8Start(const char* s) { + size_t n = s ? strlen(s) : 0; + if (!n) return nullptr; + size_t i = n - 1; + while (i > 0 && (static_cast(s[i]) & 0xC0u) == 0x80u) --i; // skip UTF-8 continuation bytes + return s + i; +} +static void accentAltCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !s_kb_mirror_ta) return; + const char* last = lastUtf8Start(lv_textarea_get_text(s_kb_mirror_ta)); + if (!last) return; + for (const auto& set : kAccentSets) { + char base[2] = { set.key, 0 }; + int idx = -1; + if (strcmp(last, base) == 0) idx = 0; + else for (uint8_t i = 0; i < set.n; ++i) if (strcmp(last, set.v[i]) == 0) { idx = i + 1; break; } + if (idx < 0) continue; // last char not in this cycle + const int next = (idx + 1) % (set.n + 1); // wrap: ...->last accent->base->... + const char* repl = (next == 0) ? base : set.v[next - 1]; + lv_textarea_del_char(s_kb_mirror_ta); // remove the char before the cursor + lv_textarea_add_text(s_kb_mirror_ta, repl); + kbMirrorSyncToReal(); + return; + } +} + +// ---- Accent box: tap-to-pick accents (issue #22) ------------------------- +// When a letter with accent variants is typed (on-screen OR physical keyboard), +// a floating box of its variants pops up; tapping one replaces the just-typed +// letter. Touch-driven, so it works on both keyboards. Both paths type into +// lv_keyboard_get_textarea(), so one trigger covers them. Dismissed by the next +// keystroke / a pick / hiding the keyboard. +static lv_obj_t* s_accbox = nullptr; +static lv_obj_t* s_accbox_ta = nullptr; // the field the box edits +static const AccentSet* accentSetFor(char c) { + for (const auto& s : kAccentSets) if (s.key == c) return &s; + return nullptr; +} +static void accentBoxHide() { + if (s_accbox) { lv_obj_del(s_accbox); s_accbox = nullptr; } + s_accbox_ta = nullptr; +} +static void accentBoxCellCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + const char* variant = static_cast(lv_event_get_user_data(e)); + lv_obj_t* ta = s_accbox_ta; + if (ta && variant) { + lv_textarea_del_char(ta); // remove the just-typed base letter + lv_textarea_add_text(ta, variant); // insert the chosen accent + if (ta == s_kb_mirror_ta) kbMirrorSyncToReal(); + } + accentBoxHide(); +} +static void accentBoxMaybeShow() { + accentBoxHide(); // each new keystroke clears the last box + if (!s_accent_popups) return; // user turned accent popups off in settings + if (!g_lv.keyboard) return; + lv_obj_t* ta = lv_keyboard_get_textarea(g_lv.keyboard); + if (!ta) return; + const char* last = lastUtf8Start(lv_textarea_get_text(ta)); + if (!last || last[1]) return; // last char must be a single ASCII byte + const AccentSet* set = accentSetFor(last[0]); + if (!set) return; + s_accbox_ta = ta; + const int cw = 34, ch = 40, gap = 4, pad = 6; + s_accbox = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_accbox); + lv_obj_set_style_bg_color(s_accbox, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_accbox, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(s_accbox, 8, LV_PART_MAIN); + lv_obj_set_style_border_color(s_accbox, lv_color_hex(0x2A3D52), LV_PART_MAIN); + lv_obj_set_style_border_width(s_accbox, 1, LV_PART_MAIN); + lv_obj_set_style_pad_all(s_accbox, pad, LV_PART_MAIN); + lv_obj_set_style_pad_column(s_accbox, gap, LV_PART_MAIN); + lv_obj_set_flex_flow(s_accbox, LV_FLEX_FLOW_ROW); + lv_obj_set_size(s_accbox, set->n * cw + (set->n - 1) * gap + pad * 2, ch + pad * 2); + lv_obj_clear_flag(s_accbox, LV_OBJ_FLAG_SCROLLABLE); + for (uint8_t i = 0; i < set->n; ++i) { + lv_obj_t* c = lv_btn_create(s_accbox); + lv_obj_set_size(c, cw, ch); + lv_obj_set_style_radius(c, 5, LV_PART_MAIN); + lv_obj_set_style_bg_color(c, lv_color_hex(0x1B2B3A), LV_PART_MAIN); + lv_obj_set_style_bg_color(c, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_add_event_cb(c, accentBoxCellCb, LV_EVENT_CLICKED, (void*)set->v[i]); + lv_obj_t* l = lv_label_create(c); + lv_label_set_text(l, set->v[i]); + lv_obj_set_style_text_font(l, &g_font_16, LV_PART_MAIN); + lv_obj_set_style_text_color(l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_center(l); + } + // Place it just above the field being edited (below if there's no room above). + lv_obj_update_layout(s_accbox); + lv_area_t a; lv_obj_get_coords(ta, &a); + const lv_coord_t bw = lv_obj_get_width(s_accbox), bh = lv_obj_get_height(s_accbox); + lv_coord_t bx = (lv_disp_get_hor_res(nullptr) - bw) / 2; + lv_coord_t by = a.y1 - bh - 4; + if (by < STATUSBAR_H + 2) by = a.y2 + 4; + // Never let the box land on the on-screen keyboard. The field it anchors to may + // be the chat composer (which sits right above the keys), and on a rotate that + // field's live coords lag — so clamp the box's bottom above the keyboard top + // (computed from the current rotation) and above the composer too in a chat. + if (g_lv.keyboard && !lv_obj_has_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN)) { + lv_coord_t limit = lv_disp_get_ver_res(nullptr) - chatKbH() - 2; + if (s_kb_panel) limit -= s_comp_h; // chat: also clear the composer above the keys + if (by + bh > limit) by = limit - bh; + if (by < STATUSBAR_H + 2) by = STATUSBAR_H + 2; + } + lv_obj_set_pos(s_accbox, bx, by); + lv_obj_move_foreground(s_accbox); +} + +static void keyboardCb(lv_event_t* e) { + lv_event_code_t code = lv_event_get_code(e); + if (code == LV_EVENT_READY || code == LV_EVENT_CANCEL) { accentExit(); accentBoxHide(); hideKb(); } + // VALUE_CHANGED fires for any keypress (incl. backspace). Fade the rotate + // arrows down to ~20% so they don't compete visually with the text the + // user is typing. Reset to full opacity on the next showKb / kbMirrorBind. + else if (code == LV_EVENT_VALUE_CHANGED) { + kbSetRotateArrowsOpa(LV_OPA_20); + accentHandleValueChanged(); + accentBoxMaybeShow(); // letter with accents -> show the tap-to-pick box + } +} + +static void composerFocusCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_FOCUSED) return; + auto* p = static_cast(lv_event_get_user_data(e)); + if (p && p->detail_open) { showKb(p); noteKbActivity(); } +} + +// Discord-style unread divider state. s_unread_at_open is the thread's unread +// count captured the instant the chat was opened (in setActiveThread, before it +// clears unread). refreshChatDetail draws a "New messages" divider above the +// first unread message and, on the first refresh after opening, scrolls to it +// instead of the bottom. Both cleared on leave (backBtnCb). +static uint16_t s_unread_at_open = 0; +static bool s_chat_just_opened = false; + +// Close an open chat/channel detail panel → back to the thread list. Shared by +// the floating HOME button (backBtnCb) and the left→right swipe-back gesture. +static void closeChatPanel(LvChatPanel* p) { + if (!p || !p->detail_open) return; // already closed (e.g. a repeat fire) + hideKb(); + if (p->overlay) lv_obj_add_flag(p->overlay, LV_OBJ_FLAG_HIDDEN); + p->detail_open = false; + s_unread_at_open = 0; // clear the unread divider when leaving the chat + s_chat_just_opened = false; + if (p->jump_btn) lv_obj_add_flag(p->jump_btn, LV_OBJ_FLAG_HIDDEN); + setChatStatusTitle(nullptr); // drop the thread name from the status bar +} + +static void backBtnCb(lv_event_t* e) { + // Fire on PRESSED rather than CLICKED so the back action triggers on + // touch-down instead of touch-up. CLICKED was sometimes dropped because + // the cap-touch driver's horizontal-swipe detector calls + // lv_indev_wait_release mid-tap, which aborts the LVGL click on the + // freshly-pressed button. Triggering on PRESSED side-steps that race. + const lv_event_code_t code = lv_event_get_code(e); + if (code != LV_EVENT_PRESSED && code != LV_EVENT_CLICKED) return; + closeChatPanel(static_cast(lv_event_get_user_data(e))); +} + +// Long-press → delete-this-chat confirmation popup. The popup uses the +// existing showConfirm helper, so we stash the thread context in module- +// static state and consume it inside the OK callback. +static int s_chat_del_thread_idx = -1; +static bool s_chat_del_is_channel = false; + +// Modal that shows the channel's 32-hex PSK with a Copy action, so the user +// can share it out-of-band (QR / chat / paper). Read-only. +static char s_channel_share_secret_hex[33] = {0}; +static char s_channel_share_name[33] = {0}; + +static void channelShareCopyCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + clipboardSet(s_channel_share_secret_hex, "secret"); +} + +static void openChannelShareModal(const char* channel_name, const uint8_t secret[16]) { + // Render hex once and stash it for the Copy button. + for (int i = 0; i < 16; ++i) { + snprintf(s_channel_share_secret_hex + i*2, 3, "%02x", secret[i]); + } + strncpy(s_channel_share_name, channel_name ? channel_name : "Channel", + sizeof(s_channel_share_name) - 1); + + lv_obj_t* body = createSettingsModal("Share channel", SettingsModalKind::AddContact); // reuse a kind + int y = 0; + + lv_obj_t* name_l = lv_label_create(body); + lv_label_set_text_fmt(name_l, LV_SYMBOL_LOOP " %s", s_channel_share_name); + lv_obj_set_style_text_color(name_l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(name_l, &g_font_14, LV_PART_MAIN); + lv_obj_set_pos(name_l, 2, y); + y += 26; + + lv_obj_t* hint = lv_label_create(body); + lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP); + lv_obj_set_width(hint, lv_pct(100)); + lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(hint, &g_font_12, LV_PART_MAIN); + lv_label_set_text(hint, TR("32-hex secret. Anyone with this can read the channel.")); + lv_obj_set_pos(hint, 2, y); + y += 32; + + lv_obj_t* sec_lbl = lv_label_create(body); + lv_label_set_long_mode(sec_lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_width(sec_lbl, lv_pct(100)); + lv_label_set_text(sec_lbl, s_channel_share_secret_hex); + lv_obj_set_style_text_color(sec_lbl, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_text_font(sec_lbl, &g_font_14, LV_PART_MAIN); + lv_obj_set_style_bg_color(sec_lbl, lv_color_hex(0x0A0B0C), LV_PART_MAIN); + lv_obj_set_style_bg_opa(sec_lbl, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_pad_all(sec_lbl, 6, LV_PART_MAIN); + lv_obj_set_style_radius(sec_lbl, 4, LV_PART_MAIN); + lv_obj_set_pos(sec_lbl, 2, y); + // Long-press copies too — handy if the Copy button is hidden by the keyboard. + lv_obj_add_flag(sec_lbl, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(sec_lbl, copyLabelLongPressCb, LV_EVENT_LONG_PRESSED, + const_cast("secret")); + y += 52; + + lv_obj_t* b = lv_btn_create(body); + lv_obj_set_size(b, lv_pct(100),36); + lv_obj_set_pos(b, 2, y); + styleButton(b); + lv_obj_set_style_bg_color(b, lv_color_hex(COLOR_STATUS_OK), LV_PART_MAIN); + lv_obj_set_style_bg_color(b, lv_color_hex(0x3B7039), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_add_event_cb(b, channelShareCopyCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* bl = lv_label_create(b); + lv_label_set_text(bl, TR("Copy secret")); + lv_obj_center(bl); +} + +// Per-channel long-press sheet: [Share secret] [Remove channel] [Cancel]. +static lv_obj_t* s_channel_long_sheet = nullptr; +static int s_channel_long_idx = -1; + +static void closeChannelLongSheet() { + if (s_channel_long_sheet) { + lv_obj_del(s_channel_long_sheet); + s_channel_long_sheet = nullptr; + } +} + +static void channelLongSheetDismissCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + closeChannelLongSheet(); +} + +static void channelLongSheetShareCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + const int t = s_channel_long_idx; + closeChannelLongSheet(); + if (t < 0 || !g_lv.task) return; + bool ch; uint16_t unread; uint32_t ts; + char tname[UITask::MAX_THREAD_NAME + 1]; + if (!g_lv.task->getThreadInfo(t, ch, unread, ts, tname, sizeof(tname)) || !ch) return; +#ifdef MAX_GROUP_CHANNELS + ChannelDetails cd{}; + for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) { + if (the_mesh.getChannel(i, cd) && cd.name[0] && + strncmp(cd.name, tname, sizeof(cd.name)) == 0) { + openChannelShareModal(cd.name, cd.channel.secret); + return; + } + } +#endif + if (g_lv.task) g_lv.task->showAlert(TR("Channel not found"), 1200); +} + +static void channelLongSheetDeleteCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + closeChannelLongSheet(); + bool ch; uint16_t unread; uint32_t ts; + char tname[UITask::MAX_THREAD_NAME + 1]; + if (!g_lv.task->getThreadInfo(s_chat_del_thread_idx, ch, unread, ts, tname, sizeof(tname))) return; + char msg[80]; + snprintf(msg, sizeof(msg), + TR("Remove channel \"%s\"?\nLeaves it on this device only."), tname); + showConfirm(msg, "Remove", chatDeleteApply); +} + +static void threadSheetMarkReadCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + const int t = s_channel_long_idx; + closeChannelLongSheet(); + if (t < 0 || !g_lv.task) return; + g_lv.task->markThreadRead(t); + g_lv.dirty_threads = true; + g_lv.task->showAlert(TR("Marked read"), 900); +} +static void threadSheetDeleteDmCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + const int t = s_channel_long_idx; + closeChannelLongSheet(); + if (t < 0 || !g_lv.task) return; + bool ch; uint16_t unread; uint32_t ts; char name[UITask::MAX_THREAD_NAME + 1] = ""; + if (!g_lv.task->getThreadInfo(t, ch, unread, ts, name, sizeof(name))) return; + s_chat_del_thread_idx = t; + s_chat_del_is_channel = false; + char msg[96]; + snprintf(msg, sizeof(msg), TR("Delete chat with \"%s\"?\nMessage history will be cleared."), name); + showConfirm(msg, "Delete", chatDeleteApply); +} + +// Long-press action sheet for a thread (DM or channel). Channels also get +// Share secret / Remove; DMs get Delete. Both get Mark as read. +static void openThreadActionSheet(int thread_idx, const char* name, bool is_channel) { + closeChannelLongSheet(); + s_channel_long_idx = thread_idx; + + s_channel_long_sheet = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_channel_long_sheet); + // Backdrop sits below the global status bar — keeps the centered card + // from being shoved partway behind the time/battery row and lets the + // bar stay readable while the sheet is open. + lv_obj_set_size(s_channel_long_sheet, lv_disp_get_hor_res(nullptr), + lv_disp_get_ver_res(nullptr) - STATUSBAR_H); + lv_obj_set_pos(s_channel_long_sheet, 0, STATUSBAR_H); + lv_obj_set_style_bg_color(s_channel_long_sheet, lv_color_hex(0x000000), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_channel_long_sheet, LV_OPA_60, LV_PART_MAIN); + lv_obj_clear_flag(s_channel_long_sheet, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_move_foreground(s_channel_long_sheet); + lv_obj_add_event_cb(s_channel_long_sheet, channelLongSheetDismissCb, LV_EVENT_CLICKED, nullptr); + + const int card_w = 220; + const int btn_h = 42; + const int pad = 10; + const int rows = is_channel ? 3 : 2; // mark-read + (share/remove | delete) + const int card_h = 36 + rows * (btn_h + 6) + pad; + lv_obj_t* card = lv_obj_create(s_channel_long_sheet); + lv_obj_remove_style_all(card); + lv_obj_set_size(card, card_w, card_h); + lv_obj_align(card, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_bg_color(card, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(card, 8, LV_PART_MAIN); + lv_obj_set_style_border_color(card, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_set_style_border_width(card, 1, LV_PART_MAIN); + lv_obj_set_style_pad_all(card, pad, LV_PART_MAIN); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + addCloseXBadge(card, channelLongSheetDismissCb); + + lv_obj_t* title = lv_label_create(card); + char nm[40]; + copyUtf8ReplacingMissingGlyphs(&g_font_14, nm, sizeof(nm), name ? name : ""); + lv_label_set_text_fmt(title, TR("%s %s"), is_channel ? LV_SYMBOL_LOOP : LV_SYMBOL_ENVELOPE, + nm[0] ? nm : (is_channel ? "(channel)" : "(chat)")); + lv_obj_set_style_text_color(title, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(title, &g_font_14, LV_PART_MAIN); + lv_label_set_long_mode(title, LV_LABEL_LONG_DOT); + // Trim 32 px on the right so long channel names don't slide under the X. + lv_obj_set_width(title, card_w - 2 * pad - 32); + lv_obj_set_pos(title, 0, 0); + + int y = 28; + auto mk = [&](const char* lbl, lv_event_cb_t cb, uint32_t bg) { + lv_obj_t* b = lv_btn_create(card); + lv_obj_set_size(b, card_w - 2 * pad, btn_h); + lv_obj_set_pos(b, 0, y); + styleButton(b); + if (bg) lv_obj_set_style_bg_color(b, lv_color_hex(bg), LV_PART_MAIN); + lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l = lv_label_create(b); + lv_label_set_text(l, lbl); + lv_obj_center(l); + y += btn_h + 6; + }; + mk(LV_SYMBOL_OK " Mark as read", threadSheetMarkReadCb, 0); + if (is_channel) { + mk(LV_SYMBOL_SHUFFLE " Share secret", channelLongSheetShareCb, 0); + mk(LV_SYMBOL_TRASH " Remove channel", channelLongSheetDeleteCb, 0xB23A48); + } else { + mk(LV_SYMBOL_TRASH " Delete chat", threadSheetDeleteDmCb, 0xB23A48); + } +} + +static void chatDeleteApply() { + if (!g_lv.task || s_chat_del_thread_idx < 0) return; + const int idx = s_chat_del_thread_idx; + const bool is_channel = s_chat_del_is_channel; + s_chat_del_thread_idx = -1; + + // For a channel thread we also drop the slot from the_mesh's channel + // table so the same secret doesn't keep matching new floods. For a DM + // thread we just drop the UI-side thread; the contact stays in the + // Contacts tab and can be removed there separately if the user wants. + int channel_slot = -1; + if (is_channel) { + bool ch; uint16_t unread; uint32_t ts; + char name[UITask::MAX_THREAD_NAME + 1]; + if (g_lv.task->getThreadInfo(idx, ch, unread, ts, name, sizeof(name)) && ch) { +#ifdef MAX_GROUP_CHANNELS + ChannelDetails cd{}; + for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) { + if (the_mesh.getChannel(i, cd) && cd.name[0] && + strncmp(cd.name, name, sizeof(cd.name)) == 0) { + channel_slot = i; + break; + } + } +#endif + } + } + + if (!g_lv.task->removeThread(idx)) { + g_lv.task->showAlert(TR("Couldn't delete"), 1200); + return; + } + if (is_channel && channel_slot >= 0) { + the_mesh.uiDeleteChannel(channel_slot); + } + g_lv.dirty_threads = true; + g_lv.task->showAlert(is_channel ? TR("Channel removed") : TR("Chat removed"), 1000); +} + +static void threadLongPressCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_LONG_PRESSED || !g_lv.task) return; + auto* ctx = static_cast(lv_event_get_user_data(e)); + if (!ctx) return; + bool ch; uint16_t unread; uint32_t ts; + char name[UITask::MAX_THREAD_NAME + 1] = ""; + if (!g_lv.task->getThreadInfo(ctx->idx, ch, unread, ts, name, sizeof(name))) return; + // Swallow the rest of this gesture so the matching CLICKED that LVGL + // would otherwise fire on release doesn't also open the chat behind the + // confirm popup. + lv_indev_t* act = lv_indev_get_act(); + if (act) lv_indev_wait_release(act); + + s_chat_del_thread_idx = ctx->idx; + s_chat_del_is_channel = ch; + + // Both DMs and channels get an action sheet: Mark as read, plus manage + // (channels: Share secret / Remove; DMs: Delete chat). + openThreadActionSheet(ctx->idx, name, ch); +} + +static void threadSelectCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + auto* ctx = static_cast(lv_event_get_user_data(e)); + if (!ctx) return; + LvChatPanel& p = ctx->channel ? g_lv.ch : g_lv.dm; + g_lv.task->enterThread(ctx->channel, ctx->idx); + // Update header name + bool ch; uint16_t unread; uint32_t ts; char name[UITask::MAX_THREAD_NAME + 1]; + if (g_lv.task->getThreadInfo(ctx->idx, ch, unread, ts, name, sizeof(name))) { + char san[UITask::MAX_THREAD_NAME + 8]; + copyUtf8ReplacingMissingGlyphs(&g_font_12, san, sizeof(san), name); + setChatStatusTitle(san); // thread name → status bar (no in-chat header bar) + } + refreshChatDetail(p); + p.detail_open = true; + hideKb(); + if (p.overlay) { + lv_obj_clear_flag(p.overlay, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(p.overlay); + } +#if defined(HAS_TDECK_KEYBOARD) + // Physical keyboard: focus the composer on open so typing goes straight in. + showKb(&p); +#endif +} + +static void sendFromPanelCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + auto* p = static_cast(lv_event_get_user_data(e)); + if (!p || !p->composer_ta) return; + const char* text = lv_textarea_get_text(p->composer_ta); + if (!text || !text[0]) return; + hideKb(); + g_lv.task->setComposerMode(true); + g_lv.task->composerReset(); + for (const char* cp = text; *cp; ++cp) g_lv.task->composerAppendChar(*cp); + if (g_lv.task->composerSend()) { + lv_textarea_set_text(p->composer_ta, ""); + refreshChatDetail(*p); + g_lv.dirty_threads = true; + } +} + +// ---- Quick-reply macro picker (composer bar → list icon) ---- +// One sheet at a time; we cache the active panel pointer so the tap handler +// can stuff the chosen text into the right composer textarea (DM vs channel +// thread both reuse this picker). +static lv_obj_t* s_qr_sheet = nullptr; +static LvChatPanel* s_qr_panel = nullptr; + +// ===== Emoji / special-character picker ===================================== +// Neither the on-screen keyboard nor the T-Deck's physical keyboard can type +// emoji or many special characters, so this popup is the way to insert them. +// The glyphs themselves render thanks to the extras_* fallback fonts (see +// initTouchFontFallbacks); this picker just inserts the UTF-8 bytes. Grid of +// tappable glyphs; tap inserts into the active composer + closes. +static lv_obj_t* s_emoji_sheet = nullptr; +static lv_obj_t* s_emoji_target_ta = nullptr; // textarea to insert into +static lv_obj_t* s_emoji_grid = nullptr; // the scrollable button grid +static int s_emoji_sel = -1; // trackball-highlighted index (-1 = none) +static int s_emoji_cols = 1; // grid columns (computed at open) + +// Curated insert set, grouped. Kept in sync with what the extras fonts bake +// (see scripts/build/regen-extras-fonts.sh) — a glyph here that isn't in the +// font would just render as the font's notdef box, so only ship baked ones. +static const char* const k_emoji_items[] = { + // faces + "\xF0\x9F\x98\x80","\xF0\x9F\x98\x83","\xF0\x9F\x98\x84","\xF0\x9F\x98\x81", + "\xF0\x9F\x98\x86","\xF0\x9F\x98\x85","\xF0\x9F\x98\x82","\xF0\x9F\x98\x89", + "\xF0\x9F\x98\x8A","\xF0\x9F\x98\x8D","\xF0\x9F\x98\x98","\xF0\x9F\x98\x8B", + "\xF0\x9F\x98\x9C","\xF0\x9F\x98\x8E","\xF0\x9F\x98\x8F","\xF0\x9F\x98\x92", + "\xF0\x9F\x98\x9E","\xF0\x9F\x98\x94","\xF0\x9F\x98\xA2","\xF0\x9F\x98\xAD", + "\xF0\x9F\x98\xA1","\xF0\x9F\x98\xA0","\xF0\x9F\x98\xB1","\xF0\x9F\x98\xB4", + "\xF0\x9F\x98\xAC","\xF0\x9F\x98\x90", + // gestures + people + "\xF0\x9F\x91\x8D","\xF0\x9F\x91\x8E","\xF0\x9F\x91\x8C","\xE2\x9C\x8C", + "\xF0\x9F\x91\x8A","\xE2\x9C\x8A","\xF0\x9F\x91\x8F","\xF0\x9F\x99\x8C", + "\xF0\x9F\x99\x8F","\xF0\x9F\x92\xAA","\xF0\x9F\x91\x8B", + // hearts + "\xE2\x9D\xA4","\xF0\x9F\x92\x94","\xF0\x9F\x92\x95","\xF0\x9F\x92\x99", + "\xF0\x9F\x92\x9A","\xF0\x9F\x92\x9B","\xF0\x9F\x92\x9C", + // symbols / celebration + "\xF0\x9F\x8E\x89","\xF0\x9F\x8E\x8A","\xE2\x9C\xA8","\xF0\x9F\x94\xA5", + "\xF0\x9F\x92\xAF","\xE2\x9C\x85","\xE2\x9D\x8C","\xE2\x9D\x97", + "\xE2\x9D\x93","\xE2\x9A\xA0","\xF0\x9F\x92\xA9","\xE2\xAD\x90", + "\xF0\x9F\x8C\x9F","\xE2\x9A\xA1","\xE2\x98\x80","\xE2\x98\x81", + "\xE2\x9D\x84","\xE2\x98\x94","\xE2\x98\x95","\xF0\x9F\x8D\xBB", + "\xF0\x9F\x8D\x95","\xF0\x9F\x8E\x82","\xF0\x9F\x8E\x81","\xF0\x9F\x92\xAC", + "\xF0\x9F\x92\xA4","\xF0\x9F\x92\xA5", + // objects + "\xF0\x9F\x9A\x80","\xF0\x9F\x93\xB7","\xF0\x9F\x93\xB1","\xF0\x9F\x92\xBB", + "\xF0\x9F\x93\x8D","\xF0\x9F\x93\x8C","\xF0\x9F\x93\x85","\xF0\x9F\x94\x8B", + "\xF0\x9F\x93\xA1","\xF0\x9F\x94\x91","\xF0\x9F\x94\x92","\xF0\x9F\x93\xA7", + "\xF0\x9F\x8F\xA0","\xF0\x9F\x9A\x97","\xE2\x8C\x9A","\xF0\x9F\x92\xA1", + // animals + "\xF0\x9F\x90\xB6","\xF0\x9F\x90\xB1","\xF0\x9F\x90\xB8","\xF0\x9F\x90\xBB", + "\xF0\x9F\x90\xA7","\xF0\x9F\x90\x9D", + // special characters / punctuation / currency / math + "\xE2\x80\x94","\xE2\x80\xA6","\xE2\x80\x9C","\xE2\x80\x9D","\xE2\x80\x98", + "\xE2\x80\x99","\xE2\x86\x92","\xE2\x86\x90","\xC2\xB0","\xC2\xB1", + "\xC3\x97","\xC3\xB7","\xE2\x82\xAC","\xC2\xA3","\xC2\xA5","\xC2\xA9", + "\xC2\xAE","\xE2\x84\xA2","\xE2\x89\xA0","\xE2\x89\xA4","\xE2\x89\xA5", + "\xC2\xBD","\xC2\xBC","\xC2\xBE", +}; +static constexpr int k_emoji_count = (int)(sizeof(k_emoji_items) / sizeof(k_emoji_items[0])); + +static void closeEmojiSheet() { + if (s_emoji_sheet) { lv_obj_del(s_emoji_sheet); s_emoji_sheet = nullptr; } + s_emoji_target_ta = nullptr; + s_emoji_grid = nullptr; + s_emoji_sel = -1; +} +static void emojiSheetCloseCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + if (lv_event_get_target(e) != lv_event_get_current_target(e)) return; // backdrop only + closeEmojiSheet(); +} + +// Insert item `idx` into the active composer and close. Shared by finger-tap +// (emojiPickCb) and the trackball selector (emojiSelectorClick). +static void emojiInsertIndex(int idx) { + lv_obj_t* ta = s_emoji_target_ta; + if (idx < 0 || idx >= k_emoji_count || !ta) { closeEmojiSheet(); return; } + const char* g = k_emoji_items[idx]; + // Insert into the keyboard MIRROR when it's bound to this field — otherwise a + // later kbMirrorSyncToReal() would overwrite our insert with the stale mirror + // contents (same trap the Wi-Fi scan picker hit). When not bound, insert + // straight into the real field. + lv_obj_t* dest = (s_kb_bind_ta == ta && s_kb_mirror_ta) ? s_kb_mirror_ta : ta; + lv_textarea_add_text(dest, g); + closeEmojiSheet(); +} +static void emojiPickCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + emojiInsertIndex((int)(intptr_t)lv_event_get_user_data(e)); +} + +// Paint the trackball-selected cell highlighted and the rest normal, and keep +// the selection scrolled into view. No-op when nothing is selected (finger-only). +static void emojiPaintSelection() { + if (!s_emoji_grid) return; + const uint32_t n = lv_obj_get_child_cnt(s_emoji_grid); + for (uint32_t i = 0; i < n; ++i) { + lv_obj_t* b = lv_obj_get_child(s_emoji_grid, i); + if (!b) continue; + const bool sel = ((int)i == s_emoji_sel); + lv_obj_set_style_bg_color(b, lv_color_hex(sel ? COLOR_MENTION : 0x1A1B1C), LV_PART_MAIN); + lv_obj_set_style_border_width(b, sel ? 2 : 0, LV_PART_MAIN); + lv_obj_set_style_border_color(b, lv_color_hex(0xCFE6FF), LV_PART_MAIN); + } + if (s_emoji_sel >= 0 && s_emoji_sel < (int)n) { + lv_obj_t* b = lv_obj_get_child(s_emoji_grid, (uint32_t)s_emoji_sel); + if (b) lv_obj_scroll_to_view(b, LV_ANIM_ON); + } +} + +// Trackball is high-resolution: one physical roll emits several motion counts, +// so stepping one cell per raw count made the selector fly across the grid. +// Accumulate raw motion and only advance one cell once the accumulator crosses +// kEmojiSelStep — so it takes a deliberate roll to move each cell. +static int s_emoji_acc_x = 0, s_emoji_acc_y = 0; +static constexpr int kEmojiSelStep = 3; // raw counts per one-cell move (higher = less sensitive) + +// Feed raw trackball motion; advances the highlighted cell at most one step per +// axis per call. Called from the trackball poll while the emoji sheet is open. +static void emojiSelectorMove(int rawdx, int rawdy) { + if (!s_emoji_grid || k_emoji_count == 0) return; + if (s_emoji_sel < 0) { // first motion just lands on cell 0 + s_emoji_sel = 0; s_emoji_acc_x = s_emoji_acc_y = 0; + emojiPaintSelection(); + return; + } + s_emoji_acc_x += rawdx; + s_emoji_acc_y += rawdy; + int dc = 0, dr = 0; + if (s_emoji_acc_x >= kEmojiSelStep) { dc = 1; s_emoji_acc_x = 0; } + else if (s_emoji_acc_x <= -kEmojiSelStep) { dc = -1; s_emoji_acc_x = 0; } + if (s_emoji_acc_y >= kEmojiSelStep) { dr = 1; s_emoji_acc_y = 0; } + else if (s_emoji_acc_y <= -kEmojiSelStep) { dr = -1; s_emoji_acc_y = 0; } + if (dc == 0 && dr == 0) return; // not enough travel yet + + const int cols = s_emoji_cols > 0 ? s_emoji_cols : 1; + int idx = s_emoji_sel; + if (dc) idx += dc; // horizontal: free move across the flat list + if (dr) { + const int ni = idx + dr * cols; // vertical: jump a full row + if (ni >= 0 && ni < k_emoji_count) idx = ni; + } + if (idx < 0) idx = 0; + if (idx >= k_emoji_count) idx = k_emoji_count - 1; + if (idx != s_emoji_sel) { s_emoji_sel = idx; emojiPaintSelection(); } +} + +// Trackball centre-click while the sheet is open: insert the highlighted glyph. +// Returns true if it consumed the click (so it isn't also injected as a tap). +static bool emojiSelectorClick() { + if (!s_emoji_sheet) return false; + if (s_emoji_sel >= 0) emojiInsertIndex(s_emoji_sel); + return true; // swallow the click even if nothing selected yet +} + +// Opened from the composer's emoji button. `ta` is the composer textarea. +static void openEmojiPicker(lv_obj_t* ta) { + closeEmojiSheet(); + s_emoji_target_ta = ta; + const lv_coord_t sw = lv_disp_get_hor_res(nullptr); + const lv_coord_t sh = lv_disp_get_ver_res(nullptr); + s_emoji_sheet = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_emoji_sheet); + lv_obj_set_size(s_emoji_sheet, sw, sh - STATUSBAR_H); + lv_obj_set_pos(s_emoji_sheet, 0, STATUSBAR_H); + lv_obj_set_style_bg_color(s_emoji_sheet, lv_color_hex(0x000000), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_emoji_sheet, LV_OPA_60, LV_PART_MAIN); + lv_obj_clear_flag(s_emoji_sheet, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_event_cb(s_emoji_sheet, emojiSheetCloseCb, LV_EVENT_CLICKED, nullptr); + + const lv_coord_t cardw = sw - 16; + const lv_coord_t cardh = sh - STATUSBAR_H - 16; + lv_obj_t* card = lv_obj_create(s_emoji_sheet); + lv_obj_remove_style_all(card); + lv_obj_set_size(card, cardw, cardh); + lv_obj_align(card, LV_ALIGN_CENTER, 0, 0); + styleSurface(card, COLOR_PANEL, 8); + lv_obj_set_style_border_color(card, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_set_style_border_width(card, 1, LV_PART_MAIN); + lv_obj_set_style_pad_all(card, 8, LV_PART_MAIN); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* title = lv_label_create(card); + lv_label_set_text(title, TR("Insert emoji / symbol")); + lv_obj_set_style_text_color(title, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(title, &g_font_14, LV_PART_MAIN); + lv_obj_set_pos(title, 2, 0); + addCloseXBadge(card, emojiSheetCloseCb); + + // Hint line: how to use the trackball (T-Deck) — finger tap also works. + lv_obj_t* hint = lv_label_create(card); + lv_label_set_text(hint, TR("Roll to highlight \xE2\x80\xA2 click to insert")); + lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(hint, &g_font_12, LV_PART_MAIN); + lv_obj_align(hint, LV_ALIGN_TOP_RIGHT, -24, 4); + + // Scrollable grid of glyph buttons. + const lv_coord_t grid_w = cardw - 16; + lv_obj_t* grid = lv_obj_create(card); + lv_obj_remove_style_all(grid); + lv_obj_set_size(grid, grid_w, cardh - 16 - 26); + lv_obj_set_pos(grid, 0, 26); + lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP); + lv_obj_set_style_pad_row(grid, 4, LV_PART_MAIN); + lv_obj_set_style_pad_column(grid, 4, LV_PART_MAIN); + lv_obj_set_scroll_dir(grid, LV_DIR_VER); + lv_obj_set_scrollbar_mode(grid, LV_SCROLLBAR_MODE_AUTO); + s_emoji_grid = grid; + // Column count for the trackball selector's row jumps: floor((w + gap) / + // (btn + gap)) with btn=38, gap=4. Matches the flex-wrap that LVGL computes. + s_emoji_cols = (int)((grid_w + 4) / (38 + 4)); + if (s_emoji_cols < 1) s_emoji_cols = 1; + s_emoji_sel = -1; // start un-highlighted; first roll selects index 0 + s_emoji_acc_x = s_emoji_acc_y = 0; + + for (int i = 0; i < k_emoji_count; ++i) { + lv_obj_t* b = lv_btn_create(grid); + lv_obj_set_size(b, 38, 38); + styleButton(b); + lv_obj_set_style_bg_color(b, lv_color_hex(0x1A1B1C), LV_PART_MAIN); + lv_obj_set_style_pad_all(b, 0, LV_PART_MAIN); + lv_obj_add_event_cb(b, emojiPickCb, LV_EVENT_CLICKED, (void*)(intptr_t)i); + lv_obj_t* l = lv_label_create(b); + lv_label_set_text(l, k_emoji_items[i]); + lv_obj_set_style_text_font(l, &g_font_16, LV_PART_MAIN); + lv_obj_set_style_text_color(l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_center(l); + } +} + +static void openEmojiPickerCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + auto* p = static_cast(lv_event_get_user_data(e)); + if (!p || !p->composer_ta) return; + openEmojiPicker(p->composer_ta); +} + +static void closeQuickReplySheet() { + if (s_qr_sheet) { + lv_obj_del(s_qr_sheet); + s_qr_sheet = nullptr; + } + s_qr_panel = nullptr; +} + +static void qrPickCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + intptr_t idx = (intptr_t)lv_event_get_user_data(e); + LvChatPanel* p = s_qr_panel; + closeQuickReplySheet(); + if (!p || !p->composer_ta) return; +#if defined(ESP32) + char buf[TOUCH_QUICK_REPLY_MAXLEN]; + int n = touchPrefsGetQuickReply((int)idx, buf, sizeof(buf)); + if (n <= 0) return; + // Stuff into the LVGL textarea (no auto-send: lets the operator tweak + // before tapping the send button — and keeps a stray tap from blasting + // out a macro). To send-on-tap, route directly through composerSend + // instead, but accidental sends are a worse UX than an extra tap. + lv_textarea_set_text(p->composer_ta, buf); + // Move keyboard focus to the textarea so a quick send tap works next. + lv_obj_add_state(p->composer_ta, LV_STATE_FOCUSED); +#else + (void)idx; +#endif +} + +static void qrSheetCloseCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + closeQuickReplySheet(); +} + +static void openQuickReplyPickerCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + auto* p = static_cast(lv_event_get_user_data(e)); + if (!p) return; + closeQuickReplySheet(); + s_qr_panel = p; + + lv_coord_t sw = lv_disp_get_hor_res(nullptr); + lv_coord_t sh = lv_disp_get_ver_res(nullptr); + s_qr_sheet = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_qr_sheet); + // Sit below the status bar — the QR card (~290 px tall with 6 macro + // rows) was getting its top row clipped behind the time/battery row. + lv_obj_set_size(s_qr_sheet, sw, sh - STATUSBAR_H); + lv_obj_set_pos(s_qr_sheet, 0, STATUSBAR_H); + lv_obj_set_style_bg_color(s_qr_sheet, lv_color_hex(0x000000), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_qr_sheet, LV_OPA_60, LV_PART_MAIN); + lv_obj_clear_flag(s_qr_sheet, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_event_cb(s_qr_sheet, qrSheetCloseCb, LV_EVENT_CLICKED, nullptr); + + const int card_w = 220; + const int btn_h = 32; // 34→32: 6 macro rows have to fit in the + const int pad = 8; // visible area (298 px) below the status + const int title_h = 26; // bar — the old sizing produced a 302 px + const int hint_h = 22; // card that clipped behind the bar. + const int card_h = title_h + TOUCH_QUICK_REPLY_COUNT * (btn_h + 4) + hint_h + pad; + lv_obj_t* card = lv_obj_create(s_qr_sheet); + lv_obj_remove_style_all(card); + lv_obj_set_size(card, card_w, card_h); + lv_obj_align(card, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_bg_color(card, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(card, 8, LV_PART_MAIN); + lv_obj_set_style_border_color(card, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_set_style_border_width(card, 1, LV_PART_MAIN); + lv_obj_set_style_pad_all(card, pad, LV_PART_MAIN); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + addCloseXBadge(card, qrSheetCloseCb); + + lv_obj_t* title = lv_label_create(card); + lv_label_set_text(title, TR("Quick reply")); + lv_obj_set_style_text_color(title, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(title, &g_font_14, LV_PART_MAIN); + lv_obj_set_pos(title, 0, 0); + + int y = title_h; +#if defined(ESP32) + for (int i = 0; i < TOUCH_QUICK_REPLY_COUNT; ++i) { + char buf[TOUCH_QUICK_REPLY_MAXLEN]; + int n = touchPrefsGetQuickReply(i, buf, sizeof(buf)); + if (n <= 0) { strncpy(buf, "(empty)", sizeof(buf) - 1); buf[sizeof(buf)-1] = '\0'; } + lv_obj_t* b = lv_btn_create(card); + lv_obj_set_size(b, card_w - 2 * pad, btn_h); + lv_obj_set_pos(b, 0, y); + styleButton(b); + lv_obj_set_style_bg_color(b, lv_color_hex(n > 0 ? 0x1A1B1C : 0x0C0D0E), LV_PART_MAIN); + lv_obj_add_event_cb(b, qrPickCb, LV_EVENT_CLICKED, (void*)(intptr_t)i); + lv_obj_t* lbl = lv_label_create(b); + lv_label_set_text(lbl, buf); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_DOT); + lv_obj_set_width(lbl, card_w - 2 * pad - 16); + lv_obj_set_style_text_font(lbl, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_text_color(lbl, lv_color_hex(n > 0 ? COLOR_TEXT : COLOR_SUB), LV_PART_MAIN); + lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 8, 0); + y += btn_h + 4; // match the 4-px gap used in card_h calc above + } +#endif + lv_obj_t* hint = lv_label_create(card); + lv_label_set_text(hint, TR("Edit in Settings \xe2\x86\x92 Quick replies")); + lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(hint, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(hint, 0, y + 2); +} + +static void tabChangedCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + scheduleHeavyRefresh(170); + closeSettingsModal(); + closeSettingsCategory(); // a category detail sheet floats on layer_top — drop it on tab change + + const int new_t = getActiveTab(); + const bool leaving_inbox = + (s_lv_tab_prev == CHAT_INBOX_TAB_INDEX && new_t != CHAT_INBOX_TAB_INDEX); + if (leaving_inbox) { + hideKb(); + if (g_lv.dm.detail_open && g_lv.dm.overlay) { + lv_obj_add_flag(g_lv.dm.overlay, LV_OBJ_FLAG_HIDDEN); + g_lv.dm.detail_open = false; + } + if (g_lv.ch.detail_open && g_lv.ch.overlay) { + lv_obj_add_flag(g_lv.ch.overlay, LV_OBJ_FLAG_HIDDEN); + g_lv.ch.detail_open = false; + } + } else { + hideKb(); + } + + // Chats / Contacts tab pages are non-scrolling shells; reset any stale scroll offset. + if (new_t == CHAT_INBOX_TAB_INDEX || new_t == CONTACTS_TAB_INDEX) { + lv_obj_t* t = getActiveTabPage(); + if (t) lv_obj_scroll_to(t, 0, 0, LV_ANIM_OFF); + } + + const int prev_t = s_lv_tab_prev; + s_lv_tab_prev = new_t; + if (new_t == CONTACTS_TAB_INDEX) refreshContactsList(); + if (new_t == MAP_TAB_INDEX) { + applyMapChrome(true); // transparent status bar + tab bar so the map shows through + onMapTabActivated(); + } else { + if (prev_t == MAP_TAB_INDEX) applyMapChrome(false); // restore opaque chrome + // Drop tile JPEGs from PSRAM the moment we leave the tab — keeps the + // working set small and lets the map cold-load with fresh data on + // the next visit. (CPU stays at 160 MHz — the whole UI runs there.) + freeMapTiles(); + } + + // The status bar is shown on every tab now: the Settings page is a clean + // category landing (not the cramped sub-tab UI that needed the extra strip), + // so the clock/battery/signal row stays put. Keep the tabview docked below it. + { + const lv_coord_t hor = lv_disp_get_hor_res(nullptr); + const lv_coord_t ver = lv_disp_get_ver_res(nullptr); + if (g_lv.tabview) { + lv_obj_set_pos(g_lv.tabview, 0, STATUSBAR_H); + lv_obj_set_size(g_lv.tabview, hor, (lv_coord_t)(ver - STATUSBAR_H)); + } + if (g_statusbar.root) lv_obj_clear_flag(g_statusbar.root, LV_OBJ_FLAG_HIDDEN); + } + if (g_lv.task) g_lv.task->onLvTabChanged(new_t); +} + +static void homeTileCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + uintptr_t idx = reinterpret_cast(lv_event_get_user_data(e)); + // goToTab is defined below; can call it here because this runs at runtime + if (g_lv.tabview) lv_tabview_set_act(g_lv.tabview, static_cast(idx), LV_ANIM_ON); + scheduleHeavyRefresh(170); + s_lv_tab_prev = static_cast(idx); + if (g_lv.task) g_lv.task->onLvTabChanged(static_cast(idx)); +} + +static void resetPathCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + g_lv.task->resetActiveDmPath(); + refreshStatusLabels(); +} + +static void toggleTcpCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + g_lv.task->isTcpEnabled() ? g_lv.task->disableTcp() : g_lv.task->enableTcp(); + g_lv.task->showAlert(g_lv.task->isTcpEnabled() ? TR("TCP on") : TR("TCP off"), 900); + refreshStatusLabels(); +} + +static void toggleBleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task || !g_lv.task->hasBleCapability()) return; + g_lv.task->isBleEnabled() ? g_lv.task->disableBle() : g_lv.task->enableBle(); + g_lv.task->showAlert(g_lv.task->isBleEnabled() ? TR("BLE on") : TR("BLE off"), 900); + refreshStatusLabels(); +} + +static void toggleGpsCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + g_lv.task->toggleGPS(); + g_lv.task->showAlert(g_lv.task->getGPSState() ? TR("GPS on") : TR("GPS off"), 900); + refreshStatusLabels(); +} + +static void toggleBuzzerCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + g_lv.task->toggleBuzzer(); + const bool quiet = g_lv.task->isBuzzerQuiet(); + // Refresh the button's own label in place — the Device page isn't rebuilt on + // tap, so a static "Sound: on" would otherwise never change. + lv_obj_t* btn = lv_event_get_target(e); + if (btn) { + lv_obj_t* lbl = lv_obj_get_child(btn, 0); + if (lbl) lv_label_set_text_fmt(lbl, TR("Sound: %s"), quiet ? "off" : "on"); + } + g_lv.task->showAlert(quiet ? TR("Sound off") : TR("Sound on"), 900); +#if defined(HAS_UI_SOUND) + if (!quiet) uiPlayNotify(); // confirmation chime when enabling +#endif + refreshStatusLabels(); +} + +static void settingsFieldFocusCb(lv_event_t* e) { + const lv_event_code_t code = lv_event_get_code(e); + // Drop LV_EVENT_PRESSED: it fires on touch-down before LVGL can tell + // whether the gesture is a tap or the start of a vertical scroll inside + // the settings body. Reacting to PRESSED meant any scroll attempt that + // started on a textarea spawned the keyboard. CLICKED only fires after + // release IF the press qualified as a click (movement under the scroll + // threshold), so it's the right hook for "open keyboard on real tap". + if (code != LV_EVENT_FOCUSED && code != LV_EVENT_CLICKED) return; + if (!g_lv.keyboard) return; + // Belt-and-suspenders: even CLICKED can fire at the tail end of a flick + // if the touch happened to lift while still on the textarea. Same guard + // used for contactSelectCb — if anything is currently being scrolled, + // treat this CLICKED as scroll spillover, not a deliberate tap. + lv_indev_t* act = lv_indev_get_act(); + if (act) { + if (lv_indev_get_scroll_obj(act)) return; + if (lv_indev_get_scroll_dir(act) != LV_DIR_NONE) return; + } + lv_obj_t* ta = lv_event_get_target(e); + if (!ta) return; + s_kb_panel = nullptr; + kbMirrorBind(ta); + noteKbActivity(); // focusing/tapping a field lights the auto backlight +} + +#if defined(HAS_TDECK_KEYBOARD) +// Recursively find the first text field under `obj` (depth-first). +static lv_obj_t* findFirstTextarea(lv_obj_t* obj) { + if (!obj) return nullptr; + uint32_t n = lv_obj_get_child_cnt(obj); + for (uint32_t i = 0; i < n; ++i) { + lv_obj_t* c = lv_obj_get_child(obj, i); + if (c && lv_obj_check_type(c, &lv_textarea_class)) return c; + lv_obj_t* d = findFirstTextarea(c); + if (d) return d; + } + return nullptr; +} + +// Deferred via lv_async from createSettingsModal so the modal's fields exist by +// the time it runs: focus the first text field of a freshly-opened popup modal +// so the physical keyboard types straight into it. Mirrors settingsFieldFocusCb. +static void tdeckModalAutoFocusAsync(void* root) { + lv_obj_t* r = static_cast(root); + if (!r || !lv_obj_is_valid(r)) return; // modal already closed in the meantime? + lv_obj_t* ta = findFirstTextarea(r); + if (!ta) return; // no text field -> nothing to focus + s_kb_panel = nullptr; // a settings field, not the chat composer + kbMirrorBind(ta); // bind the keyboard to it + lv_obj_add_state(ta, LV_STATE_FOCUSED); // show the cursor + noteKbActivity(); +} +#endif + +// ===== Clipboard ============================================================ +// Mirrors MCterm's in-RAM clipboard. Anything the user long-presses to copy +// lands here; long-press on a textarea pastes the contents at the cursor. +// One global buffer keeps the code simple and avoids heap churn. +constexpr size_t CLIPBOARD_MAX = 192; +static char s_clipboard[CLIPBOARD_MAX] = {0}; + +static void clipboardSet(const char* text, const char* tag) { + if (!text) return; + size_t n = strlen(text); + if (n >= CLIPBOARD_MAX) n = CLIPBOARD_MAX - 1; + memcpy(s_clipboard, text, n); + s_clipboard[n] = '\0'; + if (g_lv.task) { + char toast[60]; + snprintf(toast, sizeof(toast), TR("Copied %s"), + tag && tag[0] ? tag : ""); + g_lv.task->showAlert(toast, 900); + } +} + +// LV_EVENT_LONG_PRESSED handler: copies the target object's label text. +// User-data carries an optional tag shown in the "Copied X" toast. +static void copyLabelLongPressCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_LONG_PRESSED) return; + lv_obj_t* obj = lv_event_get_target(e); + if (!obj) return; + const char* text = lv_label_get_text(obj); + if (!text || !text[0]) return; + lv_indev_t* act = lv_indev_get_act(); + if (act) lv_indev_wait_release(act); // swallow the trailing CLICKED + clipboardSet(text, static_cast(lv_event_get_user_data(e))); +} + +// LV_EVENT_LONG_PRESSED handler on a textarea: pastes clipboard at cursor. +// When the keyboard is bound to a mirror, the paste goes into the mirror so +// the keyboard editor sees it and live-sync propagates it to the real ta. +static void pasteTextareaLongPressCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_LONG_PRESSED) return; + if (!s_clipboard[0]) return; + lv_obj_t* ta = lv_event_get_target(e); + if (!ta) return; + lv_obj_t* dest = (s_kb_bind_ta == ta && s_kb_mirror_ta) ? s_kb_mirror_ta : ta; + lv_textarea_add_text(dest, s_clipboard); + lv_indev_t* act = lv_indev_get_act(); + if (act) lv_indev_wait_release(act); +} + +// Touch-down on a text field = backlight activity. Hooked on PRESSED (which +// fires reliably on every tap, including on an already-focused field — unlike +// CLICKED/FOCUSED, which the swipe detector can drop via lv_indev_wait_release). +// It only bumps the auto backlight; it does NOT spawn the keyboard, so the +// scroll-vs-tap reason PRESSED is kept out of settingsFieldFocusCb doesn't apply. +static void kbActivityPressCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_PRESSED) return; + noteKbActivity(); +} + +static void attachSettingsTaEvents(lv_obj_t* ta) { + lv_obj_add_event_cb(ta, settingsFieldFocusCb, LV_EVENT_FOCUSED, nullptr); + lv_obj_add_event_cb(ta, settingsFieldFocusCb, LV_EVENT_CLICKED, nullptr); + // PRESSED is used only for the keyboard backlight (see kbActivityPressCb), + // never for focus — settingsFieldFocusCb still avoids it for the scroll case. + lv_obj_add_event_cb(ta, kbActivityPressCb, LV_EVENT_PRESSED, nullptr); + lv_obj_add_event_cb(ta, pasteTextareaLongPressCb, LV_EVENT_LONG_PRESSED, nullptr); +} + +// ===== Text selection + edit menu ========================================== +// Mobile-style editing for the chat composer: double-tap a word to highlight it, +// long-press for a Cut / Copy / Paste / Select-All menu. LVGL's text selection +// (compiled in via LV_LABEL_TEXT_SELECTION) only supports drag-select through +// its public API, so for word/all selection we set the range on the textarea's +// label directly — it sticks until the next tap (the class only rewrites the +// selection on click/drag). + +// codepoint index -> byte offset within a UTF-8 string (and the reverse). +static uint32_t taCpToByte(const char* txt, uint32_t cp_target) { + uint32_t i = 0, cp = 0; + while (txt[i] && cp < cp_target) { _lv_txt_encoded_next(txt, &i); cp++; } + return i; +} +static uint32_t taByteToCp(const char* txt, uint32_t byte_target) { + uint32_t i = 0, cp = 0; + while (txt[i] && i < byte_target) { _lv_txt_encoded_next(txt, &i); cp++; } + return cp; +} + +// Remembered ("sticky") selection. The press that *starts* a long-press makes +// the textarea reposition its cursor and clear its own highlight before +// LONG_PRESSED fires — so by the time the edit menu opens the visual selection is +// gone. We stash the last selected range here and re-apply it when the menu +// shows, so a double-tapped (or select-all'd) word stays highlighted under the +// menu. Invalidated on a single tap (cursor moved) or any text change. +static lv_obj_t* s_sel_ta = nullptr; +static uint32_t s_sel_a = 0, s_sel_b = 0; + +// Highlight [start_cp, end_cp) on a textarea. +static void taSelectRange(lv_obj_t* ta, uint32_t start_cp, uint32_t end_cp) { + if (!ta || start_cp == end_cp) return; + if (start_cp > end_cp) { uint32_t t = start_cp; start_cp = end_cp; end_cp = t; } + lv_textarea_set_text_selection(ta, true); + lv_textarea_t* t = reinterpret_cast(ta); + t->sel_start = start_cp; + t->sel_end = end_cp; + if (t->label) { + lv_label_set_text_sel_start(t->label, start_cp); + lv_label_set_text_sel_end(t->label, end_cp); + } + lv_obj_invalidate(ta); + s_sel_ta = ta; s_sel_a = start_cp; s_sel_b = end_cp; // remember for re-applying +} + +// Clear both the label highlight AND the textarea's stored range, so a later +// menu invocation doesn't act on an invisible stale selection. +static void taClearSelection(lv_obj_t* ta) { + lv_textarea_clear_selection(ta); + lv_textarea_t* t = reinterpret_cast(ta); + t->sel_start = t->sel_end = LV_DRAW_LABEL_NO_TXT_SEL; + if (s_sel_ta == ta) s_sel_ta = nullptr; +} + +// Re-apply the remembered selection if it's still valid for `ta` (the long-press +// that opens the menu clears the live highlight). No-op if nothing was selected. +static void taReapplyStickySel(lv_obj_t* ta) { + if (s_sel_ta != ta || s_sel_a == s_sel_b) return; + const char* txt = lv_textarea_get_text(ta); + uint32_t len = (txt && txt[0]) ? _lv_txt_get_encoded_length(txt) : 0; + if (s_sel_b > len) { s_sel_ta = nullptr; return; } // text changed under us + taSelectRange(ta, s_sel_a, s_sel_b); +} + +static bool taHasSelection(lv_obj_t* ta, uint32_t* s_cp, uint32_t* e_cp) { + lv_textarea_t* t = reinterpret_cast(ta); + uint32_t a = t->sel_start, b = t->sel_end; + if (a == b || a == LV_DRAW_LABEL_NO_TXT_SEL || b == LV_DRAW_LABEL_NO_TXT_SEL) return false; + if (a > b) { uint32_t tmp = a; a = b; b = tmp; } + *s_cp = a; *e_cp = b; + return true; +} + +// Select the whitespace-delimited word under the cursor (the tap set the cursor). +static void taSelectWordAtCursor(lv_obj_t* ta) { + const char* txt = lv_textarea_get_text(ta); + if (!txt || !txt[0]) return; + uint32_t len_b = (uint32_t)strlen(txt); + uint32_t cur_b = taCpToByte(txt, lv_textarea_get_cursor_pos(ta)); + if (cur_b > len_b) cur_b = len_b; + auto isWord = [](char c) { return !(c == ' ' || c == '\n' || c == '\t' || c == '\r'); }; + uint32_t s = cur_b, e = cur_b; + while (s > 0 && isWord(txt[s - 1])) s--; + while (e < len_b && isWord(txt[e])) e++; + if (s == e) return; // tapped on whitespace + taSelectRange(ta, taByteToCp(txt, s), taByteToCp(txt, e)); +} + +// Delete codepoints [s_cp, e_cp): park the cursor at the end and backspace +// (no temp buffer, UTF-8 safe). +static void taDeleteRange(lv_obj_t* ta, uint32_t s_cp, uint32_t e_cp) { + if (s_cp >= e_cp) return; + lv_textarea_set_cursor_pos(ta, (int32_t)e_cp); + for (uint32_t k = s_cp; k < e_cp; ++k) lv_textarea_del_char(ta); +} + +// ----- The floating Cut / Copy / Paste / Select-All menu ----- +static lv_obj_t* s_txtmenu = nullptr; +static lv_obj_t* s_txtmenu_ta = nullptr; + +static void txtMenuHide() { + if (s_txtmenu) { lv_obj_del(s_txtmenu); s_txtmenu = nullptr; } + s_txtmenu_ta = nullptr; +} + +enum { TXT_CUT = 0, TXT_COPY, TXT_PASTE, TXT_SELALL }; + +static void txtMenuCellCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + intptr_t act = reinterpret_cast(lv_event_get_user_data(e)); + lv_obj_t* ta = s_txtmenu_ta; + if (!ta) { txtMenuHide(); return; } + const char* txt = lv_textarea_get_text(ta); + uint32_t s_cp = 0, e_cp = 0; + bool sel = taHasSelection(ta, &s_cp, &e_cp); + + if (act == TXT_SELALL) { + uint32_t len = _lv_txt_get_encoded_length(txt); + txtMenuHide(); + if (len) taSelectRange(ta, 0, len); + return; + } + if (act == TXT_COPY || act == TXT_CUT) { + char buf[CLIPBOARD_MAX]; + if (sel) { + uint32_t sb = taCpToByte(txt, s_cp), eb = taCpToByte(txt, e_cp); + uint32_t n = eb - sb; if (n >= CLIPBOARD_MAX) n = CLIPBOARD_MAX - 1; + memcpy(buf, txt + sb, n); buf[n] = '\0'; + clipboardSet(buf, act == TXT_CUT ? "cut" : ""); + } else { + clipboardSet(txt, act == TXT_CUT ? "cut" : ""); + } + if (act == TXT_CUT) { + if (sel) taDeleteRange(ta, s_cp, e_cp); + else lv_textarea_set_text(ta, ""); + } + } else if (act == TXT_PASTE) { + if (s_clipboard[0]) { + if (sel) taDeleteRange(ta, s_cp, e_cp); + lv_textarea_add_text(ta, s_clipboard); + } + } + taClearSelection(ta); + txtMenuHide(); +} + +static void txtMenuShow(lv_obj_t* ta) { + txtMenuHide(); + if (!ta) return; + s_txtmenu_ta = ta; + static const char* const kLabels[] = { "Cut", "Copy", "Paste", "All" }; + const int n = 4, cw = 52, ch = 34, gap = 4, pad = 6; + s_txtmenu = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_txtmenu); + lv_obj_set_style_bg_color(s_txtmenu, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_txtmenu, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(s_txtmenu, 8, LV_PART_MAIN); + lv_obj_set_style_border_color(s_txtmenu, lv_color_hex(0x2A3D52), LV_PART_MAIN); + lv_obj_set_style_border_width(s_txtmenu, 1, LV_PART_MAIN); + lv_obj_set_style_pad_all(s_txtmenu, pad, LV_PART_MAIN); + lv_obj_set_style_pad_column(s_txtmenu, gap, LV_PART_MAIN); + lv_obj_set_flex_flow(s_txtmenu, LV_FLEX_FLOW_ROW); + lv_obj_set_size(s_txtmenu, n * cw + (n - 1) * gap + pad * 2, ch + pad * 2); + lv_obj_clear_flag(s_txtmenu, LV_OBJ_FLAG_SCROLLABLE); + for (int i = 0; i < n; ++i) { + lv_obj_t* c = lv_btn_create(s_txtmenu); + lv_obj_set_size(c, cw, ch); + lv_obj_set_style_radius(c, 5, LV_PART_MAIN); + lv_obj_set_style_bg_color(c, lv_color_hex(0x1B2B3A), LV_PART_MAIN); + lv_obj_set_style_bg_color(c, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_add_event_cb(c, txtMenuCellCb, LV_EVENT_CLICKED, reinterpret_cast((intptr_t)i)); + lv_obj_t* l = lv_label_create(c); + lv_label_set_text(l, kLabels[i]); + lv_obj_set_style_text_font(l, &g_font_14, LV_PART_MAIN); + lv_obj_set_style_text_color(l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_center(l); + } + // Above the field (below it if there's no room above the status bar). + lv_obj_update_layout(s_txtmenu); + lv_area_t a; lv_obj_get_coords(ta, &a); + const lv_coord_t bw = lv_obj_get_width(s_txtmenu), bh = lv_obj_get_height(s_txtmenu); + lv_coord_t bx = (lv_disp_get_hor_res(nullptr) - bw) / 2; + if (bx < 2) bx = 2; + lv_coord_t by = a.y1 - bh - 4; + if (by < STATUSBAR_H + 2) by = a.y2 + 4; + // Same keyboard clamp as the accent box: keep the menu off the on-screen keys. + if (g_lv.keyboard && !lv_obj_has_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN)) { + lv_coord_t limit = lv_disp_get_ver_res(nullptr) - chatKbH() - 2; + if (s_kb_panel) limit -= s_comp_h; + if (by + bh > limit) by = limit - bh; + if (by < STATUSBAR_H + 2) by = STATUSBAR_H + 2; + } + lv_obj_set_pos(s_txtmenu, bx, by); + lv_obj_move_foreground(s_txtmenu); +} + +// Composer gestures: double-tap (two clicks within 350 ms on the same field) +// selects the word under the tap; long-press opens the edit menu. A single tap +// dismisses an open menu. +static void composerEditClickedCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + lv_obj_t* ta = lv_event_get_target(e); + txtMenuHide(); + static uint32_t s_last_ms = 0; + static lv_obj_t* s_last_ta = nullptr; + uint32_t now = millis(); + bool dbl = (s_last_ta == ta) && (now - s_last_ms < 350); + s_last_ms = now; + s_last_ta = ta; + if (dbl) { + s_last_ms = 0; // consume so a 3rd tap doesn't re-trigger + taSelectWordAtCursor(ta); + } else { + s_sel_ta = nullptr; // a single tap moved the cursor -> drop the sticky selection + } +} + +static void composerEditLongPressCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_LONG_PRESSED) return; + lv_obj_t* ta = lv_event_get_target(e); + lv_indev_t* act = lv_indev_get_act(); + if (act) lv_indev_wait_release(act); // swallow the trailing CLICKED + taReapplyStickySel(ta); // the press just cleared the highlight — restore it under the menu + txtMenuShow(ta); +} + +// On-screen keyboard (V4): backspace with an active selection deletes the whole +// highlight instead of one character. Registered BEFORE the keyboard's default +// handler (see buildUiTree) so it can replace the default single-char delete — +// it stops processing so neither the default delete nor the accent box also run. +static void kbBackspaceSelCb(lv_event_t* e) { + lv_obj_t* kb = lv_event_get_target(e); + uint32_t btn = lv_btnmatrix_get_selected_btn(kb); + if (btn == LV_BTNMATRIX_BTN_NONE) return; + const char* txt = lv_btnmatrix_get_btn_text(kb, btn); + if (!txt || strcmp(txt, LV_SYMBOL_BACKSPACE) != 0) return; + lv_obj_t* ta = lv_keyboard_get_textarea(kb); + if (!ta) return; + uint32_t s_cp, e_cp; + if (!taHasSelection(ta, &s_cp, &e_cp)) return; // nothing selected -> default deletes one char + taDeleteRange(ta, s_cp, e_cp); + taClearSelection(ta); + accentBoxHide(); + txtMenuHide(); + lv_event_stop_processing(e); +} + +static void closeSettingsModal() { + hideKb(); + if (g_set_modal.root) { + lv_obj_del(g_set_modal.root); + } + // Pointers into the add-channel modal body get freed with the root above; + // null them so a later submit (queued event) doesn't dereference garbage. + s_addch_name_ta = nullptr; + s_addch_secret_ta = nullptr; + s_addch_hashtag_ta = nullptr; + s_addch_error_l = nullptr; + s_addct_pub_ta = nullptr; + s_addct_name_ta = nullptr; + s_addct_error_l = nullptr; + resetSettingsModalState(); +} + +static void settingsCloseCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + (void)e; + closeSettingsModal(); + refreshStatusLabels(); +} + +// When non-null, createSettingsModal() skips the modal chrome and returns an +// inline content container (a flex item, with a coloured section header) on +// this page — so the Set tab's sub-tab pages can host the exact same builders +// inline. Set per page in makeSettings; g_set_modal is reset ONCE before the +// pages are populated, and each builder writes its own distinct widget ptrs. +static lv_obj_t* s_settings_inline_parent = nullptr; +// Width (px) of the current settings content area — the inline card's inner +// width, or the modal body's inner width. Builders read it to right-align +// switches and spread multi-column rows responsively. +static lv_coord_t s_settings_content_w = 280; + +static lv_obj_t* createSettingsModal(const char* title, SettingsModalKind kind) { + if (s_settings_inline_parent) { + // Grouped-card section: a muted group header above a rounded panel card with + // internal padding. The card fills the page content width (symmetric 8 px + // page margins); the returned `content` is the card's inner area, with an + // explicit pixel width (lv_pct does NOT resolve through a flex column of + // LV_SIZE_CONTENT items). The card padding keeps every control clear of the + // screen edge and gives consistent visual separation between groups. + const lv_coord_t card_w = lv_disp_get_hor_res(nullptr) - 16; + const lv_coord_t card_pad = 8; + const lv_coord_t content_w = card_w - card_pad * 2; + + lv_obj_t* wrap = lv_obj_create(s_settings_inline_parent); + lv_obj_remove_style_all(wrap); + lv_obj_set_width(wrap, card_w); + lv_obj_set_height(wrap, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(wrap, 0, LV_PART_MAIN); + lv_obj_set_style_pad_bottom(wrap, 14, LV_PART_MAIN); // gap between cards + lv_obj_set_flex_flow(wrap, LV_FLEX_FLOW_COLUMN); + lv_obj_clear_flag(wrap, LV_OBJ_FLAG_SCROLLABLE); + if (title && title[0]) { + lv_obj_t* h = lv_label_create(wrap); + lv_label_set_text(h, TR(title)); + lv_obj_set_style_text_font(h, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_text_color(h, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_pad_left(h, 4, LV_PART_MAIN); + lv_obj_set_style_pad_bottom(h, 4, LV_PART_MAIN); + } + lv_obj_t* card = lv_obj_create(wrap); + lv_obj_remove_style_all(card); + lv_obj_set_width(card, card_w); + lv_obj_set_height(card, LV_SIZE_CONTENT); + lv_obj_set_style_bg_color(card, lv_color_hex(0x121417), LV_PART_MAIN); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(card, 10, LV_PART_MAIN); + lv_obj_set_style_border_width(card, 1, LV_PART_MAIN); + lv_obj_set_style_border_color(card, lv_color_hex(0x2A2E34), LV_PART_MAIN); + lv_obj_set_style_pad_all(card, card_pad, LV_PART_MAIN); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* content = lv_obj_create(card); + lv_obj_remove_style_all(content); + lv_obj_set_width(content, content_w); + lv_obj_set_height(content, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(content, 0, LV_PART_MAIN); + lv_obj_set_style_border_width(content, 0, LV_PART_MAIN); + lv_obj_clear_flag(content, LV_OBJ_FLAG_SCROLLABLE); + (void)kind; + s_settings_content_w = content_w; + return content; + } + closeSettingsModal(); + + lv_coord_t sw = lv_disp_get_hor_res(nullptr); + lv_coord_t sh = lv_disp_get_ver_res(nullptr); + lv_obj_t* root = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(root); + // Root sits BELOW the global status bar (always on top, drawn on + // lv_layer_sys). Without this offset the modal's own header (title + + // Close button at y=0) was clipped behind the time/battery row. + lv_obj_set_size(root, sw, sh - STATUSBAR_H); + lv_obj_set_pos(root, 0, STATUSBAR_H); + styleSurface(root, COLOR_BG, 0); + lv_obj_set_style_pad_all(root, 0, LV_PART_MAIN); + lv_obj_set_style_border_width(root, 0, LV_PART_MAIN); + lv_obj_clear_flag(root, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_move_foreground(root); + + lv_obj_t* header = lv_obj_create(root); + lv_obj_remove_style_all(header); + lv_obj_set_size(header, sw, 46); + lv_obj_set_pos(header, 0, 0); + styleSurface(header, COLOR_PANEL, 0); + lv_obj_set_style_pad_all(header, 0, LV_PART_MAIN); + lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_side(header, LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN); + lv_obj_set_style_border_width(header, 1, LV_PART_MAIN); + lv_obj_set_style_border_color(header, lv_color_hex(0x18191A), LV_PART_MAIN); + + lv_obj_t* lbl = lv_label_create(header); + lv_label_set_text(lbl, TR(title)); + lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(lbl, &g_font_14, LV_PART_MAIN); + lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 8, 0); + + lv_obj_t* close_btn = lv_btn_create(header); + lv_obj_set_size(close_btn, 58, 32); + lv_obj_align(close_btn, LV_ALIGN_RIGHT_MID, -6, 0); + styleButton(close_btn); + lv_obj_add_event_cb(close_btn, settingsCloseCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* close_lbl = lv_label_create(close_btn); + lv_label_set_text(close_lbl, TR("Close")); + lv_obj_center(close_lbl); + + lv_obj_t* body = lv_obj_create(root); + lv_obj_remove_style_all(body); + lv_obj_set_size(body, sw - 8, sh - 52); + lv_obj_set_pos(body, 4, 48); + styleSurface(body, COLOR_BG, 0); + lv_obj_set_style_pad_all(body, 6, LV_PART_MAIN); + lv_obj_set_style_border_width(body, 0, LV_PART_MAIN); + lv_obj_set_scroll_dir(body, LV_DIR_VER); + lv_obj_set_scrollbar_mode(body, LV_SCROLLBAR_MODE_AUTO); + lv_obj_add_event_cb(body, scrollClampOnEndCb, LV_EVENT_SCROLL_END, nullptr); + + // Single content child holding ALL the controls, sized to its contents. The + // scrollable `body` therefore only ever has ONE child to scroll — tall modals + // (e.g. Device, with 20+ controls) used to pile every control directly into + // the scroll view, which wedged LVGL's scroll-bounds machinery so taps showed + // their press style but never fired CLICKED ("buttons unresponsive"). Modals + // add their widgets to this container; it grows and `body` scrolls it. + lv_obj_t* content = lv_obj_create(body); + lv_obj_remove_style_all(content); + lv_obj_set_width(content, lv_pct(100)); + lv_obj_set_height(content, LV_SIZE_CONTENT); + lv_obj_set_pos(content, 0, 0); + lv_obj_set_style_pad_all(content, 0, LV_PART_MAIN); + lv_obj_set_style_border_width(content, 0, LV_PART_MAIN); + lv_obj_clear_flag(content, LV_OBJ_FLAG_SCROLLABLE); + + resetSettingsModalState(); + g_set_modal.root = root; + g_set_modal.kind = kind; + s_settings_content_w = (sw - 8) - 12; // modal body width minus its 6px padding each side +#if defined(HAS_TDECK_KEYBOARD) + // Physical keyboard: once the caller has finished adding this modal's fields + // (deferred to the next frame), focus its first text field so the user can + // type straight away. Popup modals only — the inline-settings path returns above. + lv_async_call(tdeckModalAutoFocusAsync, root); +#endif + return content; +} + +static void saveProfileNameCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task || !g_set_modal.name_ta) return; + kbMirrorSyncToReal(); + const char* name = lv_textarea_get_text(g_set_modal.name_ta); + if (g_lv.task->setNodeName(name)) { + g_lv.task->showAlert(TR("Name saved"), 1000); + refreshStatusLabels(); + } +} + +static void saveProfilePosCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + kbMirrorSyncToReal(); + float lat = 0.0f, lon = 0.0f; + if (!parseFloatField(g_set_modal.lat_ta, lat) || !parseFloatField(g_set_modal.lon_ta, lon)) { + g_lv.task->showAlert(TR("Invalid lat/lon"), 1200); + return; + } + if (g_lv.task->setPosition(static_cast(lat), static_cast(lon))) { + g_lv.task->showAlert(TR("Position saved"), 1000); + refreshStatusLabels(); + } +} + +static void saveRadioParamsCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + kbMirrorSyncToReal(); + float freq = 0.0f, bw = 0.0f, af = 0.0f; + int sf = 0, cr = 0, tx = 0; + if (!parseFloatField(g_set_modal.freq_ta, freq) || !parseFloatField(g_set_modal.bw_ta, bw) || + !parseIntField(g_set_modal.sf_ta, sf) || !parseIntField(g_set_modal.cr_ta, cr) || + !parseIntField(g_set_modal.tx_ta, tx) || !parseFloatField(g_set_modal.airtime_ta, af)) { + g_lv.task->showAlert(TR("Invalid radio values"), 1200); + return; + } + bool ok = g_lv.task->setRadioParams(freq, bw, static_cast(sf), static_cast(cr), + static_cast(tx), af); + // Region scope — independent of the freq/SF values above. Derive + persist the + // flood-scope key from the typed "#region" (blank clears it back to unscoped), + // and remember the display name for next time the form is shown. + bool has_region = false; + if (g_set_modal.region_ta) { + char region[TOUCH_REGION_SCOPE_MAXLEN] = {0}; + strncpy(region, lv_textarea_get_text(g_set_modal.region_ta), sizeof(region) - 1); + char* r = region; // trim so the stored name matches the key + while (*r == ' ' || *r == '\t') r++; + size_t rl = strlen(r); + while (rl && (r[rl-1]==' '||r[rl-1]=='\t'||r[rl-1]=='\n'||r[rl-1]=='\r')) r[--rl] = '\0'; + the_mesh.setDefaultFloodScope(r); + touchPrefsSetRegionScope(r); + has_region = (r[0] != '\0'); + } + if (ok) { + g_lv.task->showAlert(has_region ? TR("Radio + region set") : TR("Radio applied"), 1000); + refreshStatusLabels(); + } +} + +static void saveAutoAddCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + kbMirrorSyncToReal(); + int max_hops = 0; + if (!parseIntField(g_set_modal.max_hops_ta, max_hops)) { + g_lv.task->showAlert(TR("Invalid max hops"), 1200); + return; + } + if (max_hops < 0) max_hops = 0; + if (max_hops > 64) max_hops = 64; + + uint8_t mask = 0; + if (g_set_modal.auto_overwrite_sw && lv_obj_has_state(g_set_modal.auto_overwrite_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_OVERWRITE_OLDEST; + if (g_set_modal.auto_chat_sw && lv_obj_has_state(g_set_modal.auto_chat_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_CHAT; + if (g_set_modal.auto_rep_sw && lv_obj_has_state(g_set_modal.auto_rep_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_REPEATER; + if (g_set_modal.auto_room_sw && lv_obj_has_state(g_set_modal.auto_room_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_ROOM_SERVER; + if (g_set_modal.auto_sensor_sw && lv_obj_has_state(g_set_modal.auto_sensor_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_SENSOR; + uint8_t manual = 1u; // touch UI is always selective: the per-type Auto switches are authoritative (off = really off) + + g_lv.task->setAutoAddConfig(mask, static_cast(max_hops), manual); + g_lv.task->showAlert(TR("Auto-add saved"), 1000); + refreshStatusLabels(); +} + +// Auto-save on toggle: the auto-add switches persist immediately, so the user +// doesn't have to press "Save auto-add" for them to stick across reboots. +// Recomputes the whole mask from the current switch states + the hops field. +static void autoAddSwitchCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED || !g_lv.task) return; + uint8_t mask = 0; + if (g_set_modal.auto_overwrite_sw && lv_obj_has_state(g_set_modal.auto_overwrite_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_OVERWRITE_OLDEST; + if (g_set_modal.auto_chat_sw && lv_obj_has_state(g_set_modal.auto_chat_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_CHAT; + if (g_set_modal.auto_rep_sw && lv_obj_has_state(g_set_modal.auto_rep_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_REPEATER; + if (g_set_modal.auto_room_sw && lv_obj_has_state(g_set_modal.auto_room_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_ROOM_SERVER; + if (g_set_modal.auto_sensor_sw && lv_obj_has_state(g_set_modal.auto_sensor_sw, LV_STATE_CHECKED)) mask |= AUTO_ADD_SENSOR; + uint8_t manual = 1u; // touch UI is always selective: the per-type Auto switches are authoritative (off = really off) + int max_hops = 3; + int mh; + if (g_set_modal.max_hops_ta && parseIntField(g_set_modal.max_hops_ta, mh)) max_hops = mh; + if (max_hops < 0) max_hops = 0; + if (max_hops > 64) max_hops = 64; + g_lv.task->setAutoAddConfig(mask, static_cast(max_hops), manual); +} + +static void savePolicyCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + uint8_t share = (g_set_modal.share_loc_sw && lv_obj_has_state(g_set_modal.share_loc_sw, LV_STATE_CHECKED)) ? 1u : 0u; + g_lv.task->setAdvertLocationPolicy(share); // path-hash size lives in Radio settings now + g_lv.task->showAlert(TR("Advert policy saved"), 1000); + refreshStatusLabels(); +} + +static void saveExperimentalCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + uint8_t multi = (g_set_modal.exp_multi_sw && lv_obj_has_state(g_set_modal.exp_multi_sw, LV_STATE_CHECKED)) ? 1u : 0u; + uint8_t repeat = (g_set_modal.exp_repeat_sw && lv_obj_has_state(g_set_modal.exp_repeat_sw, LV_STATE_CHECKED)) ? 1u : 0u; + uint8_t boost = (g_set_modal.exp_boost_sw && lv_obj_has_state(g_set_modal.exp_boost_sw, LV_STATE_CHECKED)) ? 1u : 0u; + g_lv.task->setExperimentalFlags(multi, repeat, boost); +#if defined(ESP32) + bool dc_show = (g_set_modal.exp_dc_sw && lv_obj_has_state(g_set_modal.exp_dc_sw, LV_STATE_CHECKED)); + touchPrefsSetDutyMeterShown(dc_show); +#endif + g_lv.task->showAlert(TR("Experimental saved"), 1000); + refreshStatusLabels(); +} + +static void syncClockCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + g_lv.task->setDeviceTimeFromSystemClock(); + g_lv.task->showAlert(TR("Clock synced"), 900); +} + +static void advertNowCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + g_lv.task->showAlert(g_lv.task->sendAdvertNow() ? TR("Advert sent") : TR("Advert failed"), 900); +} + +static void rebootCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + g_lv.task->showAlert(TR("Rebooting..."), 600); + g_lv.task->rebootDevice(); +} + +#if defined(ESP32) +// True when the running image has a spare OTA app slot to write an update into. +// Standalone meshcomod ships a dual-OTA (A/B) table -> true -> in-firmware + +// phone-app update works. Installed under Launcher (single app slot) there's no +// spare partition -> false -> the update affordances hide, so the same app-only +// bin behaves correctly in both worlds. (Replaces the retired reboot-to-recovery +// helpers — the recovery firmware is gone.) +static bool touchHasOtaUpdateSlot() { + return esp_ota_get_next_update_partition(NULL) != NULL; +} +#endif + +// ---- Advert modal: pick flood (multi-hop) or zero-hop ---- +static void advertFloodCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + bool ok = g_lv.task->sendAdvertFlood(); + g_lv.task->showAlert(ok ? TR("Flood advert sent") : TR("Advert failed"), 1000); + closeSettingsModal(); +} + +static void advertZeroHopCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + bool ok = g_lv.task->sendAdvertZeroHop(); + g_lv.task->showAlert(ok ? TR("Zero-hop advert sent") : TR("Advert failed"), 1000); + closeSettingsModal(); +} + +static void openAdvertModalCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + lv_obj_t* body = createSettingsModal("Send advert", SettingsModalKind::Advert); + int y = 4; + + lv_obj_t* hint = lv_label_create(body); + lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP); + lv_obj_set_width(hint, lv_pct(100)); + lv_obj_set_pos(hint, 2, y); + lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(hint, &g_font_12, LV_PART_MAIN); + lv_label_set_text(hint, + "Flood: relays will rebroadcast your advert across the mesh (multi-hop).\n" + "Zero-hop: only neighbours hear you (no relaying)."); + y += 60; + + lv_obj_t* b_flood = lv_btn_create(body); + lv_obj_set_size(b_flood, lv_pct(100),40); + lv_obj_set_pos(b_flood, 2, y); + styleButton(b_flood); + lv_obj_add_event_cb(b_flood, advertFloodCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lf = lv_label_create(b_flood); + lv_label_set_text(lf, LV_SYMBOL_UPLOAD " Flood advert"); + lv_obj_center(lf); + y += 46; + + lv_obj_t* b_zh = lv_btn_create(body); + lv_obj_set_size(b_zh, lv_pct(100),40); + lv_obj_set_pos(b_zh, 2, y); + styleButton(b_zh); + lv_obj_add_event_cb(b_zh, advertZeroHopCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lz = lv_label_create(b_zh); + lv_label_set_text(lz, LV_SYMBOL_WIFI " Zero-hop advert"); + lv_obj_center(lz); +} + +// ---- Discovered list modal: shows adverts NOT yet in contacts[], with "Add" buttons ---- +struct DiscoveredAddCtx { int slot_idx; }; +static DiscoveredAddCtx s_disc_add_ctx[DISCOVERED_MAX]; + +// Forward declared so discoveredAddCb can re-open the list after a successful add. +static void openDiscoveredModalCb(lv_event_t* e); + +static void discoveredAddCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + auto* ctx = static_cast(lv_event_get_user_data(e)); + if (!ctx) return; + int idx = ctx->slot_idx; + if (idx < 0 || idx >= DISCOVERED_MAX) return; + LvDiscoveredEntry& e_disc = s_discovered[idx]; + if (!e_disc.used) return; + + // Add the captured ContactInfo straight to the_mesh. + if (the_mesh.addContact(e_disc.ci)) { + the_mesh.uiPersistContacts(); // write /contacts3 so the add survives reboot + e_disc.in_contacts = true; + g_lv.task->showAlert(TR("Added to contacts"), 1000); + // Re-open the Discovered modal so the list updates immediately. + closeSettingsModal(); + lv_event_t synth{}; + synth.code = LV_EVENT_CLICKED; + openDiscoveredModalCb(&synth); + } else { + g_lv.task->showAlert(TR("Contacts full"), 1200); + } +} + +static void openDiscoveredModalCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + lv_obj_t* body = createSettingsModal("Discovered", SettingsModalKind::Discovered); + int y = 0; + + // Auto-add hint: any of the four "auto-add" type bits set means new contacts + // of those kinds bypass this list and land straight in the Contacts tab. + // Surfacing it here avoids the "where did my discoveries go?" confusion. + { + NodePrefs* prefs = the_mesh.getNodePrefs(); + const uint8_t mask = prefs ? (prefs->autoadd_config & static_cast( + AUTO_ADD_CHAT | AUTO_ADD_REPEATER | AUTO_ADD_ROOM_SERVER | AUTO_ADD_SENSOR)) : 0u; + if (mask) { + char hint[120]; + char types[64]; types[0] = '\0'; + size_t off = 0; + auto add = [&](const char* s) { + if (off > 0 && off + 3 < sizeof(types)) { types[off++] = ','; types[off++] = ' '; types[off] = '\0'; } + size_t n = strlen(s); + if (off + n < sizeof(types)) { memcpy(types + off, s, n); off += n; types[off] = '\0'; } + }; + if (mask & AUTO_ADD_CHAT) add("chats"); + if (mask & AUTO_ADD_REPEATER) add("repeaters"); + if (mask & AUTO_ADD_ROOM_SERVER) add("rooms"); + if (mask & AUTO_ADD_SENSOR) add("sensors"); + snprintf(hint, sizeof(hint), "Auto-add on for %s — new %s land in Contacts automatically.", + types, types); + lv_obj_t* l = lv_label_create(body); + lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); + lv_obj_set_width(l, lv_pct(100)); + lv_label_set_text(l, hint); + lv_obj_set_style_text_color(l, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_text_font(l, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(l, 2, y); + y += 38; + } + } + + // Sort indices by recv_ms desc so newest is first. + int order[DISCOVERED_MAX]; + int n = 0; + for (int i = 0; i < DISCOVERED_MAX; ++i) { + if (s_discovered[i].used && !s_discovered[i].in_contacts) order[n++] = i; + } + // simple insertion sort by recv_ms desc + for (int i = 1; i < n; ++i) { + int v = order[i]; + int j = i - 1; + while (j >= 0 && s_discovered[order[j]].recv_ms < s_discovered[v].recv_ms) { + order[j + 1] = order[j]; + --j; + } + order[j + 1] = v; + } + + if (n == 0) { + lv_obj_t* empty = lv_label_create(body); + lv_label_set_long_mode(empty, LV_LABEL_LONG_WRAP); + lv_obj_set_width(empty, lv_pct(100)); + lv_obj_set_pos(empty, 2, y + 8); + lv_obj_set_style_text_color(empty, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(empty, &g_font_12, LV_PART_MAIN); + lv_label_set_text(empty, + "No discovered nodes yet.\n\n" + "Anything heard over the air that isn't in your contacts will show up " + "here. When auto-add is off, you can manually add nodes to your contacts."); + return; + } + + for (int k = 0; k < n; ++k) { + int idx = order[k]; + LvDiscoveredEntry& e_disc = s_discovered[idx]; + + // Card background — full content width (was a fixed 222 px, leaving a gap). + const lv_coord_t cw = s_settings_content_w; + const int pad = 4, add_w = 56, gap = 8; + const lv_coord_t card_h = 52; + const lv_coord_t name_h = lv_font_get_line_height(&g_font_14); + const lv_coord_t text_w = cw - 2 * pad - add_w - gap; // room for name/meta beside Add + + lv_obj_t* card = lv_obj_create(body); + lv_obj_remove_style_all(card); + lv_obj_set_size(card, cw, card_h); + lv_obj_set_pos(card, 0, y); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_bg_color(card, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_radius(card, 6, LV_PART_MAIN); + lv_obj_set_style_pad_all(card, pad, LV_PART_MAIN); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + + // Name label — clamped to ONE line (height = line height) so a long name + // ellipsises instead of wrapping down onto the meta row below it. + lv_obj_t* nm = lv_label_create(card); + char nm_buf[40]; + copyUtf8ReplacingMissingGlyphs(&g_font_14, nm_buf, sizeof(nm_buf), e_disc.ci.name); + lv_label_set_text(nm, nm_buf[0] ? nm_buf : "(unnamed)"); + lv_obj_set_style_text_color(nm, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_label_set_long_mode(nm, LV_LABEL_LONG_DOT); + lv_obj_set_size(nm, text_w, name_h); + lv_obj_set_pos(nm, 2, 3); + + // Type + hops + key prefix (single line, ellipsised, below the name) + lv_obj_t* meta = lv_label_create(card); + const char* type_label = "node"; + switch (e_disc.ci.type) { + case ADV_TYPE_NONE: type_label = "none"; break; + case ADV_TYPE_CHAT: type_label = "chat"; break; + case ADV_TYPE_REPEATER: type_label = "repeater"; break; + case ADV_TYPE_ROOM: type_label = "room"; break; + case ADV_TYPE_SENSOR: type_label = "sensor"; break; + default: break; + } + char keyhex[18]; + mesh::Utils::toHex(keyhex, e_disc.ci.id.pub_key, 4); + keyhex[8] = '\0'; + char meta_buf[64]; + snprintf(meta_buf, sizeof(meta_buf), "%s · %u hop · %s…", + type_label, (unsigned)e_disc.path_len, keyhex); + lv_label_set_text(meta, meta_buf); + lv_obj_set_style_text_color(meta, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(meta, &g_font_12, LV_PART_MAIN); + lv_label_set_long_mode(meta, LV_LABEL_LONG_DOT); + lv_obj_set_width(meta, text_w); + lv_obj_set_pos(meta, 2, 3 + name_h + 2); + + // "Add" button on the right, vertically centred + lv_obj_t* add_btn = lv_btn_create(card); + lv_obj_set_size(add_btn, add_w, card_h - 2 * pad - 2); + lv_obj_align(add_btn, LV_ALIGN_RIGHT_MID, 0, 0); + styleButton(add_btn); + s_disc_add_ctx[idx].slot_idx = idx; + lv_obj_add_event_cb(add_btn, discoveredAddCb, LV_EVENT_CLICKED, &s_disc_add_ctx[idx]); + lv_obj_t* add_lbl = lv_label_create(add_btn); + lv_label_set_text(add_lbl, TR("Add")); + lv_obj_center(add_lbl); + + y += card_h + 6; + } +} + +// Buffered Print over a File. The JSON export writes byte-at-a-time; on a raw +// File that's one flash op per byte (a 60 KB backup ≈ 60k syscalls → a +// multi-second UI freeze). Batching into 1 KB writes makes it ~60 ops. +class FileBufWriter : public Print { + File& _f; + uint8_t _buf[1024]; + size_t _n = 0; +public: + explicit FileBufWriter(File& f) : _f(f) {} + void flushBuf() { if (_n) { _f.write(_buf, _n); _n = 0; } } + size_t write(uint8_t b) override { _buf[_n++] = b; if (_n >= sizeof(_buf)) flushBuf(); return 1; } + size_t write(const uint8_t* d, size_t n) override { + for (size_t i = 0; i < n; ++i) { _buf[_n++] = d[i]; if (_n >= sizeof(_buf)) flushBuf(); } + return n; + } +}; + +// Import settings opens a full-screen .json picker (internal flash + SD), the +// same UX as the lock-wallpaper picker. Defined later in this file (where the +// SD mount state is in scope); forward-declared here for the button below. +static void openBackupPicker(); +static void buildBackupsSettings(); // Settings -> Backups detail page (list/delete + factory reset) +static void doExportBackupFile(const char* fname); // write a backup (SD if a card is present, else internal) + +// Heavy-flash-write watchdog guard (ref-counted, nesting-safe). A large or +// fragmenting SPIFFS write can trigger garbage collection — a multi-second +// flash burst that disables the CPU cache and starves the idle task, long +// enough to trip the TASK watchdog (confirmed by coredump: an 'ipc0' abort from +// task_wdt_isr while another thread was in spiffs_gc_clean). Bracket such writes +// so a bounded-but-slow GC pass can't reset the device. The same pattern the +// file-manager format/paste paths already use. Ref-counted so a nested guard +// (import -> persistHistoryNow -> saveHistoryToStorage) doesn't re-enable early. +static int s_wdt_heavy_depth = 0; +static inline void wdtHeavyBegin() { + if (s_wdt_heavy_depth++ == 0) { disableCore0WDT(); disableCore1WDT(); } +} +static inline void wdtHeavyEnd() { + if (s_wdt_heavy_depth > 0 && --s_wdt_heavy_depth == 0) { enableCore0WDT(); enableCore1WDT(); } +} +struct WdtHeavyGuard { WdtHeavyGuard() { wdtHeavyBegin(); } ~WdtHeavyGuard() { wdtHeavyEnd(); } }; + +// --- Settings row label helper -------------------------------------------- +// The settings detail pages lay rows out with a manual y-cursor and used to +// create labels with no width, so a long (translated) string overflowed the +// card or slid under a right-aligned switch. This makes a width-constrained, +// wrapping label and returns its laid-out height so the caller advances its +// y-cursor by the real (possibly multi-line) height. `reserve_right` leaves room +// for a control pinned to the card's right edge (~56 for a switch); pass 0 for a +// full-width label. font==nullptr keeps the inherited default (g_font_14). The +// text is run through TR() here, so callers pass the bare English literal. +static int settingsRowLabel(lv_obj_t* body, int y, int y_off, const char* text, + uint32_t color, const lv_font_t* font, int reserve_right) { + lv_obj_t* l = lv_label_create(body); + lv_obj_set_width(l, s_settings_content_w - 2 - reserve_right); + lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); + lv_label_set_text(l, TR(text)); + lv_obj_set_style_text_color(l, lv_color_hex(color), LV_PART_MAIN); + if (font) lv_obj_set_style_text_font(l, font, LV_PART_MAIN); + lv_obj_set_pos(l, 2, y + y_off); + lv_obj_update_layout(l); + return lv_obj_get_height(l); +} + +static void buildProfileSettings() { + // No "Profile" group header — it just duplicates the sub-tab button name. + lv_obj_t* body = createSettingsModal("", SettingsModalKind::Profile); + int y = 0; + const lv_coord_t cw = s_settings_content_w; + auto mk_label = [&](const char* text) { + y += settingsRowLabel(body, y, 0, text, COLOR_SUB, nullptr, 0) + 2; + }; + auto mk_ta = [&](int w, int x, const char* ph, int max_len) -> lv_obj_t* { + lv_obj_t* ta = lv_textarea_create(body); + lv_obj_set_size(ta, w, 30); + lv_obj_set_pos(ta, x, y); + lv_textarea_set_one_line(ta, true); + lv_textarea_set_placeholder_text(ta, TR(ph)); + lv_textarea_set_max_length(ta, max_len); + attachSettingsTaEvents(ta); + return ta; + }; + + mk_label("Node name"); + g_set_modal.name_ta = mk_ta(cw, 0, "Node name", 31); + if (g_lv.task) { + char nm_vis[40]; + const char* raw = g_lv.task->getNodeNameCstr(); + copyUtf8ReplacingMissingGlyphs(&g_font_14, nm_vis, sizeof(nm_vis), raw ? raw : ""); + lv_textarea_set_text(g_set_modal.name_ta, nm_vis); + } + y += 36; + mk_label("Advert location"); + const int loc_g = 8, loc_fw = (cw - loc_g) / 2; // lat / lon fill the row evenly + g_set_modal.lat_ta = mk_ta(loc_fw, 0, "Latitude", 20); + g_set_modal.lon_ta = mk_ta(loc_fw, loc_fw + loc_g, "Longitude", 20); + if (g_lv.task) { + char buf[24]; + snprintf(buf, sizeof(buf), "%.6f", g_lv.task->getNodeLat()); + lv_textarea_set_text(g_set_modal.lat_ta, buf); + snprintf(buf, sizeof(buf), "%.6f", g_lv.task->getNodeLon()); + lv_textarea_set_text(g_set_modal.lon_ta, buf); + } + y += 40; + + const int btn_g = 8, btn_fw = (cw - btn_g) / 2; // Save name / Save position fill the row + lv_obj_t* b1 = lv_btn_create(body); + lv_obj_set_size(b1, btn_fw, 34); + lv_obj_set_pos(b1, 0, y); + styleButton(b1); + lv_obj_add_event_cb(b1, saveProfileNameCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l1 = lv_label_create(b1); + lv_label_set_text(l1, TR("Save name")); + lv_obj_center(l1); + + lv_obj_t* b2 = lv_btn_create(body); + lv_obj_set_size(b2, btn_fw, 34); + lv_obj_set_pos(b2, btn_fw + btn_g, y); + styleButton(b2); + lv_obj_add_event_cb(b2, saveProfilePosCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l2 = lv_label_create(b2); + lv_label_set_text(l2, TR("Save position")); + lv_obj_center(l2); + y += 40; + + { + auto mk_label = [&](const char* text) { + y += settingsRowLabel(body, y, 0, text, COLOR_SUB, nullptr, 0) + 2; + }; + auto mk_switch = [&](const char* text, lv_obj_t** out) { + int h = settingsRowLabel(body, y, 6, text, COLOR_SUB, nullptr, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); // flush to the card's right edge + if (out) *out = sw; + y += LV_MAX(34, h + 12); + }; + + // (Path-hash size moved to Radio settings as "Multi-byte routing" — it's the + // same NodePrefs.path_hash_mode, so no duplicate control here.) + mk_label("Advert"); + mk_switch("Share location in advert", &g_set_modal.share_loc_sw); + NodePrefs* pol_prefs = the_mesh.getNodePrefs(); + if (pol_prefs && pol_prefs->advert_loc_policy) + lv_obj_add_state(g_set_modal.share_loc_sw, LV_STATE_CHECKED); + + lv_obj_t* bpol = lv_btn_create(body); + lv_obj_set_size(bpol, lv_pct(100),34); + lv_obj_set_pos(bpol, 2, y); + styleButton(bpol); + lv_obj_add_event_cb(bpol, savePolicyCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lpol = lv_label_create(bpol); + lv_label_set_text(lpol, TR("Save policy")); + lv_obj_center(lpol); + y += 40; // advance past the Save-policy button so the moved Identity block clears it + } + + // ---- Identity + Share QR (moved to the BOTTOM of the Profile page) ---- + // Public key — long-press copies. We don't surface the *private* key here + // (LocalIdentity::prv_key is library-private, and private material on a touch + // screen is a footgun). + mk_label("Identity (public key)"); + { + const uint8_t* pk = the_mesh.getSelfPubKey(); + char pk_hex[2 * PUB_KEY_SIZE + 1]; + for (int i = 0; i < (int)PUB_KEY_SIZE; ++i) { + snprintf(pk_hex + i*2, 3, "%02x", pk[i]); + } + lv_obj_t* pk_lbl = lv_label_create(body); + lv_label_set_long_mode(pk_lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_width(pk_lbl, lv_pct(100)); + lv_label_set_text(pk_lbl, pk_hex); + lv_obj_set_style_text_color(pk_lbl, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_text_font(pk_lbl, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_bg_color(pk_lbl, lv_color_hex(0x0A0B0C), LV_PART_MAIN); + lv_obj_set_style_bg_opa(pk_lbl, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_pad_all(pk_lbl, 4, LV_PART_MAIN); + lv_obj_set_style_radius(pk_lbl, 4, LV_PART_MAIN); + lv_obj_set_pos(pk_lbl, 2, y); + lv_obj_add_flag(pk_lbl, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(pk_lbl, copyLabelLongPressCb, LV_EVENT_LONG_PRESSED, + const_cast("pubkey")); + y += 50; + } + + // "Share QR" button — second entry point to the same popup as the Chats header. + { + lv_obj_t* sb = lv_btn_create(body); + lv_obj_set_size(sb, lv_pct(100),34); + lv_obj_set_pos(sb, 2, y); + styleButton(sb); + lv_obj_add_event_cb(sb, +[](lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + openShareMyContactPopup(); + }, LV_EVENT_CLICKED, nullptr); + lv_obj_t* sl = lv_label_create(sb); + lv_label_set_text(sl, LV_SYMBOL_IMAGE " Share QR"); + lv_obj_set_style_text_color(sl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(sl, &g_font_14, LV_PART_MAIN); + lv_obj_center(sl); + y += 42; + } + + // "Export settings" — write a MeshCore-app-compatible JSON backup (identity, + // radio/position, channels, contacts) to SD if a card is in, else internal + // flash. The file opens in the stock app / web client. + { + lv_obj_t* eb = lv_btn_create(body); + lv_obj_set_size(eb, lv_pct(100), 34); + lv_obj_set_pos(eb, 2, y); + styleButton(eb); + lv_obj_add_event_cb(eb, +[](lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + // Fixed, app-compatible name so the stock app / web client can read it back. + doExportBackupFile("meshcore-backup.json"); + }, LV_EVENT_CLICKED, nullptr); + lv_obj_t* el = lv_label_create(eb); + lv_label_set_text(el, TR("Export settings")); + lv_obj_set_style_text_color(el, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(el, &g_font_14, LV_PART_MAIN); + lv_obj_center(el); + y += 42; + } + + // "Import settings" — restore a MeshCore-app-compatible JSON backup from + // /meshcore-backup.json (SD if present, else internal). Replaces identity, + // channels and contacts, then reboots so radio settings take effect. + { + lv_obj_t* ib = lv_btn_create(body); + lv_obj_set_size(ib, lv_pct(100), 34); + lv_obj_set_pos(ib, 2, y); + styleButton(ib); + lv_obj_add_event_cb(ib, +[](lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + openBackupPicker(); + }, LV_EVENT_CLICKED, nullptr); + lv_obj_t* il = lv_label_create(ib); + lv_label_set_text(il, TR("Import settings")); + lv_obj_set_style_text_color(il, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(il, &g_font_14, LV_PART_MAIN); + lv_obj_center(il); + y += 42; + } +} + +static void pathHashModeChangedCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED || !g_lv.task) return; + uint8_t sel = (uint8_t)lv_dropdown_get_selected(lv_event_get_target(e)); + g_lv.task->setPathHashMode(sel); // existing setter: clamps to 0..2 + savePrefs + char m[40]; + snprintf(m, sizeof m, TR("Path hash: %u byte%s"), (unsigned)(sel + 1), sel ? "s" : ""); + g_lv.task->showAlert(m, 1400); +} + +// GPS serial baud picker (Device settings). Persisted in TouchPrefsStore and +// read back at GPS init (EnvironmentSensorManager) — the T-Deck Plus GPS runs +// at 38400, the older T-Deck v1.0 at 9600. Takes effect on reboot. +static const uint32_t kGpsBaudOpts[] = {9600, 19200, 38400, 57600, 115200}; +static const int kGpsBaudCount = (int)(sizeof(kGpsBaudOpts) / sizeof(kGpsBaudOpts[0])); +static void gpsBaudChangedCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + int sel = (int)lv_dropdown_get_selected(lv_event_get_target(e)); + if (sel < 0 || sel >= kGpsBaudCount) return; + touchPrefsSetGpsBaud(kGpsBaudOpts[sel]); + if (g_lv.task) { + char m[48]; + snprintf(m, sizeof m, "GPS %lu baud — reboot to apply", (unsigned long)kGpsBaudOpts[sel]); + g_lv.task->showAlert(m, 1800); + } +} + +// LVGL parents an open dropdown list to the screen layer and, when the button +// sits in the lower half of a page, flips the list to drop *upward* — sliding +// its top edge behind our global status bar (which lives on lv_layer_sys, above +// everything) so the first option(s) get clipped. Run after the list is open & +// positioned (post-CLICKED) to cap its height to the area below the status bar +// and nudge it back on-screen. Wire onto every settings dropdown. +static void clampDropdownListCb(lv_event_t* e) { + lv_obj_t* dd = lv_event_get_target(e); + lv_obj_t* list = lv_dropdown_get_list(dd); + if (!list || lv_obj_has_flag(list, LV_OBJ_FLAG_HIDDEN)) return; // closed/just-closed + const lv_coord_t ver = lv_disp_get_ver_res(nullptr); + const lv_coord_t avail = ver - STATUSBAR_H; // usable height below the status bar + lv_obj_update_layout(list); + if (lv_obj_get_height(list) > avail) lv_obj_set_height(list, avail); // too tall -> cap + let it scroll + lv_obj_update_layout(list); + const lv_coord_t h = lv_obj_get_height(list); + const lv_coord_t y1 = lv_obj_get_y(list); + if (y1 < STATUSBAR_H) lv_obj_set_y(list, STATUSBAR_H); // opened up into the status bar + else if (y1 + h > ver) lv_obj_set_y(list, ver - h); // hangs off the bottom edge +} + +static void buildRadioSettings() { + // No "Radio" group header — it just duplicates the sub-tab button name. + lv_obj_t* body = createSettingsModal("", SettingsModalKind::Radio); + NodePrefs* prefs = the_mesh.getNodePrefs(); + int y = 0; + auto mk_label = [&](const char* text) { + y += settingsRowLabel(body, y, 0, text, COLOR_SUB, nullptr, 0) + 2; + }; + auto mk_ta = [&](int w, int x, const char* ph, int max_len) -> lv_obj_t* { + lv_obj_t* ta = lv_textarea_create(body); + lv_obj_set_size(ta, w, 30); + lv_obj_set_pos(ta, x, y); + lv_textarea_set_one_line(ta, true); + lv_textarea_set_placeholder_text(ta, TR(ph)); + lv_textarea_set_max_length(ta, max_len); + attachSettingsTaEvents(ta); + return ta; + }; + + mk_label("Community preset (meshcomod web)"); + { + static char preset_opt_buf[1200]; + size_t o = 0; + int nw = snprintf(preset_opt_buf + o, sizeof(preset_opt_buf) - o, "Custom (manual)"); + if (nw > 0) o += static_cast(nw); + for (size_t i = 0; i < k_mesh_radio_preset_count && o + 2 < sizeof(preset_opt_buf); ++i) { + preset_opt_buf[o++] = '\n'; + nw = snprintf(preset_opt_buf + o, sizeof(preset_opt_buf) - o, "%s", k_mesh_radio_presets[i].label); + if (nw < 0) break; + o += static_cast(nw); + } + preset_opt_buf[sizeof(preset_opt_buf) - 1] = '\0'; + + g_set_modal.radio_preset_dd = lv_dropdown_create(body); + lv_obj_set_size(g_set_modal.radio_preset_dd, lv_pct(100),34); + lv_obj_set_pos(g_set_modal.radio_preset_dd, 2, y); + lv_dropdown_set_options(g_set_modal.radio_preset_dd, preset_opt_buf); + lv_obj_set_style_text_font(g_set_modal.radio_preset_dd, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_bg_color(g_set_modal.radio_preset_dd, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_text_color(g_set_modal.radio_preset_dd, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_border_color(g_set_modal.radio_preset_dd, lv_color_hex(0x18191A), LV_PART_MAIN); + /* Dropdown list (the popup once tapped) */ + lv_obj_t* preset_list = lv_dropdown_get_list(g_set_modal.radio_preset_dd); + lv_obj_set_style_bg_color(preset_list, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_text_color(preset_list, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(preset_list, &g_font_12, LV_PART_MAIN); + lv_obj_add_event_cb(g_set_modal.radio_preset_dd, radioPresetChangedCb, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_add_event_cb(g_set_modal.radio_preset_dd, clampDropdownListCb, LV_EVENT_CLICKED, nullptr); + y += 40; + } + + const lv_coord_t cw = s_settings_content_w; + mk_label("Frequency / bandwidth"); + { + const int g = 8, fw = (cw - g) / 2; // two equal fields filling the row + g_set_modal.freq_ta = mk_ta(fw, 0, "MHz", 15); + g_set_modal.bw_ta = mk_ta(fw, fw + g, "kHz", 10); + } + y += 36; + mk_label("SF / CR / TX / AF"); + { + const int g = 6, fw = (cw - 3 * g) / 4; // four equal fields filling the row + g_set_modal.sf_ta = mk_ta(fw, 0, "SF", 2); + g_set_modal.cr_ta = mk_ta(fw, fw + g, "CR", 2); + g_set_modal.tx_ta = mk_ta(fw, 2 * (fw + g), "TX", 4); + g_set_modal.airtime_ta = mk_ta(fw, 3 * (fw + g), "AF", 6); + } + y += 38; + + if (prefs) { + char buf[24]; + snprintf(buf, sizeof(buf), "%.3f", prefs->freq); lv_textarea_set_text(g_set_modal.freq_ta, buf); + snprintf(buf, sizeof(buf), "%.1f", prefs->bw); lv_textarea_set_text(g_set_modal.bw_ta, buf); + snprintf(buf, sizeof(buf), "%u", static_cast(prefs->sf)); lv_textarea_set_text(g_set_modal.sf_ta, buf); + snprintf(buf, sizeof(buf), "%u", static_cast(prefs->cr)); lv_textarea_set_text(g_set_modal.cr_ta, buf); + snprintf(buf, sizeof(buf), "%d", static_cast(prefs->tx_power_dbm)); lv_textarea_set_text(g_set_modal.tx_ta, buf); + snprintf(buf, sizeof(buf), "%.2f", prefs->airtime_factor); lv_textarea_set_text(g_set_modal.airtime_ta, buf); + } + + if (g_set_modal.radio_preset_dd) { + const int match = findMatchingMeshRadioPreset(prefs); + g_radio_preset_cb_silent = true; + lv_dropdown_set_selected(g_set_modal.radio_preset_dd, match < 0 ? 0 : static_cast(match + 1)); + g_radio_preset_cb_silent = false; + } + + // Multi-byte routing: how many bytes of each repeater's hash this node stamps + // into the path when it adverts / sends. 1 byte (legacy) collides in large + // regions; 2-3 bytes disambiguate. Applied immediately + persisted (no radio + // reconfig). All repeaters on a path must support it (MeshCore >= v1.14); + // older ones silently drop 2/3-byte packets. + mk_label("Multi-byte routing (path hash)"); + { + lv_obj_t* dd = lv_dropdown_create(body); + lv_obj_set_size(dd, lv_pct(100), 34); + lv_obj_set_pos(dd, 2, y); + lv_dropdown_set_options(dd, "1 byte (legacy)\n2 bytes\n3 bytes"); + lv_obj_set_style_text_font(dd, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_bg_color(dd, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_text_color(dd, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_border_color(dd, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_t* phlist = lv_dropdown_get_list(dd); + lv_obj_set_style_bg_color(phlist, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_text_color(phlist, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(phlist, &g_font_12, LV_PART_MAIN); + uint8_t phm = prefs ? prefs->path_hash_mode : 0; + if (phm > 2) phm = 0; + lv_dropdown_set_selected(dd, phm); + lv_obj_add_event_cb(dd, pathHashModeChangedCb, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_add_event_cb(dd, clampDropdownListCb, LV_EVENT_CLICKED, nullptr); + y += 40; + } + + // Region scope: tags outgoing floods with a region so repeaters that only + // re-flood their own region (region-scoped networks) still propagate them. + // Public "#hashtag" region -> key = SHA256("#name"); blank = unscoped (default). + mk_label("Region scope (#tag, blank = none)"); + g_set_modal.region_ta = mk_ta(cw, 0, "#region", TOUCH_REGION_SCOPE_MAXLEN - 1); + { + char rbuf[TOUCH_REGION_SCOPE_MAXLEN]; + if (touchPrefsGetRegionScope(rbuf, sizeof(rbuf)) > 0 && rbuf[0]) + lv_textarea_set_text(g_set_modal.region_ta, rbuf); + } + y += 38; + + lv_obj_t* b = lv_btn_create(body); + lv_obj_set_size(b, lv_pct(100),34); + lv_obj_set_pos(b, 2, y); + styleButton(b); + lv_obj_add_event_cb(b, saveRadioParamsCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l = lv_label_create(b); + lv_label_set_text(l, TR("Apply radio params")); + lv_obj_center(l); +} + +static void buildAutoAddSettings() { + lv_obj_t* body = createSettingsModal("Auto-add contacts", SettingsModalKind::AutoAdd); + NodePrefs* prefs = the_mesh.getNodePrefs(); + int y = 0; + auto mk_switch = [&](const char* text, lv_obj_t** out) { + int h = settingsRowLabel(body, y, 6, text, COLOR_SUB, nullptr, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); // flush to the card's right edge + lv_obj_add_event_cb(sw, autoAddSwitchCb, LV_EVENT_VALUE_CHANGED, nullptr); // auto-save on toggle + if (out) *out = sw; + y += LV_MAX(34, h + 12); + }; + mk_switch("Auto chat", &g_set_modal.auto_chat_sw); + mk_switch("Auto repeater", &g_set_modal.auto_rep_sw); + mk_switch("Auto room", &g_set_modal.auto_room_sw); + mk_switch("Auto sensor", &g_set_modal.auto_sensor_sw); + mk_switch("Overwrite oldest", &g_set_modal.auto_overwrite_sw); + g_set_modal.manual_add_sw = nullptr; // master "manual add" removed — per-type switches are authoritative now + + lv_obj_t* hops_l = lv_label_create(body); + lv_label_set_text(hops_l, TR("Max hops (0..64)")); + lv_obj_set_style_text_color(hops_l, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_pos(hops_l, 2, y + 6); + g_set_modal.max_hops_ta = lv_textarea_create(body); + lv_obj_set_size(g_set_modal.max_hops_ta, 80, 30); + lv_obj_set_pos(g_set_modal.max_hops_ta, 142, y); + lv_textarea_set_one_line(g_set_modal.max_hops_ta, true); + lv_textarea_set_max_length(g_set_modal.max_hops_ta, 3); + attachSettingsTaEvents(g_set_modal.max_hops_ta); + y += 38; + + if (prefs) { + // Old "add everything" mode (manual_add_contacts == 0) ignored the per-type + // bits, so reflect it by showing every Auto switch ON — they're authoritative + // now, so the user can genuinely turn a type off. + const bool add_all = (prefs->manual_add_contacts & 1) == 0; + if (add_all || (prefs->autoadd_config & AUTO_ADD_CHAT)) lv_obj_add_state(g_set_modal.auto_chat_sw, LV_STATE_CHECKED); + if (add_all || (prefs->autoadd_config & AUTO_ADD_REPEATER)) lv_obj_add_state(g_set_modal.auto_rep_sw, LV_STATE_CHECKED); + if (add_all || (prefs->autoadd_config & AUTO_ADD_ROOM_SERVER)) lv_obj_add_state(g_set_modal.auto_room_sw, LV_STATE_CHECKED); + if (add_all || (prefs->autoadd_config & AUTO_ADD_SENSOR)) lv_obj_add_state(g_set_modal.auto_sensor_sw, LV_STATE_CHECKED); + if (prefs->autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) lv_obj_add_state(g_set_modal.auto_overwrite_sw, LV_STATE_CHECKED); + char hops_buf[8]; + snprintf(hops_buf, sizeof(hops_buf), "%u", static_cast(prefs->autoadd_max_hops)); + lv_textarea_set_text(g_set_modal.max_hops_ta, hops_buf); + } + + lv_obj_t* b = lv_btn_create(body); + lv_obj_set_size(b, lv_pct(100),34); + lv_obj_set_pos(b, 2, y); + styleButton(b); + lv_obj_add_event_cb(b, saveAutoAddCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l = lv_label_create(b); + lv_label_set_text(l, TR("Save auto-add")); + lv_obj_center(l); +} + +static void buildExperimentalSettings() { + lv_obj_t* body = createSettingsModal("Experimental", SettingsModalKind::Experimental); + NodePrefs* prefs = the_mesh.getNodePrefs(); + int y = 0; + auto mk_switch = [&](const char* text, lv_obj_t** out) { + int h = settingsRowLabel(body, y, 6, text, COLOR_SUB, nullptr, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); // flush to the card's right edge + if (out) *out = sw; + y += LV_MAX(34, h + 12); + }; + + mk_switch("Multi ACKs", &g_set_modal.exp_multi_sw); + mk_switch("Client repeat", &g_set_modal.exp_repeat_sw); + mk_switch("RX boosted gain", &g_set_modal.exp_boost_sw); + mk_switch("Duty meter", &g_set_modal.exp_dc_sw); + + if (prefs) { + if (prefs->multi_acks) lv_obj_add_state(g_set_modal.exp_multi_sw, LV_STATE_CHECKED); + if (prefs->client_repeat) lv_obj_add_state(g_set_modal.exp_repeat_sw, LV_STATE_CHECKED); + if (prefs->rx_boosted_gain) lv_obj_add_state(g_set_modal.exp_boost_sw, LV_STATE_CHECKED); + } +#if defined(ESP32) + if (touchPrefsGetDutyMeterShown()) lv_obj_add_state(g_set_modal.exp_dc_sw, LV_STATE_CHECKED); +#endif + + lv_obj_t* b2 = lv_btn_create(body); + lv_obj_set_size(b2, lv_pct(100),34); + lv_obj_set_pos(b2, 2, y); + styleButton(b2); + lv_obj_add_event_cb(b2, saveExperimentalCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l2 = lv_label_create(b2); + lv_label_set_text(l2, TR("Save experimental")); + lv_obj_center(l2); +} + +// ---- Quick replies editor (Settings → Quick replies) ----------------------- + +static void saveQuickRepliesCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + kbMirrorSyncToReal(); + int n_saved = 0; +#if defined(ESP32) + for (int i = 0; i < TOUCH_QUICK_REPLY_COUNT; ++i) { + lv_obj_t* ta = g_set_modal.qr_tas[i]; + if (!ta) continue; + const char* text = lv_textarea_get_text(ta); + if (touchPrefsSetQuickReply(i, text ? text : "")) ++n_saved; + } +#endif + char msg[48]; + snprintf(msg, sizeof(msg), "Saved %d quick %s", n_saved, + n_saved == 1 ? "reply" : "replies"); + g_lv.task->showAlert(msg, 1100); +} + +static void buildQuickReplySettings() { + lv_obj_t* body = createSettingsModal("Quick replies", SettingsModalKind::QuickReply); + for (int i = 0; i < TOUCH_QUICK_REPLY_COUNT; ++i) g_set_modal.qr_tas[i] = nullptr; + + int y = 0; + // Compact form: one row per slot, with the slot index as a tiny prefix + // label so the user knows which macro they're editing. Single-line + // textareas to keep the modal short; macro text caps at 31 chars on the + // store side anyway, which fits one line. +#if defined(ESP32) + for (int i = 0; i < TOUCH_QUICK_REPLY_COUNT; ++i) { + char buf[TOUCH_QUICK_REPLY_MAXLEN]; + touchPrefsGetQuickReply(i, buf, sizeof(buf)); + + lv_obj_t* idxlbl = lv_label_create(body); + char idxs[4]; + snprintf(idxs, sizeof(idxs), "%d", i + 1); + lv_label_set_text(idxlbl, idxs); + lv_obj_set_style_text_color(idxlbl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(idxlbl, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(idxlbl, 2, y + 9); + + lv_obj_t* ta = lv_textarea_create(body); + lv_obj_set_size(ta, 200, 32); + lv_obj_set_pos(ta, 20, y); + styleCard(ta); + lv_textarea_set_one_line(ta, true); + lv_textarea_set_max_length(ta, TOUCH_QUICK_REPLY_MAXLEN - 1); + lv_textarea_set_text(ta, buf); + lv_obj_set_style_text_color(ta, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(ta, &g_font_12, LV_PART_MAIN); + // Bind tap -> keyboard mirror exactly like every other settings field. + // (The old composerFocusCb path is a chat-composer callback that no-ops + // for a null panel, so QR fields never bound the keyboard and couldn't be + // typed into; saveQuickRepliesCb already syncs the mirror back on save.) + attachSettingsTaEvents(ta); + g_set_modal.qr_tas[i] = ta; + y += 36; + } +#endif + + lv_obj_t* b = lv_btn_create(body); + lv_obj_set_size(b, lv_pct(100),34); + lv_obj_set_pos(b, 2, y); + styleButton(b); + lv_obj_add_event_cb(b, saveQuickRepliesCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l = lv_label_create(b); + lv_label_set_text(l, TR("Save quick replies")); + lv_obj_center(l); +} + +static void saveScreenTimeoutCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + if (!g_set_modal.screen_to_ta) return; + kbMirrorSyncToReal(); + int secs = atoi(lv_textarea_get_text(g_set_modal.screen_to_ta)); + if (secs < 0) secs = 0; + if (secs > 3600) secs = 3600; + if (g_lv.task->setScreenTimeoutSecs(static_cast(secs))) { + char msg[48]; + if (secs == 0) snprintf(msg, sizeof(msg), TR("Screen timeout: never")); + else snprintf(msg, sizeof(msg), TR("Screen timeout: %ds"), secs); + g_lv.task->showAlert(msg, 1200); + } else { + g_lv.task->showAlert(TR("Save failed"), 1200); + } +} + +// ===== System info modal ===================================================== +// Read-only diagnostic page: uptime, heap / PSRAM / flash usage, chip model +// + revision, last reset reason, MeshCore + Meshcomod versions. One-shot +// snapshot; no live updates while the modal is open. +#if defined(ESP32) +static const char* resetReasonString(esp_reset_reason_t r) { + switch (r) { + case ESP_RST_POWERON: return "Power on"; + case ESP_RST_EXT: return "External pin"; + case ESP_RST_SW: return "Software"; + case ESP_RST_PANIC: return "Panic / exception"; + case ESP_RST_INT_WDT: return "Int watchdog"; + case ESP_RST_TASK_WDT: return "Task watchdog"; + case ESP_RST_WDT: return "Other watchdog"; + case ESP_RST_DEEPSLEEP: return "Deep sleep wake"; + case ESP_RST_BROWNOUT: return "Brownout"; + case ESP_RST_SDIO: return "SDIO"; + case ESP_RST_UNKNOWN: default: return "Unknown"; + } +} +#endif + +// "Memory detail" popup — heap usage by region (the ESP32 shares one heap +// across all tasks, so there's no per-process split like a PC task manager; +// this shows internal DRAM vs PSRAM used/free + largest free block, which is +// what actually tells you where RAM is going and how fragmented it is). +static lv_obj_t* s_meminfo_root = nullptr; +static void closeMemInfo() { + if (s_meminfo_root) { lv_obj_del_async(s_meminfo_root); s_meminfo_root = nullptr; } +} +static void memInfoCloseCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + lv_indev_t* a = lv_indev_get_act(); if (a) lv_indev_wait_release(a); + closeMemInfo(); +} +static void openMemoryDetailCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + closeMemInfo(); +#if defined(ESP32) + auto kb = [](size_t v) -> unsigned { return (unsigned)((v + 512) / 1024); }; + const size_t i_tot = heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t i_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t i_min = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t i_lrg = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t s_tot = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + const size_t s_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + const size_t s_min = heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM); + const size_t s_lrg = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM); + const size_t dma_free = heap_caps_get_free_size(MALLOC_CAP_DMA); + char b[700]; int q = 0; + q += snprintf(b + q, sizeof(b) - q, + "Internal DRAM\n used %u / %u KB\n free %u KB (low %u)\n largest free %u KB\n\n", + kb(i_tot - i_free), kb(i_tot), kb(i_free), kb(i_min), kb(i_lrg)); + q += snprintf(b + q, sizeof(b) - q, + "PSRAM\n used %u / %u KB\n free %u KB (low %u)\n largest free %u KB\n\n", + kb(s_tot - s_free), kb(s_tot), kb(s_free), kb(s_min), kb(s_lrg)); + q += snprintf(b + q, sizeof(b) - q, "DMA-capable free: %u KB\n", kb(dma_free)); + q += snprintf(b + q, sizeof(b) - q, "UI loop stack free: %u B\n\n", + (unsigned)(uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t))); + q += snprintf(b + q, sizeof(b) - q, + "Note: the ESP32 shares one heap\nacross all tasks - no per-task\nsplit like a PC. 'largest free'\nvs 'free' shows fragmentation."); + + const lv_coord_t sw = lv_disp_get_hor_res(nullptr); + const lv_coord_t sh = lv_disp_get_ver_res(nullptr); + s_meminfo_root = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_meminfo_root); + lv_obj_set_size(s_meminfo_root, sw, sh - STATUSBAR_H); + lv_obj_set_pos(s_meminfo_root, 0, STATUSBAR_H); + lv_obj_set_style_bg_color(s_meminfo_root, lv_color_black(), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_meminfo_root, LV_OPA_70, LV_PART_MAIN); + lv_obj_clear_flag(s_meminfo_root, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_event_cb(s_meminfo_root, memInfoCloseCb, LV_EVENT_CLICKED, nullptr); + + lv_obj_t* card = lv_obj_create(s_meminfo_root); + lv_obj_remove_style_all(card); + lv_obj_set_size(card, sw - 24, (sh - STATUSBAR_H) - 24); + lv_obj_align(card, LV_ALIGN_CENTER, 0, 0); + styleSurface(card, COLOR_PANEL, 8); + lv_obj_set_style_border_color(card, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_set_style_border_width(card, 1, LV_PART_MAIN); + lv_obj_set_style_pad_all(card, 10, LV_PART_MAIN); + lv_obj_set_scroll_dir(card, LV_DIR_VER); + lv_obj_set_scrollbar_mode(card, LV_SCROLLBAR_MODE_AUTO); + addCloseXBadge(card, memInfoCloseCb); + + lv_obj_t* title = lv_label_create(card); + lv_label_set_text(title, TR("Memory detail")); + lv_obj_set_style_text_font(title, &g_font_14, LV_PART_MAIN); + lv_obj_set_style_text_color(title, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_pos(title, 0, 2); + + lv_obj_t* lbl = lv_label_create(card); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_width(lbl, sw - 24 - 20); + lv_obj_set_pos(lbl, 0, 28); + lv_obj_set_style_text_font(lbl, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_label_set_text(lbl, b); +#endif +} + +// Render the System-info text into buf. Re-callable so live values (uptime, +// free heap) update; the inline About page calls it once at build then +// refreshSysInfo() re-runs it ~1 Hz while that tab is visible. +static void sysInfoText(char* buf, size_t cap) { + int p = 0; +#if defined(ESP32) + const uint32_t up_s = (uint32_t)(esp_timer_get_time() / 1000000ULL); + const uint32_t up_d = up_s / 86400u; + const uint32_t up_h = (up_s % 86400u) / 3600u; + const uint32_t up_m = (up_s % 3600u) / 60u; + const uint32_t up_ss = up_s % 60u; + p += snprintf(buf + p, cap - p, + "Uptime\n %ud %02uh %02um %02us\n\n", + (unsigned)up_d, (unsigned)up_h, (unsigned)up_m, (unsigned)up_ss); + + esp_chip_info_t chip; + esp_chip_info(&chip); + const char* model = (chip.model == CHIP_ESP32S3) ? "ESP32-S3" + : (chip.model == CHIP_ESP32S2) ? "ESP32-S2" + : (chip.model == CHIP_ESP32) ? "ESP32" + : "ESP32-?"; + p += snprintf(buf + p, cap - p, + "Chip\n %s rev %d, %d core(s)\n features:%s%s%s\n\n", + model, (int)chip.revision, (int)chip.cores, + (chip.features & CHIP_FEATURE_BLE) ? " BLE" : "", + (chip.features & CHIP_FEATURE_WIFI_BGN) ? " WiFi" : "", + (chip.features & CHIP_FEATURE_BT) ? " BT" : ""); + + const size_t dram_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t dram_tot = heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t dram_min = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + const size_t psram_tot = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + p += snprintf(buf + p, cap - p, + "Memory\n DRAM free: %u KB / %u KB (min %u)\n PSRAM free: %u KB / %u KB\n\n", + (unsigned)(dram_free / 1024), + (unsigned)(dram_tot / 1024), + (unsigned)(dram_min / 1024), + (unsigned)(psram_free / 1024), + (unsigned)(psram_tot / 1024)); + + const uint32_t flash_chip_size = ESP.getFlashChipSize(); + const uint32_t sketch_size = ESP.getSketchSize(); + const uint32_t sketch_free = ESP.getFreeSketchSpace(); + p += snprintf(buf + p, cap - p, + "Flash\n chip: %u MB\n sketch: %u KB used\n app slot free: %u KB\n\n", + (unsigned)(flash_chip_size / (1024u * 1024u)), + (unsigned)(sketch_size / 1024u), + (unsigned)(sketch_free / 1024u)); + + p += snprintf(buf + p, cap - p, + "Last reset\n %s\n\n", resetReasonString(esp_reset_reason())); +#endif + p += snprintf(buf + p, cap - p, + "Build\n %s\n %s\n Meshcomod TOUCH\n", + FIRMWARE_VERSION, FIRMWARE_BUILD_DATE); + (void)p; +} + +// Re-render the System-info text in place while the About sub-tab is visible. +static void refreshSysInfo(unsigned long now) { + static unsigned long next = 0; + if ((long)(now - next) < 0) return; + next = now + 1000; + if (!s_sysinfo_lbl) return; + if (getActiveTab() != SETTINGS_TAB_INDEX) return; + if (s_settings_open_cat != CAT_ABOUT) return; // sysinfo lives on the About detail sheet + char buf[800]; + sysInfoText(buf, sizeof buf); + lv_label_set_text(s_sysinfo_lbl, buf); +} + +static void buildSystemInfoSettings() { + lv_obj_t* body = createSettingsModal("System info", SettingsModalKind::SystemInfo); + char buf[800]; + sysInfoText(buf, sizeof buf); + + lv_obj_t* lbl = lv_label_create(body); + s_sysinfo_lbl = lbl; + lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_width(lbl, lv_pct(100)); + lv_obj_set_pos(lbl, 2, 0); + lv_obj_set_style_text_font(lbl, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_label_set_text(lbl, buf); + + // "Memory detail" button below the info text (per-region heap breakdown). + lv_obj_update_layout(lbl); + lv_obj_t* membtn = lv_btn_create(body); + lv_obj_set_size(membtn, lv_pct(96), 34); + lv_obj_set_pos(membtn, 2, lv_obj_get_y(lbl) + lv_obj_get_height(lbl) + 8); + styleButton(membtn); + lv_obj_add_event_cb(membtn, openMemoryDetailCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* mbl = lv_label_create(membtn); + lv_label_set_text(mbl, TR("Memory detail")); + lv_obj_set_style_text_font(mbl, &g_font_14, LV_PART_MAIN); + lv_obj_center(mbl); +} + +// Map tile source: false = tile server + on-device cache, true = read tiles off the microSD. +// The toggle + its callback (mapOptTilesSdCb) live in the map options popup, further down. +static bool s_tiles_from_sd = false; + +// Distance units toggle (km <-> miles). Applies immediately — saves the +// pref and forces the contacts list to re-render its distance badges. +#if defined(HAS_TDECK_GT911) +// Store all data (identity/prefs/contacts/channels) on SD under /meshcomod vs +// internal SPIFFS. Read at boot before data loads, so it only takes effect on +// the next reboot. +static void useSdStorageToggleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED); +#if defined(ESP32) + touchPrefsSetUseSdStorage(on); +#endif + if (g_lv.task) g_lv.task->showAlert(on ? TR("Data -> SD card on reboot\n(card must be inserted)") + : TR("Data -> internal on reboot"), 1800); +} +#endif + +static void useMilesToggleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + const bool miles = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED); +#if defined(ESP32) + touchPrefsSetUseMiles(miles); +#endif + if (g_lv.task) g_lv.task->showAlert(miles ? TR("Distance: miles") : TR("Distance: km"), 900); + // The contacts cache now keys on the units flag, so this rebuilds the + // badges on the next refresh (and immediately if the list is visible). + refreshContactsList(); +} + +// Colourful chat bubbles toggle. On enable: the "taste the rainbow" easter egg. +// Bubbles recolour the next time a chat opens (this control lives on Settings). +static void colorfulBubblesToggleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED); +#if defined(ESP32) + touchPrefsSetColorfulBubbles(on); +#endif + if (g_lv.task) g_lv.task->showAlert(on ? TR("Taste the rainbow!") : TR("Chat bubbles: plain"), + on ? 1500 : 900); +} + +// Accent-popup picker on/off. Persisted + live: gates accentBoxMaybeShow() so +// the tap-to-pick accent box stops appearing as you type. Default ON. +static void accentPopupsToggleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED); + s_accent_popups = on; +#if defined(ESP32) + touchPrefsSetAccentPopups(on); +#endif + if (!on) accentBoxHide(); // dismiss any box already on screen + if (g_lv.task) g_lv.task->showAlert(on ? TR("Accent popups: on") : TR("Accent popups: off"), 1100); +} + +// One handler for every per-language switch in the multi-select list. The +// layout id is stashed in the switch's user_data. Flipping a switch updates the +// enabled-mask (persisted + live) and refreshes the on-screen keyboard, which +// snaps back to English if the layout that was just switched off was active. +static void kbLayoutSwitchCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + lv_obj_t* sw = lv_event_get_target(e); + int id = (int)(intptr_t)lv_obj_get_user_data(sw); + if (id <= 0 || id >= KEYBOARD_LAYOUT_COUNT) return; + bool on = lv_obj_has_state(sw, LV_STATE_CHECKED); +#if defined(ESP32) + uint16_t mask = touchPrefsGetEnabledLayouts(); + if (on) mask |= (uint16_t)(1u << id); + else mask &= (uint16_t)~(1u << id); + touchPrefsSetEnabledLayouts(mask); + keyboardLayoutsSetEnabledMask(mask); + // SetEnabledMask snaps the cached layout to EN if it was just disabled; + // re-apply it so the keyboard widget reflects the (possibly changed) layout. + keyboardLayoutsApply(g_lv.keyboard, keyboardLayoutsGetCurrent()); + touchPrefsSetKeyboardLayout(static_cast(keyboardLayoutsGetCurrent())); +#endif + if (g_lv.task) { + char buf[40]; + snprintf(buf, sizeof(buf), "%s: %s", + keyboardLayoutName(static_cast(id)), on ? "on" : "off"); + g_lv.task->showAlert(buf, 800); + } +} + +// Screen orientation: the persistent base rotation, applied at boot. Tapping +// cycles Portrait -> Landscape -> Landscape (flipped) and reboots so the whole +// UI rebuilds at the chosen resolution (rebootDevice() saves chat history +// synchronously first, so nothing is lost). +static const char* uiRotationLabel() { +#if defined(ESP32) + // Portrait (0) vs Landscape (anything else). The button toggles between the + // two so a single tap always returns to portrait from any landscape. + return (touchPrefsGetUiRotation() == LV_DISP_ROT_NONE) ? "Screen: Portrait" + : "Screen: Landscape"; +#else + return "Screen: Portrait"; +#endif +} + +static void rotateScreenCycleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; +#if defined(ESP32) + // Simple toggle: portrait <-> landscape (90 degrees). One tap each way. + const uint8_t cur = touchPrefsGetUiRotation(); + const uint8_t next = (cur == LV_DISP_ROT_NONE) ? (uint8_t)LV_DISP_ROT_90 + : (uint8_t)LV_DISP_ROT_NONE; + touchPrefsSetUiRotation(next); +#endif + if (g_lv.task) { + g_lv.task->showAlert(TR("Rotating\xe2\x80\xa6 rebooting"), 600); + g_lv.task->rebootDevice(); + } +} + +// ---- Manual clock offset (Device modal): nudge local time +/- whole hours ---- +// Applied on top of the automatic (NTP / companion / mesh) time, purely at the +// display layer via the POSIX TZ; the RTC and mesh timestamps stay UTC. +static lv_obj_t* s_time_offset_lbl = nullptr; // current offset readout (modal is a singleton) + +static void applyTimeOffsetNow() { + char tz[48]; + touchPrefsBuildLocalTz(tz, sizeof tz); // rebuilds from the saved offset + setenv("TZ", tz, 1); + tzset(); // every later localtime_r() reflects it +} +static void timeOffsetLabelRefresh() { + if (!s_time_offset_lbl) return; + int off = touchPrefsGetTimeOffsetHours(); + char b[16]; + if (off == 0) snprintf(b, sizeof b, "0 h"); + else snprintf(b, sizeof b, "%+d h", off); + lv_label_set_text(s_time_offset_lbl, b); +} +static void timeOffsetStep(int delta) { + int off = touchPrefsGetTimeOffsetHours() + delta; + if (off < -23) off = -23; + if (off > 23) off = 23; + touchPrefsSetTimeOffsetHours(off); + applyTimeOffsetNow(); + timeOffsetLabelRefresh(); + if (g_lv.task) { + char m[28]; snprintf(m, sizeof m, TR("Time offset: %+d h"), off); + g_lv.task->showAlert(m, 900); + } +} +static void timeOffsetMinusCb(lv_event_t* e) { if (lv_event_get_code(e) == LV_EVENT_CLICKED) timeOffsetStep(-1); } +static void timeOffsetPlusCb(lv_event_t* e) { if (lv_event_get_code(e) == LV_EVENT_CLICKED) timeOffsetStep(+1); } + +#if defined(HAS_TDECK_GT911) +// Lock-screen settings live in the Device modal but the picker implementation +// needs the SD-mount state (declared further down), so split: the small bits +// the modal body uses directly are here; the picker is defined below. +static lv_obj_t* s_lockwall_btn_lbl = nullptr; // Device-modal button caption (current wallpaper) +static const uint32_t kLockColors[] = { + 0xE6F2FF, 0xFFFFFF, 0x5BC0FF, 0x5BE5A0, 0xFFC857, 0xFF6B6B, 0xB39DFF, 0x9AA7B4 +}; +// Human-readable name from a stored pref path ("sd:/lock/x.jpg" -> "SD: x.jpg"). +static void lockwallDisplayName(const char* path, char* out, int cap) { + if (!out || cap <= 0) return; + if (!path || !path[0]) { snprintf(out, cap, "(default)"); return; } + const bool sd = !strncmp(path, "sd:", 3); + const char* p = sd ? path + 3 : path; + const char* base = strrchr(p, '/'); base = base ? base + 1 : p; + snprintf(out, cap, "%s%s", sd ? "SD: " : "", base); +} +static void openLockWallPickerCb(lv_event_t* e); // defined with the picker, below +static void lockColorChosenCb(lv_event_t* e); +#endif + +static void calibrateBatteryCb(lv_event_t* e); // defined with the battery helpers below +static void buildDeviceSettings(int sec) { + // One detail page per section: each block below is gated to its DSEC_* section + // (skipped blocks don't advance y, so every page lays out from the top). + lv_obj_t* body = createSettingsModal("", SettingsModalKind::Device); + int y = 0; + + if (sec == DSEC_SYSTEM) { // --- GPS --- + lv_obj_t* b_gps = lv_btn_create(body); + lv_obj_set_size(b_gps, lv_pct(100),34); + lv_obj_set_pos(b_gps, 2, y); + styleButton(b_gps); + lv_obj_add_event_cb(b_gps, toggleGpsCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lg = lv_label_create(b_gps); + lv_label_set_text(lg, TR("Toggle GPS")); + lv_obj_center(lg); + y += 38; + + // Live GPS fix status — refreshed each tick while this modal is open. + g_set_modal.gps_status = lv_label_create(body); + lv_label_set_long_mode(g_set_modal.gps_status, LV_LABEL_LONG_WRAP); + lv_obj_set_width(g_set_modal.gps_status, lv_pct(100)); + lv_obj_set_style_text_color(g_set_modal.gps_status, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(g_set_modal.gps_status, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(g_set_modal.gps_status, 4, y); + lv_label_set_text(g_set_modal.gps_status, gpsStatusStr()); + y += 22; + + // GPS serial baud. T-Deck Plus = 38400, T-Deck v1.0 = 9600. Persisted to NVS + // and read at GPS init; reboot to apply. Also settable via `set gps.baud`. + { + lv_obj_t* bl = lv_label_create(body); + lv_obj_set_style_text_color(bl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(bl, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(bl, 4, y); + lv_label_set_text(bl, TR("GPS serial baud")); + y += 18; + + lv_obj_t* dd = lv_dropdown_create(body); + lv_obj_set_size(dd, lv_pct(100), 34); + lv_obj_set_pos(dd, 2, y); + lv_dropdown_set_options(dd, "9600\n19200\n38400\n57600\n115200"); + lv_obj_set_style_text_font(dd, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_bg_color(dd, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_text_color(dd, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_border_color(dd, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_t* gblist = lv_dropdown_get_list(dd); + lv_obj_set_style_bg_color(gblist, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); + lv_obj_set_style_text_color(gblist, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(gblist, &g_font_12, LV_PART_MAIN); +#ifdef GPS_BAUD_RATE + uint32_t cur = touchPrefsGetGpsBaud(GPS_BAUD_RATE); +#else + uint32_t cur = touchPrefsGetGpsBaud(9600); +#endif + uint16_t match = 0; + for (int i = 0; i < kGpsBaudCount; i++) if (kGpsBaudOpts[i] == cur) { match = (uint16_t)i; break; } + lv_dropdown_set_selected(dd, match); + lv_obj_add_event_cb(dd, gpsBaudChangedCb, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_add_event_cb(dd, clampDropdownListCb, LV_EVENT_CLICKED, nullptr); + y += 40; + } + + } + + if (sec == DSEC_SOUND) { // --- Sound --- + // Sound toggle — T-Deck I2S speaker or Heltec V4 expansion-kit piezo buzzer. +#if defined(HAS_UI_SOUND) + lv_obj_t* b_bz = lv_btn_create(body); + lv_obj_set_size(b_bz, lv_pct(100),34); + lv_obj_set_pos(b_bz, 2, y); + styleButton(b_bz); + lv_obj_add_event_cb(b_bz, toggleBuzzerCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lb = lv_label_create(b_bz); + lv_label_set_text_fmt(lb, TR("Sound: %s"), g_lv.task && !g_lv.task->isBuzzerQuiet() ? TR("on") : TR("off")); + lv_obj_center(lb); + y += 40; +#endif + + } + + if (sec == DSEC_SYSTEM) { // --- Clock + advert --- + lv_obj_t* b_time = lv_btn_create(body); + lv_obj_set_size(b_time, lv_pct(100),34); + lv_obj_set_pos(b_time, 2, y); + styleButton(b_time); + lv_obj_add_event_cb(b_time, syncClockCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l_time = lv_label_create(b_time); + lv_label_set_text(l_time, TR("Sync clock from system")); + lv_obj_center(l_time); + y += 40; + + lv_obj_t* b_adv = lv_btn_create(body); + lv_obj_set_size(b_adv, lv_pct(100),34); + lv_obj_set_pos(b_adv, 2, y); + styleButton(b_adv); + lv_obj_add_event_cb(b_adv, advertNowCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l_adv = lv_label_create(b_adv); + lv_label_set_text(l_adv, TR("Send advert now")); + lv_obj_center(l_adv); + y += 40; + + } + + if (sec == DSEC_DISPLAY) { // --- Display --- + /* Screen timeout (seconds, 0 = never). Persists in NVS via TouchPrefsStore. */ + { + y += settingsRowLabel(body, y, 0, "Screen timeout (s, 0 = never)", COLOR_SUB, &g_font_12, 0) + 2; + g_set_modal.screen_to_ta = lv_textarea_create(body); + lv_obj_set_size(g_set_modal.screen_to_ta, 100, 30); + lv_obj_set_pos(g_set_modal.screen_to_ta, 2, y); + lv_textarea_set_one_line(g_set_modal.screen_to_ta, true); + lv_textarea_set_max_length(g_set_modal.screen_to_ta, 4); + attachSettingsTaEvents(g_set_modal.screen_to_ta); + if (g_lv.task) { + char buf[8]; + snprintf(buf, sizeof(buf), "%u", (unsigned)g_lv.task->getScreenTimeoutSecs()); + lv_textarea_set_text(g_set_modal.screen_to_ta, buf); + } + lv_obj_t* b_save_to = lv_btn_create(body); + lv_obj_set_size(b_save_to, 110, 30); + lv_obj_set_pos(b_save_to, 110, y); + styleButton(b_save_to); + lv_obj_add_event_cb(b_save_to, saveScreenTimeoutCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* ls = lv_label_create(b_save_to); + lv_label_set_text(ls, TR("Save")); + lv_obj_center(ls); + y += 38; + } + + /* Distance units: OFF = km (default), ON = miles. Applies immediately. */ + { + int h = settingsRowLabel(body, y, 6, "Distance in miles", COLOR_SUB, nullptr, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); // flush to the card's right edge +#if defined(ESP32) + if (touchPrefsGetUseMiles()) lv_obj_add_state(sw, LV_STATE_CHECKED); +#endif + lv_obj_add_event_cb(sw, useMilesToggleCb, LV_EVENT_VALUE_CHANGED, nullptr); + y += LV_MAX(40, h + 12); + } + + /* Colourful chat bubbles: colour every bubble + sender name by a hash of the + sender's name (same name -> same colour). "Taste the rainbow" on enable. */ + { + int h = settingsRowLabel(body, y, 6, "Colourful chat bubbles", COLOR_SUB, nullptr, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); +#if defined(ESP32) + if (touchPrefsGetColorfulBubbles()) lv_obj_add_state(sw, LV_STATE_CHECKED); +#endif + lv_obj_add_event_cb(sw, colorfulBubblesToggleCb, LV_EVENT_VALUE_CHANGED, nullptr); + y += LV_MAX(40, h + 12); + } + + /* Theme colour (UI accent): opens a colour-wheel + hex picker. The chosen + colour is clamped dark enough that off-white button text stays readable. */ + { + y += settingsRowLabel(body, y, 0, "Theme colour", COLOR_SUB, &g_font_12, 0) + 4; + lv_obj_t* b = lv_btn_create(body); + lv_obj_set_size(b, 150, 32); + lv_obj_set_pos(b, 2, y); + styleButton(b); + lv_obj_add_event_cb(b, openAccentPickerCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* bl = lv_label_create(b); + lv_label_set_text(bl, TR("Pick colour")); + lv_obj_center(bl); + lv_obj_t* swatch = lv_obj_create(body); + lv_obj_remove_style_all(swatch); + lv_obj_set_size(swatch, 30, 30); + lv_obj_set_pos(swatch, 162, y + 1); + lv_obj_set_style_radius(swatch, 6, LV_PART_MAIN); + lv_obj_set_style_bg_color(swatch, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); + lv_obj_set_style_bg_opa(swatch, LV_OPA_COVER, LV_PART_MAIN); + y += 40; + } + + } + + if (sec == DSEC_SYSTEM) { // --- Storage (SD) --- +#if defined(HAS_TDECK_GT911) + /* Store all data (identity/prefs/contacts/channels) on the SD card under + /meshcomod instead of internal flash — for running under Launcher, or just + to keep everything on a card. Read at boot, so it applies after a reboot. */ + { + int h = settingsRowLabel(body, y, 6, "Store data on SD (reboot)", COLOR_SUB, nullptr, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); +#if defined(ESP32) + if (touchPrefsGetUseSdStorage()) lv_obj_add_state(sw, LV_STATE_CHECKED); +#endif + lv_obj_add_event_cb(sw, useSdStorageToggleCb, LV_EVENT_VALUE_CHANGED, nullptr); + y += LV_MAX(40, h + 12); + } +#endif + + } + + if (sec == DSEC_KEYBOARD) { // --- Keyboard --- + /* Secondary keyboards (multi-select). Switch on any layouts you want in the + rotation; a double-tap of SPACE on the physical keyboard cycles + English -> each enabled layout -> back. The active layout is remembered + across reboots. */ + { + y += settingsRowLabel(body, y, 0, "Secondary keyboards", COLOR_SUB, &g_font_12, 0) + 2; + +#if defined(HAS_TDECK_KEYBOARD) + const char* kb_cycle_hint = "double-tap SPACE cycles through the ones you enable"; +#else + const char* kb_cycle_hint = "tap the language key (e.g. EN) on the keyboard to cycle the ones you enable"; +#endif + y += settingsRowLabel(body, y, 0, kb_cycle_hint, COLOR_SUB, &g_font_12, 0) + 2; + +#if defined(ESP32) + uint16_t en_mask = touchPrefsGetEnabledLayouts(); +#else + uint16_t en_mask = 0; +#endif + /* One row per non-English layout; index id-1 into the display names. + Keep in KeyboardLayoutId order (BG=1 .. AR=6). */ + static const char* k_kb_disp[] = { + "Bulgarian", "Russian", "Ukrainian", "Serbian", "Greek", "Arabic (experimental)" + }; + for (int id = 1; id < KEYBOARD_LAYOUT_COUNT; ++id) { + int h = settingsRowLabel(body, y, 4, k_kb_disp[id - 1], COLOR_TEXT, &g_font_12, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); + if (en_mask & (1u << id)) lv_obj_add_state(sw, LV_STATE_CHECKED); + lv_obj_set_user_data(sw, (void*)(intptr_t)id); + lv_obj_add_event_cb(sw, kbLayoutSwitchCb, LV_EVENT_VALUE_CHANGED, nullptr); + y += LV_MAX(34, h + 10); + } + } + + /* Accent popups. Typing a Latin letter that has accented variants pops up a + tap-to-pick box; turn this off for plain typing. Default on. */ + { + int h = settingsRowLabel(body, y, 4, "Accent popups", COLOR_TEXT, &g_font_12, 56); + lv_obj_t* sw = lv_switch_create(body); + lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y); +#if defined(ESP32) + if (touchPrefsGetAccentPopups()) lv_obj_add_state(sw, LV_STATE_CHECKED); +#else + if (s_accent_popups) lv_obj_add_state(sw, LV_STATE_CHECKED); +#endif + lv_obj_add_event_cb(sw, accentPopupsToggleCb, LV_EVENT_VALUE_CHANGED, nullptr); + y += LV_MAX(34, h + 10); + + y += settingsRowLabel(body, y, 0, "pick accented letters as you type; off = plain typing", + COLOR_SUB, &g_font_12, 0) + 2; + } + + } + + if (sec == DSEC_DISPLAY) { // --- Orientation (display) --- + /* Screen orientation. Cycles Portrait -> Landscape -> Landscape (flipped); + applied at boot, so tapping reboots the device. */ + { + y += settingsRowLabel(body, y, 0, "Orientation (tap to rotate, reboots)", COLOR_SUB, &g_font_12, 0) + 2; + lv_obj_t* b_rot = lv_btn_create(body); + lv_obj_set_size(b_rot, lv_pct(100),34); + lv_obj_set_pos(b_rot, 2, y); + styleButton(b_rot); + lv_obj_add_event_cb(b_rot, rotateScreenCycleCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lr = lv_label_create(b_rot); + lv_label_set_text(lr, uiRotationLabel()); + lv_obj_center(lr); + y += 42; + } + + } + + if (sec == DSEC_LOCK) { // --- Lock screen --- +#if defined(HAS_TDECK_GT911) + /* Lock screen: pick the wallpaper (internal /lock/ or SD) and the colour of + the clock + lock text drawn over it. */ + { + y += settingsRowLabel(body, y, 0, "Lock screen wallpaper", COLOR_SUB, &g_font_12, 0) + 2; + lv_obj_t* b_wall = lv_btn_create(body); + lv_obj_set_size(b_wall, lv_pct(100), 34); + lv_obj_set_pos(b_wall, 2, y); + styleButton(b_wall); + lv_obj_add_event_cb(b_wall, openLockWallPickerCb, LV_EVENT_CLICKED, nullptr); + s_lockwall_btn_lbl = lv_label_create(b_wall); + { + char cur[TOUCH_LOCK_WALLPAPER_MAXLEN], disp[64]; + touchPrefsGetLockWallpaper(cur, sizeof cur); + lockwallDisplayName(cur, disp, sizeof disp); + lv_label_set_text(s_lockwall_btn_lbl, disp); + } + lv_label_set_long_mode(s_lockwall_btn_lbl, LV_LABEL_LONG_DOT); + lv_obj_set_width(s_lockwall_btn_lbl, lv_pct(92)); + lv_obj_center(s_lockwall_btn_lbl); + y += 40; + + y += settingsRowLabel(body, y, 0, "Lock text colour", COLOR_SUB, &g_font_12, 0) + 2; + const int ncol = (int)(sizeof(kLockColors) / sizeof(kLockColors[0])); + const uint32_t curcol = touchPrefsGetLockTextColor(); + const int swz = 22, gap = 3; + for (int i = 0; i < ncol; ++i) { + lv_obj_t* sb = lv_btn_create(body); + lv_obj_set_size(sb, swz, swz); + lv_obj_set_pos(sb, 2 + i * (swz + gap), y); + lv_obj_set_style_radius(sb, 5, LV_PART_MAIN); + lv_obj_set_style_bg_color(sb, lv_color_hex(kLockColors[i]), LV_PART_MAIN); + lv_obj_set_style_bg_opa(sb, LV_OPA_COVER, LV_PART_MAIN); + // Highlight the current colour with a white ring. + const bool sel = (kLockColors[i] == curcol); + lv_obj_set_style_border_width(sb, sel ? 2 : 1, LV_PART_MAIN); + lv_obj_set_style_border_color(sb, lv_color_hex(sel ? 0xFFFFFF : 0x18191A), LV_PART_MAIN); + lv_obj_add_event_cb(sb, lockColorChosenCb, LV_EVENT_CLICKED, (void*)(uintptr_t)kLockColors[i]); + } + y += swz + 10; + } +#endif + + } + + if (sec == DSEC_SYSTEM) { // --- Time / actions / live info --- + /* Time offset: nudge the displayed clock +/- whole hours on top of the + automatic (NTP / companion / mesh) time. Display-only. Both boards. */ + { + y += settingsRowLabel(body, y, 0, "Time offset (vs automatic)", COLOR_SUB, &g_font_12, 0) + 2; + + lv_obj_t* bminus = lv_btn_create(body); + lv_obj_set_size(bminus, 50, 34); + lv_obj_set_pos(bminus, 2, y); + styleButton(bminus); + lv_obj_add_event_cb(bminus, timeOffsetMinusCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lm = lv_label_create(bminus); lv_label_set_text(lm, TR("-1 h")); lv_obj_center(lm); + + s_time_offset_lbl = lv_label_create(body); + lv_obj_set_width(s_time_offset_lbl, 100); + lv_obj_set_style_text_align(s_time_offset_lbl, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); + lv_obj_set_style_text_color(s_time_offset_lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(s_time_offset_lbl, &g_font_16, LV_PART_MAIN); + lv_obj_set_pos(s_time_offset_lbl, 58, y + 9); + timeOffsetLabelRefresh(); + + lv_obj_t* bplus = lv_btn_create(body); + lv_obj_set_size(bplus, 50, 34); + lv_obj_set_pos(bplus, 168, y); + styleButton(bplus); + lv_obj_add_event_cb(bplus, timeOffsetPlusCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lp = lv_label_create(bplus); lv_label_set_text(lp, TR("+1 h")); lv_obj_center(lp); + y += 42; + } + + // Re-run the first-boot setup flow (name / region / Wi-Fi) on demand. + lv_obj_t* b_setup = lv_btn_create(body); + lv_obj_set_size(b_setup, lv_pct(100), 34); + lv_obj_set_pos(b_setup, 2, y); + styleButton(b_setup); + lv_obj_add_event_cb(b_setup, setupRerunCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l_setup = lv_label_create(b_setup); + lv_label_set_text_fmt(l_setup, LV_SYMBOL_REFRESH " %s", TR("Run setup again")); + lv_obj_center(l_setup); + y += 42; + + lv_obj_t* b_reboot = lv_btn_create(body); + lv_obj_set_size(b_reboot, lv_pct(100),34); + lv_obj_set_pos(b_reboot, 2, y); + styleButton(b_reboot); + lv_obj_set_style_bg_color(b_reboot, lv_color_hex(0xC44B55), LV_PART_MAIN); + lv_obj_set_style_bg_color(b_reboot, lv_color_hex(0xA13F47), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_text_color(b_reboot, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_add_event_cb(b_reboot, rebootCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* l_reboot = lv_label_create(b_reboot); + lv_label_set_text(l_reboot, TR("Reboot device")); + lv_obj_center(l_reboot); + y += 42; + + // (Reboot-to-recovery button removed — the on-device recovery is retired.) + y += 4; + + // Calibrate battery: capture the current voltage as 100% (for custom packs / + // builds whose full voltage isn't 4.2 V). Tap = set 100%; long-press = reset. + { + lv_obj_t* b_cal = lv_btn_create(body); + lv_obj_set_size(b_cal, lv_pct(100), 34); + lv_obj_set_pos(b_cal, 2, y); + styleButton(b_cal); + lv_obj_set_style_bg_color(b_cal, lv_color_hex(0x2F6B57), LV_PART_MAIN); + lv_obj_set_style_bg_color(b_cal, lv_color_hex(0x244F41), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_text_color(b_cal, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_add_event_cb(b_cal, calibrateBatteryCb, LV_EVENT_SHORT_CLICKED, nullptr); + lv_obj_add_event_cb(b_cal, calibrateBatteryCb, LV_EVENT_LONG_PRESSED, nullptr); + lv_obj_t* l_cal = lv_label_create(b_cal); + lv_label_set_text_fmt(l_cal, LV_SYMBOL_BATTERY_FULL " %s", TR("Calibrate battery (full = 100%)")); + lv_obj_center(l_cal); + y += 46; + } + + // ----- Live info panel (below action buttons) ----- + // Mirrors the web client's "Device (live)" block: firmware, model, public + // key prefix, contacts/channels counts, battery, current device time. These + // are snapshot-at-open — re-open the Device modal to refresh. + { + lv_obj_t* sep = lv_obj_create(body); + lv_obj_set_size(sep, lv_pct(100),1); + lv_obj_set_pos(sep, 2, y); + lv_obj_set_style_bg_color(sep, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_set_style_border_width(sep, 0, LV_PART_MAIN); + lv_obj_set_style_pad_all(sep, 0, LV_PART_MAIN); + lv_obj_clear_flag(sep, LV_OBJ_FLAG_SCROLLABLE); + y += 6; + } + + auto mk_info = [&](const char* label, const char* value) { + lv_obj_t* row = lv_label_create(body); + char line[96]; + snprintf(line, sizeof(line), "%s %s", label, value); + lv_label_set_text(row, line); + /* Constrain height to one line so LONG_DOT actually ellipsizes instead of + * wrapping — long firmware strings were overlapping the next row. */ + lv_obj_set_size(row, lv_pct(100),14); + lv_label_set_long_mode(row, LV_LABEL_LONG_DOT); + lv_obj_set_style_text_color(row, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(row, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(row, 2, y); + y += 16; + }; + { + char buf[96]; + // Firmware + snprintf(buf, sizeof(buf), "%s · %s", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE); + mk_info(TR("Firmware:"), buf); + // Device model (build-time constant for this variant) +#if defined(HELTEC_LORA_V4_TFT) + mk_info(TR("Model:"), "Heltec LoRa32 V4 TFT (touch)"); +#else + mk_info(TR("Model:"), "Heltec LoRa32 V4"); +#endif + // Public key prefix (first 8 bytes = 16 hex) + { + const uint8_t* pk = the_mesh.getSelfPubKey(); + if (pk) { + char hex[20]; + mesh::Utils::toHex(hex, pk, 8); + hex[16] = '\0'; + snprintf(buf, sizeof(buf), "%s…", hex); + mk_info(TR("Public key:"), buf); + } else { + mk_info(TR("Public key:"), "—"); + } + } + // Contacts / Channels with max limits +#ifdef MAX_CONTACTS + snprintf(buf, sizeof(buf), "%d / %d", the_mesh.getNumContacts(), (int)MAX_CONTACTS); +#else + snprintf(buf, sizeof(buf), "%d", the_mesh.getNumContacts()); +#endif + mk_info(TR("Contacts:"), buf); +#ifdef MAX_GROUP_CHANNELS + snprintf(buf, sizeof(buf), "%d / %d", the_mesh.getNumChannels(), (int)MAX_GROUP_CHANNELS); +#else + snprintf(buf, sizeof(buf), "%d", the_mesh.getNumChannels()); +#endif + mk_info(TR("Channels:"), buf); + // Battery + if (g_lv.task) { + uint16_t mv = g_lv.task->getBattMilliVolts(); + if (mv > 0) { + snprintf(buf, sizeof(buf), "%u mV", (unsigned)mv); + } else { + snprintf(buf, sizeof(buf), "—"); + } + mk_info(TR("Battery:"), buf); + } + // Device time (formatted as YYYY-MM-DD HH:MM in local timezone) + { + mesh::RTCClock* rtc = the_mesh.getRTCClock(); + uint32_t now = rtc ? rtc->getCurrentTime() : 0; + if (now > 0) { + time_t t = (time_t)now; + struct tm tmv; + /* RTC stores UTC; show user-local time using TZ set via configTzTime + * in main.cpp (CET-1CEST,M3.5.0,M10.5.0/3). */ + localtime_r(&t, &tmv); + char ts[40]; + snprintf(ts, sizeof(ts), "%04d-%02d-%02d %02d:%02d %s", + tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday, + tmv.tm_hour, tmv.tm_min, + tmv.tm_isdst > 0 ? "CEST" : "CET"); + mk_info(TR("Device time:"), ts); + } else { + mk_info(TR("Device time:"), "not set"); + } + } + } + y += 6; + + lv_obj_t* hint = lv_label_create(body); + lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP); + lv_obj_set_width(hint, 216); + lv_obj_set_pos(hint, 2, y); + lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(hint, &g_font_12, LV_PART_MAIN); + lv_label_set_text(hint, TR("Diagnostics are on the About page.")); + } +} + +#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) +static void trimWifiField(char* s) { + if (!s) return; + size_t n = strlen(s); + while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t')) s[--n] = '\0'; + char* p = s; + while (*p == ' ' || *p == '\t') ++p; + if (p != s) memmove(s, p, strlen(p) + 1); +} + +static void saveTransportWifiCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + if (!g_set_modal.wifi_ssid_ta || !g_set_modal.wifi_pwd_ta || !g_set_modal.wifi_sw) return; + kbMirrorSyncToReal(); + + char ssid[WIFI_CONFIG_SSID_MAX]; + char pwd[WIFI_CONFIG_PWD_MAX]; + strncpy(ssid, lv_textarea_get_text(g_set_modal.wifi_ssid_ta), sizeof(ssid) - 1); + ssid[sizeof(ssid) - 1] = '\0'; + strncpy(pwd, lv_textarea_get_text(g_set_modal.wifi_pwd_ta), sizeof(pwd) - 1); + pwd[sizeof(pwd) - 1] = '\0'; + trimWifiField(ssid); + trimWifiField(pwd); + + if (!wifiConfigSetSsid(ssid)) { + g_lv.task->showAlert(TR("SSID too long"), 1400); + return; + } + if (!wifiConfigSetPwd(pwd)) { + g_lv.task->showAlert(TR("Password too long"), 1400); + return; + } + + const bool radio_on = lv_obj_has_state(g_set_modal.wifi_sw, LV_STATE_CHECKED); + wifiConfigSetRadioEnabled(radio_on); + if (radio_on) { + if (!wifiConfigHasRuntime() || ssid[0] == '\0') { + g_lv.task->showAlert(TR("Set an SSID first"), 1400); + return; + } + wifiConfigRequestApply(); + g_lv.task->showAlert(TR("Wi-Fi saved, reconnecting"), 1400); + } else { + wifiConfigRequestApply(); + g_lv.task->showAlert(TR("Wi-Fi saved (radio off)"), 1400); + } + refreshStatusLabels(); +} +#endif + +// ----- Confirm popup ----- +// Small floating modal with Cancel + Confirm buttons. Used by the Wi-Fi and +// Bluetooth pages so the user doesn't need to scroll a long inline hint to +// reach the Save button; the explanation appears on click instead. +namespace { +typedef void (*SimpleCb)(); +SimpleCb s_confirm_cb = nullptr; +lv_obj_t* s_confirm_modal = nullptr; + +void confirmDismiss() { + if (s_confirm_modal) { + lv_obj_del(s_confirm_modal); + s_confirm_modal = nullptr; + } + s_confirm_cb = nullptr; +} + +void confirmCancelEvt(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + confirmDismiss(); +} + +void confirmOkEvt(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + SimpleCb cb = s_confirm_cb; + confirmDismiss(); + if (cb) cb(); +} +} // namespace + +static void showConfirm(const char* msg, const char* ok_label, SimpleCb on_confirm) { + confirmDismiss(); + s_confirm_cb = on_confirm; + + // Backdrop: semi-opaque full-screen catcher so taps outside the card do nothing. + s_confirm_modal = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_confirm_modal); + // Backdrop starts below the global status bar — full screen so the card + // centers on-screen and the tap-catcher covers everything. (When this was a + // fixed 240-wide rect, in landscape the card landed partly off-screen with + // its buttons unreachable while the catcher still ate every tap — the UI + // looked "frozen".) + lv_obj_set_size(s_confirm_modal, lv_disp_get_hor_res(nullptr), + lv_disp_get_ver_res(nullptr) - STATUSBAR_H); + lv_obj_set_pos(s_confirm_modal, 0, STATUSBAR_H); + lv_obj_set_style_bg_opa(s_confirm_modal, LV_OPA_60, LV_PART_MAIN); + lv_obj_set_style_bg_color(s_confirm_modal, lv_color_black(), LV_PART_MAIN); + lv_obj_clear_flag(s_confirm_modal, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(s_confirm_modal, LV_OBJ_FLAG_FLOATING); + lv_obj_move_foreground(s_confirm_modal); + + // Card + lv_obj_t* card = lv_obj_create(s_confirm_modal); + lv_obj_remove_style_all(card); + lv_obj_set_size(card, 210, 160); + lv_obj_align(card, LV_ALIGN_CENTER, 0, 0); + styleSurface(card, COLOR_PANEL, 12); + lv_obj_set_style_border_width(card, 1, LV_PART_MAIN); + lv_obj_set_style_border_color(card, lv_color_hex(0x18191A), LV_PART_MAIN); + lv_obj_set_style_pad_all(card, 12, LV_PART_MAIN); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + addCloseXBadge(card, confirmCancelEvt); // X behaves like Cancel + + lv_obj_t* lbl = lv_label_create(card); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); + // Shrink width so the first line doesn't slide under the top-right X. + // Push the label down 4 px so the X glyph and the start of the text + // baseline don't touch optically. + lv_obj_set_width(lbl, 186 - 32); + lv_label_set_text(lbl, TR(msg)); + lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(lbl, &g_font_14, LV_PART_MAIN); + lv_obj_align(lbl, LV_ALIGN_TOP_LEFT, 0, 0); + + lv_obj_t* b_cancel = lv_btn_create(card); + lv_obj_set_size(b_cancel, 80, 34); + lv_obj_align(b_cancel, LV_ALIGN_BOTTOM_LEFT, 0, 0); + styleButton(b_cancel); + lv_obj_set_style_bg_color(b_cancel, lv_color_hex(0x3A4A5C), LV_PART_MAIN); + lv_obj_set_style_bg_color(b_cancel, lv_color_hex(0x2D3947), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_text_color(b_cancel, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_add_event_cb(b_cancel, confirmCancelEvt, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lc = lv_label_create(b_cancel); + lv_label_set_text(lc, TR("Cancel")); + lv_obj_center(lc); + + lv_obj_t* b_ok = lv_btn_create(card); + lv_obj_set_size(b_ok, 100, 34); + lv_obj_align(b_ok, LV_ALIGN_BOTTOM_RIGHT, 0, 0); + styleButton(b_ok); + lv_obj_add_event_cb(b_ok, confirmOkEvt, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lo = lv_label_create(b_ok); + lv_label_set_text(lo, ok_label ? TR(ok_label) : TR("OK")); + lv_obj_center(lo); +} + +// ----- Bluetooth settings page ----- +// Wi-Fi + BLE coexist now (NimBLE's host is light enough to share the ESP32-S3 +// internal heap with esp_wifi + LVGL), so this is a plain LIVE toggle of the BLE +// radio — no reboot, and Wi-Fi is left untouched. State is persisted (ble_en). +#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) +static void saveBluetoothCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + if (!g_set_modal.wifi_sw) return; + if (!g_lv.task->hasBleCapability()) { g_lv.task->showAlert(TR("No Bluetooth on this device"), 1400); return; } + const bool want_ble = lv_obj_has_state(g_set_modal.wifi_sw, LV_STATE_CHECKED); + const bool ble_active_now = g_lv.task->isBleEnabled(); + if (want_ble == ble_active_now) { + g_lv.task->showAlert(want_ble ? TR("Bluetooth already on") : TR("Bluetooth already off"), 1200); + return; + } + // enableBle() lazily brings NimBLE up if it wasn't started at boot. + if (want_ble) g_lv.task->enableBle(); else g_lv.task->disableBle(); + g_lv.task->showAlert(want_ble ? TR("Bluetooth on") : TR("Bluetooth off"), 1000); +} +#endif + +static void buildBluetoothSettings() { + lv_obj_t* body = createSettingsModal("Bluetooth", SettingsModalKind::Bluetooth); + int y = 0; + + // ---- Mode line ---- (Wi-Fi + BLE coexist now; show both radios' state) + const bool ble_active = g_lv.task && g_lv.task->hasBleCapability() && g_lv.task->isBleEnabled(); + lv_obj_t* mode = lv_label_create(body); +#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) + const bool wifi_on_m = wifiConfigGetRadioEnabled(); + const bool ble_cap_m = g_lv.task && g_lv.task->hasBleCapability(); + if (ble_active) + lv_label_set_text(mode, wifi_on_m ? "Mode: BLE on (+ Wi-Fi)" : "Mode: BLE on"); + else if (ble_cap_m && wifiConfigGetBleEnabled()) + lv_label_set_text(mode, TR("Mode: BLE starting / low memory")); + else + lv_label_set_text(mode, wifi_on_m ? "Mode: BLE off (Wi-Fi on)" : "Mode: BLE off"); +#else + lv_label_set_text(mode, ble_active ? "Mode: BLE on" : "Mode: BLE off"); +#endif + lv_obj_set_style_text_color(mode, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(mode, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(mode, 2, y); + y += 18; + + // ---- BLE pin line (visible regardless of mode — the pin is set at boot) ---- + if (g_lv.task) { + char blebuf[40]; + const uint32_t ble_pin = the_mesh.getBLEPin(); + if (ble_pin > 0) snprintf(blebuf, sizeof(blebuf), TR("Pairing code: %06lu"), static_cast(ble_pin)); + else snprintf(blebuf, sizeof(blebuf), TR("Pairing code: n/a")); + lv_obj_t* ble_l = lv_label_create(body); + lv_label_set_text(ble_l, blebuf); + lv_obj_set_style_text_color(ble_l, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(ble_l, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(ble_l, 2, y); + y += 22; + } + +#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) + // ---- Enable switch + save button ---- + lv_obj_t* sw_lbl = lv_label_create(body); + lv_label_set_text(sw_lbl, TR("Enable Bluetooth")); + lv_obj_set_style_text_color(sw_lbl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_pos(sw_lbl, 2, y + 6); + g_set_modal.wifi_sw = lv_switch_create(body); // reuse the same slot — only one switch lives in a modal at a time + lv_obj_align(g_set_modal.wifi_sw, LV_ALIGN_TOP_RIGHT, 0, y); // flush right + if (ble_active) lv_obj_add_state(g_set_modal.wifi_sw, LV_STATE_CHECKED); + y += 38; + + // No inline reboot hint — saveBluetoothCb shows a confirmation popup with + // the full explanation when the user actually presses Save, keeping the + // page short enough that Save is visible without scrolling. + + lv_obj_t* b_save = lv_btn_create(body); + lv_obj_set_size(b_save, lv_pct(100),34); + lv_obj_set_pos(b_save, 2, y); + styleButton(b_save); + lv_obj_add_event_cb(b_save, saveBluetoothCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lbl = lv_label_create(b_save); + lv_label_set_text(lbl, TR("Save")); + lv_obj_center(lbl); +#else + (void)y; +#endif + refreshStatusLabels(); +} + +// ----- Dedicated Wi-Fi settings page ----- +// Shows live connection state (status, IP, RSSI), web-socket count, and the +// SSID / password fields with a single Save+reconnect button. +#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) +// State captured at saveWifiCb time, applied inside the confirm callback. +static char s_pending_wifi_ssid[WIFI_CONFIG_SSID_MAX]; +static char s_pending_wifi_pwd[WIFI_CONFIG_PWD_MAX]; +static bool s_pending_wifi_radio_on = false; +static bool s_pending_wifi_was_wifi = false; // Wi-Fi already the live transport at save time? + +static void doApplyWifi() { + if (!wifiConfigSetSsid(s_pending_wifi_ssid)) { + if (g_lv.task) g_lv.task->showAlert(TR("SSID too long"), 1400); + return; + } + if (!wifiConfigSetPwd(s_pending_wifi_pwd)) { + if (g_lv.task) g_lv.task->showAlert(TR("Password too long"), 1400); + return; + } + wifiConfigSetRadioEnabled(s_pending_wifi_radio_on); + wifiConfigRequestApply(); + + // A reboot is only needed to switch transports: BLE->Wi-Fi (Bluedroid must be + // torn down before esp_wifi_init can grab its heap) or Wi-Fi->BLE (BLE only + // inits in setup()). When Wi-Fi is already live and stays on, the main loop + // re-begins with the new creds (consumeApplyRequest path) — no reboot needed. + const bool reboot_needed = !(s_pending_wifi_was_wifi && s_pending_wifi_radio_on); + if (!reboot_needed) { + if (g_lv.task) g_lv.task->showAlert(TR("Saved — reconnecting\xE2\x80\xA6"), 1600); + refreshStatusLabels(); + return; + } + if (g_lv.task) { + g_lv.task->showAlert(TR("Saved — rebooting"), 1400); + g_lv.task->persistHistoryNow(); // persist chat before reboot + } + refreshStatusLabels(); + delay(1500); + ESP.restart(); +} + +// Forward decl so wifiSlotSaveCb can refresh the modal after writing. +static void buildWifiSettings(); + +// Saved Wi-Fi profile slot: tap to load. Fills the SSID/PWD textareas with +// the stored creds — does NOT auto-apply, so the operator can still tweak +// (or change their mind by tapping Close) before the main "Save / turn on" +// commits and reboots. +static void wifiSlotLoadCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + intptr_t idx = (intptr_t)lv_event_get_user_data(e); + if (!g_set_modal.wifi_ssid_ta || !g_set_modal.wifi_pwd_ta) return; + char label[TOUCH_WIFI_LABEL_MAX]; + char ssid_b[WIFI_CONFIG_SSID_MAX]; + char pwd_b[WIFI_CONFIG_PWD_MAX]; + if (!touchPrefsGetWifiSlot((int)idx, label, sizeof(label), + ssid_b, sizeof(ssid_b), + pwd_b, sizeof(pwd_b))) { + if (g_lv.task) g_lv.task->showAlert(TR("Slot empty"), 1000); + return; + } + if (ssid_b[0] == '\0') { + if (g_lv.task) g_lv.task->showAlert(TR("Slot empty"), 1000); + return; + } + lv_textarea_set_text(g_set_modal.wifi_ssid_ta, ssid_b); + lv_textarea_set_text(g_set_modal.wifi_pwd_ta, pwd_b); + if (g_set_modal.wifi_sw) lv_obj_add_state(g_set_modal.wifi_sw, LV_STATE_CHECKED); + char msg[40]; + snprintf(msg, sizeof(msg), "Loaded slot %d — Save to apply", (int)idx + 1); + if (g_lv.task) g_lv.task->showAlert(msg, 1300); +} + +// Per-slot "Saved profiles" row labels, so a slot Save refreshes just that row +// in place (below) instead of tearing down + rebuilding the page. Rebuilding +// from this callback called buildWifiSettings() with s_settings_inline_parent +// already null, which spawned a rogue modal on lv_layer_top() over the inline +// Network sub-tab and stranded the Close button (reboot was the only way out). +static lv_obj_t* s_wifi_slot_row_lbl[TOUCH_WIFI_SLOT_COUNT] = { nullptr }; +static void wifiSlotFmtRow(int i, const char* label, const char* ssid, char* out, size_t out_sz) { + if (!ssid || ssid[0] == '\0') snprintf(out, out_sz, "%d: (empty)", i + 1); + else if (label && label[0]) snprintf(out, out_sz, "%d: %s (%s)", i + 1, label, ssid); + else snprintf(out, out_sz, "%d: %s", i + 1, ssid); +} + +// Save the current SSID/PWD form contents into a slot. Label defaults to +// "Slot N" so the row text isn't blank; user can rename via the same flow +// after editing (no inline rename UI — keeps the page short). +static void wifiSlotSaveCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + intptr_t idx = (intptr_t)lv_event_get_user_data(e); + if (!g_set_modal.wifi_ssid_ta || !g_set_modal.wifi_pwd_ta) return; + kbMirrorSyncToReal(); + const char* ssid = lv_textarea_get_text(g_set_modal.wifi_ssid_ta); + const char* pwd = lv_textarea_get_text(g_set_modal.wifi_pwd_ta); + if (!ssid || !ssid[0]) { + if (g_lv.task) g_lv.task->showAlert(TR("Enter SSID first"), 1200); + return; + } + // Use the SSID as the slot label so the row text reads naturally even + // without an explicit naming step. + char label[TOUCH_WIFI_LABEL_MAX]; + strncpy(label, ssid, sizeof(label) - 1); + label[sizeof(label) - 1] = '\0'; + if (!touchPrefsSetWifiSlot((int)idx, label, ssid, pwd)) { + if (g_lv.task) g_lv.task->showAlert(TR("Save failed"), 1200); + return; + } + char msg[40]; + snprintf(msg, sizeof(msg), TR("Saved to slot %d"), (int)idx + 1); + if (g_lv.task) g_lv.task->showAlert(msg, 1100); + // Refresh just this slot's row label in place. Do NOT close+rebuild here: on + // the inline Network sub-tab s_settings_inline_parent is null in this callback, + // so buildWifiSettings() would spawn a rogue modal over the page and strand the + // Close button. The slot's label is the saved SSID (label = ssid above). + if (idx >= 0 && idx < TOUCH_WIFI_SLOT_COUNT && s_wifi_slot_row_lbl[idx]) { + char row_text[80]; + wifiSlotFmtRow((int)idx, label, ssid, row_text, sizeof(row_text)); + lv_label_set_text(s_wifi_slot_row_lbl[idx], row_text); + lv_obj_set_style_text_color(s_wifi_slot_row_lbl[idx], lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + } +} + +// Clear a saved Wi-Fi slot (the trash button on its row). Refreshes the row +// label to "(empty)" in place — same in-place pattern as the slot Save. +static void wifiSlotDeleteCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + intptr_t idx = (intptr_t)lv_event_get_user_data(e); + if (idx < 0 || idx >= TOUCH_WIFI_SLOT_COUNT) return; + touchPrefsSetWifiSlot((int)idx, "", "", ""); + if (s_wifi_slot_row_lbl[idx]) { + char row_text[80]; + wifiSlotFmtRow((int)idx, "", "", row_text, sizeof(row_text)); + lv_label_set_text(s_wifi_slot_row_lbl[idx], row_text); + lv_obj_set_style_text_color(s_wifi_slot_row_lbl[idx], lv_color_hex(COLOR_SUB), LV_PART_MAIN); + } + if (g_lv.task) { + char m[28]; snprintf(m, sizeof m, TR("Slot %d cleared"), (int)idx + 1); + g_lv.task->showAlert(m, 1000); + } +} + +static void saveWifiCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; + if (!g_set_modal.wifi_ssid_ta || !g_set_modal.wifi_pwd_ta || !g_set_modal.wifi_sw) return; + kbMirrorSyncToReal(); + + strncpy(s_pending_wifi_ssid, lv_textarea_get_text(g_set_modal.wifi_ssid_ta), sizeof(s_pending_wifi_ssid) - 1); + s_pending_wifi_ssid[sizeof(s_pending_wifi_ssid) - 1] = '\0'; + strncpy(s_pending_wifi_pwd, lv_textarea_get_text(g_set_modal.wifi_pwd_ta), sizeof(s_pending_wifi_pwd) - 1); + s_pending_wifi_pwd[sizeof(s_pending_wifi_pwd) - 1] = '\0'; + trimWifiField(s_pending_wifi_ssid); + trimWifiField(s_pending_wifi_pwd); + + // If the user entered an SSID, treat that as intent to connect — force the + // radio switch on even if they forgot to toggle it. Otherwise honour switch. + s_pending_wifi_radio_on = lv_obj_has_state(g_set_modal.wifi_sw, LV_STATE_CHECKED); + if (!s_pending_wifi_radio_on && s_pending_wifi_ssid[0] != '\0') { + s_pending_wifi_radio_on = true; + lv_obj_add_state(g_set_modal.wifi_sw, LV_STATE_CHECKED); + } + // Capture the live transport now, before doApplyWifi flips any pref. If Wi-Fi + // is already up and stays on, saving new creds is a hot reconnect — apply + // straight away (the main loop re-begins), no reboot and no confirm to + // interrupt. Only a transport switch (to/from BLE) reboots, so keep the + // confirm prompt for that case. + s_pending_wifi_was_wifi = wifiConfigWantsWifi(); + if (s_pending_wifi_was_wifi && s_pending_wifi_radio_on) { + doApplyWifi(); + return; + } + showConfirm(s_pending_wifi_radio_on + ? "Save Wi-Fi and reboot? Bluetooth will be turned off." + : "Save Wi-Fi (radio off) and reboot?", + "Reboot", doApplyWifi); +} +#endif + +static void wifiScanPopupClose() { + if (s_wifi_scan_popup) { lv_obj_del(s_wifi_scan_popup); s_wifi_scan_popup = nullptr; } + s_wifi_scan_list = nullptr; +} +static void wifiScanPopupCloseCb(lv_event_t* e) { + if (lv_event_get_code(e) == LV_EVENT_CLICKED) wifiScanPopupClose(); +} + +// Tap a scanned SSID -> drop it into the SSID field + close the popup. +static void wifiScanSsidCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; + const int idx = (int)(intptr_t)lv_event_get_user_data(e); + const bool ok = (idx >= 0 && idx < s_wifiscan_count && g_set_modal.wifi_ssid_ta); + // setTextareaSynced (not plain set_text): if the keyboard mirror is bound to + // the SSID field, a later sync would clobber this back to the old SSID. + if (ok) setTextareaSynced(g_set_modal.wifi_ssid_ta, s_wifiscan_ssids[idx]); + wifiScanPopupClose(); + if (ok && g_lv.task) g_lv.task->showAlert(TR("SSID set — enter password, then Save"), 1700); +} + +// (Re)draw the scan-results list inside the popup from the latest scan. +static void wifiScanFillList() { + if (!s_wifi_scan_list) return; + lv_obj_clean(s_wifi_scan_list); + const lv_coord_t rw = lv_disp_get_hor_res(nullptr) - 28; + if (s_wifiscan_count <= 0) { + lv_obj_t* l = lv_label_create(s_wifi_scan_list); + lv_label_set_text(l, TR("No networks found (2.4 GHz only)")); + lv_obj_set_style_text_color(l, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(l, &g_font_14, LV_PART_MAIN); + return; + } + for (int i = 0; i < s_wifiscan_count; ++i) { + lv_obj_t* r = lv_btn_create(s_wifi_scan_list); + lv_obj_set_size(r, rw, 40); // tall rows = easy to tap + scroll + styleButton(r); + lv_obj_set_style_bg_color(r, lv_color_hex(0x1A1B1C), LV_PART_MAIN); + lv_obj_add_event_cb(r, wifiScanSsidCb, LV_EVENT_CLICKED, (void*)(intptr_t)i); + lv_obj_t* l = lv_label_create(r); + lv_label_set_text(l, s_wifiscan_ssids[i]); + lv_label_set_long_mode(l, LV_LABEL_LONG_DOT); + lv_obj_set_width(l, rw - 16); + lv_obj_set_style_text_font(l, &g_font_14, LV_PART_MAIN); + lv_obj_align(l, LV_ALIGN_LEFT_MID, 8, 0); + } +} + +// Full-screen scan overlay (covers the bottom bar + sub-tabs) so the SSID list +// is big and easy to scroll. Sits below the status bar, on lv_layer_top. +static void openWifiScanPopup() { + wifiScanPopupClose(); + const lv_coord_t sw = lv_disp_get_hor_res(nullptr); + const lv_coord_t sh = lv_disp_get_ver_res(nullptr); + s_wifi_scan_popup = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_wifi_scan_popup); + lv_obj_set_size(s_wifi_scan_popup, sw, sh - STATUSBAR_H); + lv_obj_set_pos(s_wifi_scan_popup, 0, STATUSBAR_H); + styleSurface(s_wifi_scan_popup, COLOR_BG, 0); + lv_obj_set_style_pad_all(s_wifi_scan_popup, 0, LV_PART_MAIN); + lv_obj_clear_flag(s_wifi_scan_popup, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* title = lv_label_create(s_wifi_scan_popup); + lv_label_set_text(title, TR("Select network")); + lv_obj_set_style_text_font(title, &g_font_16, LV_PART_MAIN); + lv_obj_set_style_text_color(title, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_pos(title, 8, 9); + + lv_obj_t* close = lv_btn_create(s_wifi_scan_popup); + lv_obj_set_size(close, 60, 30); + lv_obj_align(close, LV_ALIGN_TOP_RIGHT, -6, 4); + styleButton(close); + lv_obj_add_event_cb(close, wifiScanPopupCloseCb, LV_EVENT_CLICKED, nullptr); + { lv_obj_t* cl = lv_label_create(close); lv_label_set_text(cl, TR("Close")); + lv_obj_set_style_text_font(cl, &g_font_12, LV_PART_MAIN); lv_obj_center(cl); } + + s_wifi_scan_list = lv_obj_create(s_wifi_scan_popup); + lv_obj_remove_style_all(s_wifi_scan_list); + lv_obj_set_size(s_wifi_scan_list, sw - 12, sh - STATUSBAR_H - 44); + lv_obj_set_pos(s_wifi_scan_list, 6, 40); + lv_obj_set_flex_flow(s_wifi_scan_list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(s_wifi_scan_list, 6, LV_PART_MAIN); + lv_obj_set_scroll_dir(s_wifi_scan_list, LV_DIR_VER); + lv_obj_set_scrollbar_mode(s_wifi_scan_list, LV_SCROLLBAR_MODE_AUTO); +} + +// Open the picker and kick a fresh scan on the core-0 worker. Shows the last +// results instantly (if any) while the rescan lands. Shared by the Settings +// Wi-Fi page, the setup wizard's Scan button, and the wizard's auto-scan on +// arrival. Caller must have already confirmed wifiConfigWantsWifi() (radio up). +static void wifiScanOpenAndKick() { + hideKb(); // unbind the keyboard mirror so it can't later revert the picked SSID + openWifiScanPopup(); + if (s_wifiscan_count > 0) { + wifiScanFillList(); // show last results immediately, refresh when the rescan lands + } else if (s_wifi_scan_list) { + lv_obj_t* l = lv_label_create(s_wifi_scan_list); + lv_label_set_text(l, TR("Scanning\xE2\x80\xA6")); + lv_obj_set_style_text_color(l, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(l, &g_font_14, LV_PART_MAIN); + } + ensureTileFetchTaskRunning(); // the worker idles out after 5 s — respawn it + s_wifiscan_request = true; +} + +// Switch into Wi-Fi mode (reboot) so a scan can run — WITHOUT needing creds +// first. Used when the user taps Scan while BLE is the active transport. +// setRadioEnabled(true) marks Wi-Fi "chosen", so wantsWifi() brings the radio +// up STA (scannable) on the next boot even with no SSID saved. +#if defined(MULTI_TRANSPORT_COMPANION) +static void doSwitchToWifiForScan() { + wifiConfigSetRadioEnabled(true); // marks Wi-Fi chosen -> scannable boot + if (g_lv.task) g_lv.task->rebootDevice(); // persists chat history first +} +#endif + +// "Scan" button -> open the popup + queue a scan on the core-0 worker. +static void wifiScanStartCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; +#if defined(MULTI_TRANSPORT_COMPANION) + if (!wifiConfigWantsWifi()) { + // BLE is the active transport — the radio is down, so we can't scan. Offer + // to switch to Wi-Fi (reboots; comes up scannable even with no creds) so + // the user doesn't have to type an SSID blind. + showConfirm("Wi-Fi is off (Bluetooth is on). Switch to Wi-Fi and reboot so " + "you can scan and pick a network?", + "Switch", doSwitchToWifiForScan); + return; + } + wifiScanOpenAndKick(); +#endif +} + +// Loop service: when the worker finishes a scan, repopulate the popup list. +static void wifiScanService() { + if (!s_wifiscan_done) return; + s_wifiscan_done = false; + wifiScanFillList(); // no-op if the popup was closed +} + +static void buildWifiSettings() { + lv_obj_t* body = createSettingsModal("Wi-Fi", SettingsModalKind::Wifi); + int y = 0; + const lv_coord_t cw = s_settings_content_w; + +#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) + // ---- Status (+ Wi-Fi radio toggle on the same row to save vertical space) ---- + lv_obj_t* sec1 = lv_label_create(body); + lv_label_set_text(sec1, TR("Status")); + lv_obj_set_style_text_color(sec1, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(sec1, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(sec1, 2, y + 6); + g_set_modal.wifi_sw = lv_switch_create(body); + lv_obj_align(g_set_modal.wifi_sw, LV_ALIGN_TOP_RIGHT, 0, y); // toggle next to "Status" + if (wifiConfigGetRadioEnabled()) lv_obj_add_state(g_set_modal.wifi_sw, LV_STATE_CHECKED); + y += 30; + + g_set_modal.wifi_sta_status_l = lv_label_create(body); + lv_label_set_long_mode(g_set_modal.wifi_sta_status_l, LV_LABEL_LONG_WRAP); + lv_obj_set_width(g_set_modal.wifi_sta_status_l, lv_pct(100)); + lv_obj_set_style_text_color(g_set_modal.wifi_sta_status_l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_set_style_text_font(g_set_modal.wifi_sta_status_l, &g_font_14, LV_PART_MAIN); + lv_obj_set_pos(g_set_modal.wifi_sta_status_l, 2, y); + lv_label_set_text(g_set_modal.wifi_sta_status_l, TR("Loading...")); + y += 42; // room for the 2-line connected status so it can't overlap the SSID field + + // ---- SSID + Scan (same row: field on the left, Scan opens the picker) ---- + lv_obj_t* wssid_l = lv_label_create(body); + lv_label_set_text(wssid_l, TR("Network name (SSID)")); + lv_obj_set_style_text_color(wssid_l, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(wssid_l, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(wssid_l, 2, y); + y += 16; + const int scan_w = 76, scan_g = 6; + g_set_modal.wifi_ssid_ta = lv_textarea_create(body); + lv_obj_set_size(g_set_modal.wifi_ssid_ta, cw - scan_w - scan_g, 32); + lv_obj_set_pos(g_set_modal.wifi_ssid_ta, 0, y); + lv_textarea_set_one_line(g_set_modal.wifi_ssid_ta, true); + lv_textarea_set_placeholder_text(g_set_modal.wifi_ssid_ta, TR("Network name")); + lv_textarea_set_max_length(g_set_modal.wifi_ssid_ta, WIFI_CONFIG_SSID_MAX - 1); + attachSettingsTaEvents(g_set_modal.wifi_ssid_ta); + lv_obj_t* scan_btn = lv_btn_create(body); + lv_obj_set_size(scan_btn, scan_w, 32); + lv_obj_set_pos(scan_btn, cw - scan_w, y); + styleButton(scan_btn); + lv_obj_add_event_cb(scan_btn, wifiScanStartCb, LV_EVENT_CLICKED, nullptr); + { lv_obj_t* sl = lv_label_create(scan_btn); + lv_label_set_text(sl, LV_SYMBOL_WIFI " Scan"); + lv_obj_set_style_text_font(sl, &g_font_12, LV_PART_MAIN); lv_obj_center(sl); } + y += 38; + + // ---- Password ---- + lv_obj_t* wpwd_l = lv_label_create(body); + lv_label_set_text(wpwd_l, TR("Password (empty = open)")); + lv_obj_set_style_text_color(wpwd_l, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_font(wpwd_l, &g_font_12, LV_PART_MAIN); + lv_obj_set_pos(wpwd_l, 2, y); + y += 16; + g_set_modal.wifi_pwd_ta = lv_textarea_create(body); + lv_obj_set_size(g_set_modal.wifi_pwd_ta, lv_pct(100),30); + lv_obj_set_pos(g_set_modal.wifi_pwd_ta, 2, y); + lv_textarea_set_one_line(g_set_modal.wifi_pwd_ta, true); + lv_textarea_set_password_mode(g_set_modal.wifi_pwd_ta, true); + lv_textarea_set_placeholder_text(g_set_modal.wifi_pwd_ta, TR("PSK")); + lv_textarea_set_max_length(g_set_modal.wifi_pwd_ta, WIFI_CONFIG_PWD_MAX - 1); + attachSettingsTaEvents(g_set_modal.wifi_pwd_ta); + y += 36; + + { + char ssid_buf[WIFI_CONFIG_SSID_MAX]; + char pwd_buf[WIFI_CONFIG_PWD_MAX]; + wifiConfigGetSsid(ssid_buf, sizeof(ssid_buf)); + wifiConfigGetPwd(pwd_buf, sizeof(pwd_buf)); + lv_textarea_set_text(g_set_modal.wifi_ssid_ta, ssid_buf); + lv_textarea_set_text(g_set_modal.wifi_pwd_ta, pwd_buf); + } + + // No inline reboot hint — saveWifiCb shows a confirmation popup with the + // full explanation when the user actually presses Save. Keeps the page + // short enough that the button stays visible above the fold. + lv_obj_t* b_save = lv_btn_create(body); + lv_obj_set_size(b_save, lv_pct(100),36); + lv_obj_set_pos(b_save, 2, y); + styleButton(b_save); + lv_obj_set_style_bg_color(b_save, lv_color_hex(COLOR_STATUS_OK), LV_PART_MAIN); + lv_obj_set_style_bg_color(b_save, lv_color_hex(0x3B7039), LV_PART_MAIN | LV_STATE_PRESSED); + lv_obj_set_style_text_color(b_save, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_add_event_cb(b_save, saveWifiCb, LV_EVENT_CLICKED, nullptr); + lv_obj_t* lsave = lv_label_create(b_save); + lv_label_set_text(lsave, TR("Save / turn on")); + lv_obj_center(lsave); + y += 42; + + // ---- Saved profiles (3 slots) ---- + // Each row: "