From 5099d25965e426f77051781e835d08fec3918c03 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 12 Aug 2018 00:17:57 +0200 Subject: [PATCH 001/105] testing --- lib/BintrayClient/library.json | 24 +++ lib/BintrayClient/src/BintrayCertificates.h | 102 +++++++++ lib/BintrayClient/src/BintrayClient.cpp | 149 +++++++++++++ lib/BintrayClient/src/BintrayClient.h | 49 +++++ platformio.ini | 46 +++- src/SecureOTA.cpp | 225 ++++++++++++++++++++ src/SecureOTA.h | 25 +++ src/globals.h | 1 + src/main.cpp | 4 +- src/rcommand.cpp | 29 ++- src/rcommand.h | 3 + 11 files changed, 648 insertions(+), 9 deletions(-) create mode 100644 lib/BintrayClient/library.json create mode 100644 lib/BintrayClient/src/BintrayCertificates.h create mode 100644 lib/BintrayClient/src/BintrayClient.cpp create mode 100644 lib/BintrayClient/src/BintrayClient.h create mode 100644 src/SecureOTA.cpp create mode 100644 src/SecureOTA.h diff --git a/lib/BintrayClient/library.json b/lib/BintrayClient/library.json new file mode 100644 index 00000000..677cec5c --- /dev/null +++ b/lib/BintrayClient/library.json @@ -0,0 +1,24 @@ +{ + "name": "BintrayClient", + "keywords": "bintray, ota, cdn, storage", + "description": "A BintrayClient to connect to a JFrog Bintray.", + "authors": [ + { + "name": "PlatformIO", + "url": "https://platformio.org/" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/platformio/platformio-examples" + }, + "export": { + "include": "bintray-secure-ota/lib/BintrayClient" + }, + "dependencies": { + "ArduinoJson": "^5.13.1" + }, + "version": "1.0.0", + "frameworks": "arduino", + "platforms": "espressif32" +} diff --git a/lib/BintrayClient/src/BintrayCertificates.h b/lib/BintrayClient/src/BintrayCertificates.h new file mode 100644 index 00000000..ec5fdaa3 --- /dev/null +++ b/lib/BintrayClient/src/BintrayCertificates.h @@ -0,0 +1,102 @@ +/* + Copyright (c) 2014-present PlatformIO + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +**/ + +#ifndef BINTRAY_CERTIFICATES_H +#define BINTRAY_CERTIFICATES_H + +const char* BINTRAY_API_ROOT_CA = \ +"-----BEGIN CERTIFICATE-----\n" \ +"MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\n" \ +"MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\n" \ +"YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG\n" \ +"EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg\n" \ +"R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9\n" \ +"9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq\n" \ +"fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv\n" \ +"iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU\n" \ +"1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+\n" \ +"bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW\n" \ +"MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA\n" \ +"ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l\n" \ +"uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn\n" \ +"Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS\n" \ +"tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF\n" \ +"PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un\n" \ +"hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV\n" \ +"5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==\n" \ +"-----END CERTIFICATE-----\n"; + +const char* BINTRAY_AKAMAI_ROOT_CA = \ +"-----BEGIN CERTIFICATE-----\n"\ +"MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh\n"\ +"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"\ +"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n"\ +"QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT\n"\ +"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg\n"\ +"U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n"\ +"ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83\n"\ +"nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd\n"\ +"KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f\n"\ +"/ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX\n"\ +"kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0\n"\ +"/RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C\n"\ +"AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY\n"\ +"aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6\n"\ +"Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1\n"\ +"oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD\n"\ +"QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v\n"\ +"d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh\n"\ +"xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB\n"\ +"CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl\n"\ +"5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA\n"\ +"8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC\n"\ +"2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit\n"\ +"c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0\n"\ +"j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz\n"\ +"-----END CERTIFICATE-----\n"; + +const char* CLOUDFRONT_API_ROOT_CA = \ +"-----BEGIN CERTIFICATE-----\n"\ +"MIIE3zCCA8egAwIBAgIQYxgNOPuAl3ip0DWjFhj4QDANBgkqhkiG9w0BAQsFADCB\n"\ +"yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\n"\ +"ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp\n"\ +"U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW\n"\ +"ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0\n"\ +"aG9yaXR5IC0gRzUwHhcNMTcxMTA2MDAwMDAwWhcNMjIxMTA1MjM1OTU5WjBhMQsw\n"\ +"CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\n"\ +"ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjCC\n"\ +"ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALs3zTTce2vJsmiQrUp1/0a6\n"\ +"IQoIjfUZVMn7iNvzrvI6iZE8euarBhprz6wt6F4JJES6Ypp+1qOofuBUdSAFrFC3\n"\ +"nGMabDDc2h8Zsdce3v3X4MuUgzeu7B9DTt17LNK9LqUv5Km4rTrUmaS2JembawBg\n"\ +"kmD/TyFJGPdnkKthBpyP8rrptOmSMmu181foXRvNjB2rlQSVSfM1LZbjSW3dd+P7\n"\ +"SUu0rFUHqY+Vs7Qju0xtRfD2qbKVMLT9TFWMJ0pXFHyCnc1zktMWSgYMjFDRjx4J\n"\ +"vheh5iHK/YPlELyDpQrEZyj2cxQUPUZ2w4cUiSE0Ta8PRQymSaG6u5zFsTODKYUC\n"\ +"AwEAAaOCAScwggEjMB0GA1UdDgQWBBROIlQgGJXm427mD/r6uRLtBhePOTAPBgNV\n"\ +"HRMBAf8EBTADAQH/MF8GA1UdIARYMFYwVAYEVR0gADBMMCMGCCsGAQUFBwIBFhdo\n"\ +"dHRwczovL2Quc3ltY2IuY29tL2NwczAlBggrBgEFBQcCAjAZDBdodHRwczovL2Qu\n"\ +"c3ltY2IuY29tL3JwYTAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vcy5zeW1jYi5j\n"\ +"b20vcGNhMy1nNS5jcmwwDgYDVR0PAQH/BAQDAgGGMC4GCCsGAQUFBwEBBCIwIDAe\n"\ +"BggrBgEFBQcwAYYSaHR0cDovL3Muc3ltY2QuY29tMB8GA1UdIwQYMBaAFH/TZafC\n"\ +"3ey78DAJ80M5+gKvMzEzMA0GCSqGSIb3DQEBCwUAA4IBAQBQ3dNWKSUBip6n5X1N\n"\ +"ua8bjKLSJzXlnescavPECMpFBlIIKH2mc6mL2Xr/wkSIBDrsqAO3sBcmoJN+n8V3\n"\ +"0O5JelrtEAFYSyRDXfu78ZlHn6kvV5/jPUFECEM/hdN0x8WdLpGjJMqfs0EG5qHj\n"\ +"+UaxpucWD445wea4zlK7hUR+MA8fq0Yd1HEKj4c8TcgaQIHMa4KHr448cQ69e3CP\n"\ +"ECRhRNg+RAKT2I7SlaVzLvaB/8yym2oMCEsoqiRT8dbXg35aKEYmmzn3O/mnB7bG\n"\ +"Ud/EUrkIf7FVamgYZd1fSzQeg1cHqf0ja6eHpvq2bTl+cWFHaq/84KlHe5Rh0Csm\n"\ +"pZzn\n"\ +"-----END CERTIFICATE-----\n"; + +#endif // BINTRAY_CERTIFICATES_H diff --git a/lib/BintrayClient/src/BintrayClient.cpp b/lib/BintrayClient/src/BintrayClient.cpp new file mode 100644 index 00000000..b0e7543f --- /dev/null +++ b/lib/BintrayClient/src/BintrayClient.cpp @@ -0,0 +1,149 @@ +/* + Copyright (c) 2014-present PlatformIO + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +**/ + +#include +#include +#include + +#include "BintrayClient.h" +#include "BintrayCertificates.h" + +BintrayClient::BintrayClient(const String &user, const String &repository, const String &package) + : m_user(user), m_repo(repository), m_package(package), + m_storage_host("dl.bintray.com"), + m_api_host("api.bintray.com") +{ + m_certificates.emplace_back("cloudfront.net", CLOUDFRONT_API_ROOT_CA); + m_certificates.emplace_back("akamai.bintray.com", BINTRAY_AKAMAI_ROOT_CA); + m_certificates.emplace_back("bintray.com", BINTRAY_API_ROOT_CA); +} + +String BintrayClient::getUser() const +{ + return m_user; +} + +String BintrayClient::getRepository() const +{ + return m_repo; +} + +String BintrayClient::getPackage() const +{ + return m_package; +} + +String BintrayClient::getStorageHost() const +{ + return m_storage_host; +} + +String BintrayClient::getApiHost() const +{ + return m_api_host; +} + +String BintrayClient::getLatestVersionRequestUrl() const +{ + return String("https://") + getApiHost() + "/packages/" + getUser() + "/" + getRepository() + "/" + getPackage() + "/versions/_latest"; +} + +String BintrayClient::getBinaryRequestUrl(const String &version) const +{ + return String("https://") + getApiHost() + "/packages/" + getUser() + "/" + getRepository() + "/" + getPackage() + "/versions/" + version + "/files"; +} + +const char *BintrayClient::getCertificate(const String &url) const +{ + for(auto& cert: m_certificates) { + if(url.indexOf(cert.first) >= 0) { + return cert.second; + } + } + + // Return the certificate for *.bintray.com by default + return m_certificates.rbegin()->second; +} + +String BintrayClient::requestHTTPContent(const String &url) const +{ + String payload; + HTTPClient http; + http.begin(url, getCertificate(url)); + int httpCode = http.GET(); + + if (httpCode > 0) + { + if (httpCode == HTTP_CODE_OK) + { + payload = http.getString(); + } + } + else + { + Serial.printf("GET request failed, error: %s\n", http.errorToString(httpCode).c_str()); + } + + http.end(); + return payload; +} + +String BintrayClient::getLatestVersion() const +{ + String version; + const String url = getLatestVersionRequestUrl(); + String jsonResult = requestHTTPContent(url); + const size_t bufferSize = 1024; + if (jsonResult.length() > bufferSize) + { + Serial.println("Error: Could parse JSON. Input data is too big!"); + return version; + } + StaticJsonBuffer jsonBuffer; + + JsonObject &root = jsonBuffer.parseObject(jsonResult.c_str()); + // Check for errors in parsing + if (!root.success()) + { + Serial.println("Error: Could not parse JSON!"); + return version; + } + return root.get("name"); +} + +String BintrayClient::getBinaryPath(const String &version) const +{ + String path; + const String url = getBinaryRequestUrl(version); + String jsonResult = requestHTTPContent(url); + + const size_t bufferSize = 1024; + if (jsonResult.length() > bufferSize) + { + Serial.println("Error: Could parse JSON. Input data is too big!"); + return path; + } + StaticJsonBuffer jsonBuffer; + + JsonArray &root = jsonBuffer.parseArray(jsonResult.c_str()); + JsonObject &firstItem = root[0]; + if (!root.success()) + { //Check for errors in parsing + Serial.println("Error: Could not parse JSON!"); + return path; + } + return "/" + getUser() + "/" + getRepository() + "/" + firstItem.get("path"); +} diff --git a/lib/BintrayClient/src/BintrayClient.h b/lib/BintrayClient/src/BintrayClient.h new file mode 100644 index 00000000..9761c7c6 --- /dev/null +++ b/lib/BintrayClient/src/BintrayClient.h @@ -0,0 +1,49 @@ +/* + Copyright (c) 2014-present PlatformIO + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +**/ + +#ifndef BINTRAY_CLIENT_H +#define BINTRAY_CLIENT_H + +#include +#include +#include + +class BintrayClient { + +public: + BintrayClient(const String& user, const String& repository, const String& package); + String getUser() const; + String getRepository() const; + String getPackage() const; + String getStorageHost() const; + String getApiHost() const; + const char* getCertificate(const String& url) const; + String getLatestVersion() const; + String getBinaryPath(const String& version) const; + +private: + String requestHTTPContent(const String& url) const; + String getLatestVersionRequestUrl() const; + String getBinaryRequestUrl(const String& version) const; + String m_user; + String m_repo; + String m_package; + const String m_storage_host; + const String m_api_host; + std::vector> m_certificates; +}; + +#endif // BINTRAY_CLIENT_H diff --git a/platformio.ini b/platformio.ini index 98296872..91ee4e27 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,12 +11,12 @@ ; ---> SELECT TARGET PLATFORM HERE! <--- [platformio] -env_default = generic +;env_default = generic ;env_default = ebox ;env_default = heltec ;env_default = ttgov1 ;env_default = ttgov2 -;env_default = ttgov21 +env_default = ttgov21 ;env_default = ttgobeam ;env_default = lopy ;env_default = lopy4 @@ -27,10 +27,45 @@ env_default = generic ; description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around. +[bintray] +user = cyberman54 +repository = paxcounter +package = esp32-paxcounter +api_token = 9f02e2a2374c278fd79d5bcf4b4442fca9752012 +;api_token = ${env.BINTRAY_API_TOKEN} + +; Wi-Fi network settings +[wifi] +ssid = PRENZLNET-G +password = 435Huse8!? +;ssid = ${env.PIO_WIFI_SSID} +;password = ${env.PIO_WIFI_PASSWORD} + +[common] +platform = https://github.com/platformio/platform-espressif32.git + +; firmware version, please modify it between releases +; positive integer value +release_version = 1 + +; build configuration based on Bintray and Wi-Fi settings +build_flags = + '-DWIFI_SSID="${wifi.ssid}"' + '-DWIFI_PASS="${wifi.password}"' + '-DBINTRAY_USER="${bintray.user}"' + '-DBINTRAY_REPO="${bintray.repository}"' + '-DBINTRAY_PACKAGE="${bintray.package}"' + '-DVERSION=0' + +; extra dependencies +lib_deps = ArduinoJson + + [common_env_data] platform_espressif32 = espressif32@1.2.0 ;platform_espressif32 = https://github.com/platformio/platform-espressif32.git#feature/stage -board_build.partitions = no_ota.csv +;board_build.partitions = no_ota.csv +board_build.partitions = min_spiffs.csv lib_deps_all = lib_deps_display = U8g2@>=2.23.12 @@ -48,9 +83,9 @@ build_flags = ; Error ; -DCORE_DEBUG_LEVEL=1 ; Warn - -DCORE_DEBUG_LEVEL=2 +; -DCORE_DEBUG_LEVEL=2 ; Info -; -DCORE_DEBUG_LEVEL=3 + -DCORE_DEBUG_LEVEL=3 ; Debug ; -DCORE_DEBUG_LEVEL=4 ; Verbose @@ -124,6 +159,7 @@ lib_deps = ${common_env_data.lib_deps_all} ${common_env_data.lib_deps_display} build_flags = + ${common.build_flags} ${common_env_data.build_flags} [env:ttgobeam] diff --git a/src/SecureOTA.cpp b/src/SecureOTA.cpp new file mode 100644 index 00000000..7496c19c --- /dev/null +++ b/src/SecureOTA.cpp @@ -0,0 +1,225 @@ +/* + Copyright (c) 2014-present PlatformIO + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +**/ + +#include +#include +#include +#include "SecureOTA.h" + +const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); + +// Connection port (HTTPS) +const int port = 443; + +// Connection timeout +const uint32_t RESPONSE_TIMEOUT_MS = 5000; + +// Variables to validate firmware content +volatile int contentLength = 0; +volatile bool isValidContentType = false; + +void checkFirmwareUpdates() +{ + // Fetch the latest firmware version + const String latest = bintray.getLatestVersion(); + if (latest.length() == 0) + { + Serial.println("Could not load info about the latest firmware, so nothing to update. Continue ..."); + return; + } + else if (atoi(latest.c_str()) <= VERSION) + { + //Serial.println("The current firmware is up to date. Continue ..."); + return; + } + + Serial.println("There is a new version of firmware available: v." + latest); + processOTAUpdate(latest); +} + +// A helper function to extract header value from header +inline String getHeaderValue(String header, String headerName) +{ + return header.substring(strlen(headerName.c_str())); +} + +/** + * OTA update processing + */ +void processOTAUpdate(const String &version) +{ + String firmwarePath = bintray.getBinaryPath(version); + if (!firmwarePath.endsWith(".bin")) + { + Serial.println("Unsupported binary format. OTA update cannot be performed!"); + return; + } + + String currentHost = bintray.getStorageHost(); + String prevHost = currentHost; + + WiFiClientSecure client; + client.setCACert(bintray.getCertificate(currentHost)); + + if (!client.connect(currentHost.c_str(), port)) + { + Serial.println("Cannot connect to " + currentHost); + return; + } + + bool redirect = true; + while (redirect) + { + if (currentHost != prevHost) + { + client.stop(); + client.setCACert(bintray.getCertificate(currentHost)); + if (!client.connect(currentHost.c_str(), port)) + { + Serial.println("Redirect detected! Cannot connect to " + currentHost + " for some reason!"); + return; + } + } + + //Serial.println("Requesting: " + firmwarePath); + + client.print(String("GET ") + firmwarePath + " HTTP/1.1\r\n"); + client.print(String("Host: ") + currentHost + "\r\n"); + client.print("Cache-Control: no-cache\r\n"); + client.print("Connection: close\r\n\r\n"); + + unsigned long timeout = millis(); + while (client.available() == 0) + { + if (millis() - timeout > RESPONSE_TIMEOUT_MS) + { + Serial.println("Client Timeout !"); + client.stop(); + return; + } + } + + while (client.available()) + { + String line = client.readStringUntil('\n'); + // Check if the line is end of headers by removing space symbol + line.trim(); + // if the the line is empty, this is the end of the headers + if (!line.length()) + { + break; // proceed to OTA update + } + + // Check allowed HTTP responses + if (line.startsWith("HTTP/1.1")) + { + if (line.indexOf("200") > 0) + { + //Serial.println("Got 200 status code from server. Proceeding to firmware flashing"); + redirect = false; + } + else if (line.indexOf("302") > 0) + { + //Serial.println("Got 302 status code from server. Redirecting to the new address"); + redirect = true; + } + else + { + //Serial.println("Could not get a valid firmware url"); + //Unexptected HTTP response. Retry or skip update? + redirect = false; + } + } + + // Extracting new redirect location + if (line.startsWith("Location: ")) + { + String newUrl = getHeaderValue(line, "Location: "); + //Serial.println("Got new url: " + newUrl); + newUrl.remove(0, newUrl.indexOf("//") + 2); + currentHost = newUrl.substring(0, newUrl.indexOf('/')); + newUrl.remove(newUrl.indexOf(currentHost), currentHost.length()); + firmwarePath = newUrl; + //Serial.println("firmwarePath: " + firmwarePath); + continue; + } + + // Checking headers + if (line.startsWith("Content-Length: ")) + { + contentLength = atoi((getHeaderValue(line, "Content-Length: ")).c_str()); + Serial.println("Got " + String(contentLength) + " bytes from server"); + } + + if (line.startsWith("Content-Type: ")) + { + String contentType = getHeaderValue(line, "Content-Type: "); + //Serial.println("Got " + contentType + " payload."); + if (contentType == "application/octet-stream") + { + isValidContentType = true; + } + } + } + } + + // check whether we have everything for OTA update + if (contentLength && isValidContentType) + { + if (Update.begin(contentLength)) + { + Serial.println("Starting Over-The-Air update. This may take some time to complete ..."); + size_t written = Update.writeStream(client); + + if (written == contentLength) + { + Serial.println("Written : " + String(written) + " successfully"); + } + else + { + Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?"); + // Retry?? + } + + if (Update.end()) + { + if (Update.isFinished()) + { + Serial.println("OTA update has successfully completed. Rebooting ..."); + ESP.restart(); + } + else + { + Serial.println("Something went wrong! OTA update hasn't been finished properly."); + } + } + else + { + Serial.println("An error Occurred. Error #: " + String(Update.getError())); + } + } + else + { + Serial.println("There isn't enough space to start OTA update"); + client.flush(); + } + } + else + { + Serial.println("There was no valid content in the response from the OTA server!"); + client.flush(); + } +} \ No newline at end of file diff --git a/src/SecureOTA.h b/src/SecureOTA.h new file mode 100644 index 00000000..d8b17c3f --- /dev/null +++ b/src/SecureOTA.h @@ -0,0 +1,25 @@ +/* + Copyright (c) 2014-present PlatformIO + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +**/ + +#ifndef SECURE_OTA_H +#define SECURE_OTA_H + +#include + +void checkFirmwareUpdates(); +void processOTAUpdate(const String &version); + +#endif // SECURE_OTA_H \ No newline at end of file diff --git a/src/globals.h b/src/globals.h index a34851a7..1043b13a 100644 --- a/src/globals.h +++ b/src/globals.h @@ -52,6 +52,7 @@ extern portMUX_TYPE timerMux; extern volatile int SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, ChannelTimerIRQ, ButtonPressedIRQ; extern QueueHandle_t LoraSendQueue, SPISendQueue; +extern TaskHandle_t WifiLoopTask; extern std::array::iterator it; extern std::array beacons; diff --git a/src/main.cpp b/src/main.cpp index 1b258f14..b3825cb6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -41,6 +41,8 @@ hw_timer_t *channelSwitch = NULL, *displaytimer = NULL, *sendCycle = NULL, volatile int ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, SendCycleTimerIRQ = 0, DisplayTimerIRQ = 0, HomeCycleIRQ = 0; +TaskHandle_t WifiLoopTask = NULL; + // RTos send queues for payload transmit #ifdef HAS_LORA QueueHandle_t LoraSendQueue; @@ -298,7 +300,7 @@ void setup() { // gets it's seed from RF noise reset_salt(); // get new 16bit for salting hashes xTaskCreatePinnedToCore(wifi_channel_loop, "wifiloop", 2048, (void *)1, 1, - NULL, 0); + &WifiLoopTask, 0); } // setup() /* end Arduino SETUP diff --git a/src/rcommand.cpp b/src/rcommand.cpp index 8899a805..70ac4c8e 100644 --- a/src/rcommand.cpp +++ b/src/rcommand.cpp @@ -138,7 +138,7 @@ void set_loraadr(uint8_t val[]) { ESP_LOGI(TAG, "Remote command: set LoRa ADR mode to %s", val[0] ? "on" : "off"); cfg.adrmode = val[0] ? 1 : 0; -LMIC_setAdrMode(cfg.adrmode); + LMIC_setAdrMode(cfg.adrmode); #else ESP_LOGW(TAG, "Remote command: LoRa not implemented"); #endif // HAS_LORA @@ -219,6 +219,29 @@ void get_gps(uint8_t val[]) { #endif }; +void set_update(uint8_t val[]) { + ESP_LOGI(TAG, "Remote command: get firmware update"); + + ESP_LOGI(TAG, "Stopping Wifi task on core 0"); + vTaskDelete(WifiLoopTask); + + ESP_LOGI(TAG, "Connecting to %s", WIFI_SSID); + ESP_ERROR_CHECK(esp_wifi_set_promiscuous(false)); // switch off monitor mode + //tcpipInit(); + tcpip_adapter_init(); + WiFi.mode(WIFI_STA); + + WiFi.begin(WIFI_SSID, WIFI_PASS); + + while (WiFi.status() != WL_CONNECTED) { + ESP_LOGI(TAG, "."); + delay(500); + } + + ESP_LOGI(TAG, "connected!"); + checkFirmwareUpdates(); +}; + // assign previously defined functions to set of numeric remote commands // format: opcode, function, #bytes params, // flag (1 = do make settings persistent / 0 = don't) @@ -233,8 +256,8 @@ cmd_t table[] = { {0x0d, set_vendorfilter, 1, false}, {0x0e, set_blescan, 1, true}, {0x0f, set_wifiant, 1, true}, {0x10, set_rgblum, 1, true}, {0x11, set_monitor, 1, true}, {0x12, set_beacon, 7, false}, - {0x80, get_config, 0, false}, {0x81, get_status, 0, false}, - {0x84, get_gps, 0, false}}; + {0x20, set_update, 0, false}, {0x80, get_config, 0, false}, + {0x81, get_status, 0, false}, {0x84, get_gps, 0, false}}; const uint8_t cmdtablesize = sizeof(table) / sizeof(table[0]); // number of commands in command table diff --git a/src/rcommand.h b/src/rcommand.h index 6afb905f..2d1902a1 100644 --- a/src/rcommand.h +++ b/src/rcommand.h @@ -6,6 +6,9 @@ #include "lorawan.h" #include "macsniff.h" +#include +#include "SecureOTA.h" + // table of remote commands and assigned functions typedef struct { const uint8_t opcode; From cc603d4ab8f5b8662633e4512a31a02aa6c53486 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 12 Aug 2018 15:42:58 +0200 Subject: [PATCH 002/105] testing --- README.md | 1 + lib/BintrayClient/src/BintrayClient.cpp | 8 ++--- platformio.ini | 39 ++++++++++----------- src/OTA.cpp | 30 ++++++++++++++++ src/OTA.h | 11 ++++++ src/SecureOTA.cpp | 46 ++++++++++++------------- src/TTN/packed_decoder.js | 3 +- src/globals.h | 8 ++++- src/main.cpp | 5 ++- src/paxcounter.conf | 2 +- src/payload.cpp | 16 +++++---- src/payload.h | 5 ++- src/rcommand.cpp | 21 ++++------- src/rcommand.h | 2 ++ src/wifiscan.cpp | 1 + 15 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 src/OTA.cpp create mode 100644 src/OTA.h diff --git a/README.md b/README.md index 6d5b64d5..4d42fa2e 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Hereafter described is the default *plain* format, which uses MSB bit numbering. byte 3-10: Uptime [seconds] bytes 11-14: CPU temperature [°C] bytes 15-18: Free RAM [bytes] + bytes 19-20: Last reset reasons core 0 / core 1 **Port #3:** Device configuration query result diff --git a/lib/BintrayClient/src/BintrayClient.cpp b/lib/BintrayClient/src/BintrayClient.cpp index b0e7543f..4479f539 100644 --- a/lib/BintrayClient/src/BintrayClient.cpp +++ b/lib/BintrayClient/src/BintrayClient.cpp @@ -109,7 +109,7 @@ String BintrayClient::getLatestVersion() const const size_t bufferSize = 1024; if (jsonResult.length() > bufferSize) { - Serial.println("Error: Could parse JSON. Input data is too big!"); + ESP_LOGI(TAG, "Error: Could not parse JSON. Input data is too big!"); return version; } StaticJsonBuffer jsonBuffer; @@ -118,7 +118,7 @@ String BintrayClient::getLatestVersion() const // Check for errors in parsing if (!root.success()) { - Serial.println("Error: Could not parse JSON!"); + ESP_LOGI(TAG, "Error: Could not parse JSON!"); return version; } return root.get("name"); @@ -133,7 +133,7 @@ String BintrayClient::getBinaryPath(const String &version) const const size_t bufferSize = 1024; if (jsonResult.length() > bufferSize) { - Serial.println("Error: Could parse JSON. Input data is too big!"); + ESP_LOGI(TAG, "Error: Could parse JSON. Input data is too big!"); return path; } StaticJsonBuffer jsonBuffer; @@ -142,7 +142,7 @@ String BintrayClient::getBinaryPath(const String &version) const JsonObject &firstItem = root[0]; if (!root.success()) { //Check for errors in parsing - Serial.println("Error: Could not parse JSON!"); + ESP_LOGI(TAG, "Error: Could not parse JSON!"); return path; } return "/" + getUser() + "/" + getRepository() + "/" + firstItem.get("path"); diff --git a/platformio.ini b/platformio.ini index 91ee4e27..b07f4d49 100644 --- a/platformio.ini +++ b/platformio.ini @@ -56,25 +56,7 @@ build_flags = '-DBINTRAY_REPO="${bintray.repository}"' '-DBINTRAY_PACKAGE="${bintray.package}"' '-DVERSION=0' - -; extra dependencies -lib_deps = ArduinoJson - - -[common_env_data] -platform_espressif32 = espressif32@1.2.0 -;platform_espressif32 = https://github.com/platformio/platform-espressif32.git#feature/stage -;board_build.partitions = no_ota.csv -board_build.partitions = min_spiffs.csv -lib_deps_all = -lib_deps_display = - U8g2@>=2.23.12 -lib_deps_rgbled = - SmartLeds@>=1.1.3 -lib_deps_gps = - TinyGPSPlus@>=1.0.2 - Time@>=1.5 -build_flags = +; ; ---> NOTE: For production run set DEBUG_LEVEL level to NONE! <--- ; otherwise device may leak RAM ; @@ -90,13 +72,28 @@ build_flags = ; -DCORE_DEBUG_LEVEL=4 ; Verbose ; -DCORE_DEBUG_LEVEL=5 -; + +[common_env_data] +platform_espressif32 = espressif32@1.2.0 +;platform_espressif32 = https://github.com/platformio/platform-espressif32.git#feature/stage +;board_build.partitions = no_ota.csv +board_build.partitions = min_spiffs.csv +lib_deps_all = + ArduinoJson +lib_deps_display = + U8g2@>=2.23.12 +lib_deps_rgbled = + SmartLeds@>=1.1.3 +lib_deps_gps = + TinyGPSPlus@>=1.0.2 + Time@>=1.5 +build_flags = ; override lora settings from LMiC library in lmic/config.h and use main.h instead -D_lmic_config_h_ -include "src/paxcounter.conf" -include "src/hal/${PIOENV}.h" -w - + [env:ebox] platform = ${common_env_data.platform_espressif32} framework = arduino diff --git a/src/OTA.cpp b/src/OTA.cpp new file mode 100644 index 00000000..7b9849cd --- /dev/null +++ b/src/OTA.cpp @@ -0,0 +1,30 @@ +#include "OTA.h" + +const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); + +void ota_wifi_init(void) { + const int RESPONSE_TIMEOUT_MS = 5000; + unsigned long timeout = millis(); + + ESP_ERROR_CHECK(esp_wifi_set_promiscuous(false)); // switch off monitor mode + tcpip_adapter_init(); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); + + WiFi.begin(WIFI_SSID, WIFI_PASS); + WiFi.setHostname(PROGNAME); + +/* + while (WiFi.status() != WL_CONNECTED) { + ESP_LOGI(TAG, "WiFi Status %d", WiFi.status()); + if (millis() - timeout > RESPONSE_TIMEOUT_MS) { + ESP_LOGE(TAG, "WiFi connection timeout. Please check your settings!"); + } + + delay(500); + } + + configASSERT(WiFi.isConnected() == true); +*/ + +} \ No newline at end of file diff --git a/src/OTA.h b/src/OTA.h new file mode 100644 index 00000000..5133a685 --- /dev/null +++ b/src/OTA.h @@ -0,0 +1,11 @@ +#ifndef OTA_H +#define OTA_H + +#include +#include "globals.h" +#include +#include + +void ota_wifi_init(void); + +#endif // OTA_H \ No newline at end of file diff --git a/src/SecureOTA.cpp b/src/SecureOTA.cpp index 7496c19c..2ce69eca 100644 --- a/src/SecureOTA.cpp +++ b/src/SecureOTA.cpp @@ -37,16 +37,16 @@ void checkFirmwareUpdates() const String latest = bintray.getLatestVersion(); if (latest.length() == 0) { - Serial.println("Could not load info about the latest firmware, so nothing to update. Continue ..."); + ESP_LOGI(TAG, "Could not load info about the latest firmware, so nothing to update. Continue ..."); return; } else if (atoi(latest.c_str()) <= VERSION) { - //Serial.println("The current firmware is up to date. Continue ..."); + //ESP_LOGI(TAG, "The current firmware is up to date. Continue ..."); return; } - Serial.println("There is a new version of firmware available: v." + latest); + ESP_LOGI(TAG, "There is a new version of firmware available: v.%s", latest); processOTAUpdate(latest); } @@ -64,7 +64,7 @@ void processOTAUpdate(const String &version) String firmwarePath = bintray.getBinaryPath(version); if (!firmwarePath.endsWith(".bin")) { - Serial.println("Unsupported binary format. OTA update cannot be performed!"); + ESP_LOGI(TAG, "Unsupported binary format. OTA update cannot be performed!"); return; } @@ -76,7 +76,7 @@ void processOTAUpdate(const String &version) if (!client.connect(currentHost.c_str(), port)) { - Serial.println("Cannot connect to " + currentHost); + ESP_LOGI(TAG, "Cannot connect to %s", currentHost); return; } @@ -89,12 +89,12 @@ void processOTAUpdate(const String &version) client.setCACert(bintray.getCertificate(currentHost)); if (!client.connect(currentHost.c_str(), port)) { - Serial.println("Redirect detected! Cannot connect to " + currentHost + " for some reason!"); + ESP_LOGI(TAG, "Redirect detected! Cannot connect to %s for some reason!", currentHost); return; } } - //Serial.println("Requesting: " + firmwarePath); + //ESP_LOGI(TAG, "Requesting: " + firmwarePath); client.print(String("GET ") + firmwarePath + " HTTP/1.1\r\n"); client.print(String("Host: ") + currentHost + "\r\n"); @@ -106,7 +106,7 @@ void processOTAUpdate(const String &version) { if (millis() - timeout > RESPONSE_TIMEOUT_MS) { - Serial.println("Client Timeout !"); + ESP_LOGI(TAG, "Client Timeout !"); client.stop(); return; } @@ -128,17 +128,17 @@ void processOTAUpdate(const String &version) { if (line.indexOf("200") > 0) { - //Serial.println("Got 200 status code from server. Proceeding to firmware flashing"); + ESP_LOGI(TAG, "Got 200 status code from server. Proceeding to firmware flashing"); redirect = false; } else if (line.indexOf("302") > 0) { - //Serial.println("Got 302 status code from server. Redirecting to the new address"); + ESP_LOGI(TAG, "Got 302 status code from server. Redirecting to the new address"); redirect = true; } else { - //Serial.println("Could not get a valid firmware url"); + ESP_LOGI(TAG, "Could not get a valid firmware url"); //Unexptected HTTP response. Retry or skip update? redirect = false; } @@ -148,12 +148,12 @@ void processOTAUpdate(const String &version) if (line.startsWith("Location: ")) { String newUrl = getHeaderValue(line, "Location: "); - //Serial.println("Got new url: " + newUrl); + ESP_LOGI(TAG, "Got new url: %s", newUrl); newUrl.remove(0, newUrl.indexOf("//") + 2); currentHost = newUrl.substring(0, newUrl.indexOf('/')); newUrl.remove(newUrl.indexOf(currentHost), currentHost.length()); firmwarePath = newUrl; - //Serial.println("firmwarePath: " + firmwarePath); + ESP_LOGI(TAG, "firmwarePath: %s", firmwarePath); continue; } @@ -161,13 +161,13 @@ void processOTAUpdate(const String &version) if (line.startsWith("Content-Length: ")) { contentLength = atoi((getHeaderValue(line, "Content-Length: ")).c_str()); - Serial.println("Got " + String(contentLength) + " bytes from server"); + ESP_LOGI(TAG, "Got %s bytes from server", String(contentLength)); } if (line.startsWith("Content-Type: ")) { String contentType = getHeaderValue(line, "Content-Type: "); - //Serial.println("Got " + contentType + " payload."); + ESP_LOGI(TAG, "Got %s payload", contentType); if (contentType == "application/octet-stream") { isValidContentType = true; @@ -181,16 +181,16 @@ void processOTAUpdate(const String &version) { if (Update.begin(contentLength)) { - Serial.println("Starting Over-The-Air update. This may take some time to complete ..."); + ESP_LOGI(TAG, "Starting Over-The-Air update. This may take some time to complete ..."); size_t written = Update.writeStream(client); if (written == contentLength) { - Serial.println("Written : " + String(written) + " successfully"); + ESP_LOGI(TAG, "Written %s successfully", String(written)); } else { - Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?"); + ESP_LOGI(TAG, "Written only %s / %s Retry?", String(written), String(contentLength)); // Retry?? } @@ -198,28 +198,28 @@ void processOTAUpdate(const String &version) { if (Update.isFinished()) { - Serial.println("OTA update has successfully completed. Rebooting ..."); + ESP_LOGI(TAG, "OTA update has successfully completed. Rebooting ..."); ESP.restart(); } else { - Serial.println("Something went wrong! OTA update hasn't been finished properly."); + ESP_LOGI(TAG, "Something went wrong! OTA update hasn't been finished properly."); } } else { - Serial.println("An error Occurred. Error #: " + String(Update.getError())); + ESP_LOGI(TAG, "An error occurred. Error #: %s", String(Update.getError())); } } else { - Serial.println("There isn't enough space to start OTA update"); + ESP_LOGI(TAG, "There isn't enough space to start OTA update"); client.flush(); } } else { - Serial.println("There was no valid content in the response from the OTA server!"); + ESP_LOGI(TAG, "There was no valid content in the response from the OTA server!"); client.flush(); } } \ No newline at end of file diff --git a/src/TTN/packed_decoder.js b/src/TTN/packed_decoder.js index 6bf29f09..32d6dc9f 100644 --- a/src/TTN/packed_decoder.js +++ b/src/TTN/packed_decoder.js @@ -18,7 +18,7 @@ function Decoder(bytes, port) { if (port === 2) { // device status data - return decode(bytes, [uint16, uptime, temperature, uint32], ['voltage', 'uptime', 'cputemp', 'memory']); + return decode(bytes, [uint16, uptime, temperature, uint32, uint8, uint8], ['voltage', 'uptime', 'cputemp', 'memory', 'reset', 'reset']); } @@ -181,6 +181,7 @@ if (typeof module === 'object' && typeof module.exports !== 'undefined') { uint16: uint16, uint32: uint32, uptime: uptime, + reset: reset, temperature: temperature, humidity: humidity, latLng: latLng, diff --git a/src/globals.h b/src/globals.h index 1043b13a..879a30e1 100644 --- a/src/globals.h +++ b/src/globals.h @@ -51,7 +51,7 @@ extern hw_timer_t *channelSwitch, *sendCycle; extern portMUX_TYPE timerMux; extern volatile int SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, ChannelTimerIRQ, ButtonPressedIRQ; -extern QueueHandle_t LoraSendQueue, SPISendQueue; +//extern QueueHandle_t LoraSendQueue, SPISendQueue; extern TaskHandle_t WifiLoopTask; extern std::array::iterator it; @@ -68,9 +68,15 @@ extern std::array beacons; #include "payload.h" #ifdef HAS_LORA +extern QueueHandle_t LoraSendQueue; +extern TaskHandle_t LoraTask; #include "lorawan.h" #endif +#ifdef HAS_SPI +extern QueueHandle_t SPISendQueue; +#endif + #ifdef HAS_DISPLAY #include "display.h" #endif diff --git a/src/main.cpp b/src/main.cpp index b3825cb6..fe5d5742 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,6 +46,7 @@ TaskHandle_t WifiLoopTask = NULL; // RTos send queues for payload transmit #ifdef HAS_LORA QueueHandle_t LoraSendQueue; +TaskHandle_t LoraTask = NULL; #endif #ifdef HAS_SPI @@ -271,7 +272,7 @@ void setup() { ESP_LOGI(TAG, "Starting Lora task on core 1"); xTaskCreatePinnedToCore(lorawan_loop, "loraloop", 2048, (void *)1, - (5 | portPRIVILEGE_BIT), NULL, 1); + (5 | portPRIVILEGE_BIT), &LoraTask, 1); #endif // if device has GPS and it is enabled, start GPS reader task on core 0 with @@ -332,6 +333,8 @@ void loop() { processSendBuffer(); // check send cycle and enqueue payload if cycle is expired sendPayload(); + // reset watchdog + vTaskDelay(1 / portTICK_PERIOD_MS); } // loop() } diff --git a/src/paxcounter.conf b/src/paxcounter.conf index 7cf76677..bcecc56d 100644 --- a/src/paxcounter.conf +++ b/src/paxcounter.conf @@ -9,7 +9,7 @@ // Payload send cycle and encoding #define SEND_SECS 30 // payload send cycle [seconds/2] -> 60 sec. -#define PAYLOAD_ENCODER 1 // payload encoder: 1=Plain, 2=Packed, 3=CayenneLPP dynamic, 4=CayenneLPP packed +#define PAYLOAD_ENCODER 2 // payload encoder: 1=Plain, 2=Packed, 3=CayenneLPP dynamic, 4=CayenneLPP packed // Set this to include BLE counting and vendor filter functions #define VENDORFILTER 1 // comment out if you want to count things, not people diff --git a/src/payload.cpp b/src/payload.cpp index ab3d2c91..958d3fb3 100644 --- a/src/payload.cpp +++ b/src/payload.cpp @@ -52,8 +52,8 @@ void PayloadConvert::addConfig(configData_t value) { cursor += 10; } -void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, - float cputemp, uint32_t mem) { +void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, float cputemp, + uint32_t mem, uint8_t reset1, uint8_t reset2) { uint32_t temp = (uint32_t)cputemp; buffer[cursor++] = highByte(voltage); buffer[cursor++] = lowByte(voltage); @@ -73,6 +73,8 @@ void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, buffer[cursor++] = (byte)((mem & 0x00FF0000) >> 16); buffer[cursor++] = (byte)((mem & 0x0000FF00) >> 8); buffer[cursor++] = (byte)((mem & 0x000000FF)); + buffer[cursor++] = (byte)(reset1); + buffer[cursor++] = (byte)(reset2); } #ifdef HAS_GPS @@ -127,12 +129,14 @@ void PayloadConvert::addConfig(configData_t value) { value.vendorfilter ? true : false, value.gpsmode ? true : false); } -void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, - float cputemp, uint32_t mem) { +void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, float cputemp, + uint32_t mem, uint8_t reset1, uint8_t reset2) { writeUint16(voltage); writeUptime(uptime); writeTemperature(cputemp); writeUint32(mem); + writeUint8(reset1); + writeUint8(reset2); } #ifdef HAS_GPS @@ -245,8 +249,8 @@ void PayloadConvert::addConfig(configData_t value) { buffer[cursor++] = value.adrmode; } -void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, - float celsius, uint32_t mem) { +void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, float celsius, + uint32_t mem, uint8_t reset1, uint8_t reset2) { uint16_t temp = celsius * 10; uint16_t volt = voltage / 10; #ifdef HAS_BATTERY_PROBE diff --git a/src/payload.h b/src/payload.h index ba564ec7..92f2d4ee 100644 --- a/src/payload.h +++ b/src/payload.h @@ -35,7 +35,8 @@ public: uint8_t *getBuffer(void); void addCount(uint16_t value1, uint16_t value2); void addConfig(configData_t value); - void addStatus(uint16_t voltage, uint64_t uptime, float cputemp, uint32_t mem); + void addStatus(uint16_t voltage, uint64_t uptime, float cputemp, uint32_t mem, + uint8_t reset1, uint8_t reset2); void addAlarm(int8_t rssi, uint8_t message); #ifdef HAS_GPS void addGPS(gpsStatus_t value); @@ -44,7 +45,6 @@ public: void addButton(uint8_t value); #endif - #if PAYLOAD_ENCODER == 1 // format plain private: @@ -77,7 +77,6 @@ private: #else #error "No valid payload converter defined" #endif - }; extern PayloadConvert payload; diff --git a/src/rcommand.cpp b/src/rcommand.cpp index 70ac4c8e..63fcd690 100644 --- a/src/rcommand.cpp +++ b/src/rcommand.cpp @@ -203,7 +203,8 @@ void get_status(uint8_t val[]) { #endif payload.reset(); payload.addStatus(voltage, uptime() / 1000, temperatureRead(), - ESP.getFreeHeap()); + ESP.getFreeHeap(), rtc_get_reset_reason(0), + rtc_get_reset_reason(1)); SendData(STATUSPORT); }; @@ -225,21 +226,13 @@ void set_update(uint8_t val[]) { ESP_LOGI(TAG, "Stopping Wifi task on core 0"); vTaskDelete(WifiLoopTask); + ESP_LOGI(TAG, "Stopping LORA task on core 1"); + vTaskDelete(LoraTask); + ESP_LOGI(TAG, "Connecting to %s", WIFI_SSID); - ESP_ERROR_CHECK(esp_wifi_set_promiscuous(false)); // switch off monitor mode - //tcpipInit(); - tcpip_adapter_init(); - WiFi.mode(WIFI_STA); - - WiFi.begin(WIFI_SSID, WIFI_PASS); - - while (WiFi.status() != WL_CONNECTED) { - ESP_LOGI(TAG, "."); - delay(500); - } - - ESP_LOGI(TAG, "connected!"); + ota_wifi_init(); checkFirmwareUpdates(); + }; // assign previously defined functions to set of numeric remote commands diff --git a/src/rcommand.h b/src/rcommand.h index 2d1902a1..880382ed 100644 --- a/src/rcommand.h +++ b/src/rcommand.h @@ -5,6 +5,8 @@ #include "configmanager.h" #include "lorawan.h" #include "macsniff.h" +#include +#include "ota.h" #include #include "SecureOTA.h" diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 4190e182..9cb2ae98 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -36,6 +36,7 @@ void wifi_sniffer_init(void) { ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM)); // we don't need NVRAM ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL)); + ESP_ERROR_CHECK(esp_wifi_stop()); ESP_ERROR_CHECK( esp_wifi_set_promiscuous_filter(&filter)); // set MAC frame filter ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler)); From 0ee138b02229357a8653d3ac637a926ec35d2e8e Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 12 Aug 2018 23:42:39 +0200 Subject: [PATCH 003/105] JFrog Bintray OTA (experimental, not working yet) --- platformio.ini | 8 ++---- src/OTA.cpp | 64 ++++++++++++++++++++++++++++++++++++------------ src/OTA.h | 4 ++- src/cyclic.cpp | 4 +++ src/globals.h | 5 ++-- src/main.cpp | 11 ++++++--- src/rcommand.cpp | 12 +-------- src/rcommand.h | 4 --- 8 files changed, 69 insertions(+), 43 deletions(-) diff --git a/platformio.ini b/platformio.ini index b07f4d49..ddf0adc7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -32,14 +32,10 @@ user = cyberman54 repository = paxcounter package = esp32-paxcounter api_token = 9f02e2a2374c278fd79d5bcf4b4442fca9752012 -;api_token = ${env.BINTRAY_API_TOKEN} -; Wi-Fi network settings [wifi] -ssid = PRENZLNET-G -password = 435Huse8!? -;ssid = ${env.PIO_WIFI_SSID} -;password = ${env.PIO_WIFI_PASSWORD} +ssid = *** +password = *** [common] platform = https://github.com/platformio/platform-espressif32.git diff --git a/src/OTA.cpp b/src/OTA.cpp index 7b9849cd..23d64f5c 100644 --- a/src/OTA.cpp +++ b/src/OTA.cpp @@ -2,29 +2,61 @@ const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); -void ota_wifi_init(void) { - const int RESPONSE_TIMEOUT_MS = 5000; - unsigned long timeout = millis(); +bool Wifi_Connected = false; - ESP_ERROR_CHECK(esp_wifi_set_promiscuous(false)); // switch off monitor mode +static esp_err_t event_handler(void *ctx, system_event_t *event) { + switch (event->event_id) { + case SYSTEM_EVENT_STA_START: + esp_wifi_connect(); + break; + case SYSTEM_EVENT_STA_GOT_IP: + Wifi_Connected = true; + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + Wifi_Connected = false; + break; + default: + break; + } +} + +void ota_wifi_init(void) { + + // initialize the tcp stack tcpip_adapter_init(); + + // initialize the wifi event handler + ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + wifi_config_t sta_config = {}; + + strcpy((char *)sta_config.sta.ssid, WIFI_SSID); + strcpy((char *)sta_config.sta.password, WIFI_PASS); + sta_config.sta.bssid_set = false; + + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); ESP_ERROR_CHECK(esp_wifi_start()); - WiFi.begin(WIFI_SSID, WIFI_PASS); - WiFi.setHostname(PROGNAME); + // print the local IP address + tcpip_adapter_ip_info_t ip_info; + ESP_ERROR_CHECK(tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info)); + ESP_LOGI(TAG, "IP %s", ip4addr_ntoa(&ip_info.ip)); -/* - while (WiFi.status() != WL_CONNECTED) { - ESP_LOGI(TAG, "WiFi Status %d", WiFi.status()); - if (millis() - timeout > RESPONSE_TIMEOUT_MS) { - ESP_LOGE(TAG, "WiFi connection timeout. Please check your settings!"); - } +} - delay(500); - } +void start_ota_update() { + ESP_LOGI(TAG, "Stopping Wifi task on core 0"); + vTaskDelete(WifiLoopTask); - configASSERT(WiFi.isConnected() == true); -*/ + ESP_LOGI(TAG, "Stopping LORA task on core 1"); + vTaskDelete(LoraTask); + ESP_LOGI(TAG, "Connecting to %s", WIFI_SSID); + ota_wifi_init(); + checkFirmwareUpdates(); + ESP.restart(); // reached if update was not successful } \ No newline at end of file diff --git a/src/OTA.h b/src/OTA.h index 5133a685..8b6a27df 100644 --- a/src/OTA.h +++ b/src/OTA.h @@ -5,7 +5,9 @@ #include "globals.h" #include #include +#include "ota.h" +#include "SecureOTA.h" -void ota_wifi_init(void); +void start_ota_update(); #endif // OTA_H \ No newline at end of file diff --git a/src/cyclic.cpp b/src/cyclic.cpp index adbadda4..45ce16cd 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -4,6 +4,7 @@ // Basic config #include "globals.h" #include "senddata.h" +#include "ota.h" // Local logging tag static const char TAG[] = "main"; @@ -14,6 +15,9 @@ void doHomework() { // update uptime counter uptime(); + if (ota_update) + start_ota_update(); + // read battery voltage into global variable #ifdef HAS_BATTERY_PROBE batt_voltage = read_voltage(); diff --git a/src/globals.h b/src/globals.h index 879a30e1..53aad820 100644 --- a/src/globals.h +++ b/src/globals.h @@ -42,7 +42,8 @@ typedef struct { } MessageBuffer_t; // global variables -extern configData_t cfg; // current device configuration +extern configData_t cfg; // current device configuration +extern bool ota_update; extern char display_line6[], display_line7[]; // screen buffers extern uint8_t channel; // wifi channel rotation counter extern uint16_t macs_total, macs_wifi, macs_ble, batt_voltage; // display values @@ -51,7 +52,7 @@ extern hw_timer_t *channelSwitch, *sendCycle; extern portMUX_TYPE timerMux; extern volatile int SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, ChannelTimerIRQ, ButtonPressedIRQ; -//extern QueueHandle_t LoraSendQueue, SPISendQueue; +// extern QueueHandle_t LoraSendQueue, SPISendQueue; extern TaskHandle_t WifiLoopTask; extern std::array::iterator it; diff --git a/src/main.cpp b/src/main.cpp index fe5d5742..d52b4a7a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,7 +27,8 @@ licenses. Refer to LICENSE.txt file in repository for more details. #include "globals.h" #include "main.h" -configData_t cfg; // struct holds current device configuration +configData_t cfg; // struct holds current device configuration +bool ota_update = false; // triggers OTA update char display_line6[16], display_line7[16]; // display buffers uint8_t channel = 0; // channel rotation counter uint16_t macs_total = 0, macs_wifi = 0, macs_ble = 0, @@ -70,6 +71,9 @@ static const char TAG[] = "main"; void setup() { + // disable the default wifi logging + esp_log_level_set("wifi", ESP_LOG_NONE); + char features[100] = ""; // disable brownout detection @@ -92,7 +96,8 @@ void setup() { // initialize system event handler for wifi task, needed for // wifi_sniffer_init() - esp_event_loop_init(NULL, NULL); + // esp_event_loop_init(NULL, NULL); + //ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); // print chip information on startup if in verbose mode #ifdef VERBOSE @@ -333,7 +338,7 @@ void loop() { processSendBuffer(); // check send cycle and enqueue payload if cycle is expired sendPayload(); - // reset watchdog + // reset watchdog vTaskDelay(1 / portTICK_PERIOD_MS); } // loop() diff --git a/src/rcommand.cpp b/src/rcommand.cpp index 63fcd690..a052c48e 100644 --- a/src/rcommand.cpp +++ b/src/rcommand.cpp @@ -222,17 +222,7 @@ void get_gps(uint8_t val[]) { void set_update(uint8_t val[]) { ESP_LOGI(TAG, "Remote command: get firmware update"); - - ESP_LOGI(TAG, "Stopping Wifi task on core 0"); - vTaskDelete(WifiLoopTask); - - ESP_LOGI(TAG, "Stopping LORA task on core 1"); - vTaskDelete(LoraTask); - - ESP_LOGI(TAG, "Connecting to %s", WIFI_SSID); - ota_wifi_init(); - checkFirmwareUpdates(); - + ota_update = true; }; // assign previously defined functions to set of numeric remote commands diff --git a/src/rcommand.h b/src/rcommand.h index 880382ed..c656c9fd 100644 --- a/src/rcommand.h +++ b/src/rcommand.h @@ -6,10 +6,6 @@ #include "lorawan.h" #include "macsniff.h" #include -#include "ota.h" - -#include -#include "SecureOTA.h" // table of remote commands and assigned functions typedef struct { From 7a58c8dece3a8a3e0384b8c748633e82cc362314 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 16 Aug 2018 21:10:13 +0200 Subject: [PATCH 004/105] testing --- src/OTA.cpp | 47 ++++++++++++++++++++++++++++++++--------------- src/wifiscan.cpp | 2 +- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/OTA.cpp b/src/OTA.cpp index 23d64f5c..1ec3be8e 100644 --- a/src/OTA.cpp +++ b/src/OTA.cpp @@ -4,16 +4,23 @@ const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); bool Wifi_Connected = false; -static esp_err_t event_handler(void *ctx, system_event_t *event) { +esp_err_t event_handler(void *ctx, system_event_t *event) { switch (event->event_id) { case SYSTEM_EVENT_STA_START: esp_wifi_connect(); + ESP_LOGI(TAG, "Event STA_START"); break; case SYSTEM_EVENT_STA_GOT_IP: Wifi_Connected = true; + ESP_LOGI(TAG, "Event STA_GOT_IP"); + // print the local IP address + tcpip_adapter_ip_info_t ip_info; + ESP_ERROR_CHECK(tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info)); + ESP_LOGI(TAG, "IP %s", ip4addr_ntoa(&ip_info.ip)); break; case SYSTEM_EVENT_STA_DISCONNECTED: Wifi_Connected = false; + ESP_LOGI(TAG, "Event STA_DISCONNECTED"); break; default: break; @@ -22,30 +29,38 @@ static esp_err_t event_handler(void *ctx, system_event_t *event) { void ota_wifi_init(void) { + tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_STA; + // initialize the tcp stack + // nvs_flash_init(); tcpip_adapter_init(); + tcpip_adapter_set_hostname(tcpip_if, PROGNAME); + tcpip_adapter_dhcpc_start(tcpip_if); // initialize the wifi event handler ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - wifi_config_t sta_config = {}; + // switch off monitor more + ESP_ERROR_CHECK( + esp_wifi_set_promiscuous(false)); // now switch on monitor mode + ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(NULL)); - strcpy((char *)sta_config.sta.ssid, WIFI_SSID); - strcpy((char *)sta_config.sta.password, WIFI_PASS); - sta_config.sta.bssid_set = false; + wifi_sta_config_t cfg; + strcpy((char *)cfg.ssid, WIFI_SSID); + strcpy((char *)cfg.password, WIFI_PASS); + cfg.bssid_set = false; - ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + wifi_config_t sta_cfg; + sta_cfg.sta = cfg; + + wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT(); + + ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg)); + ESP_ERROR_CHECK( + esp_wifi_set_storage(WIFI_STORAGE_RAM)); // we don't need NVRAM ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); - ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_cfg)); ESP_ERROR_CHECK(esp_wifi_start()); - - // print the local IP address - tcpip_adapter_ip_info_t ip_info; - ESP_ERROR_CHECK(tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info)); - ESP_LOGI(TAG, "IP %s", ip4addr_ntoa(&ip_info.ip)); - } void start_ota_update() { @@ -57,6 +72,8 @@ void start_ota_update() { ESP_LOGI(TAG, "Connecting to %s", WIFI_SSID); ota_wifi_init(); + delay(2000); + delay(2000); checkFirmwareUpdates(); ESP.restart(); // reached if update was not successful } \ No newline at end of file diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 9cb2ae98..24d6504f 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -6,7 +6,7 @@ static const char TAG[] = "wifi"; static wifi_country_t wifi_country = {WIFI_MY_COUNTRY, WIFI_CHANNEL_MIN, - WIFI_CHANNEL_MAX, 0, + WIFI_CHANNEL_MAX, 100, WIFI_COUNTRY_POLICY_MANUAL}; // using IRAM_:ATTR here to speed up callback function From 48734bca632350cd8a55af68a7eae49f8d982e5a Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 13 Sep 2018 21:50:45 +0200 Subject: [PATCH 005/105] lmic/radio.c: Fix RSSI and SNR calculation --- lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c b/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c index 780226c2..0fffaa4f 100644 --- a/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c +++ b/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c @@ -790,8 +790,12 @@ void radio_irq_handler (u1_t dio) { // now read the FIFO readBuf(RegFifo, LMIC.frame, LMIC.dataLen); // read rx quality parameters - LMIC.snr = readReg(LORARegPktSnrValue); // SNR [dB] * 4 - LMIC.rssi = readReg(LORARegPktRssiValue) - 125 + 64; // RSSI [dBm] (-196...+63) + //LMIC.snr = readReg(LORARegPktSnrValue); // SNR [dB] * 4 + LMIC.snr = readReg(LORARegPktSnrValue) / 4; + //LMIC.rssi = readReg(LORARegPktRssiValue) - 125 + 64; // RSSI [dBm] (-196...+63) + LMIC.rssi = readReg(LORARegPktRssiValue) - 157; // RFI_HF for 868 and 915MHZ band + if (LMIC.snr < 0) + LMIC.rssi += LMIC.snr; } else if( flags & IRQ_LORA_RXTOUT_MASK ) { // indicate timeout LMIC.dataLen = 0; From 38dc2667abd921cfb529c06af5f492d2422e766a Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 14:47:13 +0200 Subject: [PATCH 006/105] ota-test first push --- platformio.ini | 17 +++++++---- publish_firmware.py | 71 +++++++++++++++++++++++++++++++++++++++++++++ src/SecureOTA.cpp | 26 ++++++++++------- 3 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 publish_firmware.py diff --git a/platformio.ini b/platformio.ini index ddf0adc7..65eb1107 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,6 +9,7 @@ ; http://docs.platformio.org/page/projectconf.html + ; ---> SELECT TARGET PLATFORM HERE! <--- [platformio] ;env_default = generic @@ -29,9 +30,9 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [bintray] user = cyberman54 -repository = paxcounter -package = esp32-paxcounter -api_token = 9f02e2a2374c278fd79d5bcf4b4442fca9752012 +repository = paxcounter-firmware +package = ttgov21_old +api_token = *** [wifi] ssid = *** @@ -42,7 +43,8 @@ platform = https://github.com/platformio/platform-espressif32.git ; firmware version, please modify it between releases ; positive integer value -release_version = 1 +;release_version = 1.4.30 +release_version = 4 ; build configuration based on Bintray and Wi-Fi settings build_flags = @@ -51,7 +53,7 @@ build_flags = '-DBINTRAY_USER="${bintray.user}"' '-DBINTRAY_REPO="${bintray.repository}"' '-DBINTRAY_PACKAGE="${bintray.package}"' - '-DVERSION=0' + -DVERSION=${common.release_version} ; ; ---> NOTE: For production run set DEBUG_LEVEL level to NONE! <--- ; otherwise device may leak RAM @@ -70,7 +72,7 @@ build_flags = ; -DCORE_DEBUG_LEVEL=5 [common_env_data] -platform_espressif32 = espressif32@1.2.0 +platform_espressif32 = espressif32@1.3.0 ;platform_espressif32 = https://github.com/platformio/platform-espressif32.git#feature/stage ;board_build.partitions = no_ota.csv board_build.partitions = min_spiffs.csv @@ -142,6 +144,7 @@ build_flags = ${common_env_data.build_flags} [env:ttgov21] +bintraypackage = ttgov21_old platform = ${common_env_data.platform_espressif32} framework = arduino board = esp32dev @@ -154,6 +157,8 @@ lib_deps = build_flags = ${common.build_flags} ${common_env_data.build_flags} +upload_protocol = custom +extra_scripts = pre:publish_firmware.py [env:ttgobeam] platform = ${common_env_data.platform_espressif32} diff --git a/publish_firmware.py b/publish_firmware.py new file mode 100644 index 00000000..7086d013 --- /dev/null +++ b/publish_firmware.py @@ -0,0 +1,71 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests +from os.path import basename +from platformio import util + +Import('env') + +project_config = util.load_project_config() +bintray_config = {k: v for k, v in project_config.items("bintray")} +version = project_config.get("common", "release_version") + +# +# Push new firmware to the Bintray storage using API +# + + +def publish_firmware(source, target, env): + firmware_path = str(source[0]) + firmware_name = basename(firmware_path) + + print("Uploading {0} to Bintray. Version: {1}".format( + firmware_name, version)) + + print(firmware_path, firmware_name) + + url = "/".join([ + "https://api.bintray.com", "content", + bintray_config.get("user"), + bintray_config.get("repository"), + bintray_config.get("package"), version, firmware_name + ]) + + print(url) + + headers = { + "Content-type": "application/octet-stream", + "X-Bintray-Publish": "1", + "X-Bintray-Override": "1" + } + + r = requests.put( + url, + data=open(firmware_path, "rb"), + headers=headers, + auth=(bintray_config.get("user"), bintray_config['api_token'])) + + if r.status_code != 201: + print("Failed to submit package: {0}\n{1}".format( + r.status_code, r.text)) + else: + print("The firmware has been successfuly published at Bintray.com!") + + +# Custom upload command and program name +env.Replace( + PROGNAME="firmware_v_%s" % version, + UPLOADCMD=publish_firmware +) \ No newline at end of file diff --git a/src/SecureOTA.cpp b/src/SecureOTA.cpp index 2ce69eca..8b35c93b 100644 --- a/src/SecureOTA.cpp +++ b/src/SecureOTA.cpp @@ -31,9 +31,13 @@ const uint32_t RESPONSE_TIMEOUT_MS = 5000; volatile int contentLength = 0; volatile bool isValidContentType = false; +// Local logging tag +static const char TAG[] = "main"; + void checkFirmwareUpdates() { // Fetch the latest firmware version + ESP_LOGI(TAG, "Checking latest firmware version..."); const String latest = bintray.getLatestVersion(); if (latest.length() == 0) { @@ -42,11 +46,11 @@ void checkFirmwareUpdates() } else if (atoi(latest.c_str()) <= VERSION) { - //ESP_LOGI(TAG, "The current firmware is up to date. Continue ..."); + ESP_LOGI(TAG, "The current firmware is up to date. Continue ..."); return; } - ESP_LOGI(TAG, "There is a new version of firmware available: v.%s", latest); + ESP_LOGI(TAG, "There is a new version of firmware available: v.%s", latest.c_str()); processOTAUpdate(latest); } @@ -76,7 +80,7 @@ void processOTAUpdate(const String &version) if (!client.connect(currentHost.c_str(), port)) { - ESP_LOGI(TAG, "Cannot connect to %s", currentHost); + ESP_LOGI(TAG, "Cannot connect to %s", currentHost.c_str()); return; } @@ -89,7 +93,7 @@ void processOTAUpdate(const String &version) client.setCACert(bintray.getCertificate(currentHost)); if (!client.connect(currentHost.c_str(), port)) { - ESP_LOGI(TAG, "Redirect detected! Cannot connect to %s for some reason!", currentHost); + ESP_LOGI(TAG, "Redirect detected! Cannot connect to %s for some reason!", currentHost.c_str()); return; } } @@ -148,12 +152,12 @@ void processOTAUpdate(const String &version) if (line.startsWith("Location: ")) { String newUrl = getHeaderValue(line, "Location: "); - ESP_LOGI(TAG, "Got new url: %s", newUrl); + ESP_LOGI(TAG, "Got new url: %s", newUrl.c_str()); newUrl.remove(0, newUrl.indexOf("//") + 2); currentHost = newUrl.substring(0, newUrl.indexOf('/')); newUrl.remove(newUrl.indexOf(currentHost), currentHost.length()); firmwarePath = newUrl; - ESP_LOGI(TAG, "firmwarePath: %s", firmwarePath); + ESP_LOGI(TAG, "firmwarePath: %s", firmwarePath.c_str()); continue; } @@ -161,13 +165,13 @@ void processOTAUpdate(const String &version) if (line.startsWith("Content-Length: ")) { contentLength = atoi((getHeaderValue(line, "Content-Length: ")).c_str()); - ESP_LOGI(TAG, "Got %s bytes from server", String(contentLength)); + ESP_LOGI(TAG, "Got %d bytes from server", contentLength); } if (line.startsWith("Content-Type: ")) { String contentType = getHeaderValue(line, "Content-Type: "); - ESP_LOGI(TAG, "Got %s payload", contentType); + ESP_LOGI(TAG, "Got %s payload", contentType.c_str()); if (contentType == "application/octet-stream") { isValidContentType = true; @@ -186,11 +190,11 @@ void processOTAUpdate(const String &version) if (written == contentLength) { - ESP_LOGI(TAG, "Written %s successfully", String(written)); + ESP_LOGI(TAG, "Written %d successfully", written); } else { - ESP_LOGI(TAG, "Written only %s / %s Retry?", String(written), String(contentLength)); + ESP_LOGI(TAG, "Written only %d / %d Retry?", written, contentLength); // Retry?? } @@ -208,7 +212,7 @@ void processOTAUpdate(const String &version) } else { - ESP_LOGI(TAG, "An error occurred. Error #: %s", String(Update.getError())); + ESP_LOGI(TAG, "An error occurred. Error #: %d", Update.getError()); } } else From 460606f6a4354e80910f1ebdc6fe2b640236688c Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 14:52:46 +0200 Subject: [PATCH 007/105] protect platformio.ini against github upload --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 030b6f3d..f1f6d754 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ .clang_complete .gcc-flags.json src/loraconf.h +platformio.ini \ No newline at end of file From ffd7260223eddb30cfdc73368c6ee9e1abd60113 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 14:53:34 +0200 Subject: [PATCH 008/105] add keys to platformio.ini wifi section --- platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index d6e092c0..764dd17c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -35,8 +35,8 @@ package = ttgov21_old api_token = *** [wifi] -ssid = *** -password = *** +ssid = testnet +password = test0815 [common] platform = https://github.com/platformio/platform-espressif32.git From 11e3ee8a8819a8d220f0eeaf7873e53ea955b4b8 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 14:55:53 +0200 Subject: [PATCH 009/105] testing --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 764dd17c..975578c6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -32,7 +32,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng user = cyberman54 repository = paxcounter-firmware package = ttgov21_old -api_token = *** +api_token = **** [wifi] ssid = testnet From 7bd0d4d2c3c0ae30c026f50c2a324076b09323c7 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 15:02:03 +0200 Subject: [PATCH 010/105] remove platformio.ini from git working tree --- platformio.ini | 271 ------------------------------------------------- 1 file changed, 271 deletions(-) delete mode 100644 platformio.ini diff --git a/platformio.ini b/platformio.ini deleted file mode 100644 index 975578c6..00000000 --- a/platformio.ini +++ /dev/null @@ -1,271 +0,0 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; http://docs.platformio.org/page/projectconf.html - - - -; ---> SELECT TARGET PLATFORM HERE! <--- -[platformio] -;env_default = generic -;env_default = ebox -;env_default = heltec -;env_default = ttgov1 -;env_default = ttgov2 -env_default = ttgov21 -;env_default = ttgobeam -;env_default = lopy -;env_default = lopy4 -;env_default = fipy -;env_default = lolin32litelora -;env_default = lolin32lora -;env_default = lolin32lite -; -description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around. - -[bintray] -user = cyberman54 -repository = paxcounter-firmware -package = ttgov21_old -api_token = **** - -[wifi] -ssid = testnet -password = test0815 - -[common] -platform = https://github.com/platformio/platform-espressif32.git - -; firmware version, please modify it between releases -; positive integer value -;release_version = 1.4.30 -release_version = 4 - -; build configuration based on Bintray and Wi-Fi settings -build_flags = - '-DWIFI_SSID="${wifi.ssid}"' - '-DWIFI_PASS="${wifi.password}"' - '-DBINTRAY_USER="${bintray.user}"' - '-DBINTRAY_REPO="${bintray.repository}"' - '-DBINTRAY_PACKAGE="${bintray.package}"' - -DVERSION=${common.release_version} -; -; ---> NOTE: For production run set DEBUG_LEVEL level to NONE! <--- -; otherwise device may leak RAM -; -; None -; -DCORE_DEBUG_LEVEL=0 -; Error -; -DCORE_DEBUG_LEVEL=1 -; Warn -; -DCORE_DEBUG_LEVEL=2 -; Info - -DCORE_DEBUG_LEVEL=3 -; Debug -; -DCORE_DEBUG_LEVEL=4 -; Verbose -; -DCORE_DEBUG_LEVEL=5 - -[common_env_data] -platform_espressif32 = espressif32@1.3.0 -;platform_espressif32 = https://github.com/platformio/platform-espressif32.git#feature/stage -;board_build.partitions = no_ota.csv -board_build.partitions = min_spiffs.csv -lib_deps_all = - ArduinoJson -lib_deps_display = - U8g2@>=2.23.16 -lib_deps_rgbled = - SmartLeds@>=1.1.3 -lib_deps_gps = - TinyGPSPlus@>=1.0.2 - Time@>=1.5 -build_flags = -; override lora settings from LMiC library in lmic/config.h and use main.h instead - -D_lmic_config_h_ - -include "src/paxcounter.conf" - -include "src/hal/${PIOENV}.h" - -w - -[env:ebox] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 115200 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} -build_flags = - ${common_env_data.build_flags} - -[env:heltec] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = heltec_wifi_lora_32 -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 115200 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_display} -build_flags = - ${common_env_data.build_flags} - -[env:ttgov1] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 115200 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_display} -build_flags = - ${common_env_data.build_flags} - -[env:ttgov2] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_display} -build_flags = - ${common_env_data.build_flags} - -[env:ttgov21] -bintraypackage = ttgov21_old -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_display} -build_flags = - ${common.build_flags} - ${common_env_data.build_flags} -upload_protocol = custom -extra_scripts = pre:publish_firmware.py - -[env:ttgobeam] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_gps} -build_flags = - ${common_env_data.build_flags} - -mfix-esp32-psram-cache-issue - -[env:fipy] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} -build_flags = - ${common_env_data.build_flags} - -[env:lopy] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} - ${common_env_data.lib_deps_gps} -build_flags = - ${common_env_data.build_flags} - -[env:lopy4] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} - ${common_env_data.lib_deps_gps} -build_flags = - ${common_env_data.build_flags} - -mfix-esp32-psram-cache-issue - -[env:lolin32litelora] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = lolin32 -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} -build_flags = - ${common_env_data.build_flags} - -[env:lolin32lora] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = lolin32 -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} -build_flags = - ${common_env_data.build_flags} - -[env:lolin32lite] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = lolin32 -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} -build_flags = - ${common_env_data.build_flags} - -[env:generic] -platform = ${common_env_data.platform_espressif32} -framework = arduino -board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} -upload_speed = 921600 -monitor_speed = 115200 -lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} - ${common_env_data.lib_deps_gps} - ${common_env_data.lib_deps_display} -build_flags = - ${common_env_data.build_flags} From 5139f7f8c76fc79be7dd866c4c23a25c92f4c169 Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Sat, 15 Sep 2018 15:20:41 +0200 Subject: [PATCH 011/105] clean platformio.ini uploaded --- platformio.ini | 270 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 platformio.ini diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 00000000..17ab99e0 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,270 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; http://docs.platformio.org/page/projectconf.html + + + +; ---> SELECT TARGET PLATFORM HERE! <--- +[platformio] +;env_default = generic +;env_default = ebox +;env_default = heltec +;env_default = ttgov1 +;env_default = ttgov2 +env_default = ttgov21 +;env_default = ttgobeam +;env_default = lopy +;env_default = lopy4 +;env_default = fipy +;env_default = lolin32litelora +;env_default = lolin32lora +;env_default = lolin32lite +; +description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around. + +[bintray] +user = cyberman54 +repository = paxcounter-firmware +package = ttgov21_old +api_token = *** + +[wifi] +ssid = *** +password = *** + +[common] +platform = https://github.com/platformio/platform-espressif32.git + +; firmware version, please modify it between releases +; positive integer value +;release_version = 1.4.30 +release_version = 3 + +; build configuration based on Bintray and Wi-Fi settings +build_flags = + '-DWIFI_SSID="${wifi.ssid}"' + '-DWIFI_PASS="${wifi.password}"' + '-DBINTRAY_USER="${bintray.user}"' + '-DBINTRAY_REPO="${bintray.repository}"' + '-DBINTRAY_PACKAGE="${bintray.package}"' + -DVERSION=${common.release_version} +; +; ---> NOTE: For production run set DEBUG_LEVEL level to NONE! <--- +; otherwise device may leak RAM +; +; None +; -DCORE_DEBUG_LEVEL=0 +; Error +; -DCORE_DEBUG_LEVEL=1 +; Warn +; -DCORE_DEBUG_LEVEL=2 +; Info + -DCORE_DEBUG_LEVEL=3 +; Debug +; -DCORE_DEBUG_LEVEL=4 +; Verbose +; -DCORE_DEBUG_LEVEL=5 + +[common_env_data] +platform_espressif32 = espressif32@1.3.0 +;platform_espressif32 = https://github.com/platformio/platform-espressif32.git#feature/stage +;board_build.partitions = no_ota.csv +board_build.partitions = min_spiffs.csv +lib_deps_all = + ArduinoJson +lib_deps_display = + U8g2@>=2.23.16 +lib_deps_rgbled = + SmartLeds@>=1.1.3 +lib_deps_gps = + TinyGPSPlus@>=1.0.2 + Time@>=1.5 +build_flags = +; override lora settings from LMiC library in lmic/config.h and use main.h instead + -D_lmic_config_h_ + -include "src/paxcounter.conf" + -include "src/hal/${PIOENV}.h" + -w + +[env:ebox] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 115200 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} +build_flags = + ${common_env_data.build_flags} + +[env:heltec] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = heltec_wifi_lora_32 +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 115200 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_display} +build_flags = + ${common_env_data.build_flags} + +[env:ttgov1] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 115200 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_display} +build_flags = + ${common_env_data.build_flags} + +[env:ttgov2] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_display} +build_flags = + ${common_env_data.build_flags} + +[env:ttgov21] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_display} +build_flags = + ${common.build_flags} + ${common_env_data.build_flags} +;upload_protocol = custom +;extra_scripts = pre:publish_firmware.py + +[env:ttgobeam] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_gps} +build_flags = + ${common_env_data.build_flags} + -mfix-esp32-psram-cache-issue + +[env:fipy] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_rgbled} +build_flags = + ${common_env_data.build_flags} + +[env:lopy] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_rgbled} + ${common_env_data.lib_deps_gps} +build_flags = + ${common_env_data.build_flags} + +[env:lopy4] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_rgbled} + ${common_env_data.lib_deps_gps} +build_flags = + ${common_env_data.build_flags} + -mfix-esp32-psram-cache-issue + +[env:lolin32litelora] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = lolin32 +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_rgbled} +build_flags = + ${common_env_data.build_flags} + +[env:lolin32lora] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = lolin32 +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_rgbled} +build_flags = + ${common_env_data.build_flags} + +[env:lolin32lite] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = lolin32 +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_rgbled} +build_flags = + ${common_env_data.build_flags} + +[env:generic] +platform = ${common_env_data.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common_env_data.board_build.partitions} +upload_speed = 921600 +monitor_speed = 115200 +lib_deps = + ${common_env_data.lib_deps_all} + ${common_env_data.lib_deps_rgbled} + ${common_env_data.lib_deps_gps} + ${common_env_data.lib_deps_display} +build_flags = + ${common_env_data.build_flags} From 2c1dd22555bf52fabc2d600077399149cb63d592 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 16:29:52 +0200 Subject: [PATCH 012/105] ota first test --- platformio.ini | 6 +- src/OTA.cpp | 261 ++++++++++++++++++++++++++++++++++++---------- src/OTA.h | 8 +- src/SecureOTA.cpp | 229 ---------------------------------------- src/SecureOTA.h | 25 ----- 5 files changed, 211 insertions(+), 318 deletions(-) delete mode 100644 src/SecureOTA.cpp delete mode 100644 src/SecureOTA.h diff --git a/platformio.ini b/platformio.ini index 17ab99e0..121cca95 100644 --- a/platformio.ini +++ b/platformio.ini @@ -32,11 +32,11 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng user = cyberman54 repository = paxcounter-firmware package = ttgov21_old -api_token = *** +api_token = 2e10f923df5d47b9c7e25752510322a1d65ee997 [wifi] -ssid = *** -password = *** +ssid = testnet +password = test0815 [common] platform = https://github.com/platformio/platform-espressif32.git diff --git a/src/OTA.cpp b/src/OTA.cpp index 1ec3be8e..888c28cd 100644 --- a/src/OTA.cpp +++ b/src/OTA.cpp @@ -1,79 +1,226 @@ -#include "OTA.h" +/* + Parts of this code: + Copyright (c) 2014-present PlatformIO + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include +#include "ota.h" const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); -bool Wifi_Connected = false; +// Connection port (HTTPS) +const int port = 443; -esp_err_t event_handler(void *ctx, system_event_t *event) { - switch (event->event_id) { - case SYSTEM_EVENT_STA_START: - esp_wifi_connect(); - ESP_LOGI(TAG, "Event STA_START"); - break; - case SYSTEM_EVENT_STA_GOT_IP: - Wifi_Connected = true; - ESP_LOGI(TAG, "Event STA_GOT_IP"); - // print the local IP address - tcpip_adapter_ip_info_t ip_info; - ESP_ERROR_CHECK(tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info)); - ESP_LOGI(TAG, "IP %s", ip4addr_ntoa(&ip_info.ip)); - break; - case SYSTEM_EVENT_STA_DISCONNECTED: - Wifi_Connected = false; - ESP_LOGI(TAG, "Event STA_DISCONNECTED"); - break; - default: - break; - } -} +// Connection timeout +const uint32_t RESPONSE_TIMEOUT_MS = 5000; -void ota_wifi_init(void) { +// Variables to validate firmware content +volatile int contentLength = 0; +volatile bool isValidContentType = false; - tcpip_adapter_if_t tcpip_if = TCPIP_ADAPTER_IF_STA; +// Local logging tag +static const char TAG[] = "main"; - // initialize the tcp stack - // nvs_flash_init(); - tcpip_adapter_init(); - tcpip_adapter_set_hostname(tcpip_if, PROGNAME); - tcpip_adapter_dhcpc_start(tcpip_if); +void start_ota_update() { + ota_update = false; // clear ota trigger switch - // initialize the wifi event handler - ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); + ESP_LOGI(TAG, "Stopping Wifi scanner"); + vTaskDelete(WifiLoopTask); + ESP_LOGI(TAG, "Starting Wifi OTA update"); // switch off monitor more ESP_ERROR_CHECK( esp_wifi_set_promiscuous(false)); // now switch on monitor mode ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(NULL)); - wifi_sta_config_t cfg; - strcpy((char *)cfg.ssid, WIFI_SSID); - strcpy((char *)cfg.password, WIFI_PASS); - cfg.bssid_set = false; + WiFi.begin(WIFI_SSID, WIFI_PASS); - wifi_config_t sta_cfg; - sta_cfg.sta = cfg; + while (WiFi.status() != WL_CONNECTED) { + delay(2000); + ESP_LOGI(TAG, "trying to connect to %s", WIFI_SSID); + } - wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_LOGI(TAG, "connected to %s", WIFI_SSID); - ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg)); - ESP_ERROR_CHECK( - esp_wifi_set_storage(WIFI_STORAGE_RAM)); // we don't need NVRAM - ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); - ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_cfg)); - ESP_ERROR_CHECK(esp_wifi_start()); + checkFirmwareUpdates(); + ESP.restart(); // reached only if update was not successful + +} // start_ota_update + +void checkFirmwareUpdates() { + // Fetch the latest firmware version + ESP_LOGI(TAG, "Checking latest firmware version..."); + const String latest = bintray.getLatestVersion(); + if (latest.length() == 0) { + ESP_LOGI(TAG, "Could not load info about the latest firmware, so nothing " + "to update. Continue ..."); + return; + } else if (atoi(latest.c_str()) <= VERSION) { + ESP_LOGI(TAG, "The current firmware is up to date. Continue ..."); + return; + } + + ESP_LOGI(TAG, "There is a new version of firmware available: v.%s", + latest.c_str()); + processOTAUpdate(latest); } -void start_ota_update() { - ESP_LOGI(TAG, "Stopping Wifi task on core 0"); - vTaskDelete(WifiLoopTask); +// A helper function to extract header value from header +inline String getHeaderValue(String header, String headerName) { + return header.substring(strlen(headerName.c_str())); +} - ESP_LOGI(TAG, "Stopping LORA task on core 1"); - vTaskDelete(LoraTask); +/** + * OTA update processing + */ +void processOTAUpdate(const String &version) { + String firmwarePath = bintray.getBinaryPath(version); + if (!firmwarePath.endsWith(".bin")) { + ESP_LOGI(TAG, "Unsupported binary format. OTA update cannot be performed!"); + return; + } - ESP_LOGI(TAG, "Connecting to %s", WIFI_SSID); - ota_wifi_init(); - delay(2000); - delay(2000); - checkFirmwareUpdates(); - ESP.restart(); // reached if update was not successful + String currentHost = bintray.getStorageHost(); + String prevHost = currentHost; + + WiFiClientSecure client; + client.setCACert(bintray.getCertificate(currentHost)); + + if (!client.connect(currentHost.c_str(), port)) { + ESP_LOGI(TAG, "Cannot connect to %s", currentHost.c_str()); + return; + } + + bool redirect = true; + while (redirect) { + if (currentHost != prevHost) { + client.stop(); + client.setCACert(bintray.getCertificate(currentHost)); + if (!client.connect(currentHost.c_str(), port)) { + ESP_LOGI(TAG, + "Redirect detected! Cannot connect to %s for some reason!", + currentHost.c_str()); + return; + } + } + + // ESP_LOGI(TAG, "Requesting: " + firmwarePath); + + client.print(String("GET ") + firmwarePath + " HTTP/1.1\r\n"); + client.print(String("Host: ") + currentHost + "\r\n"); + client.print("Cache-Control: no-cache\r\n"); + client.print("Connection: close\r\n\r\n"); + + unsigned long timeout = millis(); + while (client.available() == 0) { + if (millis() - timeout > RESPONSE_TIMEOUT_MS) { + ESP_LOGI(TAG, "Client Timeout !"); + client.stop(); + return; + } + } + + while (client.available()) { + String line = client.readStringUntil('\n'); + // Check if the line is end of headers by removing space symbol + line.trim(); + // if the the line is empty, this is the end of the headers + if (!line.length()) { + break; // proceed to OTA update + } + + // Check allowed HTTP responses + if (line.startsWith("HTTP/1.1")) { + if (line.indexOf("200") > 0) { + ESP_LOGI(TAG, "Got 200 status code from server. Proceeding to " + "firmware flashing"); + redirect = false; + } else if (line.indexOf("302") > 0) { + ESP_LOGI(TAG, "Got 302 status code from server. Redirecting to the " + "new address"); + redirect = true; + } else { + ESP_LOGI(TAG, "Could not get a valid firmware url"); + // Unexptected HTTP response. Retry or skip update? + redirect = false; + } + } + + // Extracting new redirect location + if (line.startsWith("Location: ")) { + String newUrl = getHeaderValue(line, "Location: "); + ESP_LOGI(TAG, "Got new url: %s", newUrl.c_str()); + newUrl.remove(0, newUrl.indexOf("//") + 2); + currentHost = newUrl.substring(0, newUrl.indexOf('/')); + newUrl.remove(newUrl.indexOf(currentHost), currentHost.length()); + firmwarePath = newUrl; + ESP_LOGI(TAG, "firmwarePath: %s", firmwarePath.c_str()); + continue; + } + + // Checking headers + if (line.startsWith("Content-Length: ")) { + contentLength = + atoi((getHeaderValue(line, "Content-Length: ")).c_str()); + ESP_LOGI(TAG, "Got %d bytes from server", contentLength); + } + + if (line.startsWith("Content-Type: ")) { + String contentType = getHeaderValue(line, "Content-Type: "); + ESP_LOGI(TAG, "Got %s payload", contentType.c_str()); + if (contentType == "application/octet-stream") { + isValidContentType = true; + } + } + } + } + + // check whether we have everything for OTA update + if (contentLength && isValidContentType) { + if (Update.begin(contentLength)) { + ESP_LOGI(TAG, "Starting Over-The-Air update. This may take some time to " + "complete ..."); + size_t written = Update.writeStream(client); + + if (written == contentLength) { + ESP_LOGI(TAG, "Written %d successfully", written); + } else { + ESP_LOGI(TAG, "Written only %d / %d Retry?", written, contentLength); + // Retry?? + } + + if (Update.end()) { + if (Update.isFinished()) { + ESP_LOGI(TAG, "OTA update has successfully completed. Rebooting ..."); + ESP.restart(); + } else { + ESP_LOGI(TAG, "Something went wrong! OTA update hasn't been finished " + "properly."); + } + } else { + ESP_LOGI(TAG, "An error occurred. Error #: %d", Update.getError()); + } + } else { + ESP_LOGI(TAG, "There isn't enough space to start OTA update"); + client.flush(); + } + } else { + ESP_LOGI(TAG, + "There was no valid content in the response from the OTA server!"); + client.flush(); + } } \ No newline at end of file diff --git a/src/OTA.h b/src/OTA.h index 8b6a27df..a20e54df 100644 --- a/src/OTA.h +++ b/src/OTA.h @@ -2,12 +2,12 @@ #define OTA_H #include -#include "globals.h" -#include #include -#include "ota.h" -#include "SecureOTA.h" +#include "globals.h" +#include "wifiscan.h" +void checkFirmwareUpdates(); +void processOTAUpdate(const String &version); void start_ota_update(); #endif // OTA_H \ No newline at end of file diff --git a/src/SecureOTA.cpp b/src/SecureOTA.cpp deleted file mode 100644 index 8b35c93b..00000000 --- a/src/SecureOTA.cpp +++ /dev/null @@ -1,229 +0,0 @@ -/* - Copyright (c) 2014-present PlatformIO - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -**/ - -#include -#include -#include -#include "SecureOTA.h" - -const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); - -// Connection port (HTTPS) -const int port = 443; - -// Connection timeout -const uint32_t RESPONSE_TIMEOUT_MS = 5000; - -// Variables to validate firmware content -volatile int contentLength = 0; -volatile bool isValidContentType = false; - -// Local logging tag -static const char TAG[] = "main"; - -void checkFirmwareUpdates() -{ - // Fetch the latest firmware version - ESP_LOGI(TAG, "Checking latest firmware version..."); - const String latest = bintray.getLatestVersion(); - if (latest.length() == 0) - { - ESP_LOGI(TAG, "Could not load info about the latest firmware, so nothing to update. Continue ..."); - return; - } - else if (atoi(latest.c_str()) <= VERSION) - { - ESP_LOGI(TAG, "The current firmware is up to date. Continue ..."); - return; - } - - ESP_LOGI(TAG, "There is a new version of firmware available: v.%s", latest.c_str()); - processOTAUpdate(latest); -} - -// A helper function to extract header value from header -inline String getHeaderValue(String header, String headerName) -{ - return header.substring(strlen(headerName.c_str())); -} - -/** - * OTA update processing - */ -void processOTAUpdate(const String &version) -{ - String firmwarePath = bintray.getBinaryPath(version); - if (!firmwarePath.endsWith(".bin")) - { - ESP_LOGI(TAG, "Unsupported binary format. OTA update cannot be performed!"); - return; - } - - String currentHost = bintray.getStorageHost(); - String prevHost = currentHost; - - WiFiClientSecure client; - client.setCACert(bintray.getCertificate(currentHost)); - - if (!client.connect(currentHost.c_str(), port)) - { - ESP_LOGI(TAG, "Cannot connect to %s", currentHost.c_str()); - return; - } - - bool redirect = true; - while (redirect) - { - if (currentHost != prevHost) - { - client.stop(); - client.setCACert(bintray.getCertificate(currentHost)); - if (!client.connect(currentHost.c_str(), port)) - { - ESP_LOGI(TAG, "Redirect detected! Cannot connect to %s for some reason!", currentHost.c_str()); - return; - } - } - - //ESP_LOGI(TAG, "Requesting: " + firmwarePath); - - client.print(String("GET ") + firmwarePath + " HTTP/1.1\r\n"); - client.print(String("Host: ") + currentHost + "\r\n"); - client.print("Cache-Control: no-cache\r\n"); - client.print("Connection: close\r\n\r\n"); - - unsigned long timeout = millis(); - while (client.available() == 0) - { - if (millis() - timeout > RESPONSE_TIMEOUT_MS) - { - ESP_LOGI(TAG, "Client Timeout !"); - client.stop(); - return; - } - } - - while (client.available()) - { - String line = client.readStringUntil('\n'); - // Check if the line is end of headers by removing space symbol - line.trim(); - // if the the line is empty, this is the end of the headers - if (!line.length()) - { - break; // proceed to OTA update - } - - // Check allowed HTTP responses - if (line.startsWith("HTTP/1.1")) - { - if (line.indexOf("200") > 0) - { - ESP_LOGI(TAG, "Got 200 status code from server. Proceeding to firmware flashing"); - redirect = false; - } - else if (line.indexOf("302") > 0) - { - ESP_LOGI(TAG, "Got 302 status code from server. Redirecting to the new address"); - redirect = true; - } - else - { - ESP_LOGI(TAG, "Could not get a valid firmware url"); - //Unexptected HTTP response. Retry or skip update? - redirect = false; - } - } - - // Extracting new redirect location - if (line.startsWith("Location: ")) - { - String newUrl = getHeaderValue(line, "Location: "); - ESP_LOGI(TAG, "Got new url: %s", newUrl.c_str()); - newUrl.remove(0, newUrl.indexOf("//") + 2); - currentHost = newUrl.substring(0, newUrl.indexOf('/')); - newUrl.remove(newUrl.indexOf(currentHost), currentHost.length()); - firmwarePath = newUrl; - ESP_LOGI(TAG, "firmwarePath: %s", firmwarePath.c_str()); - continue; - } - - // Checking headers - if (line.startsWith("Content-Length: ")) - { - contentLength = atoi((getHeaderValue(line, "Content-Length: ")).c_str()); - ESP_LOGI(TAG, "Got %d bytes from server", contentLength); - } - - if (line.startsWith("Content-Type: ")) - { - String contentType = getHeaderValue(line, "Content-Type: "); - ESP_LOGI(TAG, "Got %s payload", contentType.c_str()); - if (contentType == "application/octet-stream") - { - isValidContentType = true; - } - } - } - } - - // check whether we have everything for OTA update - if (contentLength && isValidContentType) - { - if (Update.begin(contentLength)) - { - ESP_LOGI(TAG, "Starting Over-The-Air update. This may take some time to complete ..."); - size_t written = Update.writeStream(client); - - if (written == contentLength) - { - ESP_LOGI(TAG, "Written %d successfully", written); - } - else - { - ESP_LOGI(TAG, "Written only %d / %d Retry?", written, contentLength); - // Retry?? - } - - if (Update.end()) - { - if (Update.isFinished()) - { - ESP_LOGI(TAG, "OTA update has successfully completed. Rebooting ..."); - ESP.restart(); - } - else - { - ESP_LOGI(TAG, "Something went wrong! OTA update hasn't been finished properly."); - } - } - else - { - ESP_LOGI(TAG, "An error occurred. Error #: %d", Update.getError()); - } - } - else - { - ESP_LOGI(TAG, "There isn't enough space to start OTA update"); - client.flush(); - } - } - else - { - ESP_LOGI(TAG, "There was no valid content in the response from the OTA server!"); - client.flush(); - } -} \ No newline at end of file diff --git a/src/SecureOTA.h b/src/SecureOTA.h deleted file mode 100644 index d8b17c3f..00000000 --- a/src/SecureOTA.h +++ /dev/null @@ -1,25 +0,0 @@ -/* - Copyright (c) 2014-present PlatformIO - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -**/ - -#ifndef SECURE_OTA_H -#define SECURE_OTA_H - -#include - -void checkFirmwareUpdates(); -void processOTAUpdate(const String &version); - -#endif // SECURE_OTA_H \ No newline at end of file From 01fa10e3fd7b9b82d09d994a87f0d0c2e8f5ccaa Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 17:04:04 +0200 Subject: [PATCH 013/105] ota (experimental) --- .gitignore | 3 +-- platformio.ini | 68 +++++++++++++++++++++----------------------------- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index f1f6d754..dbccbfe1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,4 @@ .vscode/.browse.c_cpp.db* .clang_complete .gcc-flags.json -src/loraconf.h -platformio.ini \ No newline at end of file +src/loraconf.h \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 121cca95..e42dbb31 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,15 +1,9 @@ ; PlatformIO Project Configuration File ; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; ; Please visit documentation for the other options and examples ; http://docs.platformio.org/page/projectconf.html - ; ---> SELECT TARGET PLATFORM HERE! <--- [platformio] ;env_default = generic @@ -17,8 +11,8 @@ ;env_default = heltec ;env_default = ttgov1 ;env_default = ttgov2 -env_default = ttgov21 -;env_default = ttgobeam +;env_default = ttgov21 +env_default = ttgobeam ;env_default = lopy ;env_default = lopy4 ;env_default = fipy @@ -34,42 +28,20 @@ repository = paxcounter-firmware package = ttgov21_old api_token = 2e10f923df5d47b9c7e25752510322a1d65ee997 -[wifi] -ssid = testnet -password = test0815 - [common] -platform = https://github.com/platformio/platform-espressif32.git - -; firmware version, please modify it between releases -; positive integer value -;release_version = 1.4.30 -release_version = 3 +release_version = 5 +[ota] ; build configuration based on Bintray and Wi-Fi settings +wifi_ssid = testnet +wifi_password = test0815 build_flags = - '-DWIFI_SSID="${wifi.ssid}"' - '-DWIFI_PASS="${wifi.password}"' + '-DWIFI_SSID="${ota.wifi_ssid}"' + '-DWIFI_PASS="${ota.wifi_password}"' '-DBINTRAY_USER="${bintray.user}"' '-DBINTRAY_REPO="${bintray.repository}"' '-DBINTRAY_PACKAGE="${bintray.package}"' -DVERSION=${common.release_version} -; -; ---> NOTE: For production run set DEBUG_LEVEL level to NONE! <--- -; otherwise device may leak RAM -; -; None -; -DCORE_DEBUG_LEVEL=0 -; Error -; -DCORE_DEBUG_LEVEL=1 -; Warn -; -DCORE_DEBUG_LEVEL=2 -; Info - -DCORE_DEBUG_LEVEL=3 -; Debug -; -DCORE_DEBUG_LEVEL=4 -; Verbose -; -DCORE_DEBUG_LEVEL=5 [common_env_data] platform_espressif32 = espressif32@1.3.0 @@ -91,6 +63,21 @@ build_flags = -include "src/paxcounter.conf" -include "src/hal/${PIOENV}.h" -w +; ---> NOTE: For production run set DEBUG_LEVEL level to NONE! <--- +; otherwise device may leak RAM +; +; None +; -DCORE_DEBUG_LEVEL=0 +; Error +; -DCORE_DEBUG_LEVEL=1 +; Warn +; -DCORE_DEBUG_LEVEL=2 +; Info + -DCORE_DEBUG_LEVEL=3 +; Debug +; -DCORE_DEBUG_LEVEL=4 +; Verbose +; -DCORE_DEBUG_LEVEL=5 [env:ebox] platform = ${common_env_data.platform_espressif32} @@ -102,6 +89,7 @@ monitor_speed = 115200 lib_deps = ${common_env_data.lib_deps_all} build_flags = + ${ota.build_flags} ${common_env_data.build_flags} [env:heltec] @@ -154,10 +142,7 @@ lib_deps = ${common_env_data.lib_deps_all} ${common_env_data.lib_deps_display} build_flags = - ${common.build_flags} ${common_env_data.build_flags} -;upload_protocol = custom -;extra_scripts = pre:publish_firmware.py [env:ttgobeam] platform = ${common_env_data.platform_espressif32} @@ -170,8 +155,11 @@ lib_deps = ${common_env_data.lib_deps_all} ${common_env_data.lib_deps_gps} build_flags = + ${ota.build_flags} ${common_env_data.build_flags} -mfix-esp32-psram-cache-issue +upload_protocol = custom +extra_scripts = pre:publish_firmware.py [env:fipy] platform = ${common_env_data.platform_espressif32} @@ -212,6 +200,7 @@ lib_deps = ${common_env_data.lib_deps_rgbled} ${common_env_data.lib_deps_gps} build_flags = + ${ota.build_flags} ${common_env_data.build_flags} -mfix-esp32-psram-cache-issue @@ -267,4 +256,5 @@ lib_deps = ${common_env_data.lib_deps_gps} ${common_env_data.lib_deps_display} build_flags = + ${ota.build_flags} ${common_env_data.build_flags} From b12ed126b7879ed377c9042b548dab7401bb49df Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 18:59:20 +0200 Subject: [PATCH 014/105] OTA (experimental) --- README.md | 1 + platformio.ini | 18 +++++++++--------- src/OTA.cpp | 10 +--------- src/OTA.h | 1 - src/configmanager.cpp | 13 +++++++++++++ src/cyclic.cpp | 5 +++-- src/globals.h | 4 ++-- src/lorawan.cpp | 6 ++---- src/main.cpp | 19 +++++++++++++------ src/main.h | 1 + src/rcommand.cpp | 18 +++++++++--------- 11 files changed, 54 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 8c6400aa..0889b2b6 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,7 @@ Note: all settings are stored in NVRAM and will be reloaded when device starts. 1 = reset MAC counter to zero 2 = reset device to factory settings 3 = flush send queues + 9 = OTA software update via Wifi 0x0A set LoRaWAN payload send cycle diff --git a/platformio.ini b/platformio.ini index e42dbb31..90c8fdb7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,8 +11,8 @@ ;env_default = heltec ;env_default = ttgov1 ;env_default = ttgov2 -;env_default = ttgov21 -env_default = ttgobeam +env_default = ttgov21 +;env_default = ttgobeam ;env_default = lopy ;env_default = lopy4 ;env_default = fipy @@ -29,7 +29,7 @@ package = ttgov21_old api_token = 2e10f923df5d47b9c7e25752510322a1d65ee997 [common] -release_version = 5 +release_version = 6 [ota] ; build configuration based on Bintray and Wi-Fi settings @@ -62,6 +62,7 @@ build_flags = -D_lmic_config_h_ -include "src/paxcounter.conf" -include "src/hal/${PIOENV}.h" + ${ota.build_flags} -w ; ---> NOTE: For production run set DEBUG_LEVEL level to NONE! <--- ; otherwise device may leak RAM @@ -89,7 +90,6 @@ monitor_speed = 115200 lib_deps = ${common_env_data.lib_deps_all} build_flags = - ${ota.build_flags} ${common_env_data.build_flags} [env:heltec] @@ -143,6 +143,9 @@ lib_deps = ${common_env_data.lib_deps_display} build_flags = ${common_env_data.build_flags} +;upload_protocol = custom +;extra_scripts = pre:publish_firmware.py + [env:ttgobeam] platform = ${common_env_data.platform_espressif32} @@ -155,11 +158,10 @@ lib_deps = ${common_env_data.lib_deps_all} ${common_env_data.lib_deps_gps} build_flags = - ${ota.build_flags} ${common_env_data.build_flags} -mfix-esp32-psram-cache-issue -upload_protocol = custom -extra_scripts = pre:publish_firmware.py +;upload_protocol = custom +;extra_scripts = pre:publish_firmware.py [env:fipy] platform = ${common_env_data.platform_espressif32} @@ -200,7 +202,6 @@ lib_deps = ${common_env_data.lib_deps_rgbled} ${common_env_data.lib_deps_gps} build_flags = - ${ota.build_flags} ${common_env_data.build_flags} -mfix-esp32-psram-cache-issue @@ -256,5 +257,4 @@ lib_deps = ${common_env_data.lib_deps_gps} ${common_env_data.lib_deps_display} build_flags = - ${ota.build_flags} ${common_env_data.build_flags} diff --git a/src/OTA.cpp b/src/OTA.cpp index 888c28cd..702c8fcb 100644 --- a/src/OTA.cpp +++ b/src/OTA.cpp @@ -36,16 +36,8 @@ volatile bool isValidContentType = false; static const char TAG[] = "main"; void start_ota_update() { - ota_update = false; // clear ota trigger switch - - ESP_LOGI(TAG, "Stopping Wifi scanner"); - vTaskDelete(WifiLoopTask); ESP_LOGI(TAG, "Starting Wifi OTA update"); - // switch off monitor more - ESP_ERROR_CHECK( - esp_wifi_set_promiscuous(false)); // now switch on monitor mode - ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(NULL)); WiFi.begin(WIFI_SSID, WIFI_PASS); @@ -56,7 +48,7 @@ void start_ota_update() { ESP_LOGI(TAG, "connected to %s", WIFI_SSID); - checkFirmwareUpdates(); + checkFirmwareUpdates(); // gets and flashes new firmware and restarts ESP.restart(); // reached only if update was not successful } // start_ota_update diff --git a/src/OTA.h b/src/OTA.h index a20e54df..52698c39 100644 --- a/src/OTA.h +++ b/src/OTA.h @@ -4,7 +4,6 @@ #include #include #include "globals.h" -#include "wifiscan.h" void checkFirmwareUpdates(); void processOTAUpdate(const String &version); diff --git a/src/configmanager.cpp b/src/configmanager.cpp index 4ff40a84..8fa6b2f9 100644 --- a/src/configmanager.cpp +++ b/src/configmanager.cpp @@ -31,6 +31,7 @@ void defaultConfig() { cfg.rgblum = RGBLUMINOSITY; // RGB Led luminosity (0..100%) cfg.gpsmode = 1; // 0=disabled, 1=enabled cfg.monitormode = 0; // 0=disabled, 1=enabled + cfg.runmode = 0; // 0=normal, 1=update strncpy(cfg.version, PROGVERSION, sizeof(cfg.version) - 1); } @@ -143,6 +144,10 @@ void saveConfig() { flash8 != cfg.monitormode) nvs_set_i8(my_handle, "monitormode", cfg.monitormode); + if (nvs_get_i8(my_handle, "runmode", &flash8) != ESP_OK || + flash8 != cfg.runmode) + nvs_set_i8(my_handle, "runmode", cfg.runmode); + if (nvs_get_i16(my_handle, "rssilimit", &flash16) != ESP_OK || flash16 != cfg.rssilimit) nvs_set_i16(my_handle, "rssilimit", cfg.rssilimit); @@ -326,6 +331,14 @@ void loadConfig() { saveConfig(); } + if (nvs_get_i8(my_handle, "runmode", &flash8) == ESP_OK) { + cfg.runmode = flash8; + ESP_LOGI(TAG, "Run mode = %d", flash8); + } else { + ESP_LOGI(TAG, "Run mode set to default %d", cfg.runmode); + saveConfig(); + } + nvs_close(my_handle); ESP_LOGI(TAG, "Done"); } diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 45ce16cd..ba7968bf 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -15,8 +15,9 @@ void doHomework() { // update uptime counter uptime(); - if (ota_update) - start_ota_update(); + // check if update mode trigger switch was set + if (cfg.runmode == 1) + ESP.restart(); // read battery voltage into global variable #ifdef HAS_BATTERY_PROBE diff --git a/src/globals.h b/src/globals.h index 53aad820..20acd5cb 100644 --- a/src/globals.h +++ b/src/globals.h @@ -5,7 +5,7 @@ #include // attn: increment version after modifications to configData_t truct! -#define PROGVERSION "1.4.23" // use max 10 chars here! +#define PROGVERSION "1.4.30" // use max 10 chars here! #define PROGNAME "PAXCNT" // std::set for unified array functions @@ -31,6 +31,7 @@ typedef struct { uint8_t rgblum; // RGB Led luminosity (0..100%) uint8_t gpsmode; // 0=disabled, 1=enabled uint8_t monitormode; // 0=disabled, 1=enabled + uint8_t runmode; // 0=normal, 1=update char version[10]; // Firmware version } configData_t; @@ -43,7 +44,6 @@ typedef struct { // global variables extern configData_t cfg; // current device configuration -extern bool ota_update; extern char display_line6[], display_line7[]; // screen buffers extern uint8_t channel; // wifi channel rotation counter extern uint16_t macs_total, macs_wifi, macs_ble, batt_voltage; // display values diff --git a/src/lorawan.cpp b/src/lorawan.cpp index ea0709ff..e3850847 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -196,11 +196,9 @@ void onEvent(ev_t ev) { if (LMIC.dataLen) { ESP_LOGI(TAG, "Received %d bytes of payload, RSSI %d SNR %d", - LMIC.dataLen, LMIC.rssi, (signed char)LMIC.snr / 4); - // LMIC.snr = SNR twos compliment [dB] * 4 - // LMIC.rssi = RSSI [dBm] (-196...+63) + LMIC.dataLen, LMIC.rssi, (signed char)LMIC.snr); sprintf(display_line6, "RSSI %d SNR %d", LMIC.rssi, - (signed char)LMIC.snr / 4); + (signed char)LMIC.snr); // check if command is received on command port, then call interpreter if ((LMIC.txrxFlags & TXRX_PORT) && diff --git a/src/main.cpp b/src/main.cpp index d52b4a7a..a49253f3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,8 +27,7 @@ licenses. Refer to LICENSE.txt file in repository for more details. #include "globals.h" #include "main.h" -configData_t cfg; // struct holds current device configuration -bool ota_update = false; // triggers OTA update +configData_t cfg; // struct holds current device configuration char display_line6[16], display_line7[16]; // display buffers uint8_t channel = 0; // channel rotation counter uint16_t macs_total = 0, macs_wifi = 0, macs_ble = 0, @@ -97,7 +96,7 @@ void setup() { // initialize system event handler for wifi task, needed for // wifi_sniffer_init() // esp_event_loop_init(NULL, NULL); - //ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); + // ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); // print chip information on startup if in verbose mode #ifdef VERBOSE @@ -123,6 +122,13 @@ void setup() { // read settings from NVRAM loadConfig(); // includes initialize if necessary + // reboot to firmware update mode if ota trigger switch is set + if (cfg.runmode == 1) { + cfg.runmode = 0; + saveConfig(); + start_ota_update(); + } + #ifdef VENDORFILTER strcat_P(features, " OUIFLT"); #endif @@ -302,8 +308,8 @@ void setup() { ESP_LOGI(TAG, "Starting Wifi task on core 0"); wifi_sniffer_init(); // initialize salt value using esp_random() called by random() in - // arduino-esp32 core. Note: do this *after* wifi has started, since function - // gets it's seed from RF noise + // arduino-esp32 core. Note: do this *after* wifi has started, since + // function gets it's seed from RF noise reset_salt(); // get new 16bit for salting hashes xTaskCreatePinnedToCore(wifi_channel_loop, "wifiloop", 2048, (void *)1, 1, &WifiLoopTask, 0); @@ -318,7 +324,8 @@ void setup() { void loop() { while (1) { - // state machine for switching display, LED, button, housekeeping, senddata + // state machine for switching display, LED, button, housekeeping, + // senddata #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) led_loop(); diff --git a/src/main.h b/src/main.h index 89dc0be4..58919160 100644 --- a/src/main.h +++ b/src/main.h @@ -8,6 +8,7 @@ #include "senddata.h" #include "cyclic.h" #include "beacon_array.h" +#include "ota.h" #include // needed for reading ESP32 chip attributes #include // needed for Wifi event handler diff --git a/src/rcommand.cpp b/src/rcommand.cpp index 2bae172a..7f984cdb 100644 --- a/src/rcommand.cpp +++ b/src/rcommand.cpp @@ -31,6 +31,11 @@ void set_reset(uint8_t val[]) { sprintf(display_line6, "Queue reset"); flushQueues(); break; + case 9: // reset and ask for software update via Wifi OTA + ESP_LOGI(TAG, "Remote command: software update via Wifi"); + sprintf(display_line6, "Software update"); + cfg.runmode = 1; + break; default: ESP_LOGW(TAG, "Remote command: reset called with invalid parameter(s)"); } @@ -220,27 +225,22 @@ void get_gps(uint8_t val[]) { #endif }; -void set_update(uint8_t val[]) { - ESP_LOGI(TAG, "Remote command: get firmware update"); - ota_update = true; -}; - // assign previously defined functions to set of numeric remote commands // format: opcode, function, #bytes params, -// flag (1 = do make settings persistent / 0 = don't) +// flag (true = do make settings persistent / false = don't) // cmd_t table[] = { {0x01, set_rssi, 1, true}, {0x02, set_countmode, 1, true}, {0x03, set_gps, 1, true}, {0x04, set_display, 1, true}, {0x05, set_lorasf, 1, true}, {0x06, set_lorapower, 1, true}, {0x07, set_loraadr, 1, true}, {0x08, set_screensaver, 1, true}, - {0x09, set_reset, 1, false}, {0x0a, set_sendcycle, 1, true}, + {0x09, set_reset, 1, true}, {0x0a, set_sendcycle, 1, true}, {0x0b, set_wifichancycle, 1, true}, {0x0c, set_blescantime, 1, true}, {0x0d, set_vendorfilter, 1, false}, {0x0e, set_blescan, 1, true}, {0x0f, set_wifiant, 1, true}, {0x10, set_rgblum, 1, true}, {0x11, set_monitor, 1, true}, {0x12, set_beacon, 7, false}, - {0x20, set_update, 0, false}, {0x80, get_config, 0, false}, - {0x81, get_status, 0, false}, {0x84, get_gps, 0, false}}; + {0x80, get_config, 0, false}, {0x81, get_status, 0, false}, + {0x84, get_gps, 0, false}}; const uint8_t cmdtablesize = sizeof(table) / sizeof(table[0]); // number of commands in command table From 0667ee57443e3026c383e71d0a21005d39d2273a Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 15 Sep 2018 21:10:11 +0200 Subject: [PATCH 015/105] OTA (experimental) --- src/OTA.cpp | 32 ++++++++++++++++---------------- src/main.cpp | 1 - src/main.h | 3 ++- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/OTA.cpp b/src/OTA.cpp index 702c8fcb..7904eea3 100644 --- a/src/OTA.cpp +++ b/src/OTA.cpp @@ -49,24 +49,25 @@ void start_ota_update() { ESP_LOGI(TAG, "connected to %s", WIFI_SSID); checkFirmwareUpdates(); // gets and flashes new firmware and restarts - ESP.restart(); // reached only if update was not successful + ESP.restart(); // reached only if update was not successful } // start_ota_update void checkFirmwareUpdates() { // Fetch the latest firmware version - ESP_LOGI(TAG, "Checking latest firmware version..."); + ESP_LOGI(TAG, "OTA mode, checking latest firmware version on server..."); const String latest = bintray.getLatestVersion(); if (latest.length() == 0) { - ESP_LOGI(TAG, "Could not load info about the latest firmware, so nothing " - "to update. Continue ..."); + ESP_LOGI( + TAG, + "Could not load info about the latest firmware. Rebooting to runmode."); return; } else if (atoi(latest.c_str()) <= VERSION) { - ESP_LOGI(TAG, "The current firmware is up to date. Continue ..."); + ESP_LOGI(TAG, "Current firmware is up to date. Rebooting to runmode."); return; } - ESP_LOGI(TAG, "There is a new version of firmware available: v.%s", + ESP_LOGI(TAG, "New firmware version v%s available. Downloading...", latest.c_str()); processOTAUpdate(latest); } @@ -82,7 +83,7 @@ inline String getHeaderValue(String header, String headerName) { void processOTAUpdate(const String &version) { String firmwarePath = bintray.getBinaryPath(version); if (!firmwarePath.endsWith(".bin")) { - ESP_LOGI(TAG, "Unsupported binary format. OTA update cannot be performed!"); + ESP_LOGI(TAG, "Unsupported binary format, OTA update cancelled."); return; } @@ -103,8 +104,7 @@ void processOTAUpdate(const String &version) { client.stop(); client.setCACert(bintray.getCertificate(currentHost)); if (!client.connect(currentHost.c_str(), port)) { - ESP_LOGI(TAG, - "Redirect detected! Cannot connect to %s for some reason!", + ESP_LOGI(TAG, "Redirect detected, but cannot connect to %s", currentHost.c_str()); return; } @@ -120,7 +120,7 @@ void processOTAUpdate(const String &version) { unsigned long timeout = millis(); while (client.available() == 0) { if (millis() - timeout > RESPONSE_TIMEOUT_MS) { - ESP_LOGI(TAG, "Client Timeout !"); + ESP_LOGI(TAG, "Client Timeout."); client.stop(); return; } @@ -146,7 +146,7 @@ void processOTAUpdate(const String &version) { "new address"); redirect = true; } else { - ESP_LOGI(TAG, "Could not get a valid firmware url"); + ESP_LOGI(TAG, "Could not get a valid firmware url."); // Unexptected HTTP response. Retry or skip update? redirect = false; } @@ -184,20 +184,20 @@ void processOTAUpdate(const String &version) { // check whether we have everything for OTA update if (contentLength && isValidContentType) { if (Update.begin(contentLength)) { - ESP_LOGI(TAG, "Starting Over-The-Air update. This may take some time to " - "complete ..."); + ESP_LOGI(TAG, "Starting OTA update. This will take some time to " + "complete..."); size_t written = Update.writeStream(client); if (written == contentLength) { - ESP_LOGI(TAG, "Written %d successfully", written); + ESP_LOGI(TAG, "Written %d bytes successfully", written); } else { - ESP_LOGI(TAG, "Written only %d / %d Retry?", written, contentLength); + ESP_LOGI(TAG, "Written only %d of %d bytes, OTA update cancelled.", written, contentLength); // Retry?? } if (Update.end()) { if (Update.isFinished()) { - ESP_LOGI(TAG, "OTA update has successfully completed. Rebooting ..."); + ESP_LOGI(TAG, "OTA update completed. Rebooting to runmode."); ESP.restart(); } else { ESP_LOGI(TAG, "Something went wrong! OTA update hasn't been finished " diff --git a/src/main.cpp b/src/main.cpp index a49253f3..e659ea56 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,7 +24,6 @@ licenses. Refer to LICENSE.txt file in repository for more details. */ // Basic Config -#include "globals.h" #include "main.h" configData_t cfg; // struct holds current device configuration diff --git a/src/main.h b/src/main.h index 58919160..cb0cb8e2 100644 --- a/src/main.h +++ b/src/main.h @@ -1,7 +1,8 @@ #ifndef _MAIN_H #define _MAIN_H -//#include "led.h" +#include "globals.h" +#include "led.h" #include "macsniff.h" #include "wifiscan.h" #include "configmanager.h" From fb7c32e5c88f7d09f1179e72670fba4f3f956fc2 Mon Sep 17 00:00:00 2001 From: cyberman54 Date: Sun, 16 Sep 2018 12:18:11 +0200 Subject: [PATCH 016/105] OTA (experimental) --- lib/BintrayClient/src/BintrayClient.cpp | 8 ++++---- platformio.ini | 8 +++++--- src/OTA.cpp | 25 ++++++++++++++----------- src/OTA.h | 5 +++-- src/cyclic.cpp | 2 +- src/main.h | 2 +- src/paxcounter.conf | 3 +++ 7 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/BintrayClient/src/BintrayClient.cpp b/lib/BintrayClient/src/BintrayClient.cpp index 4479f539..b0e7543f 100644 --- a/lib/BintrayClient/src/BintrayClient.cpp +++ b/lib/BintrayClient/src/BintrayClient.cpp @@ -109,7 +109,7 @@ String BintrayClient::getLatestVersion() const const size_t bufferSize = 1024; if (jsonResult.length() > bufferSize) { - ESP_LOGI(TAG, "Error: Could not parse JSON. Input data is too big!"); + Serial.println("Error: Could parse JSON. Input data is too big!"); return version; } StaticJsonBuffer jsonBuffer; @@ -118,7 +118,7 @@ String BintrayClient::getLatestVersion() const // Check for errors in parsing if (!root.success()) { - ESP_LOGI(TAG, "Error: Could not parse JSON!"); + Serial.println("Error: Could not parse JSON!"); return version; } return root.get("name"); @@ -133,7 +133,7 @@ String BintrayClient::getBinaryPath(const String &version) const const size_t bufferSize = 1024; if (jsonResult.length() > bufferSize) { - ESP_LOGI(TAG, "Error: Could parse JSON. Input data is too big!"); + Serial.println("Error: Could parse JSON. Input data is too big!"); return path; } StaticJsonBuffer jsonBuffer; @@ -142,7 +142,7 @@ String BintrayClient::getBinaryPath(const String &version) const JsonObject &firstItem = root[0]; if (!root.success()) { //Check for errors in parsing - ESP_LOGI(TAG, "Error: Could not parse JSON!"); + Serial.println("Error: Could not parse JSON!"); return path; } return "/" + getUser() + "/" + getRepository() + "/" + firstItem.get("path"); diff --git a/platformio.ini b/platformio.ini index 90c8fdb7..0bd773fe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -29,7 +29,7 @@ package = ttgov21_old api_token = 2e10f923df5d47b9c7e25752510322a1d65ee997 [common] -release_version = 6 +release_version = 7 [ota] ; build configuration based on Bintray and Wi-Fi settings @@ -42,14 +42,16 @@ build_flags = '-DBINTRAY_REPO="${bintray.repository}"' '-DBINTRAY_PACKAGE="${bintray.package}"' -DVERSION=${common.release_version} +lib_deps_ota = + https://github.com/platformio/bintray-secure-ota.git [common_env_data] platform_espressif32 = espressif32@1.3.0 ;platform_espressif32 = https://github.com/platformio/platform-espressif32.git#feature/stage -;board_build.partitions = no_ota.csv board_build.partitions = min_spiffs.csv lib_deps_all = - ArduinoJson + ArduinoJson@^5.13.1 +; ArduinoJson${ota.lib_deps_ota} lib_deps_display = U8g2@>=2.23.16 lib_deps_rgbled = diff --git a/src/OTA.cpp b/src/OTA.cpp index 7904eea3..28a58f96 100644 --- a/src/OTA.cpp +++ b/src/OTA.cpp @@ -15,10 +15,7 @@ limitations under the License. */ -#include -#include -#include -#include "ota.h" +#include "OTA.h" const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); @@ -41,15 +38,20 @@ void start_ota_update() { WiFi.begin(WIFI_SSID, WIFI_PASS); - while (WiFi.status() != WL_CONNECTED) { - delay(2000); + int i = WIFI_MAX_TRY; + while (i--) { ESP_LOGI(TAG, "trying to connect to %s", WIFI_SSID); + if (WiFi.status() == WL_CONNECTED) + break; + delay(5000); } + if (i >= 0) { + ESP_LOGI(TAG, "connected to %s", WIFI_SSID); + checkFirmwareUpdates(); // gets and flashes new firmware and restarts + } else + ESP_LOGI(TAG, "could not connect to %s, rebooting.", WIFI_SSID); - ESP_LOGI(TAG, "connected to %s", WIFI_SSID); - - checkFirmwareUpdates(); // gets and flashes new firmware and restarts - ESP.restart(); // reached only if update was not successful + ESP.restart(); // reached only if update was not successful or no wifi connect } // start_ota_update @@ -191,7 +193,8 @@ void processOTAUpdate(const String &version) { if (written == contentLength) { ESP_LOGI(TAG, "Written %d bytes successfully", written); } else { - ESP_LOGI(TAG, "Written only %d of %d bytes, OTA update cancelled.", written, contentLength); + ESP_LOGI(TAG, "Written only %d of %d bytes, OTA update cancelled.", + written, contentLength); // Retry?? } diff --git a/src/OTA.h b/src/OTA.h index 52698c39..fc66973c 100644 --- a/src/OTA.h +++ b/src/OTA.h @@ -1,9 +1,10 @@ #ifndef OTA_H #define OTA_H -#include #include -#include "globals.h" +#include +#include +#include void checkFirmwareUpdates(); void processOTAUpdate(const String &version); diff --git a/src/cyclic.cpp b/src/cyclic.cpp index ba7968bf..72b2284c 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -4,7 +4,7 @@ // Basic config #include "globals.h" #include "senddata.h" -#include "ota.h" +#include "OTA.h" // Local logging tag static const char TAG[] = "main"; diff --git a/src/main.h b/src/main.h index cb0cb8e2..4b77439a 100644 --- a/src/main.h +++ b/src/main.h @@ -9,7 +9,7 @@ #include "senddata.h" #include "cyclic.h" #include "beacon_array.h" -#include "ota.h" +#include "OTA.h" #include // needed for reading ESP32 chip attributes #include // needed for Wifi event handler diff --git a/src/paxcounter.conf b/src/paxcounter.conf index bcecc56d..68681b9b 100644 --- a/src/paxcounter.conf +++ b/src/paxcounter.conf @@ -63,6 +63,9 @@ #define DISPLAYREFRESH_MS 40 // OLED refresh cycle in ms [default = 40] -> 1000/40 = 25 frames per second #define HOMECYCLE 30 // house keeping cycle in seconds [default = 30 secs] +// OTA settings +#define WIFI_MAX_TRY 20 // maximum number of wifi connect attempts for OTA update [default = 20] + // LMIC settings // define hardware independent LMIC settings here, settings of standard library in /lmic/config.h will be ignored // define hardware specifics settings in platformio.ini as build_flag for hardware environment From 55e7d4b4e61c19871e9176431f88056ab41e89d6 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 16 Sep 2018 15:30:58 +0200 Subject: [PATCH 017/105] lmic radio.c snr/rssi fix --- lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c b/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c index 0fffaa4f..6c570545 100644 --- a/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c +++ b/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/radio.c @@ -791,7 +791,7 @@ void radio_irq_handler (u1_t dio) { readBuf(RegFifo, LMIC.frame, LMIC.dataLen); // read rx quality parameters //LMIC.snr = readReg(LORARegPktSnrValue); // SNR [dB] * 4 - LMIC.snr = readReg(LORARegPktSnrValue) / 4; + LMIC.snr = ((s1_t)readReg(LORARegPktSnrValue)) / 4; //LMIC.rssi = readReg(LORARegPktRssiValue) - 125 + 64; // RSSI [dBm] (-196...+63) LMIC.rssi = readReg(LORARegPktRssiValue) - 157; // RFI_HF for 868 and 915MHZ band if (LMIC.snr < 0) From 6476dedc5bc2b5d6d6ac8ac9b0c65c89864d0c8b Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 16 Sep 2018 16:52:26 +0200 Subject: [PATCH 018/105] OTA: changed CA certficate for api.bintray.com --- lib/BintrayClient/src/BintrayCertificates.h | 40 +++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/BintrayClient/src/BintrayCertificates.h b/lib/BintrayClient/src/BintrayCertificates.h index ec5fdaa3..d85bb4e4 100644 --- a/lib/BintrayClient/src/BintrayCertificates.h +++ b/lib/BintrayClient/src/BintrayCertificates.h @@ -18,25 +18,27 @@ #define BINTRAY_CERTIFICATES_H const char* BINTRAY_API_ROOT_CA = \ -"-----BEGIN CERTIFICATE-----\n" \ -"MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\n" \ -"MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\n" \ -"YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG\n" \ -"EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg\n" \ -"R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9\n" \ -"9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq\n" \ -"fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv\n" \ -"iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU\n" \ -"1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+\n" \ -"bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW\n" \ -"MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA\n" \ -"ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l\n" \ -"uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn\n" \ -"Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS\n" \ -"tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF\n" \ -"PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un\n" \ -"hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV\n" \ -"5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==\n" \ +"-----BEGIN CERTIFICATE-----\n" +"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n" +"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" +"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n" +"QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\n" +"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" +"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n" +"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\n" +"CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\n" +"nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n" +"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\n" +"T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\n" +"gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\n" +"BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\n" +"TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\n" +"DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\n" +"hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n" +"06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\n" +"PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n" +"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\n" +"CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n" "-----END CERTIFICATE-----\n"; const char* BINTRAY_AKAMAI_ROOT_CA = \ From 49288182e973f27583e60dcbe931d1139665cdb2 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 16 Sep 2018 17:08:37 +0200 Subject: [PATCH 019/105] update readme.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0889b2b6..9dbb980f 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,7 @@ function Converter(decoded, port) { The device listenes for remote control commands on LoRaWAN Port 2. Multiple commands per downlink are possible by concatenating them. -Note: all settings are stored in NVRAM and will be reloaded when device starts. To reset device to factory settings send remote command 09 02 09 00 unconfirmed(!) once. +Note: all settings are stored in NVRAM and will be reloaded when device starts. 0x01 set scan RSSI limit @@ -272,13 +272,13 @@ Note: all settings are stored in NVRAM and will be reloaded when device starts. useful to clear pending commands from LoRaWAN server quere, or to check RSSI on device -0x09 reset functions +0x09 reset functions (send this command with confirmed ack only to avoid boot loops!) 0 = restart device 1 = reset MAC counter to zero 2 = reset device to factory settings 3 = flush send queues - 9 = OTA software update via Wifi + 9 = reboot device to OTA update via Wifi mode 0x0A set LoRaWAN payload send cycle From 21621e54d5d9990bcdc6c741818f97534759282e Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 16 Sep 2018 17:39:18 +0200 Subject: [PATCH 020/105] code sanitization (vTaskDelay) --- src/OTA.cpp | 2 +- src/display.cpp | 6 +++--- src/gps.cpp | 6 +++--- src/lorawan.cpp | 2 +- src/main.cpp | 2 +- src/wifiscan.cpp | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/OTA.cpp b/src/OTA.cpp index 28a58f96..87609949 100644 --- a/src/OTA.cpp +++ b/src/OTA.cpp @@ -43,7 +43,7 @@ void start_ota_update() { ESP_LOGI(TAG, "trying to connect to %s", WIFI_SSID); if (WiFi.status() == WL_CONNECTED) break; - delay(5000); + vTaskDelay(5000 / portTICK_PERIOD_MS); } if (i >= 0) { ESP_LOGI(TAG, "connected to %s", WIFI_SSID); diff --git a/src/display.cpp b/src/display.cpp index b2fecf09..bfc55f05 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -36,14 +36,14 @@ void init_display(const char *Productname, const char *Version) { u8x8.draw2x2String(0, 0, Productname); u8x8.setInverseFont(0); u8x8.draw2x2String(2, 2, Productname); - delay(1500); + vTaskDelay(1500 / portTICK_PERIOD_MS); u8x8.clear(); u8x8.setFlipMode(1); u8x8.setInverseFont(1); u8x8.draw2x2String(0, 0, Productname); u8x8.setInverseFont(0); u8x8.draw2x2String(2, 2, Productname); - delay(1500); + vTaskDelay(1500 / portTICK_PERIOD_MS); u8x8.setFlipMode(0); u8x8.clear(); @@ -74,7 +74,7 @@ void init_display(const char *Productname, const char *Version) { DisplayKey(buf, 8, true); #endif // HAS_LORA - delay(5000); + vTaskDelay(3000 / portTICK_PERIOD_MS); u8x8.clear(); u8x8.setPowerSave(!cfg.screenon); // set display off if disabled u8x8.draw2x2String(0, 0, "PAX:0"); diff --git a/src/gps.cpp b/src/gps.cpp index ff0e0bae..10d05c14 100644 --- a/src/gps.cpp +++ b/src/gps.cpp @@ -42,7 +42,7 @@ void gps_loop(void *pvParameters) { while (GPS_Serial.available()) { gps.encode(GPS_Serial.read()); } - vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog + vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog } // after GPS function was disabled, close connect to GPS device GPS_Serial.end(); @@ -58,7 +58,7 @@ void gps_loop(void *pvParameters) { Wire.requestFrom(GPS_ADDR | 0x01, 32); while (Wire.available()) { gps.encode(Wire.read()); - vTaskDelay(1 / portTICK_PERIOD_MS); // polling mode: 500ms sleep + vTaskDelay(2 / portTICK_PERIOD_MS); // polling mode: 500ms sleep } } // after GPS function was disabled, close connect to GPS device @@ -67,7 +67,7 @@ void gps_loop(void *pvParameters) { #endif // GPS Type } - vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog + vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog } // end of infinite loop diff --git a/src/lorawan.cpp b/src/lorawan.cpp index e3850847..0ee8b6c0 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -227,7 +227,7 @@ void lorawan_loop(void *pvParameters) { while (1) { os_runloop_once(); // execute LMIC jobs - vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog + vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog } } diff --git a/src/main.cpp b/src/main.cpp index e659ea56..6de8432c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -345,7 +345,7 @@ void loop() { // check send cycle and enqueue payload if cycle is expired sendPayload(); // reset watchdog - vTaskDelay(1 / portTICK_PERIOD_MS); + vTaskDelay(2 / portTICK_PERIOD_MS); } // loop() } diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 24d6504f..ed3a84ee 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -59,7 +59,7 @@ void wifi_channel_loop(void *pvParameters) { esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); ESP_LOGD(TAG, "Wifi set channel %d", channel); - vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog + vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog } } // end of infinite wifi channel rotation loop From bd724557c1cdc22c4bd81479e9c9ec1ef43dea88 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Mon, 17 Sep 2018 17:23:02 +0200 Subject: [PATCH 021/105] v1.4.32 --- README.md | 1 + lib/BintrayClient/src/BintrayCertificates.h | 1 + lib/BintrayClient/src/BintrayClient.cpp | 11 +- lib/BintrayClient/src/BintrayClient.h | 1 + platformio.ini | 259 +++++++++++--------- publish_firmware.py | 16 +- src/OTA.cpp | 39 ++- src/OTA.h | 2 + src/globals.h | 4 - src/hal/ttgov21.h | 69 ------ src/hal/ttgov21new.h | 30 +++ src/hal/ttgov21old.h | 32 +++ src/main.cpp | 4 +- src/paxcounter.conf | 2 + src/payload.cpp | 10 +- src/payload.h | 1 + 16 files changed, 274 insertions(+), 208 deletions(-) delete mode 100644 src/hal/ttgov21.h create mode 100644 src/hal/ttgov21new.h create mode 100644 src/hal/ttgov21old.h diff --git a/README.md b/README.md index 9dbb980f..870f3656 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Hereafter described is the default *plain* format, which uses MSB bit numbering. byte 3-10: Uptime [seconds] byte 11: CPU temperature [°C] bytes 12-15: Free RAM [bytes] + bytes 16-17: Last CPU reset reason [core 0, core 1] **Port #3:** Device configuration query result diff --git a/lib/BintrayClient/src/BintrayCertificates.h b/lib/BintrayClient/src/BintrayCertificates.h index d85bb4e4..89854c84 100644 --- a/lib/BintrayClient/src/BintrayCertificates.h +++ b/lib/BintrayClient/src/BintrayCertificates.h @@ -1,4 +1,5 @@ /* + Parts of this file Copyright (c) 2014-present PlatformIO Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/lib/BintrayClient/src/BintrayClient.cpp b/lib/BintrayClient/src/BintrayClient.cpp index b0e7543f..955bca20 100644 --- a/lib/BintrayClient/src/BintrayClient.cpp +++ b/lib/BintrayClient/src/BintrayClient.cpp @@ -1,4 +1,5 @@ /* + Parts of this file Copyright (c) 2014-present PlatformIO Licensed under the Apache License, Version 2.0 (the "License"); @@ -94,7 +95,7 @@ String BintrayClient::requestHTTPContent(const String &url) const } else { - Serial.printf("GET request failed, error: %s\n", http.errorToString(httpCode).c_str()); + ESP_LOGE(TAG, "GET request failed, error: %s", http.errorToString(httpCode).c_str()); } http.end(); @@ -109,7 +110,7 @@ String BintrayClient::getLatestVersion() const const size_t bufferSize = 1024; if (jsonResult.length() > bufferSize) { - Serial.println("Error: Could parse JSON. Input data is too big!"); + ESP_LOGE(TAG, "Error: Firmware version data invalid."); return version; } StaticJsonBuffer jsonBuffer; @@ -118,7 +119,7 @@ String BintrayClient::getLatestVersion() const // Check for errors in parsing if (!root.success()) { - Serial.println("Error: Could not parse JSON!"); + ESP_LOGE(TAG, "Error: Firmware version data not found."); return version; } return root.get("name"); @@ -133,7 +134,7 @@ String BintrayClient::getBinaryPath(const String &version) const const size_t bufferSize = 1024; if (jsonResult.length() > bufferSize) { - Serial.println("Error: Could parse JSON. Input data is too big!"); + ESP_LOGE(TAG, "Error: Firmware download path data invalid."); return path; } StaticJsonBuffer jsonBuffer; @@ -142,7 +143,7 @@ String BintrayClient::getBinaryPath(const String &version) const JsonObject &firstItem = root[0]; if (!root.success()) { //Check for errors in parsing - Serial.println("Error: Could not parse JSON!"); + ESP_LOGE(TAG, "Error: Firmware download path not found."); return path; } return "/" + getUser() + "/" + getRepository() + "/" + firstItem.get("path"); diff --git a/lib/BintrayClient/src/BintrayClient.h b/lib/BintrayClient/src/BintrayClient.h index 9761c7c6..d0b7e67d 100644 --- a/lib/BintrayClient/src/BintrayClient.h +++ b/lib/BintrayClient/src/BintrayClient.h @@ -1,4 +1,5 @@ /* + Parts of this file Copyright (c) 2014-present PlatformIO Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/platformio.ini b/platformio.ini index 0bd773fe..97148ac3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,8 +11,9 @@ ;env_default = heltec ;env_default = ttgov1 ;env_default = ttgov2 -env_default = ttgov21 -;env_default = ttgobeam +;env_default = ttgov21old +;env_default = ttgov21new +env_default = ttgobeam ;env_default = lopy ;env_default = lopy4 ;env_default = fipy @@ -25,14 +26,11 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [bintray] user = cyberman54 repository = paxcounter-firmware -package = ttgov21_old api_token = 2e10f923df5d47b9c7e25752510322a1d65ee997 -[common] -release_version = 7 - [ota] -; build configuration based on Bintray and Wi-Fi settings +;release_version = max. 9 chars total, using decimal format "a.b.c" +release_version = 1.4.32 wifi_ssid = testnet wifi_password = test0815 build_flags = @@ -40,18 +38,24 @@ build_flags = '-DWIFI_PASS="${ota.wifi_password}"' '-DBINTRAY_USER="${bintray.user}"' '-DBINTRAY_REPO="${bintray.repository}"' - '-DBINTRAY_PACKAGE="${bintray.package}"' - -DVERSION=${common.release_version} -lib_deps_ota = - https://github.com/platformio/bintray-secure-ota.git + '-DBINTRAY_PACKAGE="${PIOENV}"' + '-DPROGVERSION="${ota.release_version}"' -[common_env_data] +[common] +; DEBUG LEVEL +; For production run setto 0, otherwise device will leak RAM while running! +; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose +debug_level = 0 +; UPLOAD MODE +; select esptool for USB/UART flashing, custom for OTA upload +upload_protocol = esptool +;upload_protocol = custom +extra_scripts = pre:publish_firmware.py platform_espressif32 = espressif32@1.3.0 -;platform_espressif32 = https://github.com/platformio/platform-espressif32.git#feature/stage board_build.partitions = min_spiffs.csv +monitor_speed = 115200 lib_deps_all = ArduinoJson@^5.13.1 -; ArduinoJson${ota.lib_deps_ota} lib_deps_display = U8g2@>=2.23.16 lib_deps_rgbled = @@ -59,204 +63,227 @@ lib_deps_rgbled = lib_deps_gps = TinyGPSPlus@>=1.0.2 Time@>=1.5 -build_flags = +build_flags = ; override lora settings from LMiC library in lmic/config.h and use main.h instead -D_lmic_config_h_ -include "src/paxcounter.conf" -include "src/hal/${PIOENV}.h" ${ota.build_flags} -w -; ---> NOTE: For production run set DEBUG_LEVEL level to NONE! <--- -; otherwise device may leak RAM -; -; None -; -DCORE_DEBUG_LEVEL=0 -; Error -; -DCORE_DEBUG_LEVEL=1 -; Warn -; -DCORE_DEBUG_LEVEL=2 -; Info - -DCORE_DEBUG_LEVEL=3 -; Debug -; -DCORE_DEBUG_LEVEL=4 -; Verbose -; -DCORE_DEBUG_LEVEL=5 + -DCORE_DEBUG_LEVEL=${common.debug_level} + [env:ebox] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 115200 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} + ${common.lib_deps_all} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:heltec] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = heltec_wifi_lora_32 -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 115200 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_display} + ${common.lib_deps_all} + ${common.lib_deps_display} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:ttgov1] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 115200 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_display} + ${common.lib_deps_all} + ${common.lib_deps_display} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:ttgov2] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_display} + ${common.lib_deps_all} + ${common.lib_deps_display} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} -[env:ttgov21] -platform = ${common_env_data.platform_espressif32} +[env:ttgov21old] +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_display} + ${common.lib_deps_all} + ${common.lib_deps_display} build_flags = - ${common_env_data.build_flags} -;upload_protocol = custom -;extra_scripts = pre:publish_firmware.py + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} +[env:ttgov21new] +platform = ${common.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common.board_build.partitions} +upload_speed = 921600 +lib_deps = + ${common.lib_deps_all} + ${common.lib_deps_display} +build_flags = + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:ttgobeam] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_gps} + ${common.lib_deps_all} + ${common.lib_deps_gps} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} -mfix-esp32-psram-cache-issue -;upload_protocol = custom -;extra_scripts = pre:publish_firmware.py +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:fipy] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} + ${common.lib_deps_all} + ${common.lib_deps_rgbled} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:lopy] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} - ${common_env_data.lib_deps_gps} + ${common.lib_deps_all} + ${common.lib_deps_rgbled} + ${common.lib_deps_gps} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:lopy4] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} - ${common_env_data.lib_deps_gps} + ${common.lib_deps_all} + ${common.lib_deps_rgbled} + ${common.lib_deps_gps} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} -mfix-esp32-psram-cache-issue +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:lolin32litelora] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = lolin32 -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} + ${common.lib_deps_all} + ${common.lib_deps_rgbled} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:lolin32lora] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = lolin32 -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} + ${common.lib_deps_all} + ${common.lib_deps_rgbled} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:lolin32lite] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = lolin32 -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} + ${common.lib_deps_all} + ${common.lib_deps_rgbled} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} [env:generic] -platform = ${common_env_data.platform_espressif32} +platform = ${common.platform_espressif32} framework = arduino board = esp32dev -board_build.partitions = ${common_env_data.board_build.partitions} +board_build.partitions = ${common.board_build.partitions} upload_speed = 921600 -monitor_speed = 115200 lib_deps = - ${common_env_data.lib_deps_all} - ${common_env_data.lib_deps_rgbled} - ${common_env_data.lib_deps_gps} - ${common_env_data.lib_deps_display} + ${common.lib_deps_all} + ${common.lib_deps_rgbled} + ${common.lib_deps_gps} + ${common.lib_deps_display} build_flags = - ${common_env_data.build_flags} + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} diff --git a/publish_firmware.py b/publish_firmware.py index 7086d013..b9848707 100644 --- a/publish_firmware.py +++ b/publish_firmware.py @@ -16,31 +16,30 @@ import requests from os.path import basename from platformio import util -Import('env') +Import("env") project_config = util.load_project_config() bintray_config = {k: v for k, v in project_config.items("bintray")} -version = project_config.get("common", "release_version") +version = project_config.get("ota", "release_version") +package = env.get("PIOENV") # # Push new firmware to the Bintray storage using API # -def publish_firmware(source, target, env): +def publish_bintray(source, target, env): firmware_path = str(source[0]) firmware_name = basename(firmware_path) print("Uploading {0} to Bintray. Version: {1}".format( firmware_name, version)) - print(firmware_path, firmware_name) - url = "/".join([ "https://api.bintray.com", "content", bintray_config.get("user"), bintray_config.get("repository"), - bintray_config.get("package"), version, firmware_name + package, version, firmware_name ]) print(url) @@ -65,7 +64,8 @@ def publish_firmware(source, target, env): # Custom upload command and program name + env.Replace( - PROGNAME="firmware_v_%s" % version, - UPLOADCMD=publish_firmware + PROGNAME="firmware_" + package + "_v%s" % version, + UPLOADCMD=publish_bintray ) \ No newline at end of file diff --git a/src/OTA.cpp b/src/OTA.cpp index 87609949..7f46ea85 100644 --- a/src/OTA.cpp +++ b/src/OTA.cpp @@ -59,16 +59,16 @@ void checkFirmwareUpdates() { // Fetch the latest firmware version ESP_LOGI(TAG, "OTA mode, checking latest firmware version on server..."); const String latest = bintray.getLatestVersion(); + if (latest.length() == 0) { ESP_LOGI( TAG, "Could not load info about the latest firmware. Rebooting to runmode."); return; - } else if (atoi(latest.c_str()) <= VERSION) { + } else if (version_compare(latest, cfg.version) <= 0) { ESP_LOGI(TAG, "Current firmware is up to date. Rebooting to runmode."); return; } - ESP_LOGI(TAG, "New firmware version v%s available. Downloading...", latest.c_str()); processOTAUpdate(latest); @@ -218,4 +218,39 @@ void processOTAUpdate(const String &version) { "There was no valid content in the response from the OTA server!"); client.flush(); } +} + +// helper function to compare two versions. Returns 1 if v2 is +// smaller, -1 if v1 is smaller, 0 if equal + +int version_compare(const String v1, const String v2) { + // vnum stores each numeric part of version + int vnum1 = 0, vnum2 = 0; + + // loop untill both string are processed + for (int i = 0, j = 0; (i < v1.length() || j < v2.length());) { + // storing numeric part of version 1 in vnum1 + while (i < v1.length() && v1[i] != '.') { + vnum1 = vnum1 * 10 + (v1[i] - '0'); + i++; + } + + // storing numeric part of version 2 in vnum2 + while (j < v2.length() && v2[j] != '.') { + vnum2 = vnum2 * 10 + (v2[j] - '0'); + j++; + } + + if (vnum1 > vnum2) + return 1; + if (vnum2 > vnum1) + return -1; + + // if equal, reset variables and go for next numeric + // part + vnum1 = vnum2 = 0; + i++; + j++; + } + return 0; } \ No newline at end of file diff --git a/src/OTA.h b/src/OTA.h index fc66973c..1a17fa24 100644 --- a/src/OTA.h +++ b/src/OTA.h @@ -1,6 +1,7 @@ #ifndef OTA_H #define OTA_H +#include "globals.h" #include #include #include @@ -9,5 +10,6 @@ void checkFirmwareUpdates(); void processOTAUpdate(const String &version); void start_ota_update(); +int version_compare(const String v1, const String v2); #endif // OTA_H \ No newline at end of file diff --git a/src/globals.h b/src/globals.h index 20acd5cb..fd057b5d 100644 --- a/src/globals.h +++ b/src/globals.h @@ -4,10 +4,6 @@ // The mother of all embedded development... #include -// attn: increment version after modifications to configData_t truct! -#define PROGVERSION "1.4.30" // use max 10 chars here! -#define PROGNAME "PAXCNT" - // std::set for unified array functions #include #include diff --git a/src/hal/ttgov21.h b/src/hal/ttgov21.h deleted file mode 100644 index 6d59065d..00000000 --- a/src/hal/ttgov21.h +++ /dev/null @@ -1,69 +0,0 @@ -/* Hardware related definitions for TTGO V2.1 Board -/ ATTENTION: check your board version! -/ Different versions are on the market which need different settings in this file: -/ - without label -> use settings (2) -/ - labeled V1.5 on pcb -> use settings (2) -/ - labeled V1.6 on pcb -> use settings (1) -/ Choose the right configuration below -*/ - -/* -// (1) settings for board labeled "T3_V1.6" on pcb - -#define HAS_LORA 1 // comment out if device shall not send data via LoRa -#define HAS_SPI 1 // comment out if device shall not send data via SPI -#define CFG_sx1276_radio 1 // HPD13A LoRa SoC - -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C -#define HAS_LED GPIO_NUM_25 // green on board LED -#define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // uses GPIO7 -#define BATT_FACTOR 2 // voltage divider 100k/100k on board - -// re-define pin definitions of pins_arduino.h -#define PIN_SPI_SS GPIO_NUM_18 // ESP32 GPIO18 (Pin18) -- HPD13A NSS/SEL (Pin4) SPI Chip Select Input -#define PIN_SPI_MOSI GPIO_NUM_27 // ESP32 GPIO27 (Pin27) -- HPD13A MOSI/DSI (Pin6) SPI Data Input -#define PIN_SPI_MISO GPIO_NUM_19 // ESP32 GPIO19 (Pin19) -- HPD13A MISO/DSO (Pin7) SPI Data Output -#define PIN_SPI_SCK GPIO_NUM_5 // ESP32 GPIO5 (Pin5) -- HPD13A SCK (Pin5) SPI Clock Input - -// non arduino pin definitions -#define RST GPIO_NUM_23 // ESP32 GPIO23 <-> HPD13A RESET -#define DIO0 GPIO_NUM_26 // ESP32 GPIO26 <-> HPD13A IO0 -#define DIO1 GPIO_NUM_33 // ESP32 GPIO33 <-> HPDIO1 <-> HPD13A IO1 -#define DIO2 GPIO_NUM_32 // ESP32 GPIO32 <-> HPDIO2 <-> HPD13A IO2 - -// Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display -#define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN -#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 -#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 - -*/ - -// (2) settings for boards without label on pcb, or labeled v1.5 on pcb - -#define HAS_LORA 1 // comment out if device shall not send data via LoRa -#define HAS_SPI 1 // comment out if device shall not send data via SPI -#define CFG_sx1276_radio 1 // HPD13A LoRa SoC -#define HAS_LED NOT_A_PIN // no usable LED on board - -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C -#define DISPLAY_FLIP 1 // rotated display -#define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // uses GPIO7 -#define BATT_FACTOR 2 // voltage divider 100k/100k on board - -// re-define pin definitions of pins_arduino.h -#define PIN_SPI_SS GPIO_NUM_18 // ESP32 GPIO18 (Pin18) -- HPD13A NSS/SEL (Pin4) SPI Chip Select Input -#define PIN_SPI_MOSI GPIO_NUM_27 // ESP32 GPIO27 (Pin27) -- HPD13A MOSI/DSI (Pin6) SPI Data Input -#define PIN_SPI_MISO GPIO_NUM_19 // ESP32 GPIO19 (Pin19) -- HPD13A MISO/DSO (Pin7) SPI Data Output -#define PIN_SPI_SCK GPIO_NUM_5 // ESP32 GPIO5 (Pin5) -- HPD13A SCK (Pin5) SPI Clock Input - -// non arduino pin definitions -#define RST LMIC_UNUSED_PIN // connected to ESP32 RST/EN (old board) -//#define RST GPIO_NUM_12 // (boards labeled v1.5) -#define DIO0 GPIO_NUM_26 // ESP32 GPIO26 <-> HPD13A IO0 -#define DIO1 GPIO_NUM_33 // ESP32 GPIO33 <-> HPDIO1 <-> HPD13A IO1 -#define DIO2 GPIO_NUM_32 // ESP32 GPIO32 <-> HPDIO2 <-> HPD13A IO2 - -// Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display -#define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN -#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 -#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 diff --git a/src/hal/ttgov21new.h b/src/hal/ttgov21new.h new file mode 100644 index 00000000..ce64323f --- /dev/null +++ b/src/hal/ttgov21new.h @@ -0,0 +1,30 @@ +/* Hardware related definitions for TTGO V2.1 Board +// ATTENTION: check your board version! +// This settings are for boards labeled v1.6 on pcb, NOT for v1.5 or older +*/ + +#define HAS_LORA 1 // comment out if device shall not send data via LoRa +#define HAS_SPI 1 // comment out if device shall not send data via SPI +#define CFG_sx1276_radio 1 // HPD13A LoRa SoC + +#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_LED GPIO_NUM_25 // green on board LED +#define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // uses GPIO7 +#define BATT_FACTOR 2 // voltage divider 100k/100k on board + +// re-define pin definitions of pins_arduino.h +#define PIN_SPI_SS GPIO_NUM_18 // ESP32 GPIO18 (Pin18) -- HPD13A NSS/SEL (Pin4) SPI Chip Select Input +#define PIN_SPI_MOSI GPIO_NUM_27 // ESP32 GPIO27 (Pin27) -- HPD13A MOSI/DSI (Pin6) SPI Data Input +#define PIN_SPI_MISO GPIO_NUM_19 // ESP32 GPIO19 (Pin19) -- HPD13A MISO/DSO (Pin7) SPI Data Output +#define PIN_SPI_SCK GPIO_NUM_5 // ESP32 GPIO5 (Pin5) -- HPD13A SCK (Pin5) SPI Clock Input + +// non arduino pin definitions +#define RST GPIO_NUM_23 // ESP32 GPIO23 <-> HPD13A RESET +#define DIO0 GPIO_NUM_26 // ESP32 GPIO26 <-> HPD13A IO0 +#define DIO1 GPIO_NUM_33 // ESP32 GPIO33 <-> HPDIO1 <-> HPD13A IO1 +#define DIO2 GPIO_NUM_32 // ESP32 GPIO32 <-> HPDIO2 <-> HPD13A IO2 + +// Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display +#define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN +#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 +#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 \ No newline at end of file diff --git a/src/hal/ttgov21old.h b/src/hal/ttgov21old.h new file mode 100644 index 00000000..f6882187 --- /dev/null +++ b/src/hal/ttgov21old.h @@ -0,0 +1,32 @@ +/* Hardware related definitions for TTGO V2.1 Board +// ATTENTION: check your board version! +// This settings are for boards without label on pcb, or labeled v1.5 on pcb +*/ + +#define HAS_LORA 1 // comment out if device shall not send data via LoRa +#define HAS_SPI 1 // comment out if device shall not send data via SPI +#define CFG_sx1276_radio 1 // HPD13A LoRa SoC +#define HAS_LED NOT_A_PIN // no usable LED on board + +#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define DISPLAY_FLIP 1 // rotated display +#define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // uses GPIO7 +#define BATT_FACTOR 2 // voltage divider 100k/100k on board + +// re-define pin definitions of pins_arduino.h +#define PIN_SPI_SS GPIO_NUM_18 // ESP32 GPIO18 (Pin18) -- HPD13A NSS/SEL (Pin4) SPI Chip Select Input +#define PIN_SPI_MOSI GPIO_NUM_27 // ESP32 GPIO27 (Pin27) -- HPD13A MOSI/DSI (Pin6) SPI Data Input +#define PIN_SPI_MISO GPIO_NUM_19 // ESP32 GPIO19 (Pin19) -- HPD13A MISO/DSO (Pin7) SPI Data Output +#define PIN_SPI_SCK GPIO_NUM_5 // ESP32 GPIO5 (Pin5) -- HPD13A SCK (Pin5) SPI Clock Input + +// non arduino pin definitions +#define RST LMIC_UNUSED_PIN // connected to ESP32 RST/EN (old board) +//#define RST GPIO_NUM_12 // (boards labeled v1.5) +#define DIO0 GPIO_NUM_26 // ESP32 GPIO26 <-> HPD13A IO0 +#define DIO1 GPIO_NUM_33 // ESP32 GPIO33 <-> HPDIO1 <-> HPD13A IO1 +#define DIO2 GPIO_NUM_32 // ESP32 GPIO32 <-> HPDIO2 <-> HPD13A IO2 + +// Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display +#define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN +#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 +#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 diff --git a/src/main.cpp b/src/main.cpp index 6de8432c..d84deb18 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -90,7 +90,7 @@ void setup() { esp_log_set_vprintf(redirect_log); #endif - ESP_LOGI(TAG, "Starting %s v%s", PROGNAME, PROGVERSION); + ESP_LOGI(TAG, "Starting %s v%s", PRODUCTNAME, PROGVERSION); // initialize system event handler for wifi task, needed for // wifi_sniffer_init() @@ -213,7 +213,7 @@ void setup() { #ifdef HAS_DISPLAY strcat_P(features, " OLED"); DisplayState = cfg.screenon; - init_display(PROGNAME, PROGVERSION); + init_display(PRODUCTNAME, PROGVERSION); // setup display refresh trigger IRQ using esp32 hardware timer // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ diff --git a/src/paxcounter.conf b/src/paxcounter.conf index 68681b9b..92380894 100644 --- a/src/paxcounter.conf +++ b/src/paxcounter.conf @@ -4,6 +4,8 @@ // // Note: After editing, before "build", use "clean" button in PlatformIO! +#define PRODUCTNAME "PAXCNT" + // Verbose enables serial output #define VERBOSE 1 // comment out to silence the device, for mute use build option diff --git a/src/payload.cpp b/src/payload.cpp index 61514f94..38c2df76 100644 --- a/src/payload.cpp +++ b/src/payload.cpp @@ -52,8 +52,8 @@ void PayloadConvert::addConfig(configData_t value) { cursor += 10; } -void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, - float cputemp, uint32_t mem) { +void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, float cputemp, + uint32_t mem, uint8_t reset1, uint8_t reset2) { buffer[cursor++] = highByte(voltage); buffer[cursor++] = lowByte(voltage); @@ -124,6 +124,7 @@ void PayloadConvert::addConfig(configData_t value) { value.screenon ? true : false, value.countermode ? true : false, value.blescan ? true : false, value.wifiant ? true : false, value.vendorfilter ? true : false, value.gpsmode ? true : false); + writeVersion(value.version); } void PayloadConvert::addStatus(uint16_t voltage, uint64_t uptime, float cputemp, @@ -160,6 +161,11 @@ void PayloadConvert::writeUptime(uint64_t uptime) { intToBytes(cursor, uptime, 8); } +void PayloadConvert::writeVersion(char * version) { + memcpy(buffer + cursor, version, 10); + cursor += 10; +} + void PayloadConvert::writeLatLng(double latitude, double longitude) { intToBytes(cursor, latitude, 4); intToBytes(cursor, longitude, 4); diff --git a/src/payload.h b/src/payload.h index 6cf453e5..5973bdd9 100644 --- a/src/payload.h +++ b/src/payload.h @@ -64,6 +64,7 @@ private: void writeUint8(uint8_t i); void writeHumidity(float humidity); void writeTemperature(float temperature); + void writeVersion(char * version); void writeBitmap(bool a, bool b, bool c, bool d, bool e, bool f, bool g, bool h); From 836572eeb241fc59756d482aece56fcf08d221af Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Mon, 17 Sep 2018 18:06:50 +0200 Subject: [PATCH 022/105] cleanup platformio.ini --- platformio.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/platformio.ini b/platformio.ini index 97148ac3..8d614480 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,20 +6,21 @@ ; ---> SELECT TARGET PLATFORM HERE! <--- [platformio] -;env_default = generic +env_default = generic ;env_default = ebox ;env_default = heltec ;env_default = ttgov1 ;env_default = ttgov2 ;env_default = ttgov21old ;env_default = ttgov21new -env_default = ttgobeam +;env_default = ttgobeam ;env_default = lopy ;env_default = lopy4 ;env_default = fipy ;env_default = lolin32litelora ;env_default = lolin32lora ;env_default = lolin32lite +;env_default = ebox, heltec, ttgobeam, lopy4, lopy, ttgov21old, ttgov21new ; description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around. @@ -72,7 +73,6 @@ build_flags = -w -DCORE_DEBUG_LEVEL=${common.debug_level} - [env:ebox] platform = ${common.platform_espressif32} framework = arduino @@ -286,4 +286,4 @@ build_flags = ${common.build_flags} upload_protocol = ${common.upload_protocol} extra_scripts = ${common.extra_scripts} -monitor_speed = ${common.monitor_speed} +monitor_speed = ${common.monitor_speed} \ No newline at end of file From 52efcb7b019152e149aa49011ee39fc79eac31b4 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Mon, 17 Sep 2018 21:57:01 +0200 Subject: [PATCH 023/105] TTN decoders updated --- src/TTN/packed_decoder.js | 14 +++++++++++--- src/TTN/plain_decoder.js | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/TTN/packed_decoder.js b/src/TTN/packed_decoder.js index 9aaf1cfc..db04e5ab 100644 --- a/src/TTN/packed_decoder.js +++ b/src/TTN/packed_decoder.js @@ -18,12 +18,12 @@ function Decoder(bytes, port) { if (port === 2) { // device status data - return decode(bytes, [uint16, uptime, uint8, uint32], ['voltage', 'uptime', 'cputemp', 'memory']); + return decode(bytes, [uint16, uptime, uint8, uint32, uint8, uint8], ['voltage', 'uptime', 'cputemp', 'memory', 'reset0', 'reset1']); } if (port === 3) { // device config data - return decode(bytes, [uint8, uint8, uint16, uint8, uint8, uint8, uint8, bitmap], ['lorasf', 'txpower', 'rssilimit', 'sendcycle', 'wifichancycle', 'blescantime', 'rgblum', 'flags']); + return decode(bytes, [uint8, uint8, uint16, uint8, uint8, uint8, uint8, bitmap, version], ['lorasf', 'txpower', 'rssilimit', 'sendcycle', 'wifichancycle', 'blescantime', 'rgblum', 'flags', 'version']); } if (port === 4) { @@ -55,6 +55,14 @@ var bytesToInt = function (bytes) { return i; }; +var version = function (bytes) { + if (bytes.length !== version.BYTES) { + throw new Error('version must have exactly 10 bytes'); + } + return String.fromCharCode.apply(null, bytes).split('\u0000')[0]; + }; +version.BYTES = 10; + var uint8 = function (bytes) { if (bytes.length !== uint8.BYTES) { throw new Error('uint8 must have exactly 1 byte'); @@ -180,12 +188,12 @@ if (typeof module === 'object' && typeof module.exports !== 'undefined') { uint16: uint16, uint32: uint32, uptime: uptime, - reset: reset, temperature: temperature, humidity: humidity, latLng: latLng, hdop: hdop, bitmap: bitmap, + version: version, decode: decode }; } \ No newline at end of file diff --git a/src/TTN/plain_decoder.js b/src/TTN/plain_decoder.js index ba674d35..0de53fc2 100644 --- a/src/TTN/plain_decoder.js +++ b/src/TTN/plain_decoder.js @@ -25,6 +25,9 @@ function Decoder(bytes, port) { decoded.uptime = ((bytes[i++] << 56) | (bytes[i++] << 48) | (bytes[i++] << 40) | (bytes[i++] << 32) | (bytes[i++] << 24) | (bytes[i++] << 16) | (bytes[i++] << 8) | bytes[i++]); decoded.temp = bytes[i++]; + decoded.memory = ((bytes[i++] << 24) | (bytes[i++] << 16) | (bytes[i++] << 8) | bytes[i++]); + decoded.reset0 = bytes[i++]; + decoded.reset1 = bytes[i++]; } if (port === 5) { From 6052588af0b5545fcafd819b777cdde4f136be91 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Mon, 17 Sep 2018 22:03:03 +0200 Subject: [PATCH 024/105] TTN decoders updated --- src/TTN/packed_decoder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TTN/packed_decoder.js b/src/TTN/packed_decoder.js index db04e5ab..fd9b955e 100644 --- a/src/TTN/packed_decoder.js +++ b/src/TTN/packed_decoder.js @@ -60,7 +60,7 @@ var version = function (bytes) { throw new Error('version must have exactly 10 bytes'); } return String.fromCharCode.apply(null, bytes).split('\u0000')[0]; - }; +}; version.BYTES = 10; var uint8 = function (bytes) { From 2201e48800863cba8c3405bd68cd3da02bb19ffd Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Tue, 18 Sep 2018 17:23:56 +0200 Subject: [PATCH 025/105] build.py, ota.conf, platformio.ini --- .gitignore | 3 +- build.py | 77 +++++++++++++++++++++++++++++++++++++++++++++ platformio.ini | 34 ++++++-------------- publish_firmware.py | 71 ----------------------------------------- src/ota.sample.conf | 8 +++++ 5 files changed, 96 insertions(+), 97 deletions(-) create mode 100644 build.py delete mode 100644 publish_firmware.py create mode 100644 src/ota.sample.conf diff --git a/.gitignore b/.gitignore index dbccbfe1..875ecd9f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ .vscode/.browse.c_cpp.db* .clang_complete .gcc-flags.json -src/loraconf.h \ No newline at end of file +src/loraconf.h +src/ota.conf \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 00000000..f0fdc73e --- /dev/null +++ b/build.py @@ -0,0 +1,77 @@ +# build.py +# pre-build script, setting up build environment + +import requests +from os.path import basename +from platformio import util + +Import("env") + +# get keyfile from platformio.ini and parse it +project_config = util.load_project_config() +keyfile = str(env.get("PROJECTSRC_DIR")) + "/" + project_config.get("common", "keyfile") +print "Parsing OTA keys from " + keyfile +mykeys = {} +with open(keyfile) as myfile: + for line in myfile: + key, value = line.partition("=")[::2] + mykeys[key.strip()] = str(value).strip() + +# get bintray credentials from keyfile +user = mykeys["BINTRAY_USER"] +repository = mykeys["BINTRAY_REPO"] +apitoken = mykeys["BINTRAY_API_TOKEN"] + +# get bintray parameters from platformio.ini +version = project_config.get("common", "release_version") +package = str(env.get("PIOENV")) + +# put bintray credentials to platformio environment +env.Replace(BINTRAY_USER=user) +env.Replace(BINTRAY_REPO=repository) +env.Replace(BINTRAY_API_TOKEN=apitoken) + +# get runtime credentials from keyfile and put them to compiler directive +env.Replace(CPPDEFINES=[ + ('WIFI_SSID', '\\"' + mykeys["OTA_WIFI_SSID"] + '\\"'), + ('WIFI_PASS', '\\"' + mykeys["OTA_WIFI_PASS"] + '\\"'), + ('BINTRAY_USER', '\\"' + mykeys["BINTRAY_USER"] + '\\"'), + ('BINTRAY_REPO', '\\"' + mykeys["BINTRAY_REPO"] + '\\"'), + ]) + +# function for pushing new firmware to bintray storage using API +def publish_bintray(source, target, env): + firmware_path = str(source[0]) + firmware_name = basename(firmware_path) + url = "/".join([ + "https://api.bintray.com", "content", + user, repository, package, version, firmware_name + ]) + + print("Uploading {0} to Bintray. Version: {1}".format( + firmware_name, version)) + print(url) + + headers = { + "Content-type": "application/octet-stream", + "X-Bintray-Publish": "1", + "X-Bintray-Override": "1" + } + + r = requests.put( + url, + data=open(firmware_path, "rb"), + headers=headers, + auth=(user, apitoken)) + + if r.status_code != 201: + print("Failed to submit package: {0}\n{1}".format( + r.status_code, r.text)) + else: + print("The firmware has been successfuly published at Bintray.com!") + +# put build file name and upload command to platformio environment +env.Replace( + PROGNAME="firmware_" + package + "_v%s" % version, + UPLOADCMD=publish_bintray +) \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 8d614480..5ca50241 100644 --- a/platformio.ini +++ b/platformio.ini @@ -24,34 +24,17 @@ env_default = generic ; description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around. -[bintray] -user = cyberman54 -repository = paxcounter-firmware -api_token = 2e10f923df5d47b9c7e25752510322a1d65ee997 - -[ota] -;release_version = max. 9 chars total, using decimal format "a.b.c" -release_version = 1.4.32 -wifi_ssid = testnet -wifi_password = test0815 -build_flags = - '-DWIFI_SSID="${ota.wifi_ssid}"' - '-DWIFI_PASS="${ota.wifi_password}"' - '-DBINTRAY_USER="${bintray.user}"' - '-DBINTRAY_REPO="${bintray.repository}"' - '-DBINTRAY_PACKAGE="${PIOENV}"' - '-DPROGVERSION="${ota.release_version}"' - [common] -; DEBUG LEVEL -; For production run setto 0, otherwise device will leak RAM while running! +; for release_version use max.10 chars total, use any decimal format like "a.b.c" +release_version = 1.4.33 +; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 -; UPLOAD MODE -; select esptool for USB/UART flashing, custom for OTA upload +; UPLOAD MODE: select esptool to flash via USB/UART, select custom to upload to cloud for OTA upload_protocol = esptool ;upload_protocol = custom -extra_scripts = pre:publish_firmware.py +extra_scripts = pre:build.py +keyfile = ota.conf platform_espressif32 = espressif32@1.3.0 board_build.partitions = min_spiffs.csv monitor_speed = 115200 @@ -69,9 +52,10 @@ build_flags = -D_lmic_config_h_ -include "src/paxcounter.conf" -include "src/hal/${PIOENV}.h" - ${ota.build_flags} -w - -DCORE_DEBUG_LEVEL=${common.debug_level} + '-DCORE_DEBUG_LEVEL=${common.debug_level}' + '-DBINTRAY_PACKAGE="${PIOENV}"' + '-DPROGVERSION="${common.release_version}"' [env:ebox] platform = ${common.platform_espressif32} diff --git a/publish_firmware.py b/publish_firmware.py deleted file mode 100644 index b9848707..00000000 --- a/publish_firmware.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2014-present PlatformIO -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import requests -from os.path import basename -from platformio import util - -Import("env") - -project_config = util.load_project_config() -bintray_config = {k: v for k, v in project_config.items("bintray")} -version = project_config.get("ota", "release_version") -package = env.get("PIOENV") - -# -# Push new firmware to the Bintray storage using API -# - - -def publish_bintray(source, target, env): - firmware_path = str(source[0]) - firmware_name = basename(firmware_path) - - print("Uploading {0} to Bintray. Version: {1}".format( - firmware_name, version)) - - url = "/".join([ - "https://api.bintray.com", "content", - bintray_config.get("user"), - bintray_config.get("repository"), - package, version, firmware_name - ]) - - print(url) - - headers = { - "Content-type": "application/octet-stream", - "X-Bintray-Publish": "1", - "X-Bintray-Override": "1" - } - - r = requests.put( - url, - data=open(firmware_path, "rb"), - headers=headers, - auth=(bintray_config.get("user"), bintray_config['api_token'])) - - if r.status_code != 201: - print("Failed to submit package: {0}\n{1}".format( - r.status_code, r.text)) - else: - print("The firmware has been successfuly published at Bintray.com!") - - -# Custom upload command and program name - -env.Replace( - PROGNAME="firmware_" + package + "_v%s" % version, - UPLOADCMD=publish_bintray -) \ No newline at end of file diff --git a/src/ota.sample.conf b/src/ota.sample.conf new file mode 100644 index 00000000..f5aed2a7 --- /dev/null +++ b/src/ota.sample.conf @@ -0,0 +1,8 @@ +[ota] +OTA_WIFI_SSID = myhomewifi +OTA_WIFI_PASS = FooBar42! + +[bintray] +BINTRAY_USER = mybintrayuser +BINTRAY_REPO = mybintrayrepo +BINTRAY_API_TOKEN = 2e10f923df5d47b9c5423432322a1d4324783997 \ No newline at end of file From ce1b774d283a435e261c47c04277533f8fb06286 Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Tue, 18 Sep 2018 17:25:18 +0200 Subject: [PATCH 026/105] Rename OTA.cpp to ota.cpp --- src/{OTA.cpp => ota.cpp} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{OTA.cpp => ota.cpp} (99%) diff --git a/src/OTA.cpp b/src/ota.cpp similarity index 99% rename from src/OTA.cpp rename to src/ota.cpp index 7f46ea85..5d94bbed 100644 --- a/src/OTA.cpp +++ b/src/ota.cpp @@ -253,4 +253,4 @@ int version_compare(const String v1, const String v2) { j++; } return 0; -} \ No newline at end of file +} From 14957bea9627de8a01822d1f621edd634471fa5c Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Tue, 18 Sep 2018 17:25:44 +0200 Subject: [PATCH 027/105] Rename OTA.h to ota.h --- src/{OTA.h => ota.h} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{OTA.h => ota.h} (94%) diff --git a/src/OTA.h b/src/ota.h similarity index 94% rename from src/OTA.h rename to src/ota.h index 1a17fa24..4b9e92d1 100644 --- a/src/OTA.h +++ b/src/ota.h @@ -12,4 +12,4 @@ void processOTAUpdate(const String &version); void start_ota_update(); int version_compare(const String v1, const String v2); -#endif // OTA_H \ No newline at end of file +#endif // OTA_H From aa9129abcad8fafc8e3d2095692ed00b8bed71d4 Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Tue, 18 Sep 2018 18:01:56 +0200 Subject: [PATCH 028/105] Update README.md --- README.md | 52 +++++++--------------------------------------------- 1 file changed, 7 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 870f3656..047b3ff3 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,13 @@ If you're using [TheThingsNetwork](https://www.thethingsnetwork.org/) (TTN) you To track a paxcounter device with on board GPS and at the same time contribute to TTN coverage mapping, you simply activate the [TTNmapper integration](https://www.thethingsnetwork.org/docs/applications/ttnmapper/) in TTN Console. The formats *plain* and *packed* generate the fields `latitude`, `longitude` and `hdop` required by ttnmapper. -Hereafter described is the default *plain* format, which uses MSB bit numbering. +Hereafter described is the default *plain* format, which uses MSB bit numbering. Under /TTN in this repository you find some ready-to-go decoders which you may copy to your TTN console: + +[**plain_decoder.js**](src/TTN/plain_decoder.js) | +[**plain_converter.js**](src/TTN/plain_converter.js) | +[**pdacked_decoder.js**](src/TTN/packed_decoder.js) | +[**packed_converter.js**](src/TTN/packed_converter.js) + **Port #1:** Paxcount data @@ -182,50 +188,6 @@ Hereafter described is the default *plain* format, which uses MSB bit numbering. byte 1: Beacon RSSI reception level byte 2: Beacon identifier (0..255) - -[**plain_decoder.js**](src/TTN/plain_decoder.js) - -```javascript -function Decoder(bytes, port) { - var decoded = {}; - - if (port === 1) { - var i = 0; - decoded.wifi = (bytes[i++] << 8) | bytes[i++]; - decoded.ble = (bytes[i++] << 8) | bytes[i++]; - if (bytes.length > 4) { - decoded.latitude = ( (bytes[i++] << 24) | (bytes[i++] << 16) | (bytes[i++] << 8) | bytes[i++] ); - decoded.longitude = ( (bytes[i++] << 24) | (bytes[i++] << 16) | (bytes[i++] << 8) | bytes[i++] ); - decoded.sats = ( bytes[i++] ); - decoded.hdop = ( bytes[i++] << 8) | (bytes[i++] ); - decoded.altitude = ( bytes[i++] << 8) | (bytes[i++] ); - } - } - - return decoded; -} -``` - -[**plain_converter.js**](src/TTN/plain_converter.js) - -```javascript -function Converter(decoded, port) { - - var converted = decoded; - - if (port === 1) { - converted.pax = converted.ble + converted.wifi; - if (converted.hdop) { - converted.hdop /= 100; - converted.latitude /= 1000000; - converted.longitude /= 1000000; - } - } - - return converted; -} -``` - # Remote control The device listenes for remote control commands on LoRaWAN Port 2. Multiple commands per downlink are possible by concatenating them. From f18c567adf7b15f1480a524b2d44cf0d3dede5e7 Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Tue, 18 Sep 2018 18:03:06 +0200 Subject: [PATCH 029/105] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 047b3ff3..69a076ed 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ Hereafter described is the default *plain* format, which uses MSB bit numbering. [**plain_decoder.js**](src/TTN/plain_decoder.js) | [**plain_converter.js**](src/TTN/plain_converter.js) | -[**pdacked_decoder.js**](src/TTN/packed_decoder.js) | +[**packed_decoder.js**](src/TTN/packed_decoder.js) | [**packed_converter.js**](src/TTN/packed_converter.js) From ed50a79297d14404c440c1a943c9dec1ecad2eb0 Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Tue, 18 Sep 2018 18:43:51 +0200 Subject: [PATCH 030/105] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 69a076ed..dfa9eafe 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,14 @@ Use PlatformIO with your preferred IDE for # Uploading -To upload the code to your ESP32 board this needs to be switched from run to bootloader mode. Boards with USB bridge like Heltec and TTGO usually have an onboard logic which allows soft switching by the upload tool. In PlatformIO this happenes automatically.

+- **Initially, using USB/UART cable:** +To upload the code via cable to your ESP32 board this needs to be switched from run to bootloader mode. Boards with USB bridge like Heltec and TTGO usually have an onboard logic which allows soft switching by the upload tool. In PlatformIO this happenes automatically.

The LoPy/LoPy4/FiPy board needs to be set manually. See these instructions how to do it. Don't forget to press on board reset button after switching between run and bootloader mode.

The original Pycom firmware is not needed, so there is no need to update it before flashing Paxcounter. Just flash the compiled paxcounter binary (.elf file) on your LoPy/LoPy4/FiPy. If you later want to go back to the Pycom firmware, download the firmware from Pycom and flash it over. + +- **During runtime, using FOTA via WIFI:** +After the ESP32 board is initially flashed and has joined a LoRaWAN network, the firmware can update itself by FOTA. This process is kicked off by sending a remote control command (see below) via LoRaWAN to the board. The board then tries to connect via WIFI to a cloud service (JFrog Bintray), checks for update, and if available downloads the binary and reboots with it. If something goes wrong during this process, the board reboots back to the current version. Prerequisites for FOTA are: 1. You own a Bintray repository (free), 2. you pushed the update binary to the Bintray repository, 3. internet access via encrypted (WPA2) WIFI is present at the board's site, 4. WIFI credentials were set in ota.conf and initially flashed to the board. Step 2 runs automated, just enter the credentials in ota.conf and set `upload_protocol = custom` in platformio.ini. Then press build and lean back watching platformio doing build and upload. # Legal note From d80d1e24e9b1a278f7e94d894dfe6c0c4c80cd43 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Tue, 18 Sep 2018 22:58:02 +0200 Subject: [PATCH 031/105] ota.cpp: added a retry loop for write-to-flash --- platformio.ini | 2 +- src/ota.cpp | 36 +++++++++++++++++++++++++----------- src/paxcounter.conf | 1 + 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/platformio.ini b/platformio.ini index 5ca50241..7cf58877 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max.10 chars total, use any decimal format like "a.b.c" -release_version = 1.4.33 +release_version = 1.4.34 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 diff --git a/src/ota.cpp b/src/ota.cpp index 5d94bbed..34136f85 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -112,7 +112,7 @@ void processOTAUpdate(const String &version) { } } - // ESP_LOGI(TAG, "Requesting: " + firmwarePath); + ESP_LOGI(TAG, "Requesting %s", firmwarePath); client.print(String("GET ") + firmwarePath + " HTTP/1.1\r\n"); client.print(String("Host: ") + currentHost + "\r\n"); @@ -185,20 +185,33 @@ void processOTAUpdate(const String &version) { // check whether we have everything for OTA update if (contentLength && isValidContentType) { - if (Update.begin(contentLength)) { - ESP_LOGI(TAG, "Starting OTA update. This will take some time to " - "complete..."); - size_t written = Update.writeStream(client); - if (written == contentLength) { - ESP_LOGI(TAG, "Written %d bytes successfully", written); - } else { - ESP_LOGI(TAG, "Written only %d of %d bytes, OTA update cancelled.", - written, contentLength); - // Retry?? + size_t written; + + if (Update.begin(contentLength)) { + + int i = FLASH_MAX_TRY; + while ((i--) && (written != contentLength)) { + + ESP_LOGI(TAG, + "Starting OTA update, attempt %d of %d. This will take some " + "time to complete...", + i, FLASH_MAX_TRY); + + written = Update.writeStream(client); + + if (written == contentLength) { + ESP_LOGI(TAG, "Written %d bytes successfully", written); + break; + } else { + ESP_LOGI(TAG, + "Written only %d of %d bytes, OTA update attempt cancelled.", + written, contentLength); + } } if (Update.end()) { + if (Update.isFinished()) { ESP_LOGI(TAG, "OTA update completed. Rebooting to runmode."); ESP.restart(); @@ -209,6 +222,7 @@ void processOTAUpdate(const String &version) { } else { ESP_LOGI(TAG, "An error occurred. Error #: %d", Update.getError()); } + } else { ESP_LOGI(TAG, "There isn't enough space to start OTA update"); client.flush(); diff --git a/src/paxcounter.conf b/src/paxcounter.conf index 92380894..1c6c6948 100644 --- a/src/paxcounter.conf +++ b/src/paxcounter.conf @@ -67,6 +67,7 @@ // OTA settings #define WIFI_MAX_TRY 20 // maximum number of wifi connect attempts for OTA update [default = 20] +#define FLASH_MAX_TRY 3 // maximum number of attempts for writing update binary to flash [default = 3] // LMIC settings // define hardware independent LMIC settings here, settings of standard library in /lmic/config.h will be ignored From 7b6c2f1090abaa008de586153b2c94289e6fcbf7 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 01:35:20 +0200 Subject: [PATCH 032/105] ota.cpp: bugfix --- platformio.ini | 4 ++-- src/ota.cpp | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/platformio.ini b/platformio.ini index 7cf58877..89b44400 100644 --- a/platformio.ini +++ b/platformio.ini @@ -25,8 +25,8 @@ env_default = generic description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around. [common] -; for release_version use max.10 chars total, use any decimal format like "a.b.c" -release_version = 1.4.34 +; for release_version use max. 10 chars total, use any decimal format like "a.b.c" +release_version = 1.4.35 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 diff --git a/src/ota.cpp b/src/ota.cpp index 34136f85..c6e7c69b 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -112,7 +112,7 @@ void processOTAUpdate(const String &version) { } } - ESP_LOGI(TAG, "Requesting %s", firmwarePath); + ESP_LOGI(TAG, "Requesting %s", firmwarePath.c_str()); client.print(String("GET ") + firmwarePath + " HTTP/1.1\r\n"); client.print(String("Host: ") + currentHost + "\r\n"); @@ -162,7 +162,6 @@ void processOTAUpdate(const String &version) { currentHost = newUrl.substring(0, newUrl.indexOf('/')); newUrl.remove(newUrl.indexOf(currentHost), currentHost.length()); firmwarePath = newUrl; - ESP_LOGI(TAG, "firmwarePath: %s", firmwarePath.c_str()); continue; } @@ -196,7 +195,7 @@ void processOTAUpdate(const String &version) { ESP_LOGI(TAG, "Starting OTA update, attempt %d of %d. This will take some " "time to complete...", - i, FLASH_MAX_TRY); + FLASH_MAX_TRY - i, FLASH_MAX_TRY); written = Update.writeStream(client); @@ -211,9 +210,12 @@ void processOTAUpdate(const String &version) { } if (Update.end()) { - + if (Update.isFinished()) { - ESP_LOGI(TAG, "OTA update completed. Rebooting to runmode."); + ESP_LOGI( + TAG, + "OTA update completed. Rebooting to runmode with new version."); + client.stop(); ESP.restart(); } else { ESP_LOGI(TAG, "Something went wrong! OTA update hasn't been finished " @@ -232,6 +234,10 @@ void processOTAUpdate(const String &version) { "There was no valid content in the response from the OTA server!"); client.flush(); } + ESP_LOGI(TAG, + "OTA update failed. Rebooting to runmode with current version."); + client.stop(); + ESP.restart(); } // helper function to compare two versions. Returns 1 if v2 is From 8b2e155576042daad5e8c1e5a42fa20d067bc54b Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Wed, 19 Sep 2018 11:04:23 +0200 Subject: [PATCH 033/105] Rename ota.sample.conf to ota.conf --- src/{ota.sample.conf => ota.conf} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{ota.sample.conf => ota.conf} (67%) diff --git a/src/ota.sample.conf b/src/ota.conf similarity index 67% rename from src/ota.sample.conf rename to src/ota.conf index f5aed2a7..b27d9092 100644 --- a/src/ota.sample.conf +++ b/src/ota.conf @@ -5,4 +5,4 @@ OTA_WIFI_PASS = FooBar42! [bintray] BINTRAY_USER = mybintrayuser BINTRAY_REPO = mybintrayrepo -BINTRAY_API_TOKEN = 2e10f923df5d47b9c5423432322a1d4324783997 \ No newline at end of file +BINTRAY_API_TOKEN = 2e10f923df5d47b9c5423432322a1d4324783997 From c33112c3b455b4eede398986952bea9af9804e1e Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 11:38:29 +0200 Subject: [PATCH 034/105] ota.cpp bugfix --- src/ota.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ota.cpp b/src/ota.cpp index c6e7c69b..dbdcc30d 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -216,7 +216,7 @@ void processOTAUpdate(const String &version) { TAG, "OTA update completed. Rebooting to runmode with new version."); client.stop(); - ESP.restart(); + return; } else { ESP_LOGI(TAG, "Something went wrong! OTA update hasn't been finished " "properly."); @@ -237,7 +237,6 @@ void processOTAUpdate(const String &version) { ESP_LOGI(TAG, "OTA update failed. Rebooting to runmode with current version."); client.stop(); - ESP.restart(); } // helper function to compare two versions. Returns 1 if v2 is From 8f36b313e33c57c860b76d5a163e3be08cfd3f76 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 13:33:48 +0200 Subject: [PATCH 035/105] ignore ota.conf --- src/ota.conf | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/ota.conf diff --git a/src/ota.conf b/src/ota.conf deleted file mode 100644 index b27d9092..00000000 --- a/src/ota.conf +++ /dev/null @@ -1,8 +0,0 @@ -[ota] -OTA_WIFI_SSID = myhomewifi -OTA_WIFI_PASS = FooBar42! - -[bintray] -BINTRAY_USER = mybintrayuser -BINTRAY_REPO = mybintrayrepo -BINTRAY_API_TOKEN = 2e10f923df5d47b9c5423432322a1d4324783997 From 421b5786e3df4c7bb58ffb50d4e6aaf214c4912f Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Wed, 19 Sep 2018 13:48:33 +0200 Subject: [PATCH 036/105] Add files via upload --- src/ota.conf.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/ota.conf.txt diff --git a/src/ota.conf.txt b/src/ota.conf.txt new file mode 100644 index 00000000..5a525116 --- /dev/null +++ b/src/ota.conf.txt @@ -0,0 +1,8 @@ +[ota] +OTA_WIFI_SSID = MyHomeWifi +OTA_WIFI_PASS = FooBar42! + +[bintray] +BINTRAY_USER = MyBintrayUser +BINTRAY_REPO = MyBintrayRepo +BINTRAY_API_TOKEN = 3894a7a51d70c6523c1b7479261c34845ebf7878 \ No newline at end of file From 8026d4426beaf2d0724ccc84586c075e2f4305ec Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Wed, 19 Sep 2018 13:49:21 +0200 Subject: [PATCH 037/105] Rename ota.conf.txt to ota.conf --- src/{ota.conf.txt => ota.conf} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{ota.conf.txt => ota.conf} (64%) diff --git a/src/ota.conf.txt b/src/ota.conf similarity index 64% rename from src/ota.conf.txt rename to src/ota.conf index 5a525116..373d7af5 100644 --- a/src/ota.conf.txt +++ b/src/ota.conf @@ -5,4 +5,4 @@ OTA_WIFI_PASS = FooBar42! [bintray] BINTRAY_USER = MyBintrayUser BINTRAY_REPO = MyBintrayRepo -BINTRAY_API_TOKEN = 3894a7a51d70c6523c1b7479261c34845ebf7878 \ No newline at end of file +BINTRAY_API_TOKEN = 3894a7a51d70c6523c1b7479261c34845ebf7878 From 8d65c84d1648b2faeef8dd01e8cb93fddf54d2b5 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 13:52:14 +0200 Subject: [PATCH 038/105] ota.conf --- src/ota.conf | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/ota.conf diff --git a/src/ota.conf b/src/ota.conf deleted file mode 100644 index 373d7af5..00000000 --- a/src/ota.conf +++ /dev/null @@ -1,8 +0,0 @@ -[ota] -OTA_WIFI_SSID = MyHomeWifi -OTA_WIFI_PASS = FooBar42! - -[bintray] -BINTRAY_USER = MyBintrayUser -BINTRAY_REPO = MyBintrayRepo -BINTRAY_API_TOKEN = 3894a7a51d70c6523c1b7479261c34845ebf7878 From 22aef4c9251fbbdcb95848564325feabf925b990 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 13:54:14 +0200 Subject: [PATCH 039/105] sample_ota.conf --- src/sample_ota.conf | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/sample_ota.conf diff --git a/src/sample_ota.conf b/src/sample_ota.conf new file mode 100644 index 00000000..a10d79dc --- /dev/null +++ b/src/sample_ota.conf @@ -0,0 +1,8 @@ +[ota] +OTA_WIFI_SSID = MyHomeWifi +OTA_WIFI_PASS = FooBar42! + +[bintray] +BINTRAY_USER = MyBintrayUser +BINTRAY_REPO = MyBintrayRepo +BINTRAY_API_TOKEN = 3894a7a51d70c6523c1b7479261c34845ebf7878 \ No newline at end of file From adef8fc9ce353ebf925ebb903832ea0b8d0a3bc2 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 13:54:51 +0200 Subject: [PATCH 040/105] ota.sample.conf --- src/{sample_ota.conf => ota.sample.conf} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{sample_ota.conf => ota.sample.conf} (100%) diff --git a/src/sample_ota.conf b/src/ota.sample.conf similarity index 100% rename from src/sample_ota.conf rename to src/ota.sample.conf From e5a4ada7d5cc022ff2112c5aa0416270c22a86df Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Wed, 19 Sep 2018 13:59:34 +0200 Subject: [PATCH 041/105] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dfa9eafe..99e7cac9 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ Before compiling the code, - **create file loraconf.h in your local /src directory** using the template [loraconf.sample.h](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/src/loraconf.sample.h) and populate it with your personal APPEUI und APPKEY for the LoRaWAN network. If you're using popular TheThingsNetwork you can copy&paste the keys from TTN console or output of ttnctl. +- **create file ota.conf in your local /src directory** using the template [ota.sample.conf](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/src/ota.sample.conf). Enter your WIFI network&key and your Bintray user account data in this file. If you do not want to use wireless firmware updates you don't need to touch the contents of the file, just rename ota.sample.conf to ota.conf. + To join the network only method OTAA is supported, not ABP. The DEVEUI for OTAA will be derived from the device's MAC adress during device startup and is shown as well on the device's display (if it has one) as on the serial console for copying it to your LoRaWAN network server settings. If your device has a fixed DEVEUI enter this in your local loraconf.h file. During compile time this DEVEUI will be grabbed from loraconf.h and inserted in the code. From 972cac75f16449ee4ac7d442b5342389e595c120 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 14:20:52 +0200 Subject: [PATCH 042/105] build.py: errorhandling improved --- build.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/build.py b/build.py index f0fdc73e..ed222a04 100644 --- a/build.py +++ b/build.py @@ -1,37 +1,54 @@ # build.py # pre-build script, setting up build environment +import sys +import os +import os.path import requests from os.path import basename from platformio import util Import("env") -# get keyfile from platformio.ini and parse it +# get platformio environment variables project_config = util.load_project_config() + +# check if file loraconf.h is present in source directory +keyfile = str(env.get("PROJECTSRC_DIR")) + "/loraconf.h" +if os.path.isfile(keyfile) and os.access(keyfile, os.R_OK): + print "Parsing LORAWAN keys from " + keyfile +else: + sys.exit("Missing file loraconf.h, please create it using template loraconf.sample.h! Aborting.") + +# check if file ota.conf is present in source directory keyfile = str(env.get("PROJECTSRC_DIR")) + "/" + project_config.get("common", "keyfile") -print "Parsing OTA keys from " + keyfile +if os.path.isfile(keyfile) and os.access(keyfile, os.R_OK): + print "Parsing OTA keys from " + keyfile +else: + sys.exit("Missing file ota.conf, please create it using template ota.sample.conf! Aborting.") + +# parse file ota.conf mykeys = {} with open(keyfile) as myfile: for line in myfile: key, value = line.partition("=")[::2] mykeys[key.strip()] = str(value).strip() -# get bintray credentials from keyfile +# get bintray user credentials user = mykeys["BINTRAY_USER"] repository = mykeys["BINTRAY_REPO"] apitoken = mykeys["BINTRAY_API_TOKEN"] -# get bintray parameters from platformio.ini +# get bintray upload parameters version = project_config.get("common", "release_version") package = str(env.get("PIOENV")) -# put bintray credentials to platformio environment +# put bintray user credentials to platformio environment env.Replace(BINTRAY_USER=user) env.Replace(BINTRAY_REPO=repository) env.Replace(BINTRAY_API_TOKEN=apitoken) -# get runtime credentials from keyfile and put them to compiler directive +# get runtime credentials and put them to compiler directive env.Replace(CPPDEFINES=[ ('WIFI_SSID', '\\"' + mykeys["OTA_WIFI_SSID"] + '\\"'), ('WIFI_PASS', '\\"' + mykeys["OTA_WIFI_PASS"] + '\\"'), From 939d4b6a3b848876de761c3413384d6aa8165b16 Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Wed, 19 Sep 2018 16:33:56 +0200 Subject: [PATCH 043/105] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 99e7cac9..2bcd4851 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This can all be done with a single small and cheap ESP32 board for less than $20 - WeMos: LoLin32 + [LoraNode32 shield](https://github.com/hallard/LoLin32-Lora), LoLin32lite + [LoraNode32-Lite shield](https://github.com/hallard/LoLin32-Lite-Lora) -*SPI only*: (coming soon) +*SPI only*: (code yet to come) - Pyom: WiPy - WeMos: LoLin32, LoLin32 Lite, WeMos D32 @@ -47,7 +47,7 @@ Depending on board hardware following features are supported: - GPS Target platform must be selected in [platformio.ini](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/platformio.ini).
-Hardware dependent settings (pinout etc.) are stored in board files in /hal directory.
+Hardware dependent settings (pinout etc.) are stored in board files in /hal directory. If you want to use a ESP32 board which is not yet supported, use hal file generic.h and tailor pin mappings to your needs. Pull requests for new boards welcome.
3D printable cases can be found (and, if wanted so, ordered) on Thingiverse, see Heltec, TTGOv2, TTGOv2.1, T-BEAM for example.
@@ -62,7 +62,7 @@ Before compiling the code, - **create file loraconf.h in your local /src directory** using the template [loraconf.sample.h](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/src/loraconf.sample.h) and populate it with your personal APPEUI und APPKEY for the LoRaWAN network. If you're using popular TheThingsNetwork you can copy&paste the keys from TTN console or output of ttnctl. -- **create file ota.conf in your local /src directory** using the template [ota.sample.conf](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/src/ota.sample.conf). Enter your WIFI network&key and your Bintray user account data in this file. If you do not want to use wireless firmware updates you don't need to touch the contents of the file, just rename ota.sample.conf to ota.conf. +- **create file ota.conf in your local /src directory** using the template [ota.sample.conf](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/src/ota.sample.conf) and enter your WIFI network&key. These settings are used for downloading updates. If you want to push own OTA updates you need a Bintray account. Enter your Bintray user account data in ota.conf. If you don't need wireless firmware updates just rename ota.sample.conf to ota.conf. To join the network only method OTAA is supported, not ABP. The DEVEUI for OTAA will be derived from the device's MAC adress during device startup and is shown as well on the device's display (if it has one) as on the serial console for copying it to your LoRaWAN network server settings. @@ -83,7 +83,7 @@ The LoPy/LoPy4/FiPy board needs to be set manually. See these The original Pycom firmware is not needed, so there is no need to update it before flashing Paxcounter. Just flash the compiled paxcounter binary (.elf file) on your LoPy/LoPy4/FiPy. If you later want to go back to the Pycom firmware, download the firmware from Pycom and flash it over. - **During runtime, using FOTA via WIFI:** -After the ESP32 board is initially flashed and has joined a LoRaWAN network, the firmware can update itself by FOTA. This process is kicked off by sending a remote control command (see below) via LoRaWAN to the board. The board then tries to connect via WIFI to a cloud service (JFrog Bintray), checks for update, and if available downloads the binary and reboots with it. If something goes wrong during this process, the board reboots back to the current version. Prerequisites for FOTA are: 1. You own a Bintray repository (free), 2. you pushed the update binary to the Bintray repository, 3. internet access via encrypted (WPA2) WIFI is present at the board's site, 4. WIFI credentials were set in ota.conf and initially flashed to the board. Step 2 runs automated, just enter the credentials in ota.conf and set `upload_protocol = custom` in platformio.ini. Then press build and lean back watching platformio doing build and upload. +After the ESP32 board is initially flashed and has joined a LoRaWAN network, the firmware can update itself by FOTA. This process is kicked off by sending a remote control command (see below) via LoRaWAN to the board. The board then tries to connect via WIFI to a cloud service (JFrog Bintray), checks for update, and if available downloads the binary and reboots with it. If something goes wrong during this process, the board reboots back to the current version. Prerequisites for FOTA are: 1. You own a Bintray repository, 2. you pushed the update binary to the Bintray repository, 3. internet access via encrypted (WPA2) WIFI is present at the board's site, 4. WIFI credentials were set in ota.conf and initially flashed to the board. Step 2 runs automated, just enter the credentials in ota.conf and set `upload_protocol = custom` in platformio.ini. Then press build and lean back watching platformio doing build and upload. # Legal note From 0dc3f1cf4f9ee0a65d33bfb25014c0bad72d71bf Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 19:34:02 +0200 Subject: [PATCH 044/105] lorawan.cpp: prepared bugfix in DEVEUI generator --- src/lorawan.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 0ee8b6c0..03cfeca8 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -37,6 +37,23 @@ void gen_lora_deveui(uint8_t *pdeveui) { } } +/* The above function should be changed to this one, but this would be a breaking change + +// DevEUI generator using devices's MAC address +void gen_lora_deveui(uint8_t *pdeveui) { + uint8_t *p = pdeveui, dmac[6]; + ESP_ERROR_CHECK(esp_efuse_mac_get_default(dmac)); + *p++ = dmac[0]; + *p++ = dmac[1]; + *p++ = dmac[2]; + *p++ = 0xff; + *p++ = 0xfe; + *p++ = dmac[3]; + *p++ = dmac[4]; + *p++ = dmac[5]; +} +*/ + // Function to do a byte swap in a byte array void RevBytes(unsigned char *b, size_t c) { u1_t i; From 902eeb1414b9ac90e4d3235124ddf0fff1651426 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 19 Sep 2018 19:36:41 +0200 Subject: [PATCH 045/105] lorawan.cpp: prepared bugfix for DEVEUI generator --- src/lorawan.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 03cfeca8..0f2a6071 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -43,14 +43,14 @@ void gen_lora_deveui(uint8_t *pdeveui) { void gen_lora_deveui(uint8_t *pdeveui) { uint8_t *p = pdeveui, dmac[6]; ESP_ERROR_CHECK(esp_efuse_mac_get_default(dmac)); - *p++ = dmac[0]; - *p++ = dmac[1]; - *p++ = dmac[2]; + *p++ = dmac[5]; + *p++ = dmac[4]; + *p++ = dmac[3]; *p++ = 0xff; *p++ = 0xfe; - *p++ = dmac[3]; - *p++ = dmac[4]; - *p++ = dmac[5]; + *p++ = dmac[2]; + *p++ = dmac[1]; + *p++ = dmac[0]; } */ From a0c15c1fa1bfa23c5577e8157012853e77675546 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 20 Sep 2018 12:53:58 +0200 Subject: [PATCH 046/105] code sanitization (i2c pins renamed) --- src/display.cpp | 2 +- src/hal/generic.h | 4 ++-- src/hal/heltec.h | 4 ++-- src/hal/lolin32litelora.h | 4 ++-- src/hal/lolin32lora.h | 4 ++-- src/hal/ttgov1.h | 4 ++-- src/hal/ttgov2.h | 4 ++-- src/hal/ttgov21new.h | 4 ++-- src/hal/ttgov21old.h | 4 ++-- src/lorawan.cpp | 6 +++++- 10 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/display.cpp b/src/display.cpp index bfc55f05..ba71a544 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -4,7 +4,7 @@ #include "globals.h" #include // needed for reading ESP32 chip attributes -HAS_DISPLAY u8x8(OLED_RST, OLED_SCL, OLED_SDA); +HAS_DISPLAY u8x8(OLED_RST, I2C_SCL, I2C_SDA); // helper string for converting LoRa spread factor values #if defined(CFG_eu868) diff --git a/src/hal/generic.h b/src/hal/generic.h index bedff5e8..d5863038 100644 --- a/src/hal/generic.h +++ b/src/hal/generic.h @@ -34,5 +34,5 @@ // pin definitions for I2C interface of OLED Display #define OLED_RST GPIO_NUM_16 // SSD1306 RST -#define OLED_SDA GPIO_NUM_4 // SD1306 D1+D2 -#define OLED_SCL GPIO_NUM_15 // SD1306 D0 \ No newline at end of file +#define I2C_SDA GPIO_NUM_4 // SD1306 D1+D2 +#define I2C_SCL GPIO_NUM_15 // SD1306 D0 \ No newline at end of file diff --git a/src/hal/heltec.h b/src/hal/heltec.h index 2671fac8..44316c63 100644 --- a/src/hal/heltec.h +++ b/src/hal/heltec.h @@ -22,5 +22,5 @@ // Hardware pin definitions for Heltec LoRa-32 Board with OLED SSD1306 I2C Display #define OLED_RST GPIO_NUM_16 // ESP32 GPIO16 (Pin16) -- SD1306 RST -#define OLED_SDA GPIO_NUM_4 // ESP32 GPIO4 (Pin4) -- SD1306 D1+D2 -#define OLED_SCL GPIO_NUM_15 // ESP32 GPIO15 (Pin15) -- SD1306 D0 +#define I2C_SDA GPIO_NUM_4 // ESP32 GPIO4 (Pin4) -- SD1306 D1+D2 +#define I2C_SCL GPIO_NUM_15 // ESP32 GPIO15 (Pin15) -- SD1306 D0 diff --git a/src/hal/lolin32litelora.h b/src/hal/lolin32litelora.h index 4275ea00..6427dbcd 100644 --- a/src/hal/lolin32litelora.h +++ b/src/hal/lolin32litelora.h @@ -31,8 +31,8 @@ // Hardware pin definitions for LoRaNode32 Board with OLED I2C Display #define OLED_RST U8X8_PIN_NONE // Not reset pin -#define OLED_SDA 14 // ESP32 GPIO14 (Pin14) -- OLED SDA -#define OLED_SCL 12 // ESP32 GPIO12 (Pin12) -- OLED SCL +#define I2C_SDA 14 // ESP32 GPIO14 (Pin14) -- OLED SDA +#define I2C_SCL 12 // ESP32 GPIO12 (Pin12) -- OLED SCL // I2C config for Microchip 24AA02E64 DEVEUI unique address #define MCP_24AA02E64_I2C_ADDRESS 0x50 // I2C address for the 24AA02E64 diff --git a/src/hal/lolin32lora.h b/src/hal/lolin32lora.h index 976092d7..7964a561 100644 --- a/src/hal/lolin32lora.h +++ b/src/hal/lolin32lora.h @@ -32,8 +32,8 @@ // Hardware pin definitions for LoRaNode32 Board with OLED I2C Display #define OLED_RST U8X8_PIN_NONE // Not reset pin -#define OLED_SDA 21 // ESP32 GPIO21 (Pin21) -- OLED SDA -#define OLED_SCL 22 // ESP32 GPIO22 (Pin22) -- OLED SCL +#define I2C_SDA 21 // ESP32 GPIO21 (Pin21) -- OLED SDA +#define I2C_SCL 22 // ESP32 GPIO22 (Pin22) -- OLED SCL // I2C config for Microchip 24AA02E64 DEVEUI unique address #define MCP_24AA02E64_I2C_ADDRESS 0x50 // I2C address for the 24AA02E64 diff --git a/src/hal/ttgov1.h b/src/hal/ttgov1.h index fb04cbb0..f64fe380 100644 --- a/src/hal/ttgov1.h +++ b/src/hal/ttgov1.h @@ -24,5 +24,5 @@ // Hardware pin definitions for TTGOv1 Board with OLED SSD1306 I2C Display #define OLED_RST GPIO_NUM_16 // ESP32 GPIO16 (Pin16) -- SD1306 Reset -#define OLED_SDA GPIO_NUM_4 // ESP32 GPIO4 (Pin4) -- SD1306 Data -#define OLED_SCL GPIO_NUM_15 // ESP32 GPIO15 (Pin15) -- SD1306 Clock +#define I2C_SDA GPIO_NUM_4 // ESP32 GPIO4 (Pin4) -- SD1306 Data +#define I2C_SCL GPIO_NUM_15 // ESP32 GPIO15 (Pin15) -- SD1306 Clock diff --git a/src/hal/ttgov2.h b/src/hal/ttgov2.h index e622af02..0df3146e 100644 --- a/src/hal/ttgov2.h +++ b/src/hal/ttgov2.h @@ -25,8 +25,8 @@ // Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display #define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN -#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 -#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 +#define I2C_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 +#define I2C_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 /* source: https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-2/11973 diff --git a/src/hal/ttgov21new.h b/src/hal/ttgov21new.h index ce64323f..5cadc961 100644 --- a/src/hal/ttgov21new.h +++ b/src/hal/ttgov21new.h @@ -26,5 +26,5 @@ // Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display #define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN -#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 -#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 \ No newline at end of file +#define I2C_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 +#define I2C_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 \ No newline at end of file diff --git a/src/hal/ttgov21old.h b/src/hal/ttgov21old.h index f6882187..c423ca67 100644 --- a/src/hal/ttgov21old.h +++ b/src/hal/ttgov21old.h @@ -28,5 +28,5 @@ // Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display #define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN -#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 -#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 +#define I2C_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2 +#define I2C_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0 diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 0f2a6071..5ffed2cb 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -43,6 +43,10 @@ void gen_lora_deveui(uint8_t *pdeveui) { void gen_lora_deveui(uint8_t *pdeveui) { uint8_t *p = pdeveui, dmac[6]; ESP_ERROR_CHECK(esp_efuse_mac_get_default(dmac)); + // deveui is LSB, we reverse it so TTN DEVEUI display + // will remain the same as MAC address + // MAC is 6 bytes, devEUI 8, set middle 2 ones + // to an arbitrary value *p++ = dmac[5]; *p++ = dmac[4]; *p++ = dmac[3]; @@ -96,7 +100,7 @@ void get_hard_deveui(uint8_t *pdeveui) { #ifdef MCP_24AA02E64_I2C_ADDRESS uint8_t i2c_ret; // Init this just in case, no more to 100KHz - Wire.begin(OLED_SDA, OLED_SCL, 100000); + Wire.begin(I2C_SDA, I2C_SCL, 100000); Wire.beginTransmission(MCP_24AA02E64_I2C_ADDRESS); Wire.write(MCP_24AA02E64_MAC_ADDRESS); i2c_ret = Wire.endTransmission(); From a0377a315c9dee408508024f329582fc6f93c408 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 20 Sep 2018 13:23:22 +0200 Subject: [PATCH 047/105] gps.cpp, lorawan.cpp: i2c bugfixes (issue #156) --- src/gps.cpp | 5 +---- src/hal/generic.h | 6 +++++- src/lorawan.cpp | 37 ++++++++++++++++++++++--------------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/gps.cpp b/src/gps.cpp index 10d05c14..6020434f 100644 --- a/src/gps.cpp +++ b/src/gps.cpp @@ -5,7 +5,7 @@ // Local logging tag static const char TAG[] = "main"; -TinyGPSPlus gps; +TinyGPSPlus gps; gpsStatus_t gps_status; // read GPS data and cast to global struct @@ -53,7 +53,6 @@ void gps_loop(void *pvParameters) { Wire.write(0x00); // dummy write to start read Wire.endTransmission(); - Wire.beginTransmission(GPS_ADDR); while (cfg.gpsmode) { Wire.requestFrom(GPS_ADDR | 0x01, 32); while (Wire.available()) { @@ -61,8 +60,6 @@ void gps_loop(void *pvParameters) { vTaskDelay(2 / portTICK_PERIOD_MS); // polling mode: 500ms sleep } } - // after GPS function was disabled, close connect to GPS device - Wire.endTransmission(); #endif // GPS Type } diff --git a/src/hal/generic.h b/src/hal/generic.h index d5863038..f25a8219 100644 --- a/src/hal/generic.h +++ b/src/hal/generic.h @@ -35,4 +35,8 @@ // pin definitions for I2C interface of OLED Display #define OLED_RST GPIO_NUM_16 // SSD1306 RST #define I2C_SDA GPIO_NUM_4 // SD1306 D1+D2 -#define I2C_SCL GPIO_NUM_15 // SD1306 D0 \ No newline at end of file +#define I2C_SCL GPIO_NUM_15 // SD1306 D0 + +// I2C config for Microchip 24AA02E64 DEVEUI unique address +#define MCP_24AA02E64_I2C_ADDRESS 0x50 // I2C address for the 24AA02E64 +#define MCP_24AA02E64_MAC_ADDRESS 0xF8 // Memory adress of unique deveui 64 bits \ No newline at end of file diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 5ffed2cb..e7cbcf5e 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -20,6 +20,8 @@ const lmic_pinmap lmic_pins = {.mosi = PIN_SPI_MOSI, .rst = RST, .dio = {DIO0, DIO1, DIO2}}; +/* old version + // DevEUI generator using devices's MAC address void gen_lora_deveui(uint8_t *pdeveui) { uint8_t *p = pdeveui, dmac[6]; @@ -37,13 +39,13 @@ void gen_lora_deveui(uint8_t *pdeveui) { } } -/* The above function should be changed to this one, but this would be a breaking change +*/ // DevEUI generator using devices's MAC address void gen_lora_deveui(uint8_t *pdeveui) { uint8_t *p = pdeveui, dmac[6]; ESP_ERROR_CHECK(esp_efuse_mac_get_default(dmac)); - // deveui is LSB, we reverse it so TTN DEVEUI display + // deveui is LSB, we reverse it so TTN DEVEUI display // will remain the same as MAC address // MAC is 6 bytes, devEUI 8, set middle 2 ones // to an arbitrary value @@ -56,7 +58,6 @@ void gen_lora_deveui(uint8_t *pdeveui) { *p++ = dmac[1]; *p++ = dmac[0]; } -*/ // Function to do a byte swap in a byte array void RevBytes(unsigned char *b, size_t c) { @@ -98,29 +99,35 @@ void os_getDevEui(u1_t *buf) { void get_hard_deveui(uint8_t *pdeveui) { // read DEVEUI from Microchip 24AA02E64 2Kb serial eeprom if present #ifdef MCP_24AA02E64_I2C_ADDRESS - uint8_t i2c_ret; + // Init this just in case, no more to 100KHz Wire.begin(I2C_SDA, I2C_SCL, 100000); Wire.beginTransmission(MCP_24AA02E64_I2C_ADDRESS); Wire.write(MCP_24AA02E64_MAC_ADDRESS); - i2c_ret = Wire.endTransmission(); - // check if device seen on i2c bus - if (i2c_ret == 0) { + + // check if device was seen on i2c bus + if (Wire.endTransmission() == 0) { char deveui[32] = ""; uint8_t data; + Wire.beginTransmission(MCP_24AA02E64_I2C_ADDRESS); Wire.write(MCP_24AA02E64_MAC_ADDRESS); - Wire.requestFrom(MCP_24AA02E64_I2C_ADDRESS, 8); - while (Wire.available()) { - data = Wire.read(); - sprintf(deveui + strlen(deveui), "%02X ", data); - *pdeveui++ = data; + Wire.endTransmission(); + + if (Wire.requestFrom(MCP_24AA02E64_I2C_ADDRESS, 8)) { + while (Wire.available()) { + data = Wire.read(); + sprintf(deveui + strlen(deveui), "%02X ", data); + *pdeveui++ = data; + } + ESP_LOGI(TAG, "Serial EEPROM found, read DEVEUI %s", deveui); + } else { + ESP_LOGI(TAG, "Could not read DEVEUI from serial EEPROM"); } - i2c_ret = Wire.endTransmission(); - ESP_LOGI(TAG, "Serial EEPROM 24AA02E64 found, read DEVEUI %s", deveui); } else { - ESP_LOGI(TAG, "Serial EEPROM 24AA02E64 not found ret=%d", i2c_ret); + ESP_LOGI(TAG, "Could not find serial EEPROM on I2C bus"); } + // Set back to 400KHz to speed up OLED Wire.setClock(400000); #endif // MCP 24AA02E64 From bf30092f547fd4012fd281b4dfc3275f869d0cfb Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 20 Sep 2018 17:33:52 +0200 Subject: [PATCH 048/105] gps code bugfixed; new vendor filter data --- README.md | 2 +- platformio.ini | 2 +- src/gps.cpp | 26 +-- src/gps.h | 11 +- src/hal/lopy.h | 10 +- src/hal/lopy4.h | 4 - src/lorawan.cpp | 39 ++--- src/lorawan.h | 8 + src/vendor_array.h | 389 +++++++++++++++++++++++---------------------- 9 files changed, 257 insertions(+), 234 deletions(-) diff --git a/README.md b/README.md index 2bcd4851..8f793552 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Depending on board hardware following features are supported: - Button - Silicon unique ID - Battery voltage monitoring -- GPS +- GPS (Generic serial NMEA, or Quectel L76 I2C) Target platform must be selected in [platformio.ini](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/platformio.ini).
Hardware dependent settings (pinout etc.) are stored in board files in /hal directory. If you want to use a ESP32 board which is not yet supported, use hal file generic.h and tailor pin mappings to your needs. Pull requests for new boards welcome.
diff --git a/platformio.ini b/platformio.ini index 89b44400..2c159c1e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.4.35 +release_version = 1.4.36 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 diff --git a/src/gps.cpp b/src/gps.cpp index 6020434f..435cd9a5 100644 --- a/src/gps.cpp +++ b/src/gps.cpp @@ -25,8 +25,21 @@ void gps_loop(void *pvParameters) { // initialize and, if needed, configure, GPS #if defined GPS_SERIAL HardwareSerial GPS_Serial(1); + GPS_Serial.begin(GPS_SERIAL); // serial connect to GPS device + #elif defined GPS_QUECTEL_L76 Wire.begin(GPS_QUECTEL_L76, 400000); // I2C connect to GPS device with 400 KHz + uint8_t i2c_ret; + Wire.beginTransmission(GPS_ADDR); + Wire.write(0x00); // dummy write to start read + i2c_ret = Wire.endTransmission(); // check if chip is seen on i2c bus + + if (i2c_ret) { + ESP_LOGE(TAG, "Quectel L76 GPS chip not found on i2c bus, bus error %d", + i2c_ret); + return; + } + #endif while (1) { @@ -34,9 +47,6 @@ void gps_loop(void *pvParameters) { if (cfg.gpsmode) { #if defined GPS_SERIAL - // serial connect to GPS device - GPS_Serial.begin(GPS_SERIAL); - while (cfg.gpsmode) { // feed GPS decoder with serial NMEA data from GPS device while (GPS_Serial.available()) { @@ -49,16 +59,14 @@ void gps_loop(void *pvParameters) { #elif defined GPS_QUECTEL_L76 - Wire.beginTransmission(GPS_ADDR); - Wire.write(0x00); // dummy write to start read - Wire.endTransmission(); - while (cfg.gpsmode) { - Wire.requestFrom(GPS_ADDR | 0x01, 32); + Wire.requestFrom(GPS_ADDR, + 128); // 128 is Wire.h buffersize arduino-ESP32 while (Wire.available()) { gps.encode(Wire.read()); - vTaskDelay(2 / portTICK_PERIOD_MS); // polling mode: 500ms sleep + vTaskDelay(2 / portTICK_PERIOD_MS); // delay see L76 datasheet } + vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog } #endif // GPS Type diff --git a/src/gps.h b/src/gps.h index 45406ff7..5121528e 100644 --- a/src/gps.h +++ b/src/gps.h @@ -1,9 +1,13 @@ #ifndef _GPS_H #define _GPS_H -#include // library for parsing NMEA data +#include // library for parsing NMEA data #include +#ifdef GPS_QUECTEL_L76 // Needed for reading from I2C Bus +#include +#endif + typedef struct { uint32_t latitude; uint32_t longitude; @@ -12,8 +16,9 @@ typedef struct { uint16_t altitude; } gpsStatus_t; -extern TinyGPSPlus gps; // Make TinyGPS++ instance globally availabe -extern gpsStatus_t gps_status; // Make struct for storing gps data globally available +extern TinyGPSPlus gps; // Make TinyGPS++ instance globally availabe +extern gpsStatus_t + gps_status; // Make struct for storing gps data globally available void gps_read(void); void gps_loop(void *pvParameters); diff --git a/src/hal/lopy.h b/src/hal/lopy.h index 0e414326..2caeb308 100644 --- a/src/hal/lopy.h +++ b/src/hal/lopy.h @@ -20,14 +20,10 @@ #define HAS_ANTENNA_SWITCH 16 // pin for switching wifi antenna #define WIFI_ANTENNA 0 // 0 = internal, 1 = external -// !!EXPERIMENTAL - not tested yet!! // uncomment this only if your LoPy runs on a Pytrack expansion board with GPS -// see http://www.quectel.com/UploadImage/Downlad/Quectel_L76-L_I2C_Application_Note_V1.0.pdf -//#define HAS_GPS 1 -//#define GPS_QUECTEL_L76 GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21) -//#define GPS_ADDR 0x10 -//#define HAS_BUTTON GPIO_NUM_37 // (P14) -//#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown +#define HAS_GPS 1 +#define GPS_QUECTEL_L76 GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21) +#define GPS_ADDR 0x10 // uncomment this only if your LoPy runs on a expansion board 3.0 //#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 diff --git a/src/hal/lopy4.h b/src/hal/lopy4.h index 9e1169ab..cf988324 100644 --- a/src/hal/lopy4.h +++ b/src/hal/lopy4.h @@ -21,14 +21,10 @@ #define HAS_ANTENNA_SWITCH 21 // pin for switching wifi antenna #define WIFI_ANTENNA 0 // 0 = internal, 1 = external -// !!EXPERIMENTAL - not tested yet!! // uncomment this only if your LoPy runs on a Pytrack expansion board with GPS -// see http://www.quectel.com/UploadImage/Downlad/Quectel_L76-L_I2C_Application_Note_V1.0.pdf //#define HAS_GPS 1 //#define GPS_QUECTEL_L76 GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21) //#define GPS_ADDR 0x10 -//#define HAS_BUTTON GPIO_NUM_37 // (P14) -//#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown // uncomment this only if your LoPy runs on a expansion board 3.0 #define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 diff --git a/src/lorawan.cpp b/src/lorawan.cpp index e7cbcf5e..5cb93af7 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -1,12 +1,7 @@ #ifdef HAS_LORA // Basic Config -#include "globals.h" -#include "rcommand.h" - -#ifdef MCP_24AA02E64_I2C_ADDRESS -#include // Needed for 24AA02E64, does not hurt anything if included and not used -#endif +#include "lorawan.h" // Local logging Tag static const char TAG[] = "lora"; @@ -20,8 +15,6 @@ const lmic_pinmap lmic_pins = {.mosi = PIN_SPI_MOSI, .rst = RST, .dio = {DIO0, DIO1, DIO2}}; -/* old version - // DevEUI generator using devices's MAC address void gen_lora_deveui(uint8_t *pdeveui) { uint8_t *p = pdeveui, dmac[6]; @@ -39,8 +32,8 @@ void gen_lora_deveui(uint8_t *pdeveui) { } } -*/ - +/* new version, does it with well formed mac according IEEE spec, but is +breaking change // DevEUI generator using devices's MAC address void gen_lora_deveui(uint8_t *pdeveui) { uint8_t *p = pdeveui, dmac[6]; @@ -58,6 +51,7 @@ void gen_lora_deveui(uint8_t *pdeveui) { *p++ = dmac[1]; *p++ = dmac[0]; } +*/ // Function to do a byte swap in a byte array void RevBytes(unsigned char *b, size_t c) { @@ -100,13 +94,16 @@ void get_hard_deveui(uint8_t *pdeveui) { // read DEVEUI from Microchip 24AA02E64 2Kb serial eeprom if present #ifdef MCP_24AA02E64_I2C_ADDRESS + uint8_t i2c_ret; + // Init this just in case, no more to 100KHz Wire.begin(I2C_SDA, I2C_SCL, 100000); Wire.beginTransmission(MCP_24AA02E64_I2C_ADDRESS); Wire.write(MCP_24AA02E64_MAC_ADDRESS); + i2c_ret = Wire.endTransmission(); // check if device was seen on i2c bus - if (Wire.endTransmission() == 0) { + if (ic2_ret == 0) { char deveui[32] = ""; uint8_t data; @@ -114,19 +111,15 @@ void get_hard_deveui(uint8_t *pdeveui) { Wire.write(MCP_24AA02E64_MAC_ADDRESS); Wire.endTransmission(); - if (Wire.requestFrom(MCP_24AA02E64_I2C_ADDRESS, 8)) { - while (Wire.available()) { - data = Wire.read(); - sprintf(deveui + strlen(deveui), "%02X ", data); - *pdeveui++ = data; - } - ESP_LOGI(TAG, "Serial EEPROM found, read DEVEUI %s", deveui); - } else { - ESP_LOGI(TAG, "Could not read DEVEUI from serial EEPROM"); + Wire.requestFrom(MCP_24AA02E64_I2C_ADDRESS, 8); + while (Wire.available()) { + data = Wire.read(); + sprintf(deveui + strlen(deveui), "%02X ", data); + *pdeveui++ = data; } - } else { - ESP_LOGI(TAG, "Could not find serial EEPROM on I2C bus"); - } + ESP_LOGI(TAG, "Serial EEPROM found, read DEVEUI %s", deveui); + } else + ESP_LOGI(TAG, "Could not read DEVEUI from serial EEPROM"); // Set back to 400KHz to speed up OLED Wire.setClock(400000); diff --git a/src/lorawan.h b/src/lorawan.h index d88062e7..32220496 100644 --- a/src/lorawan.h +++ b/src/lorawan.h @@ -1,11 +1,19 @@ #ifndef _LORAWAN_H #define _LORAWAN_H +#include "globals.h" +#include "rcommand.h" + // LMIC-Arduino LoRaWAN Stack #include #include #include "loraconf.h" +// Needed for 24AA02E64, does not hurt anything if included and not used +#ifdef MCP_24AA02E64_I2C_ADDRESS +#include +#endif + void onEvent(ev_t ev); void gen_lora_deveui(uint8_t *pdeveui); void RevBytes(unsigned char *b, size_t c); diff --git a/src/vendor_array.h b/src/vendor_array.h index fd555495..4ab56cfc 100644 --- a/src/vendor_array.h +++ b/src/vendor_array.h @@ -1,4 +1,4 @@ -std::array vendors = { +std::array vendors = { 0x38f23e, 0x807abf, 0x90e7c4, 0x7c6193, 0x485073, 0x74e28c, 0x8463d6, 0xd48f33, 0x2c8a72, 0x980d2e, 0xa826d9, 0xd4206d, 0x00155d, 0x806c1b, 0xa470d6, 0x985fd3, 0x1c69a5, 0x382de8, 0xd087e2, 0x205531, 0x5440ad, @@ -14,193 +14,210 @@ std::array vendors = { 0xd0dfc7, 0x1c62b8, 0x18e2c2, 0x001a8a, 0x002567, 0xa8f274, 0x001599, 0x0012fb, 0x7cf854, 0x8cc8cd, 0xe81132, 0xa02195, 0x8c71f8, 0x04180f, 0x9463d1, 0x0cdfa4, 0xcc051b, 0x68ebae, 0x60d0a9, 0x60a10a, 0xa07591, - 0x001fcc, 0xec107b, 0xa01081, 0xf4f524, 0x9c99a0, 0x185936, 0x98fae3, - 0x640980, 0x8cbebe, 0xf8a45f, 0xbc8385, 0x900628, 0xd4ae05, 0x3c0518, - 0xc40bcb, 0xe8bba8, 0xbc3aea, 0x8c0ee3, 0x6c5c14, 0x78abbb, 0x1816c9, + 0x001fcc, 0xec107b, 0xa01081, 0xf4f524, 0xbc8385, 0x900628, 0xd4ae05, + 0x3c0518, 0xe8bba8, 0xbc3aea, 0x8c0ee3, 0x6c5c14, 0x78abbb, 0x1816c9, 0xfc8f90, 0x244b03, 0x988389, 0x14bb6e, 0x1c3ade, 0xf83f51, 0xd8e0e1, - 0xecf342, 0x5092b9, 0xb4bff6, 0xc8d7b0, 0x982d68, 0xecd09f, 0xe446da, - 0xf4f5db, 0xd80831, 0xdc5583, 0x2c54cf, 0x001fe3, 0x0026e2, 0x001e75, - 0x6cd68a, 0x2021a5, 0x0c4885, 0xdc0b34, 0xac0d1b, 0x60e3ac, 0xf895c7, - 0xc4438f, 0xa816b2, 0xe892a4, 0x700514, 0x88c9d0, 0x2c598a, 0x18f0e4, - 0xec8350, 0x4cdd31, 0x703eac, 0x6c94f8, 0x84788b, 0x2cf0ee, 0x68d93c, - 0x24e314, 0x28f076, 0x285aeb, 0x087402, 0xccc760, 0x705aac, 0xfc643a, - 0xd4e6b7, 0x2802d8, 0x9c2ea1, 0x68967b, 0xbc6778, 0xa82066, 0xb065bd, - 0xf0dce2, 0x7cd1c3, 0x705681, 0x2cbe08, 0x783a84, 0x84b153, 0x6476ba, - 0x54ae27, 0xf437b7, 0xf0d1a9, 0x34c059, 0x04f7e4, 0x10ddb1, 0xb4f0ab, - 0x848506, 0x7831c1, 0x8c7c92, 0xd0e140, 0xacfdec, 0xf82793, 0x40a6d9, - 0x109add, 0xf0b479, 0x58b035, 0x34159e, 0x286aba, 0xec852f, 0x44d884, - 0x003ee1, 0x7c11be, 0x04e536, 0x881fa1, 0x04db56, 0x9cfc01, 0xc81ee7, - 0x34363b, 0xc01ada, 0xacbc32, 0x70700d, 0x7c5049, 0x503237, 0xd4619d, - 0xb0481a, 0x989e63, 0xdca904, 0x48a195, 0x6cab31, 0x6c96cf, 0x3035ad, - 0xa8be27, 0xb8634d, 0x9ce33f, 0xf0989d, 0xace4b5, 0xe42b34, 0x1c36bb, - 0x3c2eff, 0xf0766f, 0x40cbc0, 0x4098ad, 0x6c4d73, 0xc48466, 0xd02b20, - 0x3010e4, 0x380f4a, 0x685b35, 0xc86f1d, 0x701124, 0x38484c, 0x041552, - 0x786c1c, 0xbc3baf, 0x00a040, 0x60fec5, 0xcc4463, 0x6c72e7, 0x18af61, - 0x00cdfe, 0xac61ea, 0x38b54d, 0x60c547, 0x28e02c, 0x50ead6, 0x4860bc, - 0x403004, 0x0c74c2, 0xa4b197, 0x7cf05f, 0xa4f1e8, 0x70a2b3, 0x4c57ca, - 0x68fb7e, 0x90c1c6, 0x9cf48e, 0xfcd848, 0x64b9e8, 0x001cb3, 0x000d93, - 0x30d9d9, 0x6030d4, 0x94bf2d, 0xc49880, 0xe0338e, 0x68fef7, 0xbce143, - 0x645aed, 0xc0b658, 0x881908, 0xfc2a9c, 0x48605f, 0x188796, 0x002376, - 0x84100d, 0x04c23e, 0x5c5188, 0xe89120, 0x9c6c15, 0x4886e8, 0x2c2997, - 0x102f6b, 0x00eebd, 0x281878, 0x6045bd, 0x7ced8d, 0xe85b5b, 0x000d3a, - 0xe09861, 0xf4f1e1, 0x60beb5, 0xb4e1c4, 0x70aab2, 0x0026ff, 0x406f2a, - 0x002557, 0xf05a09, 0x503275, 0x28cc01, 0xb46293, 0x04fe31, 0x845181, - 0xd831cf, 0xf8d0bd, 0xfcc734, 0xe4b021, 0xb0ec71, 0x3cbbfd, 0x2cae2b, - 0xc488e5, 0x7c9122, 0xe8b4c8, 0x18895b, 0xe0db10, 0xe09971, 0x6077e2, - 0x680571, 0x6c2f2c, 0x300d43, 0x6c2779, 0x607edd, 0x9c2a83, 0xe45d75, - 0xe4faed, 0xc83f26, 0x54f201, 0xa06090, 0xac3743, 0x141f78, 0x006f64, - 0xdc6672, 0x001e7d, 0x3c6200, 0x0024e9, 0x002399, 0xe4e0c5, 0xe8039a, - 0xc4731e, 0x8c7712, 0x2013e0, 0x0007ab, 0x0021d2, 0xbc4760, 0xd0176a, - 0x2cbaba, 0x24920e, 0x40d3ae, 0xf01dbc, 0x24dbed, 0xac3613, 0x1449e0, - 0xc0bdd1, 0xe8508b, 0xf025b7, 0xc8ba94, 0xec1f72, 0x9852b1, 0x1489fd, - 0xccfe3c, 0x789ed0, 0xe440e2, 0x1caf05, 0xe492fb, 0x0073e0, 0xbc4486, - 0x380b40, 0x002490, 0x0023d7, 0xfca13e, 0xa00798, 0x945103, 0xc819f7, - 0x2c4401, 0x28e31f, 0x0c1daf, 0x14f65a, 0x742344, 0xf0b429, 0xec51bc, - 0xf079e8, 0x887598, 0xd0b128, 0xd00401, 0xf06d78, 0x10683f, 0x74a722, - 0x58a2b5, 0x64899a, 0x88074b, 0x64bc0c, 0xa039f7, 0x041b6d, 0x001f6b, - 0x30b4b8, 0x503cea, 0x0c9838, 0x54fcf0, 0x08aed6, 0xa816d0, 0x88bd45, - 0x544e90, 0xd03311, 0x70e72c, 0xc0cecd, 0x98e0d9, 0xe0accb, 0x78d75f, - 0x2078f0, 0x985aeb, 0xa45e60, 0x006d52, 0x5cadcf, 0xb8e856, 0x90b21f, - 0xa8bbcf, 0xc8b5b7, 0x18af8f, 0xf4f951, 0xf0c1f1, 0xec3586, 0x88cb87, - 0xac3c0b, 0xcc785f, 0xe88d28, 0x3ce072, 0xf41ba1, 0xa85b78, 0x9cf387, - 0x34a395, 0x48437c, 0xac87a3, 0x00f76f, 0xc0f2fb, 0x1caba7, 0x60facd, - 0x680927, 0x78a3e4, 0x68a86d, 0xb817c2, 0xb88d12, 0x5c8d4e, 0xe06678, - 0x1c1ac0, 0xc8f650, 0x60d9c7, 0x44fb42, 0x64a3cb, 0xd8d1cb, 0x542696, - 0x14109f, 0xbc52b7, 0xe0c97a, 0x5c95ae, 0x8cfaba, 0x0cbc9f, 0xbc4cc4, - 0x0c1539, 0x908d6c, 0x80006e, 0xb418d1, 0x1499e2, 0xe0c767, 0xa860b6, - 0x24f094, 0x90b0ed, 0xc4b301, 0xe05f45, 0x483b38, 0x88e87f, 0xb853ac, - 0x2c3361, 0x784f43, 0x404d7f, 0xbc926b, 0x0452f3, 0x241eeb, 0xf431c3, - 0x64a5c3, 0x606944, 0xe498d6, 0x0cd746, 0x440010, 0x1c9e46, 0x7c04d0, - 0xbc9fef, 0x8866a5, 0x70f087, 0x886b6e, 0x4c74bf, 0x844167, 0xb4f61c, - 0xe49adc, 0xb8c111, 0x3408bc, 0xd0817a, 0xc4618b, 0x68ab1e, 0x2c61f6, - 0x0026bb, 0x00254b, 0x002436, 0x002332, 0x002312, 0x0019e3, 0x001451, - 0x000a27, 0x003065, 0x0050e4, 0xd023db, 0xe0b9ba, 0x3451c9, 0x8c5877, - 0x9803d8, 0xc82a14, 0x88c663, 0x8c7b9d, 0x5855ca, 0xcc08e0, 0xe80688, + 0xecf342, 0x5092b9, 0xb4bff6, 0xc8d7b0, 0x982d68, 0xd80831, 0xdc5583, + 0x2c54cf, 0x001fe3, 0x0026e2, 0x001e75, 0x6cd68a, 0x2021a5, 0x0c4885, + 0xdc0b34, 0xac0d1b, 0x60e3ac, 0xf895c7, 0xc4438f, 0xa816b2, 0xe892a4, + 0x700514, 0x88c9d0, 0x2c598a, 0xec8350, 0x4cdd31, 0x705aac, 0xfc643a, + 0xd4e6b7, 0x2802d8, 0x48605f, 0xf0766f, 0x40cbc0, 0x4098ad, 0x6c4d73, + 0xc48466, 0xb8634d, 0x503237, 0xd4619d, 0xb0481a, 0x989e63, 0xdca904, + 0x48a195, 0x6cab31, 0x7c5049, 0xe42b34, 0x1c36bb, 0x3c2eff, 0x6c96cf, + 0x3035ad, 0xa8be27, 0x70a2b3, 0x4c57ca, 0x68fb7e, 0x90c1c6, 0xa4f1e8, + 0xac61ea, 0x38b54d, 0x00cdfe, 0x18af61, 0xcc4463, 0x34159e, 0x58b035, + 0xf0b479, 0x109add, 0x40a6d9, 0x7cf05f, 0xa4b197, 0x0c74c2, 0x403004, + 0x4860bc, 0xd02b20, 0x9ce33f, 0xf0989d, 0xace4b5, 0x6c72e7, 0x60fec5, + 0x00a040, 0x000d93, 0xacbc32, 0x30d9d9, 0x6030d4, 0x94bf2d, 0xc49880, + 0xe0338e, 0x68fef7, 0xbce143, 0x645aed, 0xc0b658, 0x881908, 0xfc2a9c, + 0x44d884, 0xec852f, 0x286aba, 0x705681, 0x7cd1c3, 0xf0dce2, 0xb065bd, + 0xa82066, 0xbc6778, 0x68967b, 0x848506, 0x54ae27, 0x6476ba, 0x84b153, + 0x783a84, 0x2cbe08, 0x24e314, 0x68d93c, 0x2cf0ee, 0x84788b, 0x6c94f8, + 0x703eac, 0xb4f0ab, 0x10ddb1, 0x04f7e4, 0x34c059, 0xf0d1a9, 0xbc3baf, + 0x786c1c, 0x041552, 0x38484c, 0x701124, 0xc86f1d, 0x685b35, 0x380f4a, + 0x3010e4, 0x04db56, 0x881fa1, 0x04e536, 0xf82793, 0xacfdec, 0xd0e140, + 0x8c7c92, 0x7831c1, 0xf437b7, 0x50ead6, 0x28e02c, 0x60c547, 0x7c11be, + 0x003ee1, 0xc01ada, 0x34363b, 0xc81ee7, 0x9cfc01, 0xccc760, 0x087402, + 0x285aeb, 0x28f076, 0x70700d, 0x9cf48e, 0xfcd848, 0x001cb3, 0x64b9e8, + 0x108ee0, 0x68e7c2, 0x3c576c, 0x0ce0dc, 0x702ad5, 0x889f6f, 0x1c427d, + 0x5029f5, 0x4c569d, 0x14c213, 0x38539c, 0x58e6ba, 0xb831b5, 0x90633b, + 0x782327, 0xf8a45f, 0x8cbebe, 0x640980, 0x98fae3, 0x185936, 0x9c99a0, + 0xc40bcb, 0xecd09f, 0xf4f5db, 0xe446da, 0x18f0e4, 0x9c2ea1, 0x50a009, + 0x20a60c, 0xf8e94e, 0xf40616, 0xbcb863, 0x188796, 0x002376, 0x84100d, + 0x04c23e, 0x5c5188, 0xe89120, 0x9c6c15, 0x4886e8, 0x2c2997, 0x102f6b, + 0x00eebd, 0x281878, 0x6045bd, 0x7ced8d, 0xe85b5b, 0x000d3a, 0xe09861, + 0xf4f1e1, 0x60beb5, 0xb4e1c4, 0x70aab2, 0x0026ff, 0x406f2a, 0x002557, + 0xf05a09, 0x503275, 0x28cc01, 0xb46293, 0x04fe31, 0x845181, 0xd831cf, + 0xf8d0bd, 0xfcc734, 0xe4b021, 0xb0ec71, 0x3cbbfd, 0x2cae2b, 0xc488e5, + 0x7c9122, 0xe8b4c8, 0x18895b, 0xe0db10, 0xe09971, 0x6077e2, 0x680571, + 0x6c2f2c, 0x300d43, 0x6c2779, 0x607edd, 0x9c2a83, 0xe45d75, 0xe4faed, + 0xc83f26, 0x54f201, 0xa06090, 0xac3743, 0x141f78, 0x006f64, 0xdc6672, + 0x001e7d, 0x3c6200, 0x0024e9, 0x002399, 0xe4e0c5, 0xe8039a, 0xc4731e, + 0x8c7712, 0x2013e0, 0x0007ab, 0x0021d2, 0xbc4760, 0xd0176a, 0x2cbaba, + 0x24920e, 0x40d3ae, 0xf01dbc, 0x24dbed, 0xac3613, 0x1449e0, 0xc0bdd1, + 0xe8508b, 0xf025b7, 0xc8ba94, 0xec1f72, 0x9852b1, 0x1489fd, 0xccfe3c, + 0x789ed0, 0xe440e2, 0x1caf05, 0xe492fb, 0x0073e0, 0xbc4486, 0x380b40, + 0x002490, 0x0023d7, 0xfca13e, 0xa00798, 0x945103, 0xc819f7, 0x2c4401, + 0xec51bc, 0xf079e8, 0x887598, 0xd0b128, 0xd00401, 0xf06d78, 0x10683f, + 0x74a722, 0x58a2b5, 0x64899a, 0x88074b, 0x64bc0c, 0xa039f7, 0x041b6d, + 0x001f6b, 0x30b4b8, 0x503cea, 0x54fcf0, 0x08aed6, 0xa816d0, 0x88bd45, 0x641cb0, 0x3cdcbc, 0xf47190, 0x587a6a, 0xe4c483, 0x8cf5a3, 0x14568e, 0x8058f8, 0xf0d7aa, 0xc49ded, 0xb0aa36, 0x2c5bb8, 0x1c48ce, 0x24f5aa, 0xf877b8, 0x682737, 0x5056bf, 0x9097f3, 0x58c5cb, 0xacafb9, 0x30074d, 0x5c5181, 0x389af6, 0xe0aa96, 0x507705, 0x2c4053, 0x084acf, 0x1cddea, - 0x08152f, 0xd461da, 0xc8d083, 0x88e9fe, 0x88ae07, 0x5c0947, 0x38892c, - 0x40831d, 0x50bc96, 0x9ce65e, 0x90dd5d, 0x08f69c, 0x00092d, 0xf8db7f, - 0xe899c4, 0x24da9b, 0x1c56fe, 0xe4907e, 0x80c5e6, 0x800184, 0xf8cfc5, - 0xc808e9, 0x206274, 0x30d587, 0xc0eefb, 0x502e5c, 0x847a88, 0x0025ae, - 0x002538, 0x0022a1, 0x00125a, 0x9cd917, 0x9068c3, 0x408805, 0xf8f1b6, - 0x001ccc, 0x94ebcd, 0xa4e4b8, 0x389496, 0x0cb319, 0x08ee8b, 0xa89fba, - 0xfc1910, 0x083d88, 0x5c2e59, 0x646cb2, 0xf884f2, 0x14b484, 0x608f5c, - 0x4cbca5, 0x78595e, 0xb0d09c, 0x4ca56d, 0xa48431, 0xe4f8ef, 0x1432d1, - 0xe458e7, 0x8cbfa6, 0x7840e4, 0x9000db, 0x183a2d, 0x08373d, 0x50f520, - 0xa4ebd3, 0x28987b, 0xf40e22, 0x9c3aaf, 0x0821ef, 0xa0cbfd, 0x34145f, - 0x6c8fb5, 0xac5f3e, 0x509ea7, 0xdccf96, 0x6c2483, 0xc09727, 0xd85b2a, - 0xacc33a, 0x88797e, 0x00e091, 0x6cd032, 0xc041f6, 0x0017d5, 0x001247, - 0xe4121d, 0x684898, 0xf409d8, 0xb479a7, 0x002339, 0xd487d8, 0x184617, - 0x5001bb, 0x380a94, 0xd857ef, 0x1c66aa, 0x58c38b, 0x001ee2, 0x001c43, - 0x001d25, 0x3c5a37, 0x549b12, 0x3c8bfe, 0x00265d, 0xd4e8b2, 0x0808c2, - 0xb0c4e7, 0xd890e8, 0x34aa8b, 0x24c696, 0x181eb0, 0x20d390, 0x343111, - 0x34be00, 0x78521a, 0x7825ad, 0xf4d9fb, 0x0017c9, 0x00166b, 0x00166c, - 0xe47cf9, 0x002454, 0x20d5bf, 0x30cda7, 0xc87e75, 0x00233a, 0x60a4d0, - 0x2c0e3d, 0xd4970b, 0x64cc2e, 0xb0e235, 0x38a4ed, 0xf48b32, 0x7c787e, - 0xc0d3c0, 0x440444, 0xc09f05, 0xcc2d83, 0x38295a, 0x4c1a3d, 0xa81b5a, - 0xdc6dcd, 0x54fa3e, 0x0c8910, 0xfcf136, 0x981dfa, 0x84a466, 0x1867b0, - 0xccb11a, 0xb8bbaf, 0x60c5ad, 0x28395e, 0xc4ae12, 0xdc74a8, 0xc087eb, - 0x74f61c, 0x986f60, 0x4c189a, 0x3cf591, 0x602101, 0xa89675, 0x608e08, - 0x7c2edd, 0x3cf7a4, 0x342d0d, 0x94d029, 0x308454, 0x4c49e3, 0x087808, - 0xd03169, 0xbc5451, 0x00bf61, 0xf80cf3, 0x30766f, 0x8c3ae3, 0x78f882, - 0xb4f1da, 0x0021fb, 0xd013fd, 0xa8b86e, 0x04b167, 0xd86375, 0xdcbfe9, - 0x306a85, 0x2047da, 0x8035c1, 0xd02598, 0xa8667f, 0x7014a6, 0x10417f, - 0xac293a, 0x94e96a, 0x0c4de9, 0x907240, 0xa88808, 0xc8e0eb, 0x54e43a, - 0x28e14c, 0x848e0c, 0xb03495, 0xf0f61c, 0x0c3021, 0xd89695, 0x649abe, - 0x5cf5da, 0x20a2e4, 0xf02475, 0x24a074, 0x8863df, 0x609217, 0x34e2fd, - 0x0c3e9f, 0x6c709f, 0x6c4008, 0x5c97f3, 0x90fd61, 0x006171, 0x80e650, - 0xdc2b2a, 0xb844d9, 0xe0f5c6, 0x949426, 0xcc29f5, 0x58404e, 0xdc0c5c, - 0x2c200b, 0xdca4ca, 0x8c8fe9, 0x9810e8, 0xb49cdf, 0xa4e975, 0xc0a53e, - 0x9800c6, 0x787b8a, 0x3866f0, 0x20ee28, 0x08f4ab, 0x8c8590, 0xb48b19, - 0xe49a79, 0x28a02b, 0xb44bd2, 0x2cf0a2, 0xecadb8, 0x9801a7, 0x609ac1, - 0xf07960, 0x9c8ba0, 0x4c3275, 0xe4e4ab, 0xc8334b, 0x00f4b9, 0x0c771a, - 0x74e1b6, 0x64200c, 0xc0847a, 0x183451, 0xfc253f, 0x1040f3, 0x6cc26b, - 0x182032, 0x70dee2, 0x00c610, 0x101c0c, 0x7cfadf, 0x5cf938, 0x3871de, - 0xbc5436, 0x9c4fda, 0x1c5cf2, 0x60fb42, 0x002500, 0x00236c, 0x0021e9, - 0x001ff3, 0x001f5b, 0x001e52, 0x001d4f, 0x001124, 0xa8fad8, 0x5c969d, - 0xe48b7f, 0x84fcfe, 0x444c0c, 0x8c2daa, 0x6c3e6d, 0x189efc, 0xc09f42, - 0xb8f6b1, 0x406c8f, 0xa4d1d2, 0x040cce, 0xd89e3f, 0x28e7cf, 0xc8bcc8, - 0xd8a25e, 0x90840d, 0xf81edf, 0xb0ca68, 0x98ca33, 0x68ef43, 0xcc2db7, - 0xd4a33d, 0xe4e0a6, 0x70ef00, 0x80ad16, 0x641cae, 0x14205e, 0x5c1dd9, - 0x18f1d8, 0xf86fc1, 0xf099b6, 0xdcd3a2, 0x38e7d8, 0xd8b377, 0xb4cef6, - 0xd40b1a, 0x5882a8, 0xb4ae2b, 0x0c413e, 0xd0929e, 0x4480eb, 0xb84fd5, - 0xec59e7, 0x3059b7, 0x501ac5, 0x1cb094, 0xa0f450, 0x002248, 0xec8892, - 0xb07994, 0x141aa3, 0xccc3ea, 0x34bb26, 0x40786a, 0xf40b93, 0x68ed43, - 0x34bb1f, 0x489d24, 0x000f86, 0xacee9e, 0xc08997, 0x2827bf, 0xf05b7b, - 0x7cf90e, 0xac5a14, 0xb0c559, 0xbcd11f, 0xa0b4a5, 0x80656d, 0x48137e, - 0xe83a12, 0x9c0298, 0x6c8336, 0xb8c68e, 0x74458a, 0xa49a58, 0xb4ef39, - 0x14a364, 0x3ca10d, 0x206e9c, 0x183f47, 0x0c715d, 0x0c1420, 0xa80600, - 0x6cf373, 0x78c3e9, 0xc83870, 0x288335, 0x44783e, 0x202d07, 0x98398e, - 0x348a7b, 0xbc765e, 0x78009e, 0x68c44d, 0xf8e61a, 0x888322, 0x84b541, - 0x0015b9, 0x001df6, 0xece09b, 0x606bbd, 0x0000f0, 0x4844f7, 0x1c5a3e, - 0xf47b5e, 0x008701, 0xfc4203, 0x1c232c, 0xcc61e5, 0x404e36, 0x009ec8, - 0xacf7f3, 0x102ab3, 0x584498, 0xa086c6, 0x7c1dd9, 0x9893cc, 0x3ccd93, - 0xf06bca, 0x3423ba, 0xd022be, 0xd02544, 0xbc20a4, 0x14f42a, 0xbc851f, - 0xb85e7b, 0xc462ea, 0x0023d6, 0x002491, 0x001b98, 0x44f459, 0x34c3ac, - 0x94d771, 0x4c3c16, 0x9401c2, 0xb43a28, 0xd0c1b1, 0xf008f1, 0x78471d, - 0x3816d1, 0xd48890, 0x002566, 0x00265f, 0xacc1ee, 0x5cba37, 0x7802f8, - 0x3096fb, 0xf0ee10, 0xa43d78, 0xec01ee, 0xb83765, 0xc4576e, 0x90f1aa, - 0x78bdbc, 0xd47ae2, 0x84c0ef, 0x7c1c68, 0xd463c6, 0x508f4c, 0x7c6456, - 0x448f17, 0x04d6aa, 0x9ce063, 0xf06e0b, 0x5c865c, 0xf0b0e7, 0x209bcd, - 0xcc20e8, 0x04d13a, 0x0cf346, 0x003de8, 0x485929, 0x34fcef, 0x002483, - 0x001c62, 0x583f54, 0x40b0fa, 0xa8922c, 0x98d6f7, 0x505527, 0x0034da, - 0xa09169, 0x88365f, 0x9c8c6e, 0xbcffeb, 0x685acf, 0x48746e, 0x54724f, - 0x04f13e, 0x600308, 0x80ea96, 0x24a2e1, 0x90b931, 0x280b5c, 0xa8968a, - 0x9c04eb, 0x885395, 0x80929f, 0x98b8e3, 0xd8004d, 0x98fe94, 0x68644b, - 0xf099bf, 0xfce998, 0x48e9f1, 0x4c7c5f, 0x60f81d, 0x689c70, 0x2cb43a, - 0x042665, 0xf4f15a, 0x207d74, 0x4c8d79, 0xfcfc48, 0x38c986, 0x70ece4, - 0xd81d72, 0x94f6a3, 0x78fd94, 0x48d705, 0x7c6df8, 0x3cab8e, 0x787e61, - 0xd4f46f, 0xc88550, 0xac7f3e, 0xa4c361, 0x087045, 0x40331a, 0xdc3714, - 0x789f70, 0x64b0a6, 0x84fcac, 0x6c19c0, 0x20ab37, 0xc0d012, 0xd4dccd, - 0x484baa, 0xf80377, 0x14bd61, 0x78886d, 0xa85c2c, 0x00db70, 0xbcec5d, - 0xdc415f, 0x30636b, 0x0c5101, 0x086d41, 0x04d3cf, 0x203cae, 0x748d08, - 0xa03be3, 0x186590, 0x0010fa, 0x000502, 0xb8782e, 0xa4d18c, 0xcc25ef, - 0x68dbca, 0x044bed, 0x6c8dc1, 0x38cada, 0xf45c89, 0x581faa, 0x24ab81, - 0x70cd60, 0x7cc537, 0xc42c03, 0xd83062, 0x40d32d, 0x7c6d62, 0x286ab8, - 0x403cfc, 0xb8c75d, 0xe8040b, 0xe4ce8f, 0x3c0754, 0xa46706, 0x80b03d, - 0xc83c85, 0xa04ea7, 0x409c28, 0x08e689, 0x4cb199, 0x98d6bb, 0x3cd0f8, - 0x7cc3a1, 0x002608, 0x001ec2, 0x001b63, 0x0017f2, 0x0016cb, 0x000393, - 0x804971, 0x64e682, 0xb4f7a1, 0x785dc8, 0x48c796, 0x804e70, 0x3880df, - 0x1094bb, 0xf01898, 0x48a91c, 0xa056f3, 0x549963, 0x28ff3c, 0x902155, - 0x64a769, 0xbccfcc, 0xa4516f, 0x3c8375, 0x149a10, 0x0ce725, 0xc0335e, - 0x20a99b, 0x4c0bbe, 0x7c1e52, 0xdcb4c4, 0x001dd8, 0x0017fa, 0x0003ff, - 0xf8e079, 0x1430c6, 0xe0757d, 0x9cd35b, 0x60af6d, 0xb85a73, 0x103047, - 0x109266, 0xb047bf, 0x7c0bc6, 0x804e81, 0x244b81, 0x50a4c8, 0x8425db, - 0xd8c4e9, 0x50c8e5, 0x446d6c, 0x38d40b, 0x647791, 0x781fdb, 0x08fc88, - 0x30c7ae, 0x18227e, 0x00f46f, 0x9ce6e7, 0xe498d1, 0x5cca1a, 0x70288b, - 0x4849c7, 0x205ef7, 0x182666, 0xc06599, 0xcc07ab, 0xe84e84, 0x50fc9f, - 0xe432cb, 0x889b39, 0xbcb1f3, 0x38ece4, 0xccf9e8, 0xf0e77e, 0x5ce8eb, - 0xb8d9ce, 0x70f927, 0x301966, 0x28bab5, 0x103b59, 0x6cb7f4, 0x001ee1, - 0x0018af, 0xbc72b1, 0x78f7be, 0xf49f54, 0x00214c, 0x001632, 0xd0667b, - 0x001377, 0x50b7c3, 0x8018a7, 0x444e1a, 0xe8e5d6, 0x5492be, 0x101dc0, - 0x0021d1, 0x68dfdd, 0xc46ab7, 0xfc64ba, 0x2082c0, 0x3480b3, 0x7451ba, - 0x64b473, 0xcc2d8c, 0x949aa9, 0x20dbab, 0x5c9960, 0x948bc1, 0x4827ea, - 0x388c50, 0xa09347, 0xc8f230, 0x1c77f6, 0xe44790, 0xd4503f, 0x40163b, - 0x5c497d, 0xe47dbd, 0x503da1, 0x508569, 0x1077b1, 0x5cf6dc, 0x380195, - 0xbc1485, 0x88d50c, 0x947be7, 0x00ec0a, 0x54bd79, 0xdc44b6, 0x1007b6, - 0xc0174d, 0xa407b6, 0x149f3c, 0x88b4a6, 0x2c5491, 0x5c70a3, 0x10f96f, - 0xf01c13, 0x00aa70, 0xbcf5ac, 0xccfa00, 0xf8a9d0, 0x805a04, 0x5caf06, - 0xb81daa, 0x10f1f2, 0x0025e5, 0x0022a9, 0xc49a02, 0x344df7, 0xd41a3f, - 0xcc6ea4, 0xa46cf1, 0x0ca8a7, 0x54b802, 0x0469f8, 0xbc6c21, 0xc869cd, - 0x80d605, 0x587f57, 0xa4b805, 0x70480f, 0x18f643, 0x748114, 0x18ee69, - 0xf0dbe2, 0xb8098a, 0x549f13, 0x2c1f23, 0x507a55, 0x9c35eb, 0xa43135, - 0xd0034b, 0xa01828, 0xd0a637, 0xd04f7e, 0xd8bb2c, 0x80be05, 0xe0b52d, - 0x68ae20, 0xe8802e, 0x7c0191, 0x9c293f, 0x341298, 0x903c92, 0x24240e, - 0xa0999b, 0xe0f847, 0x442a60, 0x1093e9, 0xdc2b61, 0xb8ff61, 0x18e7f4, - 0x78ca39, 0x5c5948, 0x60334b, 0x9027e4, 0xd49a20, 0xb09fba, 0x8c006d, - 0xc06394, 0x843835, 0xe4c63d, 0x54eaa8, 0xa886dd, 0xaccf5c, 0xf0dbf8, - 0x98f0ab, 0xdc9b9c, 0x8c2937, 0xdc86d8, 0xa88e24, 0xd8cf9c, 0x04489a, - 0x3c15c2, 0x20c9d0, 0x74e2f5, 0x842999, 0x9c207b, 0x283737, 0x148fc6, - 0x28cfda, 0x145a05, 0xa0edcd, 0x1ce62b, 0x3090ab, 0x7073cb, 0xf0cba1, - 0x045453, 0x40b395, 0x008865, 0x30f7c5, 0x20768f, 0xc0ccf8, 0x80ed2c, - 0xe8b2ac, 0x8489ad, 0x8c8ef2, 0xf40f24, 0x84a134, 0x1c9148, 0x5cf7e6, - 0xa0d795, 0xcc088d, 0x00b362, 0xf86214, 0xb0702d, 0xd0c5f3, 0x60f445, - 0x5082d5, 0x9c84bf, 0x48bf6b, 0x245ba7, 0xbca920, 0xb019c6, 0x58e28f, - 0xac1f74, 0x080007, 0xe425e7, 0x28cfe9, 0x9060f1, 0x741bb2, 0x28ed6a, - 0x34ab37, 0x60a37d, 0x0056cd, 0x7081eb, 0x086698, 0x24f677, 0x7867d7, - 0x5433cb, 0xd0d2b0, 0xd88f76, 0x3c2ef9, 0xdc56e7, 0x347c25, 0xd4909c, - 0x041e64, 0x0026b0, 0x00264a, 0x0025bc, 0x0023df, 0x002241, 0x000a95, - 0x38e60a, 0x24181d, 0xf4c248, 0xa8515b, 0xc048e6, 0xd07714, 0x749eaf, - 0xb841a4, 0xf895ea, 0x50a67f, 0x647033, 0x846878}; + 0x08152f, 0xb8c111, 0x3408bc, 0x844167, 0xb4f61c, 0x68ab1e, 0x2c61f6, + 0xe49adc, 0xd0817a, 0xc4618b, 0x3451c9, 0xe0b9ba, 0xd023db, 0xb88d12, + 0xb817c2, 0x68a86d, 0x78a3e4, 0x680927, 0x60facd, 0x1caba7, 0x784f43, + 0x404d7f, 0x7c04d0, 0xbc9fef, 0x8866a5, 0x88e87f, 0xb853ac, 0x2c3361, + 0xa860b6, 0x24f094, 0x90b0ed, 0xc4b301, 0xe05f45, 0x483b38, 0xe0c767, + 0x1c9e46, 0x0cd746, 0x440010, 0xe498d6, 0x606944, 0x0452f3, 0x241eeb, + 0xf431c3, 0x64a5c3, 0xbc926b, 0x0050e4, 0x003065, 0x000a27, 0x001451, + 0x8c7b9d, 0x88c663, 0xc82a14, 0x9803d8, 0x8c5877, 0x0019e3, 0x002312, + 0x002332, 0x002436, 0x00254b, 0x0026bb, 0x70f087, 0x886b6e, 0x4c74bf, + 0xe80688, 0xcc08e0, 0x5855ca, 0x5c0947, 0x38892c, 0x40831d, 0x50bc96, + 0x985aeb, 0x2078f0, 0x78d75f, 0xe0accb, 0x98e0d9, 0xc0cecd, 0x70e72c, + 0xd03311, 0x5cadcf, 0x006d52, 0x48437c, 0x34a395, 0x9cf387, 0xa85b78, + 0x908d6c, 0x0c1539, 0xbc4cc4, 0x0cbc9f, 0xa45e60, 0x544e90, 0x9ce65e, + 0x90dd5d, 0x08f69c, 0xd461da, 0xc8d083, 0x88e9fe, 0x88ae07, 0x18af8f, + 0xc8b5b7, 0xa8bbcf, 0x90b21f, 0xb8e856, 0x1499e2, 0xb418d1, 0x80006e, + 0x60d9c7, 0xc8f650, 0x1c1ac0, 0xe06678, 0x5c8d4e, 0xc0f2fb, 0x00f76f, + 0xac87a3, 0x542696, 0xd8d1cb, 0x64a3cb, 0x44fb42, 0xf41ba1, 0x3ce072, + 0xe88d28, 0xcc785f, 0xac3c0b, 0x88cb87, 0xec3586, 0xf0c1f1, 0xf4f951, + 0x8cfaba, 0x5c95ae, 0xe0c97a, 0xbc52b7, 0x14109f, 0x00c3f4, 0x74eb80, + 0xa82bb9, 0x7c6b9c, 0x1cc3eb, 0xbca58b, 0x70fd46, 0xd07fa0, 0x9caa1b, + 0x18d717, 0xb4cb57, 0x74b587, 0xd81c79, 0x8cfe57, 0xc0a600, 0xa823fe, + 0xfcaab6, 0xc0bdc8, 0xa887b3, 0x742344, 0xd832e3, 0xe06267, 0x482ca0, + 0x1801f1, 0x70bbe9, 0xf0b429, 0x0c9838, 0x0c1daf, 0x28e31f, 0x14f65a, + 0xd4c94b, 0x703a51, 0xdc080f, 0xf82d7c, 0x9c648b, 0x14d00d, 0x00092d, + 0xf8db7f, 0xe899c4, 0x24da9b, 0x1c56fe, 0xe4907e, 0x80c5e6, 0x800184, + 0xf8cfc5, 0xc808e9, 0x206274, 0x30d587, 0xc0eefb, 0x502e5c, 0x847a88, + 0x0025ae, 0x002538, 0x0022a1, 0x00125a, 0x9cd917, 0x9068c3, 0x408805, + 0xf8f1b6, 0x001ccc, 0x94ebcd, 0xa4e4b8, 0x389496, 0x0cb319, 0x08ee8b, + 0xa89fba, 0xfc1910, 0x083d88, 0x5c2e59, 0x646cb2, 0xf884f2, 0x14b484, + 0x608f5c, 0x4cbca5, 0x78595e, 0xb0d09c, 0x4ca56d, 0xa48431, 0xe4f8ef, + 0x1432d1, 0xe458e7, 0x8cbfa6, 0x7840e4, 0x9000db, 0x183a2d, 0x08373d, + 0x50f520, 0xa4ebd3, 0x28987b, 0xf40e22, 0x9c3aaf, 0x0821ef, 0xa0cbfd, + 0x34145f, 0x6c8fb5, 0xac5f3e, 0x509ea7, 0xdccf96, 0x6c2483, 0xc09727, + 0xd85b2a, 0xacc33a, 0x88797e, 0x00e091, 0x6cd032, 0xc041f6, 0x0017d5, + 0x001247, 0xe4121d, 0x684898, 0xf409d8, 0xb479a7, 0x002339, 0xd487d8, + 0x184617, 0x5001bb, 0x380a94, 0xd857ef, 0x1c66aa, 0x58c38b, 0x001ee2, + 0x001c43, 0x001d25, 0x3c5a37, 0x549b12, 0x3c8bfe, 0x00265d, 0xd4e8b2, + 0x0808c2, 0xb0c4e7, 0xd890e8, 0x34aa8b, 0x24c696, 0x181eb0, 0x20d390, + 0x343111, 0x34be00, 0x78521a, 0x7825ad, 0xf4d9fb, 0x0017c9, 0x00166b, + 0x00166c, 0xe47cf9, 0x002454, 0x20d5bf, 0x30cda7, 0xc87e75, 0x00233a, + 0x60a4d0, 0x2c0e3d, 0x7c787e, 0xc0d3c0, 0x440444, 0xc09f05, 0xcc2d83, + 0x38295a, 0x4c1a3d, 0xa81b5a, 0xdc6dcd, 0x54fa3e, 0x0c8910, 0xfcf136, + 0x981dfa, 0x84a466, 0x1867b0, 0xccb11a, 0xb8bbaf, 0x60c5ad, 0x28395e, + 0xc4ae12, 0xdc74a8, 0xc087eb, 0x74f61c, 0x986f60, 0x4c189a, 0x3cf591, + 0x602101, 0xa89675, 0x608e08, 0x7c2edd, 0x3cf7a4, 0x342d0d, 0x94d029, + 0x308454, 0x087808, 0xd03169, 0xbc5451, 0x641cae, 0xa4e975, 0xc0a53e, + 0x9800c6, 0x787b8a, 0x3866f0, 0x20ee28, 0x08f4ab, 0x8c8590, 0x68ef43, + 0xcc2db7, 0xd4a33d, 0xe4e0a6, 0x70ef00, 0xb0ca68, 0x9810e8, 0xb49cdf, + 0xdca4ca, 0x8c8fe9, 0x98ca33, 0xfc253f, 0x183451, 0xc0847a, 0x64200c, + 0x74e1b6, 0x0c771a, 0x00f4b9, 0xc8334b, 0xb8f6b1, 0xc09f42, 0x189efc, + 0x6c3e6d, 0x8c2daa, 0xe4e4ab, 0x58404e, 0xdc0c5c, 0x2c200b, 0x609ac1, + 0xf07960, 0x9c8ba0, 0x28a02b, 0xb44bd2, 0x9c4fda, 0x1c5cf2, 0x3871de, + 0xbc5436, 0x5cf938, 0x4c3275, 0x2cf0a2, 0xecadb8, 0x9801a7, 0xb48b19, + 0xe49a79, 0x406c8f, 0x00c610, 0x70dee2, 0x182032, 0x6cc26b, 0x1040f3, + 0x001d4f, 0x001e52, 0x001f5b, 0x001ff3, 0x0021e9, 0x00236c, 0x002500, + 0x60fb42, 0xf81edf, 0x90840d, 0xd8a25e, 0xc8bcc8, 0x28e7cf, 0xd89e3f, + 0x040cce, 0xa4d1d2, 0x7cfadf, 0x101c0c, 0x001124, 0x6c709f, 0x0c3e9f, + 0x34e2fd, 0x609217, 0x8863df, 0x80e650, 0x006171, 0x90fd61, 0x5c97f3, + 0x6c4008, 0x24a074, 0xf02475, 0x20a2e4, 0x5cf5da, 0x649abe, 0x94e96a, + 0xac293a, 0x10417f, 0xb844d9, 0xdc2b2a, 0x14205e, 0x5c1dd9, 0x18f1d8, + 0xf86fc1, 0xf099b6, 0x907240, 0x0c4de9, 0xd89695, 0x0c3021, 0xf0f61c, + 0xb03495, 0x848e0c, 0x949426, 0xe0f5c6, 0x28e14c, 0x54e43a, 0xc8e0eb, + 0xa88808, 0x444c0c, 0x84fcfe, 0xe48b7f, 0x5c969d, 0xa8fad8, 0x7014a6, + 0xa8667f, 0xd02598, 0xcc29f5, 0xdcd3a2, 0x08c5e1, 0x00bf61, 0xf80cf3, + 0x30766f, 0x8c3ae3, 0x78f882, 0xb4f1da, 0x0021fb, 0xd013fd, 0xa8b86e, + 0xdcbfe9, 0x306a85, 0x4466fc, 0xfca621, 0x0ccb85, 0xa4d990, 0xd003df, + 0x24fce5, 0xe4b2fb, 0xf83880, 0x241b7a, 0x402619, 0xbcfed9, 0x808223, + 0x3830f9, 0x6c006b, 0x38a4ed, 0xb0e235, 0x64cc2e, 0xd86375, 0x80ad16, + 0x2047da, 0x8035c1, 0x9487e0, 0x7c03ab, 0xd4970b, 0xf48b32, 0x4c49e3, + 0x04b167, 0xd8ce3a, 0xb8c74a, 0xfc183c, 0xc0e862, 0xec2ce2, 0x64c753, + 0x38e7d8, 0xd8b377, 0xb4cef6, 0xd40b1a, 0x5882a8, 0xb4ae2b, 0x0c413e, + 0xd0929e, 0x4480eb, 0xb84fd5, 0xec59e7, 0x3059b7, 0x501ac5, 0x1cb094, + 0xa0f450, 0x002248, 0xec8892, 0xb07994, 0x141aa3, 0xccc3ea, 0x34bb26, + 0x40786a, 0xf40b93, 0x68ed43, 0x34bb1f, 0x489d24, 0x000f86, 0xacee9e, + 0xc08997, 0x2827bf, 0xf05b7b, 0x7cf90e, 0xac5a14, 0xb0c559, 0xbcd11f, + 0xa0b4a5, 0x80656d, 0x48137e, 0xe83a12, 0x9c0298, 0x6c8336, 0xb8c68e, + 0x74458a, 0xa49a58, 0xb4ef39, 0x14a364, 0x3ca10d, 0x206e9c, 0x183f47, + 0x0c715d, 0x0c1420, 0xa80600, 0x6cf373, 0x78c3e9, 0xc83870, 0x288335, + 0x44783e, 0x202d07, 0x98398e, 0x348a7b, 0xbc765e, 0x78009e, 0x68c44d, + 0xf8e61a, 0x888322, 0x84b541, 0x0015b9, 0x001df6, 0xece09b, 0x606bbd, + 0x0000f0, 0x4844f7, 0x1c5a3e, 0xf47b5e, 0x008701, 0xfc4203, 0x1c232c, + 0xcc61e5, 0x404e36, 0x9893cc, 0x3ccd93, 0xf06bca, 0x3423ba, 0xd022be, + 0xd02544, 0xbc20a4, 0x14f42a, 0xbc851f, 0xb85e7b, 0xc462ea, 0x0023d6, + 0x002491, 0x001b98, 0x44f459, 0x34c3ac, 0x94d771, 0x4c3c16, 0x9401c2, + 0xb43a28, 0xd0c1b1, 0xf008f1, 0x78471d, 0x3816d1, 0xd48890, 0x002566, + 0x00265f, 0x5cba37, 0x3096fb, 0xf0ee10, 0xa43d78, 0xec01ee, 0xb83765, + 0xc4576e, 0x90f1aa, 0x78bdbc, 0xd47ae2, 0x84c0ef, 0x7c1c68, 0xd463c6, + 0x7c6456, 0x448f17, 0x04d6aa, 0x9ce063, 0xf06e0b, 0x5c865c, 0x003de8, + 0x08e689, 0x7836cc, 0x08d46a, 0x485929, 0x34fcef, 0x002483, 0x001c62, + 0x583f54, 0x40b0fa, 0xa8922c, 0x98d6f7, 0x505527, 0x0034da, 0xa09169, + 0x88365f, 0x9c8c6e, 0xbcffeb, 0x685acf, 0xb4f7a1, 0x785dc8, 0x48c796, + 0x804e70, 0x3880df, 0xdc415f, 0x30636b, 0xf45c89, 0x68dbca, 0x044bed, + 0x6c8dc1, 0x38cada, 0xa4d18c, 0x186590, 0x64b0a6, 0x84fcac, 0x6c19c0, + 0x20ab37, 0x203cae, 0x748d08, 0xa03be3, 0x7c6d62, 0x40d32d, 0xd83062, + 0xc42c03, 0x7cc537, 0x70cd60, 0xc0d012, 0xd4dccd, 0x484baa, 0xf80377, + 0x14bd61, 0xcc25ef, 0xb8782e, 0x000502, 0x0010fa, 0x000393, 0x0016cb, + 0x409c28, 0x78886d, 0xa85c2c, 0x00db70, 0x0c5101, 0x086d41, 0x04d3cf, + 0xbcec5d, 0x80b03d, 0xc83c85, 0xa04ea7, 0x0017f2, 0x001b63, 0x001ec2, + 0x002608, 0xa4c361, 0xac7f3e, 0x280b5c, 0x90b931, 0x24a2e1, 0x80ea96, + 0x600308, 0x04f13e, 0x54724f, 0x48746e, 0xd4f46f, 0x787e61, 0x60f81d, + 0x4c7c5f, 0x48e9f1, 0xfce998, 0xf099bf, 0x68644b, 0x789f70, 0x24ab81, + 0x581faa, 0xa46706, 0x3c0754, 0xe4ce8f, 0xe8040b, 0xb8c75d, 0x403cfc, + 0x98fe94, 0xd8004d, 0x98b8e3, 0x80929f, 0x885395, 0x9c04eb, 0xa8968a, + 0xdc3714, 0x40331a, 0x94f6a3, 0xd81d72, 0x70ece4, 0x38c986, 0xfcfc48, + 0x4c8d79, 0x207d74, 0xf4f15a, 0x042665, 0x2cb43a, 0x689c70, 0x087045, + 0x3cab8e, 0x7c6df8, 0x48d705, 0x78fd94, 0xc88550, 0x286ab8, 0x7cc3a1, + 0x3cd0f8, 0x98d6bb, 0x4cb199, 0x64e682, 0x804971, 0xcc20e8, 0x209bcd, + 0xf0b0e7, 0xa056f3, 0x549963, 0x28ff3c, 0x1094bb, 0xf01898, 0x48a91c, + 0x58b10f, 0x304b07, 0x1496e5, 0x80ceb9, 0xcc2119, 0x0057c1, 0x14c697, + 0xfc039f, 0x9c0cdf, 0x007204, 0x90e17b, 0x18810e, 0x608c4a, 0xa4d931, + 0x6cc7ec, 0x647bce, 0x584498, 0xacc1ee, 0x7802f8, 0x508f4c, 0x04d13a, + 0x0cf346, 0x082525, 0xf460e2, 0xa45046, 0x009ec8, 0x7c1dd9, 0xa086c6, + 0x102ab3, 0xacf7f3, 0x601d91, 0x38f9d3, 0x44e66e, 0xe83617, 0x344262, + 0xc09ad0, 0x902155, 0x64a769, 0xbccfcc, 0xa4516f, 0x3c8375, 0x149a10, + 0x0ce725, 0xc0335e, 0x20a99b, 0x4c0bbe, 0x7c1e52, 0xdcb4c4, 0x7c6f06, + 0x001dd8, 0x0017fa, 0x000a75, 0x0003ff, 0xf8e079, 0x1430c6, 0xe0757d, + 0x9cd35b, 0x60af6d, 0xb85a73, 0x103047, 0x109266, 0xb047bf, 0x7c0bc6, + 0x804e81, 0x244b81, 0x50a4c8, 0x8425db, 0xd8c4e9, 0x50c8e5, 0x446d6c, + 0x38d40b, 0x647791, 0x781fdb, 0x08fc88, 0x30c7ae, 0x18227e, 0x00f46f, + 0x9ce6e7, 0xe498d1, 0x5cca1a, 0x70288b, 0x4849c7, 0x205ef7, 0x182666, + 0xc06599, 0xcc07ab, 0xe84e84, 0x50fc9f, 0xe432cb, 0x889b39, 0xbcb1f3, + 0x38ece4, 0xccf9e8, 0xf0e77e, 0x5ce8eb, 0xb8d9ce, 0x70f927, 0x301966, + 0x28bab5, 0x103b59, 0x6cb7f4, 0x001ee1, 0x0018af, 0xbc72b1, 0x78f7be, + 0xf49f54, 0x00214c, 0x001632, 0xd0667b, 0x001377, 0x50b7c3, 0x8018a7, + 0x444e1a, 0xe8e5d6, 0x5492be, 0x101dc0, 0x0021d1, 0xcc2d8c, 0x949aa9, + 0x20dbab, 0x5c9960, 0x88b4a6, 0x2c5491, 0x5c70a3, 0x10f96f, 0xf01c13, + 0x00aa70, 0xbcf5ac, 0xccfa00, 0xf8a9d0, 0x805a04, 0x5caf06, 0xb81daa, + 0x10f1f2, 0x0025e5, 0x0022a9, 0xc49a02, 0x344df7, 0xd41a3f, 0xcc6ea4, + 0xa46cf1, 0x0ca8a7, 0x54b802, 0x24181d, 0xf4c248, 0xa8515b, 0xc048e6, + 0xd07714, 0x2816a8, 0x84a134, 0x1c9148, 0xc0ccf8, 0x80ed2c, 0xe8b2ac, + 0x8489ad, 0x20768f, 0x28ed6a, 0x34ab37, 0x60a37d, 0x0056cd, 0xbca920, + 0x5082d5, 0x9c84bf, 0x00b362, 0xf86214, 0xb0702d, 0xd0c5f3, 0x0023df, + 0x0025bc, 0x00264a, 0x0026b0, 0x041e64, 0xd49a20, 0x9027e4, 0x60334b, + 0x5c5948, 0x60f445, 0x5cf7e6, 0xa0d795, 0xcc088d, 0x8c8ef2, 0xf40f24, + 0x24f677, 0x7867d7, 0x5433cb, 0xd0d2b0, 0xd88f76, 0x3c2ef9, 0x7081eb, + 0x086698, 0x9060f1, 0x741bb2, 0x28cfe9, 0xe425e7, 0xb019c6, 0x58e28f, + 0xac1f74, 0x48bf6b, 0x245ba7, 0xdc56e7, 0x347c25, 0xd4909c, 0x080007, + 0x000a95, 0x002241, 0x18ee69, 0x748114, 0x18f643, 0xd0a637, 0xa01828, + 0xd0034b, 0xa43135, 0x9c35eb, 0x507a55, 0xa0999b, 0x24240e, 0x903c92, + 0xa88e24, 0xe8802e, 0x68ae20, 0xe0b52d, 0x80be05, 0xd8bb2c, 0xd04f7e, + 0x2c1f23, 0x549f13, 0xb8098a, 0xf0dbe2, 0x8c2937, 0xdc9b9c, 0x98f0ab, + 0xf0dbf8, 0xaccf5c, 0x3c15c2, 0x04489a, 0xd8cf9c, 0xa886dd, 0x54eaa8, + 0xe4c63d, 0x843835, 0xc06394, 0x8c006d, 0xb09fba, 0xdc86d8, 0x78ca39, + 0x18e7f4, 0xb8ff61, 0xdc2b61, 0x1093e9, 0x442a60, 0xe0f847, 0x145a05, + 0x28cfda, 0x148fc6, 0x283737, 0x045453, 0xf0cba1, 0x30f7c5, 0x008865, + 0x40b395, 0x3090ab, 0x1ce62b, 0xa0edcd, 0x842999, 0x74e2f5, 0x20c9d0, + 0x7073cb, 0x9c207b, 0x341298, 0x9c293f, 0x7c0191, 0x70480f, 0xa4b805, + 0x587f57, 0x80d605, 0xc869cd, 0xbc6c21, 0x0469f8, 0x749eaf, 0xb841a4, + 0xf895ea, 0x50a67f, 0x647033, 0x846878, 0x948bc1, 0x4827ea, 0x388c50, + 0xa09347, 0xc8f230, 0x1c77f6, 0xe44790, 0xd4503f, 0x40163b, 0x5c497d, + 0xe47dbd, 0x503da1, 0x508569, 0x1077b1, 0x5cf6dc, 0x380195, 0xbc1485, + 0x88d50c, 0x947be7, 0x54bd79, 0xdc44b6, 0x1007b6, 0xc0174d, 0xa407b6, + 0x149f3c, 0xd868c3, 0xc493d9, 0x00b5d0, 0x8c83e1, 0xfcb6d8, 0x6ce85c, + 0x007c2d, 0xf47def, 0x7c8bb5, 0xdcf756, 0x68dfdd, 0x64b473, 0x7451ba, + 0x3480b3, 0x2082c0, 0xfc64ba, 0xc46ab7, 0x00ec0a, 0x38e60a, 0x04e598, + 0x2ca9f0, 0x586b14, 0x94b01f, 0x94f6d6, 0x40bc60}; From ebc3aac991fa47ef98719841636e2462f8727013 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 20 Sep 2018 19:36:32 +0200 Subject: [PATCH 049/105] gps-code restructured, i2c gps now working --- src/globals.h | 1 + src/gps.cpp | 51 ++++++++++++++++++++----------------------------- src/lorawan.cpp | 2 +- src/main.cpp | 16 +++++++++------- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/globals.h b/src/globals.h index fd057b5d..2d8d692f 100644 --- a/src/globals.h +++ b/src/globals.h @@ -55,6 +55,7 @@ extern std::array::iterator it; extern std::array beacons; #ifdef HAS_GPS +extern TaskHandle_t GpsTask; #include "gps.h" #endif diff --git a/src/gps.cpp b/src/gps.cpp index 435cd9a5..15dd9638 100644 --- a/src/gps.cpp +++ b/src/gps.cpp @@ -25,19 +25,23 @@ void gps_loop(void *pvParameters) { // initialize and, if needed, configure, GPS #if defined GPS_SERIAL HardwareSerial GPS_Serial(1); - GPS_Serial.begin(GPS_SERIAL); // serial connect to GPS device + GPS_Serial.begin(GPS_SERIAL); #elif defined GPS_QUECTEL_L76 + uint8_t ret; Wire.begin(GPS_QUECTEL_L76, 400000); // I2C connect to GPS device with 400 KHz - uint8_t i2c_ret; Wire.beginTransmission(GPS_ADDR); - Wire.write(0x00); // dummy write to start read - i2c_ret = Wire.endTransmission(); // check if chip is seen on i2c bus + Wire.write(0x00); // dummy write + ret = Wire.endTransmission(); // check if chip is seen on i2c bus - if (i2c_ret) { - ESP_LOGE(TAG, "Quectel L76 GPS chip not found on i2c bus, bus error %d", - i2c_ret); - return; + if (ret) { + ESP_LOGE(TAG, + "Quectel L76 GPS chip not found on i2c bus, bus error %d. " + "Stopping GPS-Task.", + ret); + vTaskDelete(GpsTask); + } else { + ESP_LOGI(TAG, "Quectel L76 GPS chip found."); } #endif @@ -46,31 +50,18 @@ void gps_loop(void *pvParameters) { if (cfg.gpsmode) { #if defined GPS_SERIAL - - while (cfg.gpsmode) { - // feed GPS decoder with serial NMEA data from GPS device - while (GPS_Serial.available()) { - gps.encode(GPS_Serial.read()); - } - vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog + // feed GPS decoder with serial NMEA data from GPS device + while (GPS_Serial.available()) { + gps.encode(GPS_Serial.read()); } - // after GPS function was disabled, close connect to GPS device - GPS_Serial.end(); - #elif defined GPS_QUECTEL_L76 - - while (cfg.gpsmode) { - Wire.requestFrom(GPS_ADDR, - 128); // 128 is Wire.h buffersize arduino-ESP32 - while (Wire.available()) { - gps.encode(Wire.read()); - vTaskDelay(2 / portTICK_PERIOD_MS); // delay see L76 datasheet - } - vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog + Wire.requestFrom(GPS_ADDR, 32); // caution: this is a blocking call + while (Wire.available()) { + gps.encode(Wire.read()); + vTaskDelay(2 / portTICK_PERIOD_MS); // 2ms delay according L76 datasheet } - -#endif // GPS Type - } +#endif + } // if (cfg.gpsmode) vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 5cb93af7..a9939893 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -103,7 +103,7 @@ void get_hard_deveui(uint8_t *pdeveui) { i2c_ret = Wire.endTransmission(); // check if device was seen on i2c bus - if (ic2_ret == 0) { + if (i2c_ret == 0) { char deveui[32] = ""; uint8_t data; diff --git a/src/main.cpp b/src/main.cpp index d84deb18..aeb9ffd4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -52,6 +52,10 @@ TaskHandle_t LoraTask = NULL; QueueHandle_t SPISendQueue; #endif +#ifdef HAS_GPS +TaskHandle_t GpsTask = NULL; +#endif + portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; // sync main loop and ISR when modifying IRQ // handler shared variables @@ -280,7 +284,7 @@ void setup() { // (note: arduino main loop runs on core 1, too) // https://techtutorialsx.com/2017/05/09/esp32-get-task-execution-core/ - ESP_LOGI(TAG, "Starting Lora task on core 1"); + ESP_LOGI(TAG, "Starting Lora..."); xTaskCreatePinnedToCore(lorawan_loop, "loraloop", 2048, (void *)1, (5 | portPRIVILEGE_BIT), &LoraTask, 1); #endif @@ -289,22 +293,20 @@ void setup() { // higher priority than wifi channel rotation task since we process serial // streaming NMEA data #ifdef HAS_GPS - if (cfg.gpsmode) { - ESP_LOGI(TAG, "Starting GPS task on core 0"); - xTaskCreatePinnedToCore(gps_loop, "gpsloop", 2048, (void *)1, 2, NULL, 0); - } + ESP_LOGI(TAG, "Starting GPS..."); + xTaskCreatePinnedToCore(gps_loop, "gpsloop", 2048, (void *)1, 2, &GpsTask, 0); #endif // start BLE scan callback if BLE function is enabled in NVRAM configuration #ifdef BLECOUNTER if (cfg.blescan) { - ESP_LOGI(TAG, "Starting BLE task on core 1"); + ESP_LOGI(TAG, "Starting Bluetooth..."); start_BLEscan(); } #endif // start wifi in monitor mode and start channel rotation task on core 0 - ESP_LOGI(TAG, "Starting Wifi task on core 0"); + ESP_LOGI(TAG, "Starting Wifi..."); wifi_sniffer_init(); // initialize salt value using esp_random() called by random() in // arduino-esp32 core. Note: do this *after* wifi has started, since From 67832da09d61d2e5f36f8b52aa98279286d839fb Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Fri, 21 Sep 2018 12:25:52 +0200 Subject: [PATCH 050/105] Timer bugfix (issue #145 #148) --- src/gps.cpp | 2 +- src/lorawan.cpp | 2 +- src/main.cpp | 37 +++++++++++++++++++------------------ src/wifiscan.cpp | 4 ++-- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/gps.cpp b/src/gps.cpp index 15dd9638..849ca30e 100644 --- a/src/gps.cpp +++ b/src/gps.cpp @@ -63,7 +63,7 @@ void gps_loop(void *pvParameters) { #endif } // if (cfg.gpsmode) - vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog + vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU } // end of infinite loop diff --git a/src/lorawan.cpp b/src/lorawan.cpp index a9939893..95babc37 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -248,7 +248,7 @@ void lorawan_loop(void *pvParameters) { while (1) { os_runloop_once(); // execute LMIC jobs - vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog + vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU } } diff --git a/src/main.cpp b/src/main.cpp index aeb9ffd4..7b207d71 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -236,19 +236,22 @@ void setup() { channelSwitch = timerBegin(1, 800, true); timerAttachInterrupt(channelSwitch, &ChannelSwitchIRQ, true); timerAlarmWrite(channelSwitch, cfg.wifichancycle * 1000, true); - timerAlarmEnable(channelSwitch); + //ESP_LOGI(TAG, "chanelswitch alarm threshold %d", cfg.wifichancycle * 1000); // setup send cycle trigger IRQ using esp32 hardware timer 2 sendCycle = timerBegin(2, 8000, true); timerAttachInterrupt(sendCycle, &SendCycleIRQ, true); timerAlarmWrite(sendCycle, cfg.sendcycle * 2 * 10000, true); - timerAlarmEnable(sendCycle); // setup house keeping cycle trigger IRQ using esp32 hardware timer 3 homeCycle = timerBegin(3, 8000, true); timerAttachInterrupt(homeCycle, &homeCycleIRQ, true); timerAlarmWrite(homeCycle, HOMECYCLE * 10000, true); + + //enable timers, caution: order is critical here timerAlarmEnable(homeCycle); + timerAlarmEnable(sendCycle); + timerAlarmEnable(channelSwitch); // show payload encoder #if PAYLOAD_ENCODER == 1 @@ -324,33 +327,31 @@ void setup() { void loop() { - while (1) { - // state machine for switching display, LED, button, housekeeping, - // senddata + // state machine for switching display, LED, button, housekeeping, + // senddata #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) - led_loop(); + led_loop(); #endif #ifdef HAS_BUTTON - readButton(); + readButton(); #endif #ifdef HAS_DISPLAY - updateDisplay(); + updateDisplay(); #endif - // check housekeeping cycle and if expired do homework - checkHousekeeping(); - // check send queue and process it - processSendBuffer(); - // check send cycle and enqueue payload if cycle is expired - sendPayload(); - // reset watchdog - vTaskDelay(2 / portTICK_PERIOD_MS); + // check housekeeping cycle and if expired do homework + checkHousekeeping(); + // check send queue and process it + processSendBuffer(); + // check send cycle and enqueue payload if cycle is expired + sendPayload(); + // yield to CPU + vTaskDelay(2 / portTICK_PERIOD_MS); - } // loop() -} +} // loop() /* end Arduino main loop * ------------------------------------------------------------ */ diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index ed3a84ee..4c240738 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -58,10 +58,10 @@ void wifi_channel_loop(void *pvParameters) { channel = (channel % WIFI_CHANNEL_MAX) + 1; esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); ESP_LOGD(TAG, "Wifi set channel %d", channel); - - vTaskDelay(2 / portTICK_PERIOD_MS); // reset watchdog } + vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU + } // end of infinite wifi channel rotation loop } From 5b9327512b1e803867b19ab174984e1a24e8a945 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Fri, 21 Sep 2018 18:23:34 +0200 Subject: [PATCH 051/105] tasking + statemachine restructured --- src/cyclic.cpp | 17 ++++----- src/cyclic.h | 3 +- src/main.cpp | 89 +++++++++++++++++++++++++++++++----------------- src/main.h | 3 ++ src/ota.cpp | 2 +- src/senddata.cpp | 54 ++++++++++++++--------------- 6 files changed, 93 insertions(+), 75 deletions(-) diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 72b2284c..95185a98 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -10,7 +10,11 @@ static const char TAG[] = "main"; // do all housekeeping -void doHomework() { +void doHousekeeping() { + + portENTER_CRITICAL(&timerMux); + HomeCycleIRQ = 0; + portEXIT_CRITICAL(&timerMux); // update uptime counter uptime(); @@ -49,16 +53,7 @@ void doHomework() { if (esp_get_minimum_free_heap_size() <= MEM_LOW) // check again esp_restart(); // memory leak, reset device } -} // doHomework() - -void checkHousekeeping() { - if (HomeCycleIRQ) { - portENTER_CRITICAL(&timerMux); - HomeCycleIRQ = 0; - portEXIT_CRITICAL(&timerMux); - doHomework(); - } -} +} // doHousekeeping() void IRAM_ATTR homeCycleIRQ() { portENTER_CRITICAL(&timerMux); diff --git a/src/cyclic.h b/src/cyclic.h index fbe1309a..0a169e4e 100644 --- a/src/cyclic.h +++ b/src/cyclic.h @@ -1,8 +1,7 @@ #ifndef _CYCLIC_H #define _CYCLIC_H -void doHomework(void); -void checkHousekeeping(void); +void doHousekeeping(void); void homeCycleIRQ(void); uint64_t uptime(void); void reset_counters(void); diff --git a/src/main.cpp b/src/main.cpp index 7b207d71..9a85115e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,14 +33,14 @@ uint16_t macs_total = 0, macs_wifi = 0, macs_ble = 0, batt_voltage = 0; // globals for display // hardware timer for cyclic tasks -hw_timer_t *channelSwitch = NULL, *displaytimer = NULL, *sendCycle = NULL, - *homeCycle = NULL; +hw_timer_t *channelSwitch, *displaytimer, *sendCycle, *homeCycle; // this variables will be changed in the ISR, and read in main loop volatile int ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, SendCycleTimerIRQ = 0, DisplayTimerIRQ = 0, HomeCycleIRQ = 0; TaskHandle_t WifiLoopTask = NULL; +TaskHandle_t StateTask = NULL; // RTos send queues for payload transmit #ifdef HAS_LORA @@ -68,9 +68,6 @@ PayloadConvert payload(PAYLOAD_BUFFER_SIZE); // local Tag for logging static const char TAG[] = "main"; -/* begin Aruino SETUP - * ------------------------------------------------------------ */ - void setup() { // disable the default wifi logging @@ -229,6 +226,7 @@ void setup() { // reload interrupt after each trigger of display refresh cycle timerAlarmWrite(displaytimer, DISPLAYREFRESH_MS * 1000, true); // enable display interrupt + yield(); timerAlarmEnable(displaytimer); #endif @@ -236,7 +234,6 @@ void setup() { channelSwitch = timerBegin(1, 800, true); timerAttachInterrupt(channelSwitch, &ChannelSwitchIRQ, true); timerAlarmWrite(channelSwitch, cfg.wifichancycle * 1000, true); - //ESP_LOGI(TAG, "chanelswitch alarm threshold %d", cfg.wifichancycle * 1000); // setup send cycle trigger IRQ using esp32 hardware timer 2 sendCycle = timerBegin(2, 8000, true); @@ -248,9 +245,13 @@ void setup() { timerAttachInterrupt(homeCycle, &homeCycleIRQ, true); timerAlarmWrite(homeCycle, HOMECYCLE * 10000, true); - //enable timers, caution: order is critical here + // enable timers + // caution, see: https://github.com/espressif/arduino-esp32/issues/1313 + yield(); timerAlarmEnable(homeCycle); + yield(); timerAlarmEnable(sendCycle); + yield(); timerAlarmEnable(channelSwitch); // show payload encoder @@ -283,13 +284,35 @@ void setup() { // join network LMIC_startJoining(); + /* + + Overview Tasks & Timer + + Task Core Prio Purpose + ==================================================================== + IDLE 0 0 ESP32 arduino scheduler + wifiloop 0 1 switches Wifi channels + gpsloop 0 2 reasd data from GPS over serial or i2c + IDLE 1 0 Arduino loop() -> unused + loraloop 1 1 runs the LMIC stack + statemachine 1 3 switches process logic + + Timers + ====== + 0 Display-Refresh + 1 Wifi Channel Switch + 2 Send Cycle + 3 Housekeeping + + */ + // start lmic runloop in rtos task on core 1 // (note: arduino main loop runs on core 1, too) // https://techtutorialsx.com/2017/05/09/esp32-get-task-execution-core/ ESP_LOGI(TAG, "Starting Lora..."); - xTaskCreatePinnedToCore(lorawan_loop, "loraloop", 2048, (void *)1, - (5 | portPRIVILEGE_BIT), &LoraTask, 1); + xTaskCreatePinnedToCore(lorawan_loop, "loraloop", 2048, (void *)1, 1, + &LoraTask, 1); #endif // if device has GPS and it is enabled, start GPS reader task on core 0 with @@ -317,41 +340,43 @@ void setup() { reset_salt(); // get new 16bit for salting hashes xTaskCreatePinnedToCore(wifi_channel_loop, "wifiloop", 2048, (void *)1, 1, &WifiLoopTask, 0); + + // start state machine + ESP_LOGI(TAG, "Starting Statemachine..."); + xTaskCreatePinnedToCore(stateMachine, "stateloop", 2048, (void *)1, 3, + &StateTask, 1); + } // setup() -/* end Arduino SETUP - * ------------------------------------------------------------ */ +void stateMachine(void *pvParameters) { -/* begin Arduino main loop - * ------------------------------------------------------ */ + configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check -void loop() { - - // state machine for switching display, LED, button, housekeeping, - // senddata + while (1) { #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) - led_loop(); + led_loop(); #endif #ifdef HAS_BUTTON - readButton(); + readButton(); #endif #ifdef HAS_DISPLAY - updateDisplay(); + updateDisplay(); #endif - // check housekeeping cycle and if expired do homework - checkHousekeeping(); - // check send queue and process it - processSendBuffer(); - // check send cycle and enqueue payload if cycle is expired - sendPayload(); - // yield to CPU - vTaskDelay(2 / portTICK_PERIOD_MS); + // check housekeeping cycle and if expired do the work + if (HomeCycleIRQ) + doHousekeeping(); + // check send queue and process it + processSendBuffer(); + // check send cycle and enqueue payload if cycle is expired + if (SendCycleTimerIRQ) + sendPayload(); + // yield to CPU + vTaskDelay(2 / portTICK_PERIOD_MS); + } +} -} // loop() - -/* end Arduino main loop - * ------------------------------------------------------------ */ +void loop() { vTaskDelay(2 / portTICK_PERIOD_MS); } diff --git a/src/main.h b/src/main.h index 4b77439a..e1d8b775 100644 --- a/src/main.h +++ b/src/main.h @@ -13,5 +13,8 @@ #include // needed for reading ESP32 chip attributes #include // needed for Wifi event handler +#include // needed for timers + +void stateMachine(void *pvParameters); #endif \ No newline at end of file diff --git a/src/ota.cpp b/src/ota.cpp index dbdcc30d..6be81127 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -246,7 +246,7 @@ int version_compare(const String v1, const String v2) { // vnum stores each numeric part of version int vnum1 = 0, vnum2 = 0; - // loop untill both string are processed + // loop until both string are processed for (int i = 0, j = 0; (i < v1.length() || j < v2.length());) { // storing numeric part of version 1 in vnum1 while (i < v1.length() && v1[i] != '.') { diff --git a/src/senddata.cpp b/src/senddata.cpp index f7e3824c..73eca826 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -34,39 +34,36 @@ void SendData(uint8_t port) { } } // SendData -// cyclic called function to prepare payload to send +// interrupt triggered function to prepare payload to send void sendPayload() { - if (SendCycleTimerIRQ) { - portENTER_CRITICAL(&timerMux); - SendCycleTimerIRQ = 0; - portEXIT_CRITICAL(&timerMux); + portENTER_CRITICAL(&timerMux); + SendCycleTimerIRQ = 0; + portEXIT_CRITICAL(&timerMux); - // append counter data to payload - payload.reset(); - payload.addCount(macs_wifi, cfg.blescan ? macs_ble : 0); - // append GPS data, if present + // append counter data to payload + payload.reset(); + payload.addCount(macs_wifi, cfg.blescan ? macs_ble : 0); + // append GPS data, if present #ifdef HAS_GPS - // show NMEA data in debug mode, useful for debugging GPS on board - // connection - ESP_LOGD(TAG, "GPS NMEA data: passed %d / failed: %d / with fix: %d", - gps.passedChecksum(), gps.failedChecksum(), - gps.sentencesWithFix()); - // log GPS position if we have a fix and gps data mode is enabled - if ((cfg.gpsmode) && (gps.location.isValid())) { - gps_read(); - payload.addGPS(gps_status); - ESP_LOGD(TAG, "lat=%.6f | lon=%.6f | %u Sats | HDOP=%.1f | Altitude=%um", - gps_status.latitude / (float)1e6, - gps_status.longitude / (float)1e6, gps_status.satellites, - gps_status.hdop / (float)100, gps_status.altitude); - } else { - ESP_LOGD(TAG, "No valid GPS position or GPS data mode disabled"); - } -#endif - SendData(COUNTERPORT); + // show NMEA data in debug mode, useful for debugging GPS on board + // connection + ESP_LOGD(TAG, "GPS NMEA data: passed %d / failed: %d / with fix: %d", + gps.passedChecksum(), gps.failedChecksum(), gps.sentencesWithFix()); + // log GPS position if we have a fix and gps data mode is enabled + if ((cfg.gpsmode) && (gps.location.isValid())) { + gps_read(); + payload.addGPS(gps_status); + ESP_LOGD(TAG, "lat=%.6f | lon=%.6f | %u Sats | HDOP=%.1f | Altitude=%um", + gps_status.latitude / (float)1e6, + gps_status.longitude / (float)1e6, gps_status.satellites, + gps_status.hdop / (float)100, gps_status.altitude); + } else { + ESP_LOGD(TAG, "No valid GPS position or GPS data mode disabled"); } +#endif + SendData(COUNTERPORT); } // sendpayload() // interrupt handler used for payload send cycle timer @@ -76,9 +73,8 @@ void IRAM_ATTR SendCycleIRQ() { portEXIT_CRITICAL(&timerMux); } -// cyclic called function to eat data from RTos send queues and transmit it +// interrupt triggered function to eat data from RTos send queues and transmit it void processSendBuffer() { - MessageBuffer_t SendBuffer; #ifdef HAS_LORA From 5678a5b23c76a354b2929adef8dfed8c77dc2015 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Fri, 21 Sep 2018 19:30:02 +0200 Subject: [PATCH 052/105] v1.5.0 --- platformio.ini | 4 ++-- src/ota.cpp | 2 +- src/ota.h | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 2c159c1e..97f71896 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.4.36 +release_version = 1.5.0 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 @@ -76,7 +76,7 @@ platform = ${common.platform_espressif32} framework = arduino board = heltec_wifi_lora_32 board_build.partitions = ${common.board_build.partitions} -upload_speed = 115200 +upload_speed = 921600 lib_deps = ${common.lib_deps_all} ${common.lib_deps_display} diff --git a/src/ota.cpp b/src/ota.cpp index 6be81127..93a94e6c 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -15,7 +15,7 @@ limitations under the License. */ -#include "OTA.h" +#include "ota.h" const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); diff --git a/src/ota.h b/src/ota.h index 4b9e92d1..27c1b748 100644 --- a/src/ota.h +++ b/src/ota.h @@ -6,6 +6,7 @@ #include #include #include +#include void checkFirmwareUpdates(); void processOTAUpdate(const String &version); From ff759e153353e0d7ec94c6ee4cf4d050eadc90fc Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 12:20:24 +0200 Subject: [PATCH 053/105] restructured wifi channel rotation --- src/main.cpp | 14 +++++++------- src/senddata.cpp | 4 ++-- src/senddata.h | 2 +- src/wifiscan.cpp | 21 +++++---------------- src/wifiscan.h | 2 +- 5 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 9a85115e..2d0ea2fd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -291,8 +291,7 @@ void setup() { Task Core Prio Purpose ==================================================================== IDLE 0 0 ESP32 arduino scheduler - wifiloop 0 1 switches Wifi channels - gpsloop 0 2 reasd data from GPS over serial or i2c + gpsloop 0 2 read data from GPS over serial or i2c IDLE 1 0 Arduino loop() -> unused loraloop 1 1 runs the LMIC stack statemachine 1 3 switches process logic @@ -338,8 +337,6 @@ void setup() { // arduino-esp32 core. Note: do this *after* wifi has started, since // function gets it's seed from RF noise reset_salt(); // get new 16bit for salting hashes - xTaskCreatePinnedToCore(wifi_channel_loop, "wifiloop", 2048, (void *)1, 1, - &WifiLoopTask, 0); // start state machine ESP_LOGI(TAG, "Starting Statemachine..."); @@ -366,12 +363,15 @@ void stateMachine(void *pvParameters) { updateDisplay(); #endif - // check housekeeping cycle and if expired do the work + // check wifi scan cycle and if due rotate channel + if (ChannelTimerIRQ) + switchWifiChannel(channel); + // check housekeeping cycle and if due do the work if (HomeCycleIRQ) doHousekeeping(); // check send queue and process it - processSendBuffer(); - // check send cycle and enqueue payload if cycle is expired + enqueuePayload(); + // check send cycle and if due enqueue payload to send if (SendCycleTimerIRQ) sendPayload(); // yield to CPU diff --git a/src/senddata.cpp b/src/senddata.cpp index 73eca826..ad647233 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -74,7 +74,7 @@ void IRAM_ATTR SendCycleIRQ() { } // interrupt triggered function to eat data from RTos send queues and transmit it -void processSendBuffer() { +void enqueuePayload() { MessageBuffer_t SendBuffer; #ifdef HAS_LORA @@ -98,7 +98,7 @@ void processSendBuffer() { } #endif -} // processSendBuffer +} // enqueuePayload void flushQueues() { #ifdef HAS_LORA diff --git a/src/senddata.h b/src/senddata.h index 806899d6..eba9d8bd 100644 --- a/src/senddata.h +++ b/src/senddata.h @@ -4,7 +4,7 @@ void SendData(uint8_t port); void sendPayload(void); void SendCycleIRQ(void); -void processSendBuffer(void); +void enqueuePayload(void); void flushQueues(); #endif // _SENDDATA_H_ \ No newline at end of file diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 4c240738..55c690ff 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -43,28 +43,17 @@ void wifi_sniffer_init(void) { ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); // now switch on monitor mode } -// Wifi channel rotation task -void wifi_channel_loop(void *pvParameters) { - - configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check - - while (1) { - - if (ChannelTimerIRQ) { +// Wifi channel rotation +void switchWifiChannel(uint8_t &ch) { portENTER_CRITICAL(&timerMux); ChannelTimerIRQ = 0; portEXIT_CRITICAL(&timerMux); // rotates variable channel 1..WIFI_CHANNEL_MAX - channel = (channel % WIFI_CHANNEL_MAX) + 1; - esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); - ESP_LOGD(TAG, "Wifi set channel %d", channel); + ch = (ch % WIFI_CHANNEL_MAX) + 1; + esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE); + ESP_LOGD(TAG, "Wifi set channel %d", &ch); } - vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU - - } // end of infinite wifi channel rotation loop -} - // IRQ handler void IRAM_ATTR ChannelSwitchIRQ() { portENTER_CRITICAL(&timerMux); diff --git a/src/wifiscan.h b/src/wifiscan.h index afd5700e..5e8aae9a 100644 --- a/src/wifiscan.h +++ b/src/wifiscan.h @@ -28,6 +28,6 @@ typedef struct { void wifi_sniffer_init(void); void wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type); void ChannelSwitchIRQ(void); -void wifi_channel_loop(void *pvParameters); +void switchWifiChannel(uint8_t &ch); #endif \ No newline at end of file From e553dc70a2bfd556f9313a0c16374eafff828966 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 12:25:03 +0200 Subject: [PATCH 054/105] comments in main.cpp --- src/main.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 2d0ea2fd..4552a1a5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -216,6 +216,16 @@ void setup() { DisplayState = cfg.screenon; init_display(PRODUCTNAME, PROGVERSION); +/* + Usage of ESP32 hardware timers + ============================== + + 0 Display-Refresh + 1 Wifi Channel Switch + 2 Send Cycle + 3 Housekeeping +*/ + // setup display refresh trigger IRQ using esp32 hardware timer // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ @@ -286,8 +296,6 @@ void setup() { /* - Overview Tasks & Timer - Task Core Prio Purpose ==================================================================== IDLE 0 0 ESP32 arduino scheduler @@ -296,13 +304,6 @@ void setup() { loraloop 1 1 runs the LMIC stack statemachine 1 3 switches process logic - Timers - ====== - 0 Display-Refresh - 1 Wifi Channel Switch - 2 Send Cycle - 3 Housekeeping - */ // start lmic runloop in rtos task on core 1 From 8e2ace3adf328c016cf767d4b539c5fb286aff1f Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 13:43:12 +0200 Subject: [PATCH 055/105] main.cpp: moved LED control to loop() --- src/main.cpp | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 4552a1a5..7f0d1ada 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -216,15 +216,15 @@ void setup() { DisplayState = cfg.screenon; init_display(PRODUCTNAME, PROGVERSION); -/* - Usage of ESP32 hardware timers - ============================== + /* + Usage of ESP32 hardware timers + ============================== - 0 Display-Refresh - 1 Wifi Channel Switch - 2 Send Cycle - 3 Housekeeping -*/ + 0 Display-Refresh + 1 Wifi Channel Switch + 2 Send Cycle + 3 Housekeeping + */ // setup display refresh trigger IRQ using esp32 hardware timer // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ @@ -300,9 +300,9 @@ void setup() { ==================================================================== IDLE 0 0 ESP32 arduino scheduler gpsloop 0 2 read data from GPS over serial or i2c - IDLE 1 0 Arduino loop() -> unused + IDLE 1 0 Arduino loop() -> used for LED switching loraloop 1 1 runs the LMIC stack - statemachine 1 3 switches process logic + statemachine 1 3 switches application process logic */ @@ -352,10 +352,6 @@ void stateMachine(void *pvParameters) { while (1) { -#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) - led_loop(); -#endif - #ifdef HAS_BUTTON readButton(); #endif @@ -375,9 +371,19 @@ void stateMachine(void *pvParameters) { // check send cycle and if due enqueue payload to send if (SendCycleTimerIRQ) sendPayload(); - // yield to CPU + + // give yield to CPU vTaskDelay(2 / portTICK_PERIOD_MS); } } -void loop() { vTaskDelay(2 / portTICK_PERIOD_MS); } +void loop() { + +// switch LED states if device has a LED +#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) + led_loop(); +#endif + + // give yield to CPU + vTaskDelay(2 / portTICK_PERIOD_MS); +} From 63dc01249160bfe82a6bb0360f2633cde211efa8 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 13:43:57 +0200 Subject: [PATCH 056/105] v1.5.1 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 97f71896..98e23235 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.0 +release_version = 1.5.1 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 From 2e38cda6ace17463d1d72c36810100b0b02652b8 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 17:05:09 +0200 Subject: [PATCH 057/105] ota update stabilized --- src/update.cpp | 354 +++++++++++++++++++++++++++++++++++++++++++++++++ src/update.h | 181 +++++++++++++++++++++++++ 2 files changed, 535 insertions(+) create mode 100644 src/update.cpp create mode 100644 src/update.h diff --git a/src/update.cpp b/src/update.cpp new file mode 100644 index 00000000..f69603cb --- /dev/null +++ b/src/update.cpp @@ -0,0 +1,354 @@ +/* + +this file copied from esp32-arduino library and patched, see PR +https://github.com/espressif/arduino-esp32/pull/1886 + +*/ + +#include "update.h" +#include "Arduino.h" +#include "esp_spi_flash.h" +#include "esp_ota_ops.h" +#include "esp_image_format.h" + +static const char * _err2str(uint8_t _error){ + if(_error == UPDATE_ERROR_OK){ + return ("No Error"); + } else if(_error == UPDATE_ERROR_WRITE){ + return ("Flash Write Failed"); + } else if(_error == UPDATE_ERROR_ERASE){ + return ("Flash Erase Failed"); + } else if(_error == UPDATE_ERROR_READ){ + return ("Flash Read Failed"); + } else if(_error == UPDATE_ERROR_SPACE){ + return ("Not Enough Space"); + } else if(_error == UPDATE_ERROR_SIZE){ + return ("Bad Size Given"); + } else if(_error == UPDATE_ERROR_STREAM){ + return ("Stream Read Timeout"); + } else if(_error == UPDATE_ERROR_MD5){ + return ("MD5 Check Failed"); + } else if(_error == UPDATE_ERROR_MAGIC_BYTE){ + return ("Wrong Magic Byte"); + } else if(_error == UPDATE_ERROR_ACTIVATE){ + return ("Could Not Activate The Firmware"); + } else if(_error == UPDATE_ERROR_NO_PARTITION){ + return ("Partition Could Not be Found"); + } else if(_error == UPDATE_ERROR_BAD_ARGUMENT){ + return ("Bad Argument"); + } else if(_error == UPDATE_ERROR_ABORT){ + return ("Aborted"); + } + return ("UNKNOWN"); +} + +static bool _partitionIsBootable(const esp_partition_t* partition){ + uint8_t buf[4]; + if(!partition){ + return false; + } + if(!ESP.flashRead(partition->address, (uint32_t*)buf, 4)) { + return false; + } + + if(buf[0] != ESP_IMAGE_HEADER_MAGIC) { + return false; + } + return true; +} + +static bool _enablePartition(const esp_partition_t* partition){ + uint8_t buf[4]; + if(!partition){ + return false; + } + if(!ESP.flashRead(partition->address, (uint32_t*)buf, 4)) { + return false; + } + buf[0] = ESP_IMAGE_HEADER_MAGIC; + + return ESP.flashWrite(partition->address, (uint32_t*)buf, 4); +} + +UpdateClass::UpdateClass() +: _error(0) +, _buffer(0) +, _bufferLen(0) +, _size(0) +, _progress_callback(NULL) +, _progress(0) +, _command(U_FLASH) +, _partition(NULL) +{ +} + +UpdateClass& UpdateClass::onProgress(THandlerFunction_Progress fn) { + _progress_callback = fn; + return *this; +} + +void UpdateClass::_reset() { + if (_buffer) + delete[] _buffer; + _buffer = 0; + _bufferLen = 0; + _progress = 0; + _size = 0; + _command = U_FLASH; +} + +bool UpdateClass::canRollBack(){ + if(_buffer){ //Update is running + return false; + } + const esp_partition_t* partition = esp_ota_get_next_update_partition(NULL); + return _partitionIsBootable(partition); +} + +bool UpdateClass::rollBack(){ + if(_buffer){ //Update is running + return false; + } + const esp_partition_t* partition = esp_ota_get_next_update_partition(NULL); + return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition); +} + +bool UpdateClass::begin(size_t size, int command) { + if(_size > 0){ + log_w("already running"); + return false; + } + + _reset(); + _error = 0; + + if(size == 0) { + _error = UPDATE_ERROR_SIZE; + return false; + } + + if (command == U_FLASH) { + _partition = esp_ota_get_next_update_partition(NULL); + if(!_partition){ + _error = UPDATE_ERROR_NO_PARTITION; + return false; + } + log_d("OTA Partition: %s", _partition->label); + } + else if (command == U_SPIFFS) { + _partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL); + if(!_partition){ + _error = UPDATE_ERROR_NO_PARTITION; + return false; + } + } + else { + _error = UPDATE_ERROR_BAD_ARGUMENT; + log_e("bad command %u", command); + return false; + } + + if(size == UPDATE_SIZE_UNKNOWN){ + size = _partition->size; + } else if(size > _partition->size){ + _error = UPDATE_ERROR_SIZE; + log_e("too large %u > %u", size, _partition->size); + return false; + } + + //initialize + _buffer = (uint8_t*)malloc(SPI_FLASH_SEC_SIZE); + if(!_buffer){ + log_e("malloc failed"); + return false; + } + _size = size; + _command = command; + _md5.begin(); + return true; +} + +void UpdateClass::_abort(uint8_t err){ + _reset(); + _error = err; +} + +void UpdateClass::abort(){ + _abort(UPDATE_ERROR_ABORT); +} + +bool UpdateClass::_writeBuffer(){ + //first bytes of new firmware + if(!_progress && _command == U_FLASH){ + //check magic + if(_buffer[0] != ESP_IMAGE_HEADER_MAGIC){ + _abort(UPDATE_ERROR_MAGIC_BYTE); + return false; + } + //remove magic byte from the firmware now and write it upon success + //this ensures that partially written firmware will not be bootable + _buffer[0] = 0xFF; + } + if(!ESP.flashEraseSector((_partition->address + _progress)/SPI_FLASH_SEC_SIZE)){ + _abort(UPDATE_ERROR_ERASE); + return false; + } + if (!ESP.flashWrite(_partition->address + _progress, (uint32_t*)_buffer, _bufferLen)) { + _abort(UPDATE_ERROR_WRITE); + return false; + } + //restore magic or md5 will fail + if(!_progress && _command == U_FLASH){ + _buffer[0] = ESP_IMAGE_HEADER_MAGIC; + } + _md5.add(_buffer, _bufferLen); + _progress += _bufferLen; + _bufferLen = 0; + return true; +} + +bool UpdateClass::_verifyHeader(uint8_t data) { + if(_command == U_FLASH) { + if(data != ESP_IMAGE_HEADER_MAGIC) { + _abort(UPDATE_ERROR_MAGIC_BYTE); + return false; + } + return true; + } else if(_command == U_SPIFFS) { + return true; + } + return false; +} + +bool UpdateClass::_verifyEnd() { + if(_command == U_FLASH) { + if(!_enablePartition(_partition) || !_partitionIsBootable(_partition)) { + _abort(UPDATE_ERROR_READ); + return false; + } + + if(esp_ota_set_boot_partition(_partition)){ + _abort(UPDATE_ERROR_ACTIVATE); + return false; + } + _reset(); + return true; + } else if(_command == U_SPIFFS) { + _reset(); + return true; + } + return false; +} + +bool UpdateClass::setMD5(const char * expected_md5){ + if(strlen(expected_md5) != 32) + { + return false; + } + _target_md5 = expected_md5; + return true; +} + +bool UpdateClass::end(bool evenIfRemaining){ + if(hasError() || _size == 0){ + return false; + } + + if(!isFinished() && !evenIfRemaining){ + log_e("premature end: res:%u, pos:%u/%u\n", getError(), progress(), _size); + _abort(UPDATE_ERROR_ABORT); + return false; + } + + if(evenIfRemaining) { + if(_bufferLen > 0) { + _writeBuffer(); + } + _size = progress(); + } + + _md5.calculate(); + if(_target_md5.length()) { + if(_target_md5 != _md5.toString()){ + _abort(UPDATE_ERROR_MD5); + return false; + } + } + + return _verifyEnd(); +} + +size_t UpdateClass::write(uint8_t *data, size_t len) { + if(hasError() || !isRunning()){ + return 0; + } + + if(len > remaining()){ + _abort(UPDATE_ERROR_SPACE); + return 0; + } + + size_t left = len; + + while((_bufferLen + left) > SPI_FLASH_SEC_SIZE) { + size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen; + memcpy(_buffer + _bufferLen, data + (len - left), toBuff); + _bufferLen += toBuff; + if(!_writeBuffer()){ + return len - left; + } + left -= toBuff; + } + memcpy(_buffer + _bufferLen, data + (len - left), left); + _bufferLen += left; + if(_bufferLen == remaining()){ + if(!_writeBuffer()){ + return len - left; + } + } + return len; +} + +size_t UpdateClass::writeStream(Stream &data) { + data.setTimeout(10000); + size_t written = 0; + size_t toRead = 0; + if(hasError() || !isRunning()) + return 0; + + if(!_verifyHeader(data.peek())) { + _reset(); + return 0; + } + if (_progress_callback) { + _progress_callback(0, _size); + } + while(remaining()) { + toRead = data.readBytes(_buffer + _bufferLen, (SPI_FLASH_SEC_SIZE - _bufferLen)); + if(toRead == 0) { //Timeout + delay(100); + toRead = data.readBytes(_buffer + _bufferLen, (SPI_FLASH_SEC_SIZE - _bufferLen)); + if(toRead == 0) { //Timeout + _abort(UPDATE_ERROR_STREAM); + return written; + } + } + _bufferLen += toRead; + if((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) + return written; + written += toRead; + if(_progress_callback) { + _progress_callback(_progress, _size); + } + } + if(_progress_callback) { + _progress_callback(_size, _size); + } + return written; +} + +void UpdateClass::printError(Stream &out){ + out.println(_err2str(_error)); +} + +UpdateClass Update; diff --git a/src/update.h b/src/update.h new file mode 100644 index 00000000..2bf4dc46 --- /dev/null +++ b/src/update.h @@ -0,0 +1,181 @@ +#ifndef ESP8266UPDATER_H +#define ESP8266UPDATER_H + +#include +#include +#include +#include "esp_partition.h" + +#define UPDATE_ERROR_OK (0) +#define UPDATE_ERROR_WRITE (1) +#define UPDATE_ERROR_ERASE (2) +#define UPDATE_ERROR_READ (3) +#define UPDATE_ERROR_SPACE (4) +#define UPDATE_ERROR_SIZE (5) +#define UPDATE_ERROR_STREAM (6) +#define UPDATE_ERROR_MD5 (7) +#define UPDATE_ERROR_MAGIC_BYTE (8) +#define UPDATE_ERROR_ACTIVATE (9) +#define UPDATE_ERROR_NO_PARTITION (10) +#define UPDATE_ERROR_BAD_ARGUMENT (11) +#define UPDATE_ERROR_ABORT (12) + +#define UPDATE_SIZE_UNKNOWN 0xFFFFFFFF + +#define U_FLASH 0 +#define U_SPIFFS 100 +#define U_AUTH 200 + +class UpdateClass { + public: + typedef std::function THandlerFunction_Progress; + + UpdateClass(); + + /* + This callback will be called when Update is receiving data + */ + UpdateClass& onProgress(THandlerFunction_Progress fn); + + /* + Call this to check the space needed for the update + Will return false if there is not enough space + */ + bool begin(size_t size=UPDATE_SIZE_UNKNOWN, int command = U_FLASH); + + /* + Writes a buffer to the flash and increments the address + Returns the amount written + */ + size_t write(uint8_t *data, size_t len); + + /* + Writes the remaining bytes from the Stream to the flash + Uses readBytes() and sets UPDATE_ERROR_STREAM on timeout + Returns the bytes written + Should be equal to the remaining bytes when called + Usable for slow streams like Serial + */ + size_t writeStream(Stream &data); + + /* + If all bytes are written + this call will write the config to eboot + and return true + If there is already an update running but is not finished and !evenIfRemainanig + or there is an error + this will clear everything and return false + the last error is available through getError() + evenIfRemaining is helpfull when you update without knowing the final size first + */ + bool end(bool evenIfRemaining = false); + + /* + Aborts the running update + */ + void abort(); + + /* + Prints the last error to an output stream + */ + void printError(Stream &out); + + /* + sets the expected MD5 for the firmware (hexString) + */ + bool setMD5(const char * expected_md5); + + /* + returns the MD5 String of the sucessfully ended firmware + */ + String md5String(void){ return _md5.toString(); } + + /* + populated the result with the md5 bytes of the sucessfully ended firmware + */ + void md5(uint8_t * result){ return _md5.getBytes(result); } + + //Helpers + uint8_t getError(){ return _error; } + void clearError(){ _error = UPDATE_ERROR_OK; } + bool hasError(){ return _error != UPDATE_ERROR_OK; } + bool isRunning(){ return _size > 0; } + bool isFinished(){ return _progress == _size; } + size_t size(){ return _size; } + size_t progress(){ return _progress; } + size_t remaining(){ return _size - _progress; } + + /* + Template to write from objects that expose + available() and read(uint8_t*, size_t) methods + faster than the writeStream method + writes only what is available + */ + template + size_t write(T &data){ + size_t written = 0; + if (hasError() || !isRunning()) + return 0; + + size_t available = data.available(); + while(available) { + if(_bufferLen + available > remaining()){ + available = remaining() - _bufferLen; + } + if(_bufferLen + available > 4096) { + size_t toBuff = 4096 - _bufferLen; + data.read(_buffer + _bufferLen, toBuff); + _bufferLen += toBuff; + if(!_writeBuffer()) + return written; + written += toBuff; + } else { + data.read(_buffer + _bufferLen, available); + _bufferLen += available; + written += available; + if(_bufferLen == remaining()) { + if(!_writeBuffer()) { + return written; + } + } + } + if(remaining() == 0) + return written; + available = data.available(); + } + return written; + } + + /* + check if there is a firmware on the other OTA partition that you can bootinto + */ + bool canRollBack(); + /* + set the other OTA partition as bootable (reboot to enable) + */ + bool rollBack(); + + private: + void _reset(); + void _abort(uint8_t err); + bool _writeBuffer(); + bool _verifyHeader(uint8_t data); + bool _verifyEnd(); + + + uint8_t _error; + uint8_t *_buffer; + size_t _bufferLen; + size_t _size; + THandlerFunction_Progress _progress_callback; + uint32_t _progress; + uint32_t _command; + const esp_partition_t* _partition; + + String _target_md5; + MD5Builder _md5; +}; + +extern UpdateClass Update; + +#endif From f1e3e8ffa16838883ed9c3a849d1e1c5663a7513 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 17:06:31 +0200 Subject: [PATCH 058/105] update.h --- src/ota.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ota.h b/src/ota.h index 27c1b748..c2eb69c9 100644 --- a/src/ota.h +++ b/src/ota.h @@ -2,9 +2,10 @@ #define OTA_H #include "globals.h" +#include "update.h" #include #include -#include +//#include #include #include From 2f3767759bf30126870cbf73d13430db50a73e2b Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 17:07:11 +0200 Subject: [PATCH 059/105] v1.5.2 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 98e23235..f7f55f18 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.1 +release_version = 1.5.2 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 From 7a53b724cbcf5a7386ceffafa17d9d9654e64b10 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 19:39:31 +0200 Subject: [PATCH 060/105] ota.cpp: display dialog while updating --- src/display.h | 1 + src/globals.h | 2 -- src/main.cpp | 15 +++++++------- src/ota.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/display.h b/src/display.h index 52fb4d7e..e85b9ee1 100644 --- a/src/display.h +++ b/src/display.h @@ -4,6 +4,7 @@ #include extern uint8_t DisplayState; +extern HAS_DISPLAY u8x8; void init_display(const char *Productname, const char *Version); void refreshtheDisplay(void); diff --git a/src/globals.h b/src/globals.h index 2d8d692f..9d79575d 100644 --- a/src/globals.h +++ b/src/globals.h @@ -48,8 +48,6 @@ extern hw_timer_t *channelSwitch, *sendCycle; extern portMUX_TYPE timerMux; extern volatile int SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, ChannelTimerIRQ, ButtonPressedIRQ; -// extern QueueHandle_t LoraSendQueue, SPISendQueue; -extern TaskHandle_t WifiLoopTask; extern std::array::iterator it; extern std::array beacons; diff --git a/src/main.cpp b/src/main.cpp index 7f0d1ada..f7bbcbf1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,7 +39,6 @@ hw_timer_t *channelSwitch, *displaytimer, *sendCycle, *homeCycle; volatile int ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, SendCycleTimerIRQ = 0, DisplayTimerIRQ = 0, HomeCycleIRQ = 0; -TaskHandle_t WifiLoopTask = NULL; TaskHandle_t StateTask = NULL; // RTos send queues for payload transmit @@ -122,13 +121,6 @@ void setup() { // read settings from NVRAM loadConfig(); // includes initialize if necessary - // reboot to firmware update mode if ota trigger switch is set - if (cfg.runmode == 1) { - cfg.runmode = 0; - saveConfig(); - start_ota_update(); - } - #ifdef VENDORFILTER strcat_P(features, " OUIFLT"); #endif @@ -240,6 +232,13 @@ void setup() { timerAlarmEnable(displaytimer); #endif + // reboot to firmware update mode if ota trigger switch is set + if (cfg.runmode == 1) { + cfg.runmode = 0; + saveConfig(); + start_ota_update(); + } + // setup channel rotation trigger IRQ using esp32 hardware timer 1 channelSwitch = timerBegin(1, 800, true); timerAttachInterrupt(channelSwitch, &ChannelSwitchIRQ, true); diff --git a/src/ota.cpp b/src/ota.cpp index 93a94e6c..863b61c7 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -17,6 +17,9 @@ #include "ota.h" +#include +using namespace std; + const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); // Connection port (HTTPS) @@ -32,9 +35,33 @@ volatile bool isValidContentType = false; // Local logging tag static const char TAG[] = "main"; +void display(const uint8_t x, const uint8_t y, char* text) { +#ifdef HAS_DISPLAY + u8x8.setCursor(x, y); + u8x8.print(text); +#endif +} + void start_ota_update() { +#ifdef HAS_DISPLAY + u8x8.begin(); + u8x8.setFont(u8x8_font_chroma48medium8_r); + u8x8.clear(); +#ifdef DISPLAY_FLIP + u8x8.setFlipMode(1); +#endif + u8x8.draw2x2String(0, 0, "UPDATING"); + u8x8.setCursor(0, 3); + u8x8.print("Wifi connect ..\n"); + u8x8.print("Get Update? ..\n"); + u8x8.print("Downloading ..\n"); + u8x8.print("Flashing ..\n"); + u8x8.print("Rebooting .."); +#endif + ESP_LOGI(TAG, "Starting Wifi OTA update"); + display(14, 3, "**"); WiFi.begin(WIFI_SSID, WIFI_PASS); @@ -47,10 +74,15 @@ void start_ota_update() { } if (i >= 0) { ESP_LOGI(TAG, "connected to %s", WIFI_SSID); + display(14, 3, "OK"); checkFirmwareUpdates(); // gets and flashes new firmware and restarts - } else + } else { ESP_LOGI(TAG, "could not connect to %s, rebooting.", WIFI_SSID); + display(14, 3, " E"); + } + display(14, 7, "**"); + delay(5000); ESP.restart(); // reached only if update was not successful or no wifi connect } // start_ota_update @@ -58,19 +90,24 @@ void start_ota_update() { void checkFirmwareUpdates() { // Fetch the latest firmware version ESP_LOGI(TAG, "OTA mode, checking latest firmware version on server..."); + display(14, 4, "**"); const String latest = bintray.getLatestVersion(); if (latest.length() == 0) { ESP_LOGI( TAG, "Could not load info about the latest firmware. Rebooting to runmode."); + display(14, 4, " E"); return; } else if (version_compare(latest, cfg.version) <= 0) { ESP_LOGI(TAG, "Current firmware is up to date. Rebooting to runmode."); + display(14, 4, "NO"); return; } ESP_LOGI(TAG, "New firmware version v%s available. Downloading...", latest.c_str()); + display(14, 4, "OK"); + processOTAUpdate(latest); } @@ -83,9 +120,11 @@ inline String getHeaderValue(String header, String headerName) { * OTA update processing */ void processOTAUpdate(const String &version) { + display(14, 5, "**"); String firmwarePath = bintray.getBinaryPath(version); if (!firmwarePath.endsWith(".bin")) { ESP_LOGI(TAG, "Unsupported binary format, OTA update cancelled."); + display(14, 5, " E"); return; } @@ -97,6 +136,7 @@ void processOTAUpdate(const String &version) { if (!client.connect(currentHost.c_str(), port)) { ESP_LOGI(TAG, "Cannot connect to %s", currentHost.c_str()); + display(14, 5, " E"); return; } @@ -108,6 +148,7 @@ void processOTAUpdate(const String &version) { if (!client.connect(currentHost.c_str(), port)) { ESP_LOGI(TAG, "Redirect detected, but cannot connect to %s", currentHost.c_str()); + display(14, 5, " E"); return; } } @@ -123,6 +164,7 @@ void processOTAUpdate(const String &version) { while (client.available() == 0) { if (millis() - timeout > RESPONSE_TIMEOUT_MS) { ESP_LOGI(TAG, "Client Timeout."); + display(14, 5, " E"); client.stop(); return; } @@ -182,6 +224,8 @@ void processOTAUpdate(const String &version) { } } + display(14, 5, "OK"); + // check whether we have everything for OTA update if (contentLength && isValidContentType) { @@ -196,16 +240,19 @@ void processOTAUpdate(const String &version) { "Starting OTA update, attempt %d of %d. This will take some " "time to complete...", FLASH_MAX_TRY - i, FLASH_MAX_TRY); + display(14, 6, "**"); written = Update.writeStream(client); if (written == contentLength) { ESP_LOGI(TAG, "Written %d bytes successfully", written); + display(14, 6, "**"); break; } else { ESP_LOGI(TAG, "Written only %d of %d bytes, OTA update attempt cancelled.", written, contentLength); + display(14, 6, " E"); } } @@ -215,27 +262,33 @@ void processOTAUpdate(const String &version) { ESP_LOGI( TAG, "OTA update completed. Rebooting to runmode with new version."); + display(14, 7, "OK"); client.stop(); return; } else { ESP_LOGI(TAG, "Something went wrong! OTA update hasn't been finished " "properly."); + display(14, 7, " E"); } } else { ESP_LOGI(TAG, "An error occurred. Error #: %d", Update.getError()); + display(14, 7, " E"); } } else { ESP_LOGI(TAG, "There isn't enough space to start OTA update"); + display(14, 7, " E"); client.flush(); } } else { ESP_LOGI(TAG, "There was no valid content in the response from the OTA server!"); + display(14, 7, " E"); client.flush(); } ESP_LOGI(TAG, "OTA update failed. Rebooting to runmode with current version."); + display(14, 7, " E"); client.stop(); } From cf3ec23ef94b0480d65cec6a7283bd299f442c04 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 22 Sep 2018 21:26:11 +0200 Subject: [PATCH 061/105] code sanitization (statemachine.cpp) --- src/main.cpp | 236 ++++++++++++++++++------------------------- src/main.h | 19 ++-- src/senddata.cpp | 6 +- src/senddata.h | 2 +- src/statemachine.cpp | 35 +++++++ src/statemachine.h | 12 +++ 6 files changed, 158 insertions(+), 152 deletions(-) create mode 100644 src/statemachine.cpp create mode 100644 src/statemachine.h diff --git a/src/main.cpp b/src/main.cpp index f7bbcbf1..8f4bd99c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,6 +21,25 @@ NOTICE: Parts of the source files in this repository are made available under different licenses. Refer to LICENSE.txt file in repository for more details. +//////////////////////// ESP32-Paxcounter \\\\\\\\\\\\\\\\\\\\\\\\\\ + +Uused tasks and timers: + +Task Core Prio Purpose +==================================================================== +IDLE 0 0 ESP32 arduino scheduler +gpsloop 0 2 read data from GPS over serial or i2c +IDLE 1 0 Arduino loop() -> used for LED switching +loraloop 1 1 runs the LMIC stack +statemachine 1 3 switches application process logic + +ESP32 hardware timers +========================== + 0 Display-Refresh + 1 Wifi Channel Switch + 2 Send Cycle + 3 Housekeeping + */ // Basic Config @@ -90,39 +109,66 @@ void setup() { esp_log_set_vprintf(redirect_log); #endif - ESP_LOGI(TAG, "Starting %s v%s", PRODUCTNAME, PROGVERSION); - - // initialize system event handler for wifi task, needed for - // wifi_sniffer_init() - // esp_event_loop_init(NULL, NULL); - // ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); - - // print chip information on startup if in verbose mode -#ifdef VERBOSE - esp_chip_info_t chip_info; - esp_chip_info(&chip_info); - ESP_LOGI(TAG, - "This is ESP32 chip with %d CPU cores, WiFi%s%s, silicon revision " - "%d, %dMB %s Flash", - chip_info.cores, (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", - (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "", - chip_info.revision, spi_flash_get_chip_size() / (1024 * 1024), - (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" - : "external"); - ESP_LOGI(TAG, "ESP32 SDK: %s", ESP.getSdkVersion()); - ESP_LOGI(TAG, "Free RAM: %d bytes", ESP.getFreeHeap()); - -#ifdef HAS_GPS - ESP_LOGI(TAG, "TinyGPS+ v%s", TinyGPSPlus::libraryVersion()); -#endif - -#endif // verbose - - // read settings from NVRAM + // read (and initialize on first run) runtime settings from NVRAM loadConfig(); // includes initialize if necessary -#ifdef VENDORFILTER - strcat_P(features, " OUIFLT"); + // initialize leds +#if (HAS_LED != NOT_A_PIN) + pinMode(HAS_LED, OUTPUT); + strcat_P(features, " LED"); +#endif +#ifdef HAS_RGB_LED + rgb_set_color(COLOR_PINK); + strcat_P(features, " RGB"); +#endif + + // initialize wifi antenna +#ifdef HAS_ANTENNA_SWITCH + strcat_P(features, " ANT"); + antenna_init(); + antenna_select(cfg.wifiant); +#endif + +// switch off bluetooth, if not compiled +#ifdef BLECOUNTER + strcat_P(features, " BLE"); +#else + bool btstop = btStop(); +#endif + +// initialize battery status +#ifdef HAS_BATTERY_PROBE + strcat_P(features, " BATT"); + calibrate_voltage(); + batt_voltage = read_voltage(); +#endif + + // reboot to firmware update mode if ota trigger switch is set + if (cfg.runmode == 1) { + cfg.runmode = 0; + saveConfig(); + start_ota_update(); + } + + // initialize button +#ifdef HAS_BUTTON + strcat_P(features, " BTN_"); +#ifdef BUTTON_PULLUP + strcat_P(features, "PU"); + // install button interrupt (pullup mode) + pinMode(HAS_BUTTON, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, RISING); +#else + strcat_P(features, "PD"); + // install button interrupt (pulldown mode) + pinMode(HAS_BUTTON, INPUT_PULLDOWN); + attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, FALLING); +#endif // BUTTON_PULLUP +#endif // HAS_BUTTON + +// initialize gps +#ifdef HAS_GPS + strcat_P(features, " GPS"); #endif // initialize LoRa @@ -149,58 +195,32 @@ void setup() { SEND_QUEUE_SIZE * PAYLOAD_BUFFER_SIZE); #endif - // initialize led -#if (HAS_LED != NOT_A_PIN) - pinMode(HAS_LED, OUTPUT); - strcat_P(features, " LED"); +#ifdef VENDORFILTER + strcat_P(features, " OUIFLT"); #endif -#ifdef HAS_RGB_LED - rgb_set_color(COLOR_PINK); - strcat_P(features, " RGB"); -#endif + ESP_LOGI(TAG, "Starting %s v%s", PRODUCTNAME, PROGVERSION); - // initialize button -#ifdef HAS_BUTTON - strcat_P(features, " BTN_"); -#ifdef BUTTON_PULLUP - strcat_P(features, "PU"); - // install button interrupt (pullup mode) - pinMode(HAS_BUTTON, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, RISING); -#else - strcat_P(features, "PD"); - // install button interrupt (pulldown mode) - pinMode(HAS_BUTTON, INPUT_PULLDOWN); - attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, FALLING); -#endif // BUTTON_PULLUP -#endif // HAS_BUTTON + // print chip information on startup if in verbose mode +#ifdef VERBOSE + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + ESP_LOGI(TAG, + "This is ESP32 chip with %d CPU cores, WiFi%s%s, silicon revision " + "%d, %dMB %s Flash", + chip_info.cores, (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", + (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "", + chip_info.revision, spi_flash_get_chip_size() / (1024 * 1024), + (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" + : "external"); + ESP_LOGI(TAG, "ESP32 SDK: %s", ESP.getSdkVersion()); + ESP_LOGI(TAG, "Free RAM: %d bytes", ESP.getFreeHeap()); - // initialize wifi antenna -#ifdef HAS_ANTENNA_SWITCH - strcat_P(features, " ANT"); - antenna_init(); - antenna_select(cfg.wifiant); -#endif - -// switch off bluetooth on esp32 module, if not compiled -#ifdef BLECOUNTER - strcat_P(features, " BLE"); -#else - bool btstop = btStop(); -#endif - -// initialize gps #ifdef HAS_GPS - strcat_P(features, " GPS"); + ESP_LOGI(TAG, "TinyGPS+ v%s", TinyGPSPlus::libraryVersion()); #endif -// initialize battery status -#ifdef HAS_BATTERY_PROBE - strcat_P(features, " BATT"); - calibrate_voltage(); - batt_voltage = read_voltage(); -#endif +#endif // verbose // initialize display #ifdef HAS_DISPLAY @@ -208,16 +228,6 @@ void setup() { DisplayState = cfg.screenon; init_display(PRODUCTNAME, PROGVERSION); - /* - Usage of ESP32 hardware timers - ============================== - - 0 Display-Refresh - 1 Wifi Channel Switch - 2 Send Cycle - 3 Housekeeping - */ - // setup display refresh trigger IRQ using esp32 hardware timer // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ @@ -232,13 +242,6 @@ void setup() { timerAlarmEnable(displaytimer); #endif - // reboot to firmware update mode if ota trigger switch is set - if (cfg.runmode == 1) { - cfg.runmode = 0; - saveConfig(); - start_ota_update(); - } - // setup channel rotation trigger IRQ using esp32 hardware timer 1 channelSwitch = timerBegin(1, 800, true); timerAttachInterrupt(channelSwitch, &ChannelSwitchIRQ, true); @@ -293,18 +296,6 @@ void setup() { // join network LMIC_startJoining(); - /* - - Task Core Prio Purpose - ==================================================================== - IDLE 0 0 ESP32 arduino scheduler - gpsloop 0 2 read data from GPS over serial or i2c - IDLE 1 0 Arduino loop() -> used for LED switching - loraloop 1 1 runs the LMIC stack - statemachine 1 3 switches application process logic - - */ - // start lmic runloop in rtos task on core 1 // (note: arduino main loop runs on core 1, too) // https://techtutorialsx.com/2017/05/09/esp32-get-task-execution-core/ @@ -332,6 +323,8 @@ void setup() { // start wifi in monitor mode and start channel rotation task on core 0 ESP_LOGI(TAG, "Starting Wifi..."); + // esp_event_loop_init(NULL, NULL); + // ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); wifi_sniffer_init(); // initialize salt value using esp_random() called by random() in // arduino-esp32 core. Note: do this *after* wifi has started, since @@ -345,40 +338,9 @@ void setup() { } // setup() -void stateMachine(void *pvParameters) { - - configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check - - while (1) { - -#ifdef HAS_BUTTON - readButton(); -#endif - -#ifdef HAS_DISPLAY - updateDisplay(); -#endif - - // check wifi scan cycle and if due rotate channel - if (ChannelTimerIRQ) - switchWifiChannel(channel); - // check housekeeping cycle and if due do the work - if (HomeCycleIRQ) - doHousekeeping(); - // check send queue and process it - enqueuePayload(); - // check send cycle and if due enqueue payload to send - if (SendCycleTimerIRQ) - sendPayload(); - - // give yield to CPU - vTaskDelay(2 / portTICK_PERIOD_MS); - } -} - void loop() { -// switch LED states if device has a LED +// switch LED state if device has LED(s) #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) led_loop(); #endif diff --git a/src/main.h b/src/main.h index e1d8b775..40e9c208 100644 --- a/src/main.h +++ b/src/main.h @@ -1,20 +1,17 @@ #ifndef _MAIN_H #define _MAIN_H -#include "globals.h" -#include "led.h" -#include "macsniff.h" -#include "wifiscan.h" -#include "configmanager.h" -#include "senddata.h" -#include "cyclic.h" -#include "beacon_array.h" -#include "OTA.h" - #include // needed for reading ESP32 chip attributes #include // needed for Wifi event handler #include // needed for timers -void stateMachine(void *pvParameters); +#include "globals.h" +#include "led.h" +#include "wifiscan.h" +#include "configmanager.h" +#include "cyclic.h" +#include "beacon_array.h" +#include "ota.h" +#include "statemachine.h" #endif \ No newline at end of file diff --git a/src/senddata.cpp b/src/senddata.cpp index ad647233..f738ee6b 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -73,8 +73,8 @@ void IRAM_ATTR SendCycleIRQ() { portEXIT_CRITICAL(&timerMux); } -// interrupt triggered function to eat data from RTos send queues and transmit it -void enqueuePayload() { +// interrupt triggered function to eat data from send queues and transmit it +void checkSendQueues() { MessageBuffer_t SendBuffer; #ifdef HAS_LORA @@ -98,7 +98,7 @@ void enqueuePayload() { } #endif -} // enqueuePayload +} // checkSendQueues void flushQueues() { #ifdef HAS_LORA diff --git a/src/senddata.h b/src/senddata.h index eba9d8bd..df7b53f5 100644 --- a/src/senddata.h +++ b/src/senddata.h @@ -4,7 +4,7 @@ void SendData(uint8_t port); void sendPayload(void); void SendCycleIRQ(void); -void enqueuePayload(void); +void checkSendQueues(void); void flushQueues(); #endif // _SENDDATA_H_ \ No newline at end of file diff --git a/src/statemachine.cpp b/src/statemachine.cpp new file mode 100644 index 00000000..9dfac811 --- /dev/null +++ b/src/statemachine.cpp @@ -0,0 +1,35 @@ +#include "statemachine.h" + +// Local logging tag +static const char TAG[] = "main"; + +void stateMachine(void *pvParameters) { + + configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check + + while (1) { + +#ifdef HAS_BUTTON + readButton(); +#endif + +#ifdef HAS_DISPLAY + updateDisplay(); +#endif + + // check wifi scan cycle and if due rotate channel + if (ChannelTimerIRQ) + switchWifiChannel(channel); + // check housekeeping cycle and if due do the work + if (HomeCycleIRQ) + doHousekeeping(); + // check send cycle and if due enqueue payload to send + if (SendCycleTimerIRQ) + sendPayload(); + // check send queues and process due payload to send + checkSendQueues(); + + // give yield to CPU + vTaskDelay(2 / portTICK_PERIOD_MS); + } +} \ No newline at end of file diff --git a/src/statemachine.h b/src/statemachine.h new file mode 100644 index 00000000..7390e5c9 --- /dev/null +++ b/src/statemachine.h @@ -0,0 +1,12 @@ +#ifndef _STATEMACHINE_H +#define _STATEMACHINE_H + +#include "globals.h" +#include "led.h" +#include "wifiscan.h" +#include "senddata.h" +#include "cyclic.h" + +void stateMachine(void *pvParameters); + +#endif From a411cac7fe2d5ee1a51cec3035df2e18bc8b4ab6 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 23 Sep 2018 15:07:00 +0200 Subject: [PATCH 062/105] ota.cpp: showing update progress --- src/ota.cpp | 121 ++++++++++++++++++++++++++++++++----------------- src/ota.h | 1 + src/update.cpp | 2 +- 3 files changed, 81 insertions(+), 43 deletions(-) diff --git a/src/ota.cpp b/src/ota.cpp index 863b61c7..c4f64c90 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -35,15 +35,36 @@ volatile bool isValidContentType = false; // Local logging tag static const char TAG[] = "main"; -void display(const uint8_t x, const uint8_t y, char* text) { +void display(const uint8_t row, std::string status, std::string msg) { #ifdef HAS_DISPLAY - u8x8.setCursor(x, y); - u8x8.print(text); + u8x8.setCursor(14, row); + u8x8.print((status.substr(0, 2)).c_str()); + if (!msg.empty()) { + u8x8.clearLine(7); + u8x8.setCursor(0, 7); + u8x8.print(msg.substr(0, 16).c_str()); + } #endif } +// callback function to show download progress while streaming data +void show_progress(size_t current, size_t size) { + char buf[17]; + snprintf(buf, 17, "%-9lu (%3lu%%)", current, current*100 / size); + display(4, "**", buf); +} + void start_ota_update() { +// turn on LED +#if (HAS_LED != NOT_A_PIN) +#ifdef LED_ACTIVE_LOW + digitalWrite(HAS_LED, LOW); +#else + digitalWrite(HAS_LED, HIGH); +#endif +#endif + #ifdef HAS_DISPLAY u8x8.begin(); u8x8.setFont(u8x8_font_chroma48medium8_r); @@ -51,67 +72,80 @@ void start_ota_update() { #ifdef DISPLAY_FLIP u8x8.setFlipMode(1); #endif - u8x8.draw2x2String(0, 0, "UPDATING"); - u8x8.setCursor(0, 3); - u8x8.print("Wifi connect ..\n"); - u8x8.print("Get Update? ..\n"); + u8x8.setInverseFont(1); + u8x8.print("SOFTWARE UPDATE \n"); + u8x8.setInverseFont(0); + u8x8.print("WiFi connect ..\n"); + u8x8.print("Has Update? ..\n"); u8x8.print("Downloading ..\n"); u8x8.print("Flashing ..\n"); u8x8.print("Rebooting .."); #endif ESP_LOGI(TAG, "Starting Wifi OTA update"); - display(14, 3, "**"); + display(1, "**", WIFI_SSID); WiFi.begin(WIFI_SSID, WIFI_PASS); int i = WIFI_MAX_TRY; + while (i--) { - ESP_LOGI(TAG, "trying to connect to %s", WIFI_SSID); + ESP_LOGI(TAG, "Trying to connect to %s", WIFI_SSID); if (WiFi.status() == WL_CONNECTED) break; vTaskDelay(5000 / portTICK_PERIOD_MS); } + if (i >= 0) { - ESP_LOGI(TAG, "connected to %s", WIFI_SSID); - display(14, 3, "OK"); - checkFirmwareUpdates(); // gets and flashes new firmware and restarts + ESP_LOGI(TAG, "Connected to %s", WIFI_SSID); + display(1, "OK", "WiFi connected"); + checkFirmwareUpdates(); // gets and flashes new firmware } else { - ESP_LOGI(TAG, "could not connect to %s, rebooting.", WIFI_SSID); - display(14, 3, " E"); + ESP_LOGI(TAG, "Could not connect to %s, rebooting.", WIFI_SSID); + display(1, " E", "no WiFi connect"); } - display(14, 7, "**"); - delay(5000); - ESP.restart(); // reached only if update was not successful or no wifi connect + display(5, "**", ""); // mark line rebooting + +// turn off LED +#if (HAS_LED != NOT_A_PIN) +#ifdef LED_ACTIVE_LOW + digitalWrite(HAS_LED, HIGH); +#else + digitalWrite(HAS_LED, LOW); +#endif +#endif + + vTaskDelay(5000 / portTICK_PERIOD_MS); + ESP.restart(); } // start_ota_update void checkFirmwareUpdates() { // Fetch the latest firmware version - ESP_LOGI(TAG, "OTA mode, checking latest firmware version on server..."); - display(14, 4, "**"); + ESP_LOGI(TAG, "Checking latest firmware version on server..."); + display(2, "**", "checking version"); const String latest = bintray.getLatestVersion(); if (latest.length() == 0) { ESP_LOGI( TAG, "Could not load info about the latest firmware. Rebooting to runmode."); - display(14, 4, " E"); + display(2, " E", "file not found"); return; } else if (version_compare(latest, cfg.version) <= 0) { ESP_LOGI(TAG, "Current firmware is up to date. Rebooting to runmode."); - display(14, 4, "NO"); + display(2, "NO", "no update found"); return; } ESP_LOGI(TAG, "New firmware version v%s available. Downloading...", latest.c_str()); - display(14, 4, "OK"); + display(2, "OK", ""); processOTAUpdate(latest); } -// A helper function to extract header value from header +// helper function to extract header value from header inline String getHeaderValue(String header, String headerName) { return header.substring(strlen(headerName.c_str())); } @@ -120,11 +154,13 @@ inline String getHeaderValue(String header, String headerName) { * OTA update processing */ void processOTAUpdate(const String &version) { - display(14, 5, "**"); + + char buf[17]; + display(3, "**", "requesting file"); String firmwarePath = bintray.getBinaryPath(version); if (!firmwarePath.endsWith(".bin")) { ESP_LOGI(TAG, "Unsupported binary format, OTA update cancelled."); - display(14, 5, " E"); + display(3, " E", "file type error"); return; } @@ -136,7 +172,7 @@ void processOTAUpdate(const String &version) { if (!client.connect(currentHost.c_str(), port)) { ESP_LOGI(TAG, "Cannot connect to %s", currentHost.c_str()); - display(14, 5, " E"); + display(3, " E", "connection lost"); return; } @@ -148,7 +184,7 @@ void processOTAUpdate(const String &version) { if (!client.connect(currentHost.c_str(), port)) { ESP_LOGI(TAG, "Redirect detected, but cannot connect to %s", currentHost.c_str()); - display(14, 5, " E"); + display(3, " E", "server error"); return; } } @@ -164,7 +200,7 @@ void processOTAUpdate(const String &version) { while (client.available() == 0) { if (millis() - timeout > RESPONSE_TIMEOUT_MS) { ESP_LOGI(TAG, "Client Timeout."); - display(14, 5, " E"); + display(3, " E", "client timeout"); client.stop(); return; } @@ -224,35 +260,38 @@ void processOTAUpdate(const String &version) { } } - display(14, 5, "OK"); + display(3, "OK", ""); // line download // check whether we have everything for OTA update if (contentLength && isValidContentType) { - size_t written; + size_t written, current, size; if (Update.begin(contentLength)) { + // register callback function for showing progress while streaming data + Update.onProgress(&show_progress); + int i = FLASH_MAX_TRY; while ((i--) && (written != contentLength)) { ESP_LOGI(TAG, - "Starting OTA update, attempt %d of %d. This will take some " + "Starting OTA update, attempt %u of %u. This will take some " "time to complete...", FLASH_MAX_TRY - i, FLASH_MAX_TRY); - display(14, 6, "**"); + display(4, "**", "writing..."); written = Update.writeStream(client); if (written == contentLength) { - ESP_LOGI(TAG, "Written %d bytes successfully", written); - display(14, 6, "**"); + ESP_LOGI(TAG, "Written %u bytes successfully", written); + snprintf(buf, 17, "%u kB Done!", (uint16_t)(written / 1024)); + display(4, "OK", buf); break; } else { ESP_LOGI(TAG, - "Written only %d of %d bytes, OTA update attempt cancelled.", + "Written only %u of %u bytes, OTA update attempt cancelled.", written, contentLength); - display(14, 6, " E"); } } @@ -262,33 +301,31 @@ void processOTAUpdate(const String &version) { ESP_LOGI( TAG, "OTA update completed. Rebooting to runmode with new version."); - display(14, 7, "OK"); client.stop(); return; } else { ESP_LOGI(TAG, "Something went wrong! OTA update hasn't been finished " "properly."); - display(14, 7, " E"); } } else { ESP_LOGI(TAG, "An error occurred. Error #: %d", Update.getError()); - display(14, 7, " E"); + snprintf(buf, 17, "Error #: %d", Update.getError()); + display(4, " E", buf); } } else { ESP_LOGI(TAG, "There isn't enough space to start OTA update"); - display(14, 7, " E"); + display(4, " E", "disk full"); client.flush(); } } else { ESP_LOGI(TAG, "There was no valid content in the response from the OTA server!"); - display(14, 7, " E"); + display(4, " E", "response error"); client.flush(); } ESP_LOGI(TAG, "OTA update failed. Rebooting to runmode with current version."); - display(14, 7, " E"); client.stop(); } diff --git a/src/ota.h b/src/ota.h index c2eb69c9..25ccbb11 100644 --- a/src/ota.h +++ b/src/ota.h @@ -13,5 +13,6 @@ void checkFirmwareUpdates(); void processOTAUpdate(const String &version); void start_ota_update(); int version_compare(const String v1, const String v2); +void show_progress(size_t current, size_t size); #endif // OTA_H diff --git a/src/update.cpp b/src/update.cpp index f69603cb..a43a9809 100644 --- a/src/update.cpp +++ b/src/update.cpp @@ -310,7 +310,7 @@ size_t UpdateClass::write(uint8_t *data, size_t len) { } size_t UpdateClass::writeStream(Stream &data) { - data.setTimeout(10000); + data.setTimeout(20000); size_t written = 0; size_t toRead = 0; if(hasError() || !isRunning()) From 99e0b4e85a0a6b41e040961be4205b109038f31d Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 23 Sep 2018 15:11:34 +0200 Subject: [PATCH 063/105] v1.5.3 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index f7f55f18..518aaf2b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.2 +release_version = 1.5.3 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 From 9dd7a6279d223897532679af753efb425e84f7db Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 23 Sep 2018 15:21:56 +0200 Subject: [PATCH 064/105] OTA.h -> ota.h --- src/cyclic.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 95185a98..b0c1a0b5 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -4,7 +4,7 @@ // Basic config #include "globals.h" #include "senddata.h" -#include "OTA.h" +#include "ota.h" // Local logging tag static const char TAG[] = "main"; From 5323f82bee587fdf0fa2794f3cd3cd8eca668f06 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 23 Sep 2018 16:45:54 +0200 Subject: [PATCH 065/105] code sanitization --- src/ota.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ota.h b/src/ota.h index 25ccbb11..bda9fbe1 100644 --- a/src/ota.h +++ b/src/ota.h @@ -5,7 +5,6 @@ #include "update.h" #include #include -//#include #include #include From d39e2bd8a89f39153438e22115e1d9a5dd04a242 Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Sun, 23 Sep 2018 17:34:26 +0200 Subject: [PATCH 066/105] OTA screenshot image uploaded --- img/OTA-Screenshot.png | Bin 0 -> 165813 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/OTA-Screenshot.png diff --git a/img/OTA-Screenshot.png b/img/OTA-Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..a81177470585ab362e2026e6c25200d4b7ee6515 GIT binary patch literal 165813 zcmeFYQ*fnS*EO7uZQFK-9d&G@W81dVF?Vd+wr$%sI_fz4%l+K%bNU~BZ`FVB9qg)g z)!wyh)wtGNbBsCW3|Ej7M}WnJ1pxs;kdzQn0s(>eT7u3(gMxs3gSVH3k6z7D37XMdcn9gvLAE2Sa$U3m)rWA2`DGfA#AI6I5Q4*3PTeyG4hiE6T-Y97jroBuaR$ zgSV^h`fYkVAJq!uaSGwf#`vnp5Yp826-!jI+eX@YSb3UR6l`eGe3$SB_PIF~$w=v{+8S$xeM-F!tI8wM)h%3qvSe*B^mO(P-IZhdcU!lS<_tal z7T?(!gekSg_+soBjWeES@%D0@h`jM~lhhDf`S&I1kYb(?4c4i86P^%V*64Zz6$ZF` zMaS3jqEF4TnsXIv8)wd(yf`MvmFBPxZO7;fuij`zODMIDH4nxY?@0H)$0|3Y`;G2U z1@~$^&ENa?8=qLnFOAat(W4mDTq_hSE&EP7`x>0w8wl1v$F#H#SvgrMuDrGpDy{19 z1==&12(>8t%JY$#h)wKw_AAvIxMTQy(pYP;xC8M^PY_nX|39^kL|kVczz^RpmbHC_D0+EY1!4INl?19Wn#+d#7J2;rykIr2ykV7=?dh^f!7^H-$Dd1Ln$MN#gQ&|M3;nvYl29V6;z&MCS@Sn=rbFs9LD6qkws=J0eUGO~_oWG=d36yr?X?iz)}YA{?JTC=&6juCaMC z1Z;B`J||tY7|#6KZ#K?~Xt|5e;!&v9ecZZkzg9Lx6yob>oIKHo|_gnfQ;shD!k zeQSyzcxxy8LX+Zy1*1UT@jlreZ$N7oLn_bUjR>1vywxu6Ol{O)U43uGI4Rcf2UFfd zV9&g$r8B1zd(1n%hE0slbL-8l#`|e>!#z<@Y!#ZI8 z<|pjh#$XI3ik_;ML#iPJR;$HiDE_b&dwkZyqnfT`bNLD@63j@GWKs`c4$FeJ%U`O& zcvo5~bU4EwRL^Qn-U5VA^cw!Nx!hrKhX;`{xvBO>Y~nGrddyM0Mc+juNQD%yX^x_*If4 zQK#tnqr)IxHLyu-SLe`cAlUK?Vx1o^h~S{dG;T^go*k_NX=BFS9z!|mYjVn-0FE*o zGV}{*PTRvmeuHC)eCOQn2LtCGNMZ-U-TT57R3<>(l#gQG_qq1HJ?pS5Z$aUKcJU+J{Ookd7#^`x|0wo-R09j_daW2z5<8WrAysT7l9891fh@uOo~+W@U&h9d$T0p@A(Ubs)r|4wmI(h+k?CS z@b5iuT4${jP9BPfi<8!g_&|i6sOG|_1h*!d^-;ZN?2pJ_VMR7O1JJZv+H#YH5xI-z z@F@_BlXU(auOu zkF^Bdu!`pWO}ZEleb+><%2zO`5d2-P?Knq1>SmKrqF7^M^8O7y>XqCLw>+k1!q+VF;#uEP2On}@qy`uDx}e{+LG zpEaPHK(_OkLbk4yG{p;>o>h`Q~sB8vN!cu_B(-lpsr#`QL0=zk7#-aPEw zNftXu@LEtB1zY#Ym}&gI^dh^>oFOW(6<)_rOuMD=d#}pVhwp4I*wX%y7TTox0|Ith zjP&;TC?o2I55CBL8k-0<_DBotA4;0UJwjS zSeYSTJ-LSn3Q%CiMHHUaU(0|AHQbY!UNENzJK58xCX6X0t#jr=g$q7N%fyZu>{x^7 zq7>{4&%_$TC+AB00~((tDY(3#-Umbo49jlkf1#1$j9F0mJ_|}$1x#r}!)gmtYtBmnnR8%Y;SR$UMh2E zJ6N;lg<1>dwr`gk)Q8E62)QM_00t7H-fyB))PF#kdu67Np`Vi8Xf2w)Awt)Pl?_Q` zbkDifSWT~!%k#~#zJ){B*ml$;#I~j73)2(M83&ngt#@4ZVmYE`F)5@OvgjF8tL+X7 zJbx5;y}<9q>8&19d-M>!HkbtddGP`GGwoBo=Pqw9-3XI{wRv)3fA8_A5TJQ`|4Q4} zaL=RmT(B^N-Pczt<-K~JYFrd^1EFbVn`x>g;2$VPJ5E3M<8~#W>7IMy!^Dr6vPSAV z?Y%nGx2hC!l{|F2i&O;}0z(ukq^47!lIm5AfH**kO;*|}6>@`)ywM0P%OO_V+! zXz9h}5u&#`fkY(F5vh}if1n`c%*|}T%bX+Na&H<HgY66oYs)yL z(+x0$u`D#_V*MZLqQET_V~~|DGmILp!U5)+EJqO^SLjTtI*O4y;LqC7X8j{!!uWfc zGe?5tkU?33ER{0z_nfK2hBHHctx3~P2y9j4RMMG4ZrYmghktScO9nYKDfaL0O)1*X zv8RWtJa$m;bLQ~p)arD7rkqdPT;xp)u6BW#QhKMoX#~*0{q}I?ocOSPd;h)ojXw1P z{x_NX|EetbU;Xn22vpwgGI>H;8y@X+*6Kr-VO2`RRWvB+AeROW_6&+H!wL#zMjXR2 z^oe$3i@>t*5ZY7PlCVljCscWhSriRbeHB2n%G@$}SbW}3p7x9x`)7V-Sy}VMDbt~H z9@8#k_KKoz%{I;Eaf*sduB3QXhLrMI2apK|>kSUs1xI=?&E96`k$S0QMaHydn3-8d zEwLq*ie?yji!ZV!(fl)zB4$}-Vf9SJY-NFd!uitj$UjF7=`-9$BZz8}nx|+UaU`wW zz&xbda<)Hm(vm_nnL)P2WndnDgjuRsF@!m){op4Xom>IDfw0*ZB{JB!MKDNlnupu$ zy|;C@rK|16&^UZmYZ-m9akR+r20@Y50si1@UZu!!RQ^&OSb!Ud6n9(z-uj?rN+-h5 z3Goi>N>8gOZ_Mf%W81XscKujO-=o07bQSj21Xcn=oWAV-m9QVh!&TLTpGGUuOU_SE zVD|fNK*$>UQ%|MC99+1|D2GjWid87EpCcUEPYg8u!;HX$vDRQlLQgzi$grBc<%xj>qfld7mtry6m7fm?*KRq3 zJ;0IGD;r=t$8G6_*BnKTZNf}P_sG`s71`mX+U17)%Sm&aMNV;d^N(k0Qliin(TyHV zKy2F@M8;QQ2{x9dPBlx~nx@dTKx67iH-#c{4W|+t{k5LY(zktVZzycF`G92IKp$r5 zQBsrfBa^k_P8aI|;l>y{*<@9^WEcXw~RuuP#KO9VVtq6z%G{i5DnOB50!fEM9R%tM-(qyP?&23*Xx8 zddO(OIguXs{dL!oHGrq==s}W_01F|>@){NjXxkdQ`?IGah`%+375@c%b!q}GkG~j% zV4?_j6LwN&y6rOfHZh@7#Zo}G9#9t`D>`o?8JfH|H48@Fi3`&SAI`*+RIYqg!I6<9 z=2wNZZrTL-&GPRL46mN!Yr;y5Ls2P7vbz|Y+1&qg| zThslx`+cVwgb!$4BZIlmpeCo?4RU5=p%$_O)%a1GOgr?KRwKzViVv^!yv5rjJZWgL zr9W={l5r$!s4$Gj*y+U)qOh9jAwXqxhCi?~bof-^f&C5jqgKRvqtw3<%TyUw+1T)2 zXCpdctLS+mj8{D+XETxxOc%R-y*@6!P)Q(QgBO84Q%O{UO zm0l2t+79ObITbLxhn>1kV(rqReuWEx6?H@p&0H+bzs2t6T{XF0W*gwK0B>K^`P zMb$Ba_1VQdX(2J><%9JoZyOC`OV!0GkiQB}MV|n8{1iGXD?&*%+TXt}t~jaT0swZy z)!0kf4(w(mtT0vFj$deY9U%3F^_-b1DU8C#pd%q%m9YjMQjK&CAC=LY#r2Y5upCnW z>jmgvj{|!L`z_0z9xb+L|JImy+oGWEKX9^^oOb8%<7fuMN-G{R7OoGtnq)T5;2hJE zuehouDH+o2i(s%Jl$a|jaDva z&h?A#LrPW+%BxE{HLZpto!m)rObq8+AX|Mgcz-$8JsNCO_=7riQrxJY0{AiFq=5eb z3krjo8pu|QrMI5VPC1up;%AdikGCw?vp-2Aqo_cx%xBPSu#_k{;w8Pz=c_%+O3whD z2@eB_;uO`3K(wD2N;@KJr82U1vZ-8wD2*0_`bg=roM3W7lqk6uw{vrURGf(mL#4dj zT_!_fbtM(tn7RpnPPF=`)zP^AYbRAc5}8AZGPcy=h~E@F zLcDyFYFGL6b7P#Dc?1)tI&?CM;JAVaj#zZ|WB7g1&9U#Nn8_dLW2_qEQ2EL17Q$uQ zf8vZj2?jjq-8m8S)w^QD!OpbtpD0g zREh?DC`-M)%c#;PW8%CLk*}(F` zgE|FE26 z4gfL~F1i|U$!@-EYC7TVB-l(~{}vjmEgoI=cfNG_9I7r~Pyebih|ZlLbDy9c6=r;x zb>=6zk)E{3c3p1FF6-lTE~=6z8M(*M_Lp0-eZ~~sl1mD?@=+mzgMBIxBf~zEWua5{ z844!a(}u{MWNz{?c%7I*k5V@>lb}rXYT$w@U6*k~3@PzvP5uV`2SEq;fVvV)th~OR z&PqZT9Y(F(2<>A^W1U744<=Sup2)K6v^}a2wZ2FQgzRmcRS#+18X>AI2R#YciL%+)_CObf98k!^Z#Tlt=`f_Cdn zOXk5j6c0dF`{M_JnkiM}xPfm%WVoDF3jNXM(}bDWyZ8wGDoZ$5=tCdbt*-v zbm;dGqnM2^^ijvVYN-jbW_bx=!+F7Faq7*fp}CGY;RPP;#Qn6HFHnS)6KsOW{5R+b zx-$J5xSuC&j>I5o*O@H_D8~qavmIpH@6+JeFc3joFnjh~=&*udMJU$&)W%qJ*bNl_ zYbL<|ZMKgsgA@c2)%U=x-uwT*WkVCRIwrL+hK)|0OPlo>@~i_qM*ev!^EI~S5UX_Z z9=3e~!(OLZV`ycJJR~yYS`F64h@xLM1=g0TS~V$?=|(@H`dOCd2A)g2k9r9j(yS>{ z_8_0oAiL=i2uG0!dpVyBEWPTcJ_ZAVR$sTZeTee>{PNm}R#sDNuE}C~M6kIe2;EXy zy;Hf|Yq{(}Hl^|T)v@`*t~O$C1Qc-GQrVnqL4rP@qz(`dMu^6u1(Z_l*#xesZBL?Ok{E+uMi)?4dcmu?3vD1e{CRJ|#@%=jICw3`8>BE17R444$DK-T~*$dKgsw z&n$O?1w&(cbIk&hJGba29d?OS1fvRzrw01`NLrEps^O~S-FHhDW6{+>_Cvomg6D#a z9wKgK#HyuND8%m37Q`37n^)lJ>yGo3kr|$;?AEc!UDxvS4ZTxld&9tOu$I=4IG{SM z*mf+Qb&pCOdn!vsHB@%PnSLPo!Srwalf-Z5CS$zjf(?oSmFCOxgC>lPvZV1X{>Esf zHVsBjsacw(i6Yyc8l^HG#Ku}~g$hA<_H*GTN53^fz9_#4QhWR<$Bi5*8LNc_@%j}K zLzr>BJ46Sf2yY;QAw#{L@U7N*HwD8EYRc!F^q~>MU7Om&CFf`;ve&@KU{|#>Twpga z)Gvn5Ka5IYSMacBN;c=Ogn?$prvE4w%D1cUVd^KZ9K=AhF!w%$_xG$-8wQ{<;|uv6 z-+73~Q3>_x`>bxrsB|sfbPVpu2de|rJ<3mcW%Oxs z9JAs_&ST-76{u4q;*i&r^qv3P6P|f)JgeEXSvytFm{H79YSEmxUaT7k5NqmDyGe0< z`4*r(V7XW!yHW06Tj5%CtdeJ=V)^kXR-hDoTCtR&YUh+YcUiLxWr+ZN3cuWSTBAnN z{;_9jRH}wk%2g10D{pXW$e%G$df+{wdDp~noUCU3O=@QuwR-hbFJ$zM^X0gCjKWqw z;6g8~e#Q2V!5YP_V@^x6@5t8IfTy!HD{bSB!4A-%74U}aARno|0L%2V&f;oc^=k8x zVgFl-J2J{sQ@GdqU>7y76}ejloR1?8=q*}>&ORf0Yg1Bt(N)d@rCRgY5M$jY!i+5% zowgwasaj+B(N!;LVkbh8Sx0wLwciVL-C}SuN5mrv5yG`a=kJmY-2=DQFQD4A4XPkV? z$joLTZE|sHtuEqbbR}O8oR8?d>JWO<=Pd1E&jbYkue~BmCxT59N-KnV%>;VZzDwmo ztYQAN=Fx>s`=*9H4=Em!Pk3-Z99iBU3Q-Qo=CZzE z(Qc}D2Xz$_t!9zw{@gg5$t#dpy6#K1J8 zhOWnp%5R^q+oU@8;U!ya+3i;-kr;$Wm^ZeGobPD+ImK~YMwkb&z@U1^;f>h2B;CF= z!1AHaQQ(C>ehGHwg16?-Nrq%(HnGtb7KPIX7SDWl# z6oUBw#Zq}vv5kJD9)Ds%R0?3M0e)mrPwkZ1`~6Q&VL9b0Wn*A&dHk3Y>r8pNCAaGo z+c;=NZ`deZxwUgag|U1utIBvt?^ycrsi!0W9!F@=d5M+HE&$;hE+bEptvfCwXRb}+ z#qP{(V4(n>&r&fb6uK@11&{0q?pI(-@ux@@DTTYM@Lj77RXZN(c#Q2?@?fOv?U~@o zsfqUGa>?$x#H>V42`9ib|BReMv-Q_Xx{{B7_-49!yE&)~@1@TA8abdRd*unRhL;i+ zhyJUQ2V~a0o;cr_KZu#AXCF!vLGWYXd~jLHrVoogl#HE=?xGPaqNdUKGN-)x#ob)! zVADx6W?386M;Dd@1h8_0BtPQCQveJ_hM_67(Y@7{^F>$}J37gKfIAuPi*-U6rElWP1deyI*FAa*i@OgAZ@ZR7+ zkWLauI>-SPPm!S{+f(?%LazPTvfC)Xe;{XQ2a)2`5V>QLc@@sX@p9aPW|3UH$800W z44Zi9g;BTcItF1o$i>Ecu=PyhRNCd2540lNDX}#87;L|O7xb^Vq*YoEh z=}IGLjn{?!&K6@^`&CE7l3PE5y&?8LhmSHVN~|EyzR__>Z{A7Am1F*~SUXz^DVuyu zRq~?X(PnMFxl>^p)~D@`(4u-ACg$%^4xAI9kWIvgx%(q{vej<3ctIOek469-eA;N5}Rji z)e3c23|{VA^Kzs-v&_jcA}L4#5=Va~^5;b8kF#tS z9p1zXZtbbztwQGpdc6soF~2nofr=hMPHWRxscI6fJg*%bUCBXL3cYl1i8cxH2E*GW4T=5vYVh#RXopgjS`uqw^Thrt(*Sv}DKQb=K740u-Cw-Ap*9Y;Y<%bI zUJZICw<68urZAs}AN`h0y=iHh@DEgcZ$m!r1@1Z}D}`~4R=gDrJ*afo#RaUm>5|fP zX7p{J8y)u&+WBa-_)^oG>ztRM(2Y#*yxA0^sOkacw7AltWRm6ub28+8Bl~F*)ZaKR zVA#_`K+Xc!0^>=3luwKF-%5~wiwF)AG5oT{@ngiu#?-+>3=2X-UU;n;K#xm#2hgM7 zmP3g|`XD5_K57g-%w`UQ1NnX4Tf*3w4nHlqC)e;Ew%=xI&{1{z)jJJp)95($~ z2SlLu1q%R?jPF0P3#xV0pni|+PhmprjNS)0vyr_Ow++hTDIdsF=iD+oL|$|>F-WoR z%ZH(ckk*jt(tlq-Zo{3cr1{kMQOQQvi&pPW-hP60%)Zczb%ZEVKt~(%W=oC`od?*X zHsW46_ve8()|Hm%##xafR%e8c7DmpxLcj|_(8Y-CA7|C7k19b>qQYN@f6(Gb1P|;f zP=CGh)R}>0Qhfl!Jew-_5uzsvEu#<`qwkLfaGFed>2zZ1#~S(jEjU6-%#1ft!LE^Q zbYF1H-}`@2<={#?GmS~YE4fyP@#i&os4--35 zQ%+7`BLKRM!$7q=!e5H$(rFw_`YJ(9_D{k^=pAs&*-=QqI~dkcZ;FkWuscHIkepAV4v_`Ojz zw$p^zNskJ~Ufj0D7$(x!lW@G}GlT0uL{qPOI!u5D51G^2R6yB;v^P&hjw&pem7U)}PM0KU6S2wmtJVcr$u<4Gho z>t5Yjn%x*S!Zl+&IkMYJ`^&@{1b=!i;-B({k#5x7c+e_S)vqwI8}9285FcxME4q7# z1*@sDg+(n=-RmLf%-$wWU^NGxuu1joDw>!aq=K~9A@3c=2aW;1Q2ngJ$Ni1i!%fHz z;{oReSQimv3ZT+X@I$P@?*efb0;sh`x8Hh@l4tpepx20?e+LSdM)bvZah%5XOY{=K zLKuA$z$Sb)1AU$KWW<#m>8~Y}6Wr0l_DYv=!y29yB7*FbILMM)aW}w!wNG$+nl@eq z@42eZ>EZy$Q}J>_;s`rO?p=fO;7n2eT&go>1%z;5MhAns!+hL)z2Q|A2=@hLQN|`n zh-=Y@!WvxVZd}daN;o1+^|WFSqiDCi@e0l?eyeY^+#f~r&+DDbvkyJV=?Se_GgFKr z<3fcRK|w2>u{mQ$ zm6Ye`_9tW8&ThU}4ww7P%MvA-`BF>a&Ws)du-v+<$q_$~F|O`f8rRWdf?c@i?Bhq_ zocW)Sj$U`_R0zxUk(ucW1-l7QyrOro9^Qm~iA1n9XKMwJVIceW^hxm{v1CI9$MU$S zYtbBP)y(s9gZrE4ZbT}vA%`H*h^?9D^@$8WTjiO96KCcbGtW6N zhCi}@&-zb2QvXR;T;`pN9x=mqNZ5(tGCCYl#0OQ&FWSiJ{-Ep)J*ckJtnr#gF3 zh!Fu!P{=^upZ^l^2}+{q->Bj099m(X7+Ud6cFLnzj%kKJCWiNKzghz%ix$$nEynB4 z8FwJ054j|_U$o}`>V4Tje;17!AYw49gsOYb!SJbI@#!5zecw?oXGuC)B&lUhp#B!T z{goX?OA{&X{bd-g!kcD7*Wqa(>kl<%;u!3mOpET6W=)cE)7X~z1lP+ZJjaX}G$fQw zd#GOiQ`w_E?w}NuKCjKt^?~`5CT3bx$k$%qkN6bnxn5%&sluWkOEPjB2#jeN4#DY@ zA&-ZEW3L91qZ{cXFK^s7BGau33Y5|ij^Wg05<)g0gm{0(#gcp1B0`ZxJNdGLPz5Sb zgW8N-1>L%noIWv%6fJni*ioXDsnDkETb99NMT#{fV#15pXA;6eG(gZW$p41ubQA9p zK!qLX`Z>9$p#Q+koD*6l+KcvD*r6oN6HGJA|J#x8hU=Il=Oy2vpUpL2}s!tCp ztzn)&r$CPXTNXRCf$(8~1eq$ae|z*;(A zxkzK2Y`u!w(OE>F6)8m1w@%hSuf1>*M+^8_c>h(xkl!i$K!^1`vFY{Ui$nk4^8exR z|8)pbtAu*JWNf1BPL)HXl|!0eQ>nCkb>GL*$>TaxB5@x6LMvVj~-9&vx3&i0&@4+NdI#aym!$ zQX+HNLeXu0p)|CNi&0Ty`LvSNZnP+^R%N_2LD@K!zVM6gX=NhXvdkE7DMFcXZOO4s zseXb2E?=p6qFJYop|#6+!(`;EyML^X?oW=5M)DaKW>i}7(U$FbvWZr4l|{OdX0yd= zvaV|zqfLP}>qHFab*ksu9pnTicxN`H$X698{1-)X$=$zH1_PAed_^$Lh+dWh z{oD2r)e-u%bR%q_rHgLw)sXV^yO3Q-J~xe|R}s^Jq6G>8YpoYmlN`?-jFjm9_F1kb>Q0 zL(rk^I^M2mlgTTs3@^LntePM?rr>glosMX59+FquZYj$Pyq!M`!jRty)XA@Y`Q*%GI)2$1T%zs4V5OyL>^94ry^BDB5z0MR2}A{7!z% zOesx<;}13Llv9FRes5&;4Fo5D%TdM(=}L=mq@p7rLp|)BsXZLxYVjQfSU~nq`CRnS zFHd&+AYMLqH50S0_Q(s-RU>7aTyT>Qw=$oMqbMQ6baN$J=m}&2RvAH|raOk=4 zo0gpvt!t~kVM*~u{c&}g*?dEj;=LWcb3SZq_m*S(c0&4B?NBQ)%xv>v3NN9hIltkn zO{h6GY_Qm}leVNpc849I^U|!klc#cZ0qiphh?WRRsy96^C&4)w1{wy@j;H5u z6oVPyE*IuhQSG=*>G9g0e_0+T4Jxb#RTcqJbj`{xQd`Yxv?~)!Vr8HaRRPY@a(Yek zzslI8icH(!3BvxsX!9no1v#pDiBrFhk8K;v($YsEXonYfbEdW%#p!dgwQG6kH72{oyRQ{FdN1G~dRHYpcwe<^ z_letn?L(AFko~i71i6i{sJmhNiJWw~W$pViWX-mOt-rX{+e*N(&fTqSefbv(cL>=3Th{g26d@Am7g@BFS}0Fur~C63oes{25aC69%V7ww1FTuXDC`0Vbk zNJFRWE=}UgQAScu=OqyOC~Hdwp`3qY*2V^ygWBW9j)TAUI~_6qa&aJ*Y+m#nB34tZ z?aS-iQ_Sqjc5$u{3vD?$)}7Z^;(&b{@3k zs5gfD&xJ6cFhqLg*DE_|{ifD*fYnK$yZo1LHD>epmCzx9s+uB&40u4C%JG68G4nNJ zv)OA@?G6_)ZGB{Gaa8Tj!|e3C`&{`B>8jr?8*x%>PHSTjGEpaBw1dwiex_N_J^R-7 z=Vy+e6mOJ}8|mGW>!%yh>oM7m@+i_t;2W}ecBg~UWjc~K+}5cQx+f@#eC{dE3~Fps zc*%OO22ngx6WCMBn2XKM0mEG%=F2EF!!HBH@Bh{Y>VB(HWEZJw+;@faV}0Z~TnG7_ z2q?aNY687%)Em#gz3hxe9$p3%Ef)0WNhgZMFCqgj?h+bQGt674d+6LzcMM~?ZD0SoP)RbHJQiiuxoS!I9Ev(5&>k!`h zXYkeT9mj1hUfKfa+^{VHNpeAdQqEzu-J7jdUtpShFU7SL+LODD<*^trlcNqYpva5N zttEWP_U>5ijw7XYfFoL74{z`A{tXG%;FF?-Mm4~|~EA1kFLdUR7no)Q#9#d7H zoPO6Y@+*&%N;}Y3(NMn7lj@nHT&y2PGQLF6-7PtG$!{HX4DM(SIx*~@q3j=Vyom7t z!Ev$EmF%MMQa^tg+C;h+C*@q9Cyh0$HbvK0r;c-o|7aoMBEwF%Ij{9)6#S-TV+{wI z%;a9U-IJA<(wiwez_@YLu3V8~$zB3`ZliXgg9eG?mc%=2j<|?78yI2$I_i$hVbxTP z5qonJ>e>2I1CvU7s1)AVy4A86#58{yz9>9OFUwr*9(u5+Ay1lZ_omkPZlw@HOI5{p zej+ewC97*;ucQ%&7thhlj<{Uy9-Hwj+tHTb`Y?5Vv%PTc#WEGtS|L#Wxv$#wm*Q>l z=)H*uhiY;Guj9MkD~;Ed_`~d;no-K9wC;-CvjWXcX*!yrllBEx4V)u)3Rbk||Gl6b#WsaU`&Nk5yXmN@%tjmfGKl z{85Jsrt3xglJ)MoCn9sr6XS~sg1yOeB`J_uuN?bTWZ?*<%lHCZSHk< z$g4@-!cmjB3fi;t7%G$};?rKqZ$g-CW{eS)Vj^%agtozIq+T1s9Y+phj69f&UF^(z z-Ii$i8W#RkULP@d!X*`lb5ojKijTC1wiKI42MRGMz#HMM$pO7V_ywRj*M`K?v7lU>>OR!pr z;vSC_?vsT67=gcf@1B<{H^-1Ma!kVgV-@;G^c!g5X4;OfTAE?7^9ts=bobFo!w%mu zyoh}t^$Lyb4!DvIk_-bT%+9ovO>rbu88n)?rVFb*Ht6LMyF*dEfRlc)7h1uCc|M}L z=)rzxQar~%!wY-;7{fBs5r;A!#htPR>7;skntbY8{1WPP&8;+d=fTBF$1=q-w$~bW zRlR6^3t1+iXri5q-ue~}^B)6!CCDiy=#rrd7zt(=r$0>)s(QzULD9WIAg-U*PrD?l zZVG82O9|MtU1^h)n)cn{&Z?t0-F{9YXPeWhGL2_Pb!22`ljPDeZukJx!A=_5+q51ObRyl0Br7eDbXwIE#|PoU4iFS+0{oLEfHL&9-y5mROhhZnJcU)x&mYvh zt1~h~jSFI1vHFxcfh~52lsP0>Y&kEtug)J~@1%n`33Sa>ipeUON!56}j9J4?ygU4#** zdUDH^9EZI{lWzneUT&vSr=lJAyCNynF)L1c3eohkY>$l)l1mQ z(hl5f*W4>w52u}rM7obAF}*mQfP06IEP z4IDo<1{Iv`c<`*c;$vDblRlCnQ$Q1(=+NdP{T%*?EVuMxh>20$GUY8=H-)WNd@g6C zH<fSR%*3gAZS z7do3cgl-WHNy>5RSP9D`A1#%Pa+JZN)&0@IiPO3QV@3(=(7%=l?3X2i4~?ys>I^WX z#?_3grlr%5M`gl-Wgd*kue_jKtR~i!WF=R`au)?4$UsL zFi@CbDSdL zpN@37y>kmovN^zW6ZtL%e%*>L>E$DkMT1GhpJPOtWPeiF-R6>ga5NT zC^f-uX6LoceS&J;PItoWUcdieJwt)0O?}ufOaKC(3pd(y!Yj^+AH(lS zZdZr=+U46~Z#NdQEZulC(Q8W6GZAmb%ssDNeWfln^h0=LgYpbr@?V)s;l5DC;q?@E z7?^PohgiI~t+%ry+x1i_dgpMH4Ay8PlPMko^tN*1U*3MghmI@VhZwrm!!Ng!Rkoz6 zlg6da3-v)$0l4l9dV6&YUhE!N{Cer{;+3;1)A9e+YX>LkB;`RF=E*~9d#;jSB z|7F>!7hLunNz+(&_#;@oPJ9~-{rhQo+##6iAtf}YaG)iBIzj$U%;Qf;>i6sS#*xfyFU>aoW{ws~5fs-jG*bxjf_7_1y+t|FJ_*n}|cZ{Mmb zpE|IuB=mE7Y_&LcSfJI*QWbFcyUTf#`lqU`R*Ct@?+n(4GAmr9U$k!Q;}T`*42d_a z)utPk;iYt<*45b{QhRQh(K~%_QAm(hN$H!mVmkN`a~ovZjyAG4I7)@)9lIY!lj+_v zhX<#UosR$$BAS!d;2j{U9jt_7azTxQ(LFF&=rtrZJSnV);MSXF`bmpwGkJ`4O$OyR z>^Z}sMwl&>F?>+CJ4f0mMZU_JRf}ksD9+@(9KuWNE5F+j{sy;>C7DwU6HJAPlG7{FLRDRtsF^qyi}!mr6Fq z?|9}Nt+2Zy8hQrTEo@_6+Vz-J!6F3)w=3LNows1QC$-i{&+)8)r0QDaG_?gkrn?%! zIjFcwt_aQgbRKxNk%Uo-8>@s0?Hwwz!-kAe_S&;o6jt_y&z0_?M7iR?zMrg9hj!v^ z8;YKHoa_~`Y4(oWndd50=vRDa-2JqvOR+XL2I*ntnV!E1$ADyJSoSiBZ*m_;oZuN{>xMi0^!K>0aTqQVT zBgk=PjL^8_tX6(=wlE{2#P+>Vq=lI|rU{Xz%N98zr=ep+4;Ac5ux4Zbr-FBZ%$zi1 zvH)N48+eGyNoMFOy$VFVn!WF3Ai3<4r&1&PSUk!lzhh{zry{+!Y%tUdaiG+>hss__ zv|}RaY3XZON9YR7Ed?t$)+qBLfO|O8xHiLY)%qUfzo*s;qqfKlot)hR^<~5yb~dZ6 z^*v#U@cIva`Ve%;ttn^e$m8O+=?OZ0+7>NUS`z=oq?0ZvMuvP~iX;MXHyH z^bPk?qqr|eGyZ%XyLT^3d_BX1 z@S>8mlKQ?iYlyDxkM85ob3)@7to8bMQOSzm0oGS%h8tQGfmqi$1bakk(_g>Y14_eL zK&~9h-fZ`c7T?9FolIrczPTYegx^_z;eOkl12z21vo&W!iKK`hli^L4Ay>-XcBe;) z9p1hCS73Y<8v+;#mBT?9f_-E6M;L;ea$12k3p<*OQ37h$0=Y_Okz1isa(-n$L^^{P z!I&atfvJk2N{Q^=w+)@86Fb@xm79kG$9bbficj(I^i_GASG1VF&mVcV;r4Wn&coJ} zn=Au<1%+^{MbW!P3izg_HiR?YKu%~D)MY#^kTsCnxCz=wC%O% zATv8GK(9Ia5RpL98eiz4+Vf({p!)8I)$+shfa}UqkdLOcJ%HSqH@_ z`Jh^o)#5BhvHLZg_DVD_6K)A_dSft0&#;5OCIL5P)1mScTHQlemv?Hmp*Ls(A7~C| zP-WvJQ3?SKKbt#whCKD8iY$2kGLJi87;0p+HY5pni_wZw8CobON=1l*O`q8{Xd?%e9Pi@%}FXp--48R9Y=B_P|ID^p(cyQ@~HCi zffL#_R$x6v1LfcoxOw2a`S5zz=~c?J^Lso-3aS1mFnC&-*3`9FTxjq5 z|FCzKPjLlbgHF%{3GNy^cyJ2@clY4#?(S~E9fG^NySux)4h%BDAUnTz<^3Oaw`xAz zsyiQU-RYk0KIb{lnbYUT(F*aW>>iyR(x%eWifiY^yqvB({xYuEB;ZEJ@-sTzH%ua zx5TlXmaKp`sfV&7tWgjj@K;cIB1%QdxGK2kE3OFGZ-_cKSlGaRSAphCYmMsK4s0im zN&bvepuntSyyRZjul-@szpYcB0etWI@wN}zWGrcb*NNXg$fw`Y=Ze~^da_Pkp%oQ7 zsd*>$yQ2~#PP*@#ZrA!np)i9VMh$u3=m7&N$2$7n9}+vU`bt(=WFh7MOP+jdBsGIFs>>^l?ThN>BKu0SzzDRK)6 zr$VwDsumPT^)g{l4yVDwU&E(?T*!1M?nsw7ix*#Um*?xu;hYH&L=A6MC=l{Puw3}i zBSpz;t7DBme{5J|L8Sp7OqQBVFV4o+~joiv_er?)#rP?3)A2a0tvFN%QDY*(L27823z;@mj0?SUu3HD}buk?6EIbX_1(w%w2E>>K66N^SU$1~AFzkH*{ zY@kmzOv` zoWf55z1Q*WY53gq&OeT52zX2JfyaPxxsF-<(#juws;7Pw`ZSc12}myxqGN{+LVEw^ z%ALY&c!0)88n6%TJbC7JD(U1;Hi$uT#up#AWY3OY5Y1GgHgh6lQk@^ElsLkQTxtNi z8^j?hi)N~yhnUd7EBRg0K|Jx>|HKUokqFiPL3#^}2~OJNtGpaWf{&|;qz{SSK7CXO zIWjN4y=L0|;eIN{DS9!uHQGaX*J<8f;v#Sq?rNiYqge#psct$1 z5RaKhYTI9T-gOrR(3zibC54bLrFYJc%}CDzPn?>d$2zp~j`XL?UcHle4P2dVc{5Gu z5EZiU%yjO+hrQ}$I@2dJp0r8-ehhzM68bl+L7!?t_?tmIf(^@L7{~P)t>HtpPAI*j z3+t!{QzO6-t>y(8wlA1>C1qgDoedf4+MFvh+Dp`qQ93ArHp8w z_w8cd+ap(jLYOla^Bf+=$KY>o0#`|CXeNm>1v#B{GvA@^ISajl`I%0=KG z3l1s-=r9CmcusH&&XI3jH_pMOWtvZh(s4$ZC&{CyfR2)f+~K=E3@jk73U02VbK=-SI0Th?2z{o5diooAMDxgbCH@lj28F zkfGIz+{_@+{34tZ85m1*EBkucc&CWE*izTYi5@D1gWcO^KiaW7`9+R}ArKVkY48=< z4L~2)WyqC0N@SEjPUyP zIma4D&BvU_=TIlS)hkhZo3RH0tz!M~O+xMJR5)Dw2;c3yf*nY`W(bNpr#mgvk*vu$ z(cF&2V+y=2apPdj9d!SE5#B@t#4J~##!~l`FK~QfvZ;vkt~Hs)gZwBKD6NADYA+Ur z&irDg7O_BD5E-A-m7bQ6avOypDycp|$(b1j>e~}}pT4`wWPrw}B%<(seHz?bTLckd zLHV%RVWF}q^@Q*GwO#?`yz@2&7)j4-S{<3Q0jG?4oba1T_xzGT>o%|t{iz%F(wYj= z@@ktgdT~I&$iW;;mW9&SSDn-Unhh9m*6t%GtQJ0$QrW|2ZE0NLaUY(RdYV@CrQT0)kSG(_;~r)tYHyFqJOhL};l2@w{eV-R>_x}_ z^cnoNO)aH><@VAo2VZ``yvo{`Q{I;tR43<-4_5+<=Gue+>!d!(4CRA+>HDj|%Zlq0 z$MqI$KHb(7KHQuTMW-H-hk8{f|Kfmy9R^#L)EmjK5{zeOmQ+a-jGut8;5r)mL8Jfq ze@p~Ld=HZ)Rs2vh^=112Sn=0%>4;z(kx4jE|Dt{Eb=fAuSS}uUqH-MkroD4f*&R^! z-~_;6@*S&%7o0pNqG|Xtw*GV;Vx=jjzSOQmtlx8gu+PV4a4aUdXf9Q3(WH>X-t>L5 zKK}g?A+|2aF8}p`q<+I!x*>DjKz)3HKg0grC(?4o?j_UlAncC`fliagpS1hL4p>DK z+_l#8>|qxGHW0H^eOKf&enx${Q`GW^~qP2deLX|j%g&-P=UFb6dVj#Yq5zXRJ zXx}?H{S}@YRtnsncd;v}02?opo@lFdwx-sdMi@W3t$QF&>GBly_7p7 z^!?GcicIf_n3SU%^qiOXM}*J7+0<$~Oeg*N2M#CJ{qX{2 zxrdwKG|tJ(dxV6RdBu0Ob>F4bBJa;P%HVPC3i7$6*CPN<4VW48N1)&NO4o_@gBX1Z z1r+zoIw+xuT>t9I=LZWRz_(r6*v2o9DLeGYApm@(H8i#(LD3-G+X z;qLpQO%QRoIB+>`rO5Z5;+aOO7;!)1PH92mJ;9}oFLUSW74o`^5JCt9zHWz*sdwdm z!ifnG2vYaLm-F@Wc^|QIf~9g<&{O(B+e&PLprlU}Y4&uQ?@E!ynA1f&9$Qe7la%xN za4JgzbN$Lj_(hy-ZW;xt3!oSZ)#r`Rd($(Zw%@D9fhS907~&nU>93J7ReX1LW^-IP zw8;k}ePI~?_%0=$^V4>_bn;0B;!2d^V`)VHdbhdT&!NQ|Eb#WNgN@J^PO^bt1?9EM zW$>td?dDfheUyUl*KsVVLCYi!|5Gf9L*!c=0-C(xdMO}UQX6y;&iCQ0Q3XC7V{SO@ zRQMo<Dha~f#zNGzCxsT>&CqY-i3tW&W8IkVX5+yvXP z19u{RKX$K$YsV&7DbVMf_|{TcUS|;T|M_)BwHg8_a;1l<0R07#7uv(XAW5G(6Z%Ep zL%?J%`=5zYREBFR-kWu)rU_@-Jaov{kT7NTmb?rldb$ceS?1%rMsWKC=>Dol5DQQ4 zOx^7j`$A^?WKBB&I>c!zo6;;(ySa3fN2hRUOuX*kTOJrRbL6P)qz6He^89kGIZ{iO zoC&d`eiA!tZAa8MUud_F^)n*WIzxi)n&wfnN<)Dl6nqyQ?Vde}= zi1*l2>bq(E=y5v_a)QErXjfeDn=#uM9C`ZhlhEquE|iSd&m>=|(!vi{8~F&M5BqW< zX17NDp?V%%BifM=78aESh2KGk!hIBiFL^j};eEIpRh0&^+g$$alM$_uqb0!(6WMiuZO3@k94R2AP5Kvv#}2 zs0*bOR(&?W{s=Q?@2s;I(YUeUUzq50Uvg@~I(_jpJi+1Cp3Yq{ z#xcM)r@WzslG{D?RdkSN`^`ZneH%?(XiEb{?vOM%_m44W+(|`OKZ-Ef2m!BV#=S=( zu;t&Nvdnksqj^$MJBKXUfV%<0v|c6d_#aZ%im4ttz4$ekJMmP4W}a>X8{TRb zlU9~IgU8gE_JSq)q8e*9!Ya*|)OQa>X(RTsQUo?>lQQo}22+)svzk(2q80*O%eT(aUpm9|Cj#XdIcljI&8F-Fy)T%WcOV(YuVNmD5RAMh0D0a#^%RUbXinK z7NvAcO|)8&hd7qdon>Ifq>8U`nfp4vQmI=PSbHSqxgat9L&31tsbl;4M;S{N%k{gk zRsXe0pMr?&v1){}=<9f6kTFAfArJA z*qrF1cUGz3f*y-XW6OD&zb(MH_o;}hkXdGi_&RyD45q5kYft|AoFiq5S6%Z=UR_BbEY- zUj6q0eQ07{Z;OrcK^*pYc=6A1@qLgGV+xk&9MXW2D$ySuPUI~GELAKRWo2tGvox zXwQ)}nJ-J?cqjFGp}t2_&)#M+Z9Y0<@WIYEZNHofPhZ2fwfw=^R7fBee8JB!jcZVB zlV99&WI^zd$#K5GamHy=U(%AC&M44TwasO?T|3uqP~f^H_OLGhl=-JWI)x3Ic=pJw z9XM6Zg>lBJZPHFBYc)_c>_WcYlxw--OQzQnpZ>Fd+I+zHZ5ZuhIK^in+;4 z{e&u6F^&xzYg$@u8)EYdT1OMQg;D4uEGl(e8E{C)0!SRYbwTq#-a8dUa!xG0vcEHm zl)*CkcxC3qtZr;THQu4}k~rMWC1q|aV-0Xz)+<-1SS=|HIA%P&^)Ss?m7L^V645xp zk28v-i_)~M(LQS+*HuxF}wbv_8Jw zK$r562%>$PuKLjq>jI}jtPn?2Yy{dU`i}LAGmecF$yzERGeVnetpU9{`&oef*Bz0P zE8nk1I5hc|;kR8|RizFTlM6U^%dy7t*aKWD=XRU5KyaLxTZ195YRNrlxtfRBP?a7d z|C&p6qI}JjlLKVXsmUn3L6iJKi*>5b(6JGKZTL^^qDBqgaw)*mrB?B(CK>)bxz7?W z-!{nRqXv&zhkiDMPZUct)XbHe{Dy|itOZ@&c2avuO8Z{-W*u$kpc=5p;lLTW)B3}C ztavw>{BX%;$6BP?>5X7>R~=wb4FRMrshPHto|eMx)S|pMDX+~-e4iD8gI@JLo^h=X zgCC93-JKEMi^%V%H36xpn^Bv)!?sqicAFB_c8L?4H%nEsHCpi%TJIBD5EcnkPtZ<6 z*#ow8ev!CocYPXny{B4fDHg4E!8qyu{qb6pzSAtM-V{!-KDe0z0F8bGUii&Dbj?PX z=>YLkgI%G4*$+4$o`*L4VSyDozxY(2L4&sF+!7k$b!5KBc+%0ryWKo zCPaL%(q0AYYpyPW?&R=T%5&19zU65 z(`WuLsQnyAB%=xiV(+wS&Z@?HoKwfJBi+%;+;?j{4GGurZ>0>^i98(&*9%RM-WW?Z z->~p&!fa5gR(E^CXI(*_v7^LkdV&q_ftEx>Ye5}j_V_8q;9^uUcSBfMnrvyL!618{ z8)Ybs>ai03QLXmSdQmy7(&0A}Frfz$SfWyEEGshK(DcLN8oMtyo?z9dQKl?%z8N}v zW4!%c0a%r0Uu%>>9JktV?sYTx=G@u6M-v1NArU&v-Jo#%-WWx^pi-(%G+{q(YT=|p z58sJR-@M^^g`KZPD@?NP*#>pH#?M73(!h6K6C>4D^XKZmLMQdA#v^mBd#UCfly&R=@B8G+IhohEz0&;N2Iu%5f4dG{@UM|CCjyeB@rJ3bfWekhS*w$oqXRrmUrT`p;-BTu3mH2^CEqo zp=Q`#A>%}D*7dtPk_;v9Frwn1LD5*O!~UUo^&ag%f}>4Y-wzMVb&1_(?4J!sUh|ZC z4wALoX7Oe%x)+*W)a$!_?+x2P0odj6Q5*$xECgL3XYJ$WX3QdN4QfmC7@m!zT4rm} zmDf6K?l(U$_YRK;-vv6s`W~e%5ZIHz)XciTZ_MaEBw$40?nKAEAWPUr(TM%L`#IMZ zekK3OEr&A4F@jnf7{PJdnAv{ob5@KJB>mSe@4|2XAplVAw$}$KAt0I5+#n;tyEVL$ zFfhoB&n&jZUH@1YLI`?3`Oo_5jI5zb0Z``OYW&uj<_&8-sfq#)QO%s^>~yj*9~s|` zeLHY-2u0a9B{xi*;0v@aPy^(eU%Gr&fg9W)-`8~0Otz^Q=ui)Jt&v@A;C9fnnl}jH z{lgy&lqX%l7vSQu(AQV0;-^=2!%JgD?=lK*^~1Pb{8Hx|+-g_I<+kA3;`?trIYE<- zcPT-qX(}?$W2H`Y4K;etp<(-?=Ln;$u8aPn*v*8%y2&^b(s$;!iuBZN(8diMJL4a#w3vG3ngFAe^4PDjTC`d^D2&3+Z>_rJ?BfrnP2XY>_ttER)+?7^ zESWVfTuFI?mfqVrSJh!TAK<>j;mg(>#Q0AvcffV7Mf5p$z#&~J6nxG$xY_meX|3U} zRwSxS>GjVeni~ATEl)Gk%%H`dA4B@(-T0v^vb`Lu2Nb+E11O~0(d)(txsmR4*?e?W z-OFHh>?bZ#k`yw%kGgG884i4D+eUuB-|CgpAZ{8H?{(85!M%ajg@0x7cn*u-SRVwU z-B1J1QUC^=F7JBzi2uo1@*OIv`y~0}J692OogX}rrGA?6EhTEjfn|Eb+?IZaSG;e@ z?LPo74^(f>GB7%*XKHMG)4QjB40SyZbv<^o1)gKKtv>W1y|0I^<8)#~>KH;p!Bh4< z=2&64H3zk{E1ZRvv_RdVdB|@#U2rpeWQThbBh??QG2i_-)QS>1>CEWNBkJxx>S;de z7XprV=0JAWAWvA;*;zpr|@O!4l#@>``V8>2>jHqUg&M7zl2-@%{lWZfLj3Er)6Z%7n6mT6w|-X*-blx}LCEfx z|01#|fcc}+`Ts`CyJzG_Q{w@N*w3dWgV5ofHKN-mRWCl#^;nMfUV0q$*=%fK`lu$_gz$ zJ$ffo|Ej&!N>E?b#nQW&Pw5(`fxbJhM-5_l&-eP6MnvK=cB)5^y!j%$)Hkknw>o@F z6Z47YlWx@xC=P9>^4-3zW=L^D&*KV#e#NYiGw1hqSKjrqI9Iq#A86*b$>jYtIW=TVm8Mdz(HFR1yX^BFbiz;Gt(-8qQ~GugbQcqK zm%}b&bs31fOpToXpsQR^vn<&j(s@S%9pGfM0vL2!yKfiF#&P(!p4LKNxKY?Wdy zAdM?<9J}ug*JQk$U%0@V;zO3pv4^~>FEGogBy*U0yt?_uC1O29q#V=i=Fv?dLQ zU%mZQ6Wo9DOk%A+qcf6o7woZ}_y~KEeYdh9;GOWxi@|3YV~BOR%uK-Y!@kO88{;^R zSbfHq^f9rA!j$y5vS~UUL{PG0lkJ=8u`YGcnEcYBzPi&j@$`s(un|RPR@o@82W8-G z74I=_xL}nkRN;3pJ*nuV%xO(#+Vs}aNanP4c_uG8vLokrkuyl23SMVxGs1(osWgdYq2caG;0SJ3`xIbv82TeJP0W#Lr-|fm`5qozorlGjDz?jY z-#xD)IbW(@obJX~F4Q~u&&)vKm4~ZKN^QEdoJThD-}uAB-Nb}ie6-(}Ccb6a`#pf? zYjmq$kJ-Tq>OpsZx?^abG4~tux&v%zmmHRF-Z%4BJd;N%s#ab6==55<9~Y2sGX=hS z?}l=cnvUGv2mC_nDx7F8{-XeP)yj-qe2 z#CwQqH+A`Q>Y<#*H5f+lD7*U!`J%QWd!VM8=!sRe=EwGmXBQwu@N?HQ&}FUcw}UpF zYM&>#T-LmU1L^JB)UT9SB>UD35FI4;h)~&yVF2j+`sv(T zBw@pe6e)81NbVmuKMaguwppfzIXCF>*ka$BrKUB{`3)4GPGWyt9UT?a{B5EoXRpBF zBTl~_r=G&`&{`nKP*ZJeH)T`p%FFbb{aN{W>C)(`tVpltEO!A|Mfi2vDaJ~`_yU}k zJa%L^IW{HO&Cb|{Pl=QF;+A50bk&;e_nR*^oSnf?m6xkTwc`bPd9AKgz8IVABg$sq zS7h^~_*!JTj+6UAXU#_5)J}Y`Yt$`Qy3$d)$48wWK!J~u0kKl{tXA|JINB1kbURmH zBN3SPcn`8G1Sa|kg*=C=!nqVMPH@OhU{0MlIy253-pO!C3ku{KnM)a)Yoa1)x;l4R zTDmCGQ0GN=Ka#)G-7Oy2NfY~(e9TA)>@81T3y^(83vt?vPi_kY1kk9k{)H@LFUNDv2(OUDTMxCJ8qN1Ep5@9Ba5dNsX zkjAo1WsXfb$IH(PFR^WD7^9o*8q@)zMw$4gM0La4tGncF-4>lt)gE zLI&(|^?OB-OG*ug=>9G-uz`V?t4xn5u{UnGx~vlVt|;E<}YkSz-`ljN;ds$PVHt zX9bqlz7nFsZm8Pf&mppn$1GkcqQuRlu30SgyfC&_l-KKhsC2PSwV4Os$Cd)Q^aC zPqAx%i*uQzD_(8YuvM#V*o8)-x|`LdD+8XS2s$W4vTza8EJcP^KtpT zfoQ%RcL}`O+8X|YK{bC16H3)!$BrSwKR;+%nO_|mNi=_q@ghzz&CAH-m7zrHdIb^5 z13^*r0CkN>YoXi&m3C4pJ+WK5?V}c6t@Y>2r&M18cbqZtouqeQNPxiim)*>!?CHa4 zugR8icE7sssRn0cqBs@-7-9?leXn7CaD1-*4IEmNKSTIbVCZ#FVe&lgDUUF0X{2I% zg)Po}j&QM`B55k>$?W+m??nZnSdX~_Ok2B!nJT@HUOF{!+-qBvn&#NC=lu%j0S!2zGbKVJzZ;5c<(BV)L!{QXEi5ZZI$Cp4y z_+A$ezhHEg*a36@Y_Gk2H=nmtR09#*8<2%b4DTcFcd~IgIyf%zY}&RT)K8reP0TwU zJJ;I!Z2PPTuu99jnC#hUbxAhyQ&`@zOL|suslcb%D-J5^Gz%!Y?dSn-q+c^^kj)N zXI#2o#GS>)jC@E(iG<1WWp+Pg2Jv{&Qit`bw!q$6o|#O{in**l>Dba_{a{1cP&eR! z-kzOc-w{na(wz9V#2x#M>7SfJ-6!vTTTIQM^9KHyI}A)GF7;=Y7MNSGP9;9{eEdoS zoq`yyX>k2RrT_$^LO55D+X`*RNbYV?;edm&A-qfW2w9@><&3NrYT{K8#7lYTOKPfN z!wI<-8(RtoA4|o$(fm1piJ~3HGz&Kq*i4Ht{7Q>81Mx|^Go;zBK0U+kH&Xq-w<0RB3wPuw41|>cbY^z?u_icCxBhf*pKwTy72@MHeg( zizlRj4O4jGze9#Jd}($(nUaca*8Hh?G~;y0xS@IG8tZrT)Ig}GYLFADYS1SFIF#cq z0m0v_Na{58gcm;E^pY%?E^E~mp&XwL${^^*Pc#ADh%8O!`h6h6>3G-DiDt^LGIfP zZe@$rQUS*%9$@+m-_`BUN_khjWz#wCRxTpw4S)w>c2mck83n})>O_{)GxSc!cXDjG z6ED||Pplf|LHS8MrMwt}Ha&S+%sX7ZIh_MP>i+;~b=nft;m7rIt$b`^efP7I3u?v= zNr%YN)Hp_t5QUn1iyGKeRU#WLiCM|JVSYk;u?DJQo*&<*G-66VHYX3%aLVac=XUmU z-O3Gbt1g2vjU*bvOq?Vtu0(jfXq*%*)_2_7^jfH~7H=OpuT|u=wV#KHU_G)5AIsDP zCQ=c3-$*$~u!`*rsR;+HWJO74ryN4&ug^JCa7!u~iM$#PFZGkCYrR7t;QL^9xOT)V z9h%=5$=&*(b10L$e6p-r5z=2jtAQj3|!4k9-%E zr0D)hvByUX*R9l2PJ#m^mOs3(ti(PDtk~ljqXCI|^vi8svIA6I1=!O^vF{)`G_H(gF>tHQ?##H%Pt|#Xuw@nX<)h`K5`;|iY|@HI6F^jE zD3dx054lQrKUm37{;Y%DMYy?NYaY|I9&l^VQ8)!uTSSN4ZC0x}5w5TEVDAp|H}tD>L@ zy)A!VYB9evnCz*#Hp6Z0U&4r?It(lA2Tf8$9C-`RO%PhQcdJHMxCe>Y?vxU`Qod?^ z_&H zY&MPMJbarl?|0QhoEAJpPfa^OzuN*v%R?#BCNd~t7OI+QCL3VVc;9)%AIRKh(nYAF z@;sygEk3#$Y{1 z+dtWAvB@i%5zYopo2UJU{-3N3B?3Z|PEk&1Iy>aa`7C(le~lb>3t+#I0dzJEx+qOf ze7@*zbOASs0HMviQm0cjm+P~zEHKd!J37jM-48SDEoQXkab5Xx&=no6kl`>>z$!JKZK4{zVS( zJ*j9wY{lV&#|?lYvJE-Sr~`3+S(@7?xVFX9pU>e;y_6cNRI3a%Ng^uQVMG?XSbd9S zHBW^{>X>2a65uf8_Gw;fj1DznN2)56uB8T`IQH=vvSEA}0k?OACx;IlX#5A_?YGz1 zpSYy7ACpQ^B#w=*2agUrXq4V8AH=P?c2s9<(VW%?3#u-4NN^%)mA7+xp5zB#IXtu` zdQd5G3e4*=W`y(Eb2ur+pa??wSM53IM#J>v`4(ffANk#u#34=Xz&OqGpn`B8^TY2n z$Q%7fe#m+cUmZwjub;^^uUdQ`>j86WPdI)PZ|A>ugs(OuIgG3a1DJw-S-;j6NZ)6-R#X4jtLD**tNbei$xi9Y7Pmc;7edI!lqR`yvsp z3@M6(TM??{|2`NB8b|JS4+$|sN&_N20#<0IX@MLA_@dkQSxuwNooQnMM0XG!Nz5#v z>LEf1*@RW&tay~H*frfv(X>VEvAO6hzO$l&s5C0Oq`iBc5{-RkOo|a->O-aath=BQ zUUprJQPx)4{B$&jq`qoK2vi7sOQ^p^PM#|xyD;MWN*52ZPpLB}*mlJ#Z_Bz1HD|&V zX|-XsCv}newC&QY+S)5;s-n`Irz6+s%Bi~z1e?s{qrPAjs*0B3r2(&FN#dN|{ZUjD z1a0pU4c={R5aB_^$r`xq(jl^yvc4`gfG%NuGt6F zOm{JFEXi;9B}~PK#hMwUGVcgp=d`!?s;^~qLpHIg(L7c6oN`+IS>004+g-;k*OORC z*z?qyqJPGW&qP>qWt1ZbwTvkOSh?yF=&pvd+V~yT5BA*v?6F-D3mJ_~R@Qit(_jtW zH^IATFV=NqRXgCzC-S7V+mSxDvrG71-lnbTq-cIoqH($dtCOvo?6>fn?69GRWk<)L zc)Bd5B6I3$nfPvwRD8Z@EJDe!ThH+{OCON!`{EMt3{)YmJsqK{TV|h?J??OHU^kAR z*nr2KJ}La1Dd$Dp*_L&sLEq2@3tb%P{T;=m4oAN$n}%XP-JPB&su)5$@ez`|t|KSS ze?pv5hIk6&cyB|sb^k>|>1=O5jXNKlroYT_j$u5?7Xxq2`LH9X3~#ORKN!p>Kgj=+ zPFNLc{yT%+p8D^HV)}a-i29Rylh4GT!Fg=<9aT4O$!&mmA2_}Z;o3dHP|z0FKzX>Q>H@dYH``&RhZf>#5F?2}jv-$PB`VT{b)C&46oH zPfB!r@J9038aw%5g^i<2RHqq*GpE!}6jgSMkm`7f<}nqa*iIBa{M$c8B2KnOn@e#w z3_lw%U`vF54JdF1SM$zxvw;%mXPaoV^1*@7xeupj39oA%xpz2hrCk{P?ttH5UL>nuE*!42&ZHe}YC#KAob) zx_DAomn9oQQTD?bL83T=%5lM=!Clf2%|MDpp3=*0A~$Y&kWvfzK~{;QP|UGS&ce}~ zujqEz>r9@Ih4|7btMq}f4Jog8JsU>R_3!j&h2-f%cX5u;JwaNtEfTH3erd&LHxiSp zY*8PD=3yt`r;Y*5S6iH6Mvn;)d{X%}iY)_8Q|V*TDq-Q;}EIE?cB^5C`3j3&k$ci(p5K_bWvhipc@`{d=N+SWKe|dH)hjX zgHwNuAh6ICA_mWbV}v_49~ZXlZ-TGXy%FF4kzV6NYJS4}echOoAxp%V3JV6Z_%0Sa zN+?lW@#mmtl>Y>8doeo!x+e2uLj3B5>@O*+`65XSU6WPd$z4g%us zCu_2lpb4kJE#WYDn-lc}m0d>O$m4vCbMyE|z$c|9J-%z~eY?>X#{$2Yiw=@!5-a!} zku3g78p_y}qygVdE5b}_V z#=FUa+Ar~-wzUpatu@|T`zY^W21e0IM>h9IL*03sj_v1(S*)RSF8kZ4V<3fb;AdJ&DaV2Z+Cd@fjz4#d4G5p(F|aa%Ca(WJN`Q~u}; z2%`N*o^eTxD{#($^jF9B{PfKD&V17-{dylXn@MAMDUK-X`BHoXmt;$ffycvLONizB zAj2_H?kPa64m9K6_kulYveD4ciAb(&_HkxkJq$~C*g+t!9|b{;6Fw5moz84{&8NPG zXm3;DbQ~_&y4Fe49hB`I7|8DG1q)5=E!hTrE~$W@IMy#Wr(5Yf@q;}weP^Hf6R30q zL+5sBQ(G{qyK)26UB9VwavBVsT=o&o}YLe(I{7bw;qrm17QMIIQ_ z#)sy)W9pt_z6!t{6T}9B1K~^1$NoE^nr8ToR-enwZ2n9rK&jY=@Rc=kM)(X?w@;AX zVr9A~Z0myhFlw}=Fk3qqJ*v~sDzEX=&vW}Vi_#{Y1}la4_!J9{%qhvEm%ko|Z2MR$ zT&@|aIhEXrF$4n9-J$MnxkpLvp=TY}TX}jm`u`HJ8DiC=x4IFVnm+8pwd=~Wz2<2v_ejgy9K8#Sn>8gFp)jG3tJNed=b&u7s;fdDT zHmGN}PqfHfHaUe1V23HNN=ED$5t8|0)>$DD)BncUFxYglp?YyJUSv83(eSlHWKwM& zPOq^o-Bu3(-Lm@!kZh#hdpJtlVL|37fmXMPjiV&*O=`1U!AbXfGa@Hiiw#07shx2m z!PBSY$DUSWZpw-B~&DjxuVb9G+8P%Vo}r~=OzGDh)koNb4~Mcs8XllXP1r>jXlY_$>COn8vWt&eKfDXy0GuFIQ76F8 zC={HTQyghp^Wnr58hEUgIqdmt|Ai zrzec`swR{Sef@rt0y+ZJM1&md_7OqH4jw?H_zA;s(a(Oc5V8b%1lyu~*XN^6W7K%z z^(OE6jZwKIi_8`z>k3>!IOrkcaILQxn17^R7@0JP)9WW>PsQWYCFJU@&zN`BWAEY5 z6gXEq-aUKCQ%~*Y(8A|iEtlC1jjfY!-QqWr-C3*PHcE499y`%YNyU@;hX5ryCK?%U zyaiVO{19sJXhNM^mi@t^x}{!QBIT&LQf>*_Nqxvtx(Gf=C6ZR_P59oM*_YfgjHGJ4 zqWN(>TR?*D_4o>MICTbZ3D#~;#a*t|mU2Hb>#b>>3#btA&)Qn+0d|&WGogFjJ#X@< zE5{ELSoPD{H_BZm^cHBW%73UA_qsQ&qikE-`WZi;5Io)Ge%ovw<8&%`3>t~PBWCq& zTz=gfh;Yhz+}Q$F5#MetAZ#6&PJbMr-3 z1+K8}Do~&zje&JVazTbKCw7H?@1}eC0_!K$F-o!ds!>hQmjD5ytWGwxDXMwQp>(#I z+0JZZxcOK8z-os-vJEeqWrK80)_qb7OGwI**mP_8<6}#KU0OR!fWjCZJQpcNLVe9B zY$+romP$Ocu*%%dluV$G8vUKUTba<;ZWG`JORf##w8bSud(&TKeb+nFWn~2PKp9KiGv3Zf2;=2oYLrX@77a zKnT*TGLmXP!k;9XojAf1)`24HOV~jjq)=M6Uz!k9ZoI0_haL)c$6_8d?`BT>U|#hf z`9`L0Tn0Ix;vjm2no6g3{$2OSY|8uNvJR=5`*O>=!;%%c{gl9Qhq2RfbvcPv%+|Ed zDc3M8$=MC~2m;w{$KQ#E69^pJ5tU^*`;46#(RTc75F6!0Ngzdq+uOT;WlwR@IhWf8o&+U}U#$~{UyKrZiP zA$#xLKOUo3M%~1etp1qVtcZOiy$Afr+BB;XzP;O-%mKQ;Y{bD&gMFTqRW|M>{}d^? zLi&=uWrjjhQXir#dLO(Bp9i=G{>k&7eG}>P7dGhGA^8OV2o{c~_nLx-AzX3E47cJO z@_X&PU9!ld$ev;4_VC_sM=|ON>j3v-wvKcx-#=Ep5bfK>@ASN^<2o2Y?6w>Gfz1*QZa=Qo5HM+U3(Xy* z!iBy_hGS5Fr|8>482Z7WN|^AgVH@CKX5;ws)&t186DaA`RDw zVUVgT6bqmhIYFtE-rrb}A_U$m6AgEfO6v_?AiFLS)Qt$Q{ai_5T(Swzx!UAzMWiYg z_xlbG3^IsUV1~;N85+pL-6BFZe5>xiS243RpJn}m!*gHAkm2t`s$VKWW9^6d1t=T@ zwe5+zhBQ<79Y*-JQ=$D4d4;af%rlA~WJBaYkB@%x+N6X}qXysl25|NLogcQ7!t|he zL5|g+*Y<|7f*9Neb}|5-Od%eRb=qc_OjTN13i5lD9blmI-l$E{pLuFxyTryTYu0@P zid&9sWll(S{;SBhFjpfC&t4pQFb-Z&!ClT-!6^b8&Qg_KW-zXeZ}I-@3A*bc8=J1 zDI=7P`R)tmWT9n!DueJ~rMD>VY0%Qa71}=xCy=`8)>t?|Vkkji=C`(aiRLU4_wSp9 zJsla^HO&st29JkKoi5$jGw+NYY@q91Y}U(}%lc*7n>d#80fq-9!V|i|uFn*bA=Hqw zl*^A}1dkK+It%3;BUmlHUJWOwp0#j@FQ*-Udc?zo_-$v)>m%z4J0*P!fSAk4LUuLO z6iTx6*xv12h58JQw8WZKa!3ZkiZ;-%rD&CHYTrjox?C zqrj9hQlfk4FH-5#ZE8pf4lA0DO=`DPY<(erjTv(}#IQ7$2 zO^_*bEnRg)QcN=A5g<3{VaIIF7DJnt)26xcsk{|@JwIMJ>?a5lgr<(K~R?Z#8g~j z`%iu~#~2S@vr(1tRx@Vt(S2G{3#;sa6U#u>w)9!Vh@|gW(G@P5wbe9X#tc#qSA?%- z>}+1F-2$gSo;?5_e2aT)2@-ZJotB4NL+m5V0O|<&-<4$>Dh_d>Fa>*I)#E(ZgcOu@ z0f)BI23vD9@Kb_Brw}c%0#f#bC$mMy6&osYv*e|Q8ZpZS6 z<|4{|%=%J;uY3tM19?z3!xkh{GvO8Je=8x6ROYIfF_Gn&vF>LHN1YLSy(rt9w z9oWcmpiE4y)L({2<=CXM#+SZ*wf977OM!vb8;Kb4l`hBEU&N>vLjFpGv5Nrz9%U5H zI!`7jhi+InRp)L}{`;-rPkfqWOF8+kr@iiMkfPszc;TJi{?vhSt3G&Zn7f8h5gW7B zdEBZ_XuIvnB8ZKX8t-|2dXc&x<*XVK*SBm8OgTan15WoRa}wgkaFLVyh|>c%iQp#uuLTXUhb8wXYRB-Xxjm z_t#8YVhOa6X>+gWGh#Npt6zTQJ(=fR%6|MeIKsZ4IgL&WZ_~yJ8UfU<(4K?!-+4}g zd5%LrFsq?f@7t|_J+S&UfWy~wzH&8v+6dn#Wg)Pg*aM>nwPu7~+50K)QA~m#m{`o#2Z2cPa-&oBmjcSf3Vt3!B_cXx1Z8u|HOp>TTZsx4cKqiNY7c(FTmD*0qaKz*~|q z0&P~#C&>hoc|BPbgixP?lQtC6rzU0trhZu+i(O4rq|C;4r!0hgB~5so|o2lx7VSC>u5 z&7lHYlRg_3)YSoMmfZ+ssb>%QTkdynU%%zF9XK4Q*Skz}|0l8oB*}tOmeGy%8!UJ7 zQ_~V+_q`L0m-P=1y^VD~@SB&`Du>PJ#1_BecHFg0j-v9HY{_>wmgT@ z=q6`}0mxIC#(ahoQ}P#)mShZ<&3Cyz@29+M0y)5&0c!_l4y|T!(04Mw7&%`ae4%_u zxgBi_$TNSW1lG^IFM4$Ek3kCTkRQ$5q}?dsYi_+MufBgK6X_dg%|wAK#AGzC z(4&7xee>eUeJQ-5=hP5N4}Q4IGwuuzG>+y|=?;Z_=l^yl6`QRa#s}$A4S~|Z3jW}92V)r%7 z-Z+*zt$^N8pCjG-sah0zKtcTv({>Q2vOG?v1L%YJn6W})sXe2?g9g=bk!>UmBvIpf z^BaZDiT7kCI1o4L=U80&+e_(d&t41+t^ommW z1BI>^qQ0G{J?embi@yn4Q?6O>1>9Vf$W~v?Y76XGWz^?AnFu#`^tte;Ag@rEU_>~Q0Qy#1t^F@l%0OFI?_c9FLRo@uXZLa;_HRqpO>33N_c3Vna8MJNXD6WhH)c#Xgq3OS5&yO%;U-&@)5)(iU`J53 zU>rH7W3v9XQ)CK!6#)K1>|I=T+>ByG;5w%3yqfR&eV&~tYYuJI@I@hnr9v(`d~gC^ zK+9^Vup({3Pkov%YuGTY^K}aR06NMIoF(eStNa_Yr9dGR~cb z?jv{q*2VJ;DL`-3J9E@v$Dhrr#vTb&r1R&nR)>PRCDl`jJcp7&(@=B3HeRZli3upN) zU0yFbW2D!L$-U&w@?*WFm@lq;oY0^9jSeqINz4K<-_&1MVR$uI4@yUZ#ClorQw8%D zTx@GscaXh8$$liL{3iHpbx0@`+{XD61ATFguQVRot74==>PaJb1Zm$l94{01c@|)# z$tSnR$A3RCz?SaO!Ji8r0%4=MqcwMgC|D%OH{}}`-XD6i8m*DXJ-*y@nHo{--_l(R z<{bPQyL(U%a0=rYCVtW%IQ6mUXvm(%zeJ#_$Cvv%>V9Smy;S801++U6~myR z8(krfEjb?EE8R6i#K9s2Cs4TOjYKeUkzld^RuJ3v<$VEpr2F@&cuOn&pp zLwEH2$h{%6-qD0lNM6SUt)>lGVJ|;Kauv*Du~Q@8zx|unoG_tJG`A?S9DfQ+tmB>5 z9?{-5`JTnapV6!cZOc`xe?R;#!Mz%pzG9&kcEU1a8KdQ2FWZkpJo>(Uzl0>B)Ru&R zD^HE`S4+rD$v?Q*;5FHKIv)x8Tp6$jFV6I%k~%y|dMqL7{wtNR6lds&Mx`0jP~R{q zc?0x$pexYzCr`A9)Ht02Pg6&c)==nqtRqdZyM&Q!!(=Fb@J8wY;3g_>Zesk;k_J_V zjQMUi>txLrTL%P9CSs6~)l@4@bDuMuJK?1h1z<$>9yt)*n|t)jIe9L_NVn`WYJa?gWqhK}gQaI4dnix{>UE&8#j{p|D)VMEY(EsiK!jp|myf?P2ZKk39Pd zpZ$Gx2eChADofoYk)vxDI)|NU@{=A1Jt8I8xc@j7F63qdKi60ejJW|b{OQ0($L-5Z z2!DY+p?Y)(P_T79>JxSoOj9#1F>>jNtZ!HM9|vpt)o~TQuPUS*>J9_r*s~~~`N?FZ zDJCNelT?`TqQxKl#e6{ZN-$`>w9FyT^nZd{$%aucG6y5|o}x#w7(v7hZ^I2pz&%8xA4 z`F0z%I$v$uonF7O!!tH>>>UvtzD{@NV~*ss98b3Fa3rr>{3= z$+JK~G_$^`Ipn#1Ygc|Nc>CH%B7FF00MzfzmjBwR&t1{_5Qtqw;C5Dr)%sl|RsW#F zM&KaRM#P_*NwN>cg1(9+uI5c?dV1D6Ms854*>qNMmO!S9Ai-=6*wZJ=$A)~Ab7Ne{ z#UiJfPb6$uAkJ+A$+f^bA44ize~_Pd;>$8m!3Yb4i3K#Do`P|E)1DYz*&OT_(CZ0g zsu9HuSVN3h)?-STIKG_`jlP~9e2IZur7ABjdYo665Suku$D-In5@Mb}!o_UdD?+s9%&wtaYFyEn@8gAa^YQ5PBS&aN0c_1}P_@{EBY7%* zj-uj8Db)=e1-4WBCW1b)T2r?S>QOOF$%~SpP>*Yj%SgrrV;c8F6P-hJ$i^T0Kyie9 zKfYd(L3!o&5I6@^&l7pj^?Cz_wgc0=2s7HXiBQv>w`Ojg67Y`Ro<1o0?>j;R2`~7g z$I&T2fr5QLsc=^)5$&OXexF2+Zo6xT7C#y42vrgRf2PMl@xVnAOx-LDi@hL9AnNBU z*#E{z?;^U4@SOK#gBx496Yn7^m2)in6BV(Bu4V-BjXSzT(FZ>sva*5d7H?LqDY|7z z_$h}CxoU9ZHOYx9+5(w`dYO)++*Xy)``eA`_L@wXe=RdeYO%uXSCa40eyh)`r2u~O zixYNzhg~@@FK8`RM}b7Sb!{4@-H^b=Uuioul4*E!;xEl3kqMV+Ip-Yn9o~5N?~PPC z5`1mh#p3|Lsn1kM5HQ9+=DSm-k{^fjOM!s9F> z`r1zI>k*#jII)lH59B@ZT2GO8N}W+D!QzZBoR*`~OZJn_w)bMlk#}w?TANVDR6QYl z5psEkRUZH&OrVp7*Y0g9_7Qr=-`_PJs~=$)a6{@r1i$l@1kKuHE5f{GxO zgV*n35C=l@;89wL+)Wz?-pppQR64u@eFs(SIb-D%Tyk( zK!6>?*Ol9SqPd^^BNI8FEr(kjukx}fJ=>w(@kPJws`|#Fmgz^2z*T%gqiv7PKZ)uI zMruO$#H7c$cRe2*2b~omEl`uwk7Xzwn~)`UA}04q%W~fS>sZP5O`FTt$p#zt%2e5X zH64}_>#Uq4CHX8j)-)L|od`2;THtrYoX@m)4^v!et^+Wv#NgGP&tkoPtXrpEcwNZf z-8+19^0bvHcFv-`N!+X$+!>pcGFGv#KM|BNeM(xws~$OZZ$H?uIcPIrM+kB1-8pgY z3=t-a>JMn8J32gE^BBiJyDaXSEo3w85Vx;0TZ^YS{8bPVTr`4PcH$$a%&rJ|`Yv2! zt=xY@2Xqweu7?jZU>jY;hg#Ym)mXtbx)Z=sQ%d=oyqHX7Oh5%@Nk5|N34JaQsL*H;t@`TYS?9$38%?q?_#0y~tJJ(?D;q9~1kA^%oj#Q?&vuiC zPgN{fkew}a2$^2Fu?G6^g7)0SL*b3b57LaM!|FWBbK2%P{^pkIgfla=KC930TmD-q zk>7g7MEx-eUbtzj=binCu&$_s|4T*7cInrJnQ^Nky^kD~Y}r-6!KFdCBKQ|RIizo2 zNzu-BmkT2KY<%-rPwrxO-Q-2~(LOs+0*2XW+jFOR(c6W5E`e86Fw}y5nGu^xXb~j{ zE}Wl*>UY{7VOcWfv5_pW)B&0`Yn!f5&kFfJd(uRnS#92!Od+DR)s&8&z2s2>ej$$- zh@ujOT+Guh$p>KaPlwlVg%78&2pf3oPw?k)>6h81CaBToQ{+>F@!2~8Ip&@{B!lF? zXbsSmA1X2i@BvstKa)Md{I+nxsyVt(eGo#cz_uV2#gcHa&* zpCG;d(M8~uUo;`SJEw%`9(ixuFLI;W5m5Hzoy>3Ner|JpL^n15;_?^bp3Lo$T;jjy zHx$EKJWY?nOgW?{l0LqQ)M>Qi7djZ|^Tp1dTEi3Du9InaP~x9L?U$NmuoQZSsK?BL z#;tKpQwk0Kq#sKy$mNGHv0q+MnwaWTrLIUelVl$46!9x27z!AC^&CO=ZAi$QVHMosKMj+ zlZ3E56WrYppv+?n1Ut#A>5Uy^S7g;$*`M}81?eI24sIh|=yc1!_4KWFARmxh!X{Iw zrcm+#P2__4!8GLvqH&R2(N}<}W|}(SWc@Ja77&d37Jcu_X&Crb(BJjqU!ae)!TZho z_$O4X_5BGIdWLgv8&gK-^#vEh*0K8W@eODad9uxKIGqyo_SGlK%U=TT!~W<9bw?|9 zu5A8&OwbFld=DHA(%GXszOe`5%l|% z9$LIc7n9gAehp!%jF}U0lPJ@O$&AO-AwmN%e}j=GzC=gLyfz7hWd2~er^Oa)UG20g&65og&Y zzG-5Wg!lF87ety@csp|;4#`TT*J@}d$U_#co9iUq{`eX(({clhtO_$P-+X~3|$ol%uUx+}dyy&FzMr!Vj;vRraUl_AB)R3=h2*67U+bKLn zW|EyE-|DCTFr@Z6de}_uh~gdEo{Z>`%rf=S8Inf|`Gr^Nhx-}6RZP{Q4zs%`BE{)8rx=Q9JwB;0|( z{7p`kn;h%f^T)xL1y~7z`v`3sQpAzuXnPNk-4cS#L%ed^H3(E2D1a&3Zz+8ntQhXk zrZDJ&X%ohqU#G@^6Nv(^N(ed7h%^fKha*l=K} z(5CZW{m4l&pA2Ip3JTdVL%Oiwh)1?BW$57<*waRbsX0{mKOI}sAPHdErz(Z|tIb+- z)>f)&b7KTS$1GAP+Ew_IgLH0SHZ;DbDM{>E@kP0)&uc zvFALg@`BRDF_inew1uz=h*6U$o-r=Qw#4DL5?n(ZnZo)%iu&RCkSTuJ_{2+srTSDF zx;K!BYEn=C$?g+|4h}OM%)YD6YF}b!YzO~;368*436t}qsPNu69%B^l%2rD4+Cn+^ zk%v-S2w()Y!~J&VAHXd$!qQi)_Y9VmFU~F0d@Yach=|Rghb{An`D1NRvB3rK9ieH6 zj#40_N&A>H-O&S#K7&>&2`&}vJ?7W zpvU`ZT7$Z4>R#jv^)_JS!O&xjuo})(sTSK`7M5T$DI5aMU+z z3_a)pc~jLi-6Cw6&2+oa5be;5EXq}%&BxtHP~-XHNXK43e7~0qkxetN(jahl z%+>%w;{W{+hNamARG2c7WGHfSGY-3!6QuSQnTuG_=g`*w9<)|tt-X{Kr_aKguxexo z2D%8$$m$yZ;iN+Ti19D+6Jc;Y=a|G-%;}JB(|Uc*vF|6qq65Qq6n=sQA~Q}mF@ou$ z`DG0x#tbDc!bf`vf`8r%Nx9!r>{%!Gj4x@b*kR?AX-&cpy@XN}`X3wrd`l$nN|ovL zoqdS@R7GMqPP0?@8K=7!8@`u)i9Tv186y^p;P7? zTW1a!k@^Gf{YX!Au#tP|eIcvnmE7Ml)IjVvj*gu9!JJHa3#@neBnI*-{*q6qp)$>Z z8Rv!TmoI1{e!9EKRM{Fs%o)E1GV&C{uoKC}gcDg|H6$?v1U-WP$ht3!>mgaAt?mC! zN)YiNcttQQrWQ6F4R3jOVru9tXx7oz3Qxep_sSi76~2L23tEqC{aEZi>}lEFJ6}gY zisClhCk>M}@cFUtz_%~E|Fe%&7DAjgbnEm4AUubj{>cdbAmNj%pyUA4eIvEby@|;j z0^5K*J7Ebr_&j>9(+J)aGmr{TVPrtyUR08E++vFy_t}U#FJ>OBDMS(l@9%-IV$S`! zXLzgjqga5kO7G_oiqCU!|NHyD&*6XL;D6tP|IGvcn+N{?$pd=mX1>j{-NS9K{cU~Q zORxRBf5%tjE9|+FTd)OL7Q&LglsQQKpo_1v7c3Dmk??+wnrrLBQuuBMTI!p-$J|r( zhb!ZFk?iDJVn-l;2-XGkSl%pNO;*h6@eoUPtIqi{Nj0KW>#$fi7iuw-eNm5Sv6hh( zpQzLy!vK1(utCy7(5m%0zYwU004Vo(KJMeY%Vy|g>dcs{3o^#-H&a)8fS1-h0z}UB)zIIPD@e`d+`Uh;Oj9=`kwB-vKJ4 z8FG@=*WQgP?6E^aUApu*r~}Lyz!Dog6U)AdY$*|=Aj%HMOiROaV|d|oA0E>n&`$A4;h!hzMl<2q(WzkSOr(R z#aJtr_Box=o79j;Q#ECi{KIpn6LtjxhW0B%0xlI(@|k&Hf59fAt)4Q#e=v3ld1rsy%f;=1G{NZ7W8(S;s)O< zy|quF?&7s)$wVf>ZaQlOdIRf5Y?T~scq$z*N&(eg-2HC++$-=9ns7y=|( zivx(o(vb>^>OT{;#h4haKh(JDMRrV#a;m9{s=)zb5t@gRIxKQ>Vu{x(>=9{hhG7=h z4WD9XS~0$a?%|@vs7t7Z@(irbhEx-Y?QQ9$p?YbmqLfXI{2~7!m#iuKPh!bM{lLeV z{n>?IDI}}HzMtkr_DeWKQZ>~=Cmc{w_S-b`>vLv_37)%ft(|xJmUe_m!?GJEZh~Y) z5A3IhEwPVI!f~f;NcQkuFke3bLooaLB*hs9EJZS5lY+Qk3?Cjb{=pE7-sB^}!-3X8qy5U}|Q| zKmM%wq4B$2c4mCix4DIoI^Kg`1-*PC%}@TaDR47UtkDsS4BHpRzNj;i#l0C9cr&e5 z{WQ0B($5!X7lhyPF--dYCYo;H!?^a=swC$SWt7;Ysk)jd#dBx=Tg4aA-#6C(>HRDa z`(*df>r2>%f7|exy;*|2J6{Q)l)a7|wD#j*HKY8z@86wqMQ7|}cliE>8W^Ak4$${! z19+0tw>UWg0?7e^ffl=z+nd32n_}amp~b3^y)-w)mbXNwcn?WKW1#1W>|oHB>NhK}{}@s?7X)qGYb&xZE- z!({U-0kjsc|4#@VOvdW}Lvy7agqKyNkyLl59~c#{Jf^k3V``v6qza>5dPP78%{<=w zE(S6tAuT5)LgA7x(Fw!53jW9-##fe!EY&w-v!b@3L^CCxH%k0a`0+JjHp1pxSI|$( zG9)jeCe-Cm@bfUr;`bpQKoZFh33F&&JbwJXD9om|Uuig_WPT^}#k>^4j45bR9=PL0 zE_$S*i=z8^br^qCi-XrAuEYG0jl<|ikO^j3KSOU-!MM?=wkP;<-=}G-(`moc?}6Cl zFYxHKRtaiWN@R?Bg4uI`pq}JoPT&F%?~!|?66D9-b%b1&%v{xbQT z<_h5+DZJiXU8I8DaOC`ae-)1dlJhpEgg@k`|Kwuc4|p6~Mv(E4B9^i&X>EY&_3x&; zom=h!CdYRt=bCuA=EtXk@<-=Nnid!?=9`-j9nL;7d5U%|SX!Z!t4E}cW#2HcIImzJ zRCe@y5tWI1(mQ%_aEP#2bGmGlJrj$~RUEX*PJN0jV#p3-0FSHlm7YB@in!9>V#tb0 zsJT|1`1P|?Jh=}Xy-#XGCm78){49)oe&?z;2SM!>Yx-$Sr-w^K4Gz=Svg#R8uWf(- z(zRO65u1)02F6P#tFJ8glm!$s;^2Vjbdx1b zj!E4Dl}X6)5LHvIQn_jfV?T*?Uvsx+Ci9IJ(0@^_QTL=a#P8ua7Ry_WTTkaGn>RJ{{*Ek$sr!U+A^c8q3wR)Sp=QoHn74B*83u#`LUpdQ& zy2jo18joS4nDstg>i6&pSmo;3^O|x2tat!y1vY|s^lC)4r$8+4Lt?$b_V_k3I`%`K z>QkRwe*6~FX}uc!XsbM2zqZqStA~B<8qJHpeVHwGaM4qg1(DUg`rCQy9>S>dn0!^I za9BBZ;SlZF<-x`f)-#_W7iK2}@}>rK$Ek^yXY>|*Htc%_pN*D6S6QFE^j3~IFtVucq_{v?dS_fxhk}XypNy8aA}%n zOikq9#F+Sii@z(fC6KtUB9woj0YnYrYpdQ@ ze(fRkUXq)pYc@M)KVT5yAt)zYlr<4NO8bkKb34R2Gpwa=FoS8m{l^&VZ~o=i`jE4yl{ z%{|vSVJyUzDsMnyVjx&(Xz?%j1FOk^V`3?Qe#Fu{!T2l@ZdyuumU?}2QlML|J(m4P z+ghx{SjW1rLGP3Obl6c@>%xy}UZ6$UhFo#2ZZO^>dte3hcCnup-#qRnEVW~QuDMj3vS)v^T%#`M20W|+oxPa(1xkX~P>)fMP0oT4awXPFX$?EsYwbOuO}DC-u44+hE$di?-;I%UTP4!d+IKKH!n z9o;F`_^##&uI8=sc4lBhfm-HpvN;bLk9S(nXq8qjs{)HI!tyzR)`CV(voIe+<-}Z6 z&UFFaEYPM&)+Ln>PXLE@*0+gk9A|*6kX7Jj?uG{#?4e{QcG8^OAvG zg~;>w^tY@&8zx4(uZXh(u}k0Pq4-oMub`I7q##H)I8tW2>2_{GF)u6qNm7O1dB95K ztp29RV{?P@d?EchINfZD7809&T>PK1{(7k8IGmSY{W=1U`g&wq#pGB;?f+HOQ5fO= z3Z_El7+YtWp1M=o!Q1w|SBzJz+1}j%TWqU0De3b9K4Lw+FwD3SN?6nO{+1km}uZIURr~936Ndj;zc2O z)|Hw3&zgq>_{?^d zP{DP)7XG$(UNypsQmF=8kP(x|a5FVq(=C>{4uQ^Ep`O>`+0tn`Q5op8rAI52sO$#< zQFGrrHC^kBWsex{uMYQr5<7w<3zsbB6bpfN*0l#^C|A0#l%R+Oz4Hj@P)`qlurlR+ zn)hYU_a>#UdGOMD^@TCIhE^C9c=kLK!?I?te`RXBqVjVtk@)JE+5uA7VwMGGl4K;K z*7Ha$p)Aw1DY^teRC=6)*6n>l7FoHgqS#W;_pGK2E~fG8TOE^*wh|6r%j!PQ&UQJKd5q-JCPss;?}*?bL14Mtn?nDBilvWkva>nvNF@y z2Dp`bBDi*PVarGFM7IPIa-?IJb((RD@lZC~(x%o}7i!Z_Hjtbr_p9~G$CMC6q(^3a z>+rY>S8CVh;A!-Gl8#R>QU7yFk$&!9sNp%IL4cTE3{$fU9B=>_3lfZboTfLN+O^)g z40-(A5$yTC%AD~#&Be)qfB#OiT;I2HaY{b7NUwUOCVUF-{JS0gDIep@?fAEMk+I=n zElwZ6f3N?7?~4}L(Xj+cR?|6U7zj}$A$!8#gmHil=$!lN!tTfsT z7IkneT~`1Gd~%_z}pC11F;5t0HI%|7n!|)miWbKm;pOg)G2Q4*H{srwoYQeFRzC9iHx}FfLOY@)3l(4$HC z^=kYBxWn*Z3A#*)F!}^{!J*l3tOxsR(dy%rXhmhFBi}s8!w>A{U|^8IF3CgC~dM|7R_Yt z4PgCtV}0ROco(p~xbJw9gx+r?4#{76pg#L^XLE;-+aF8#!oZY9iV=JS+y*ghHgq|s_L zFO1p^%_%zZzl!M_=_r|cgXw(C@Ff{`e{psjFVf>R34Lx;-;)EVaef;xHaZARlmDYv zCRL0y?4V`W5>x=|>QmWlSq$&sgj#|0VoA7S8`zDQyJ5Tjua3olzO!+tbEY2XaUCV) z`sd?iE2jzzss_;5Xy)R-089avWZCnk$ux#gf}iWJ$KJ`-9_C7`okgd*v+;%x8F^pQ zpW4&U8v=tO431ZW!%y#FwWf281Vm}J;IfOr{zv*75#}dr?@Al71j0_r>+UrqQMrCd z*O|6_&M^x}596t7;%0j9So8{f>+rK-NK}TZRA|pH?LLpPywONuiaI#!bP1N_EE(P8 zFOrMH$w~qz)Ne0`11HDCyY_zJR_NpP0|@J`Bd{Z{K1Ue~LiAp3D${pvTds>+20Yjg zFi4&eE`*)gn!C8Z1rI(n!7Z=&um3o+`j$iEGbQL)Aq(V~c44(s&j#2$&<3XY{KS55 z+4kw8j<2RSN!Qs@HV>ye_c0+|>2|V$o*=98&bOnW+n#UhByEp77)2Ku%#c_&HaF>B zVt+dm;^IAhI|1@gBbn7pZpB^-V+b-!2PI5Jz>rIiB$D*n7ulPxu&whCVgX+NZR2y9 zT)iamcqBEiUglu4*YRWlGB}O3b2&={?c&9Oox^3h@WUszBwWA;z~1xB_T7VfV_=*P zX{qwVoJG?8u>`Zd9@$zk438=3F;m#q49c01cY2@5|LVA>+=kvY4;F1{6{t`lE6crP z=S#S$3hAl7Xsv~lEwu9PWN$S9zs0B($SRYtY5<3HH~!$Giw9_aD)&B$d%o#OX!prw zw2hEr8{xjC@&e(pqtvATE;bz?2-H5E5)~wK5RmPJ_?vQj@=VGVFO_|dXGxXENJYj7 ze3Qnh!DDPT#r@iBT5VXq)S27Y))L(Z@VH$hW?wGTcs#nL-CZF%z*EqVe7LAsKdcNf zjSQ`=Hq>kN2&|h8`gay*&5Q7RQMLOvK1iNo^PJ;jBSrur#!t?y?ILi%rcYnYL<7~2 z=P&**8A*D8TX6GOb22}lt}wbI+Qz)x+Ka>Xqw?RBRmY!;o#}QKf?m)>v!O&Y2jGJj z@PVMsYP21&w>EQE-PD!wve^7C6>rLMn(H2a)yXdO^xM;gld|{Sw~se`#2-mX_sqrZ ze6;Hjzdz(1<^1_(In9aDvIlTYK`45iNF?6gsiK4k$t2R$MMwnPiuvg>ahB({#d?)A z8*5JSy;szgyF9Coz|1q$lJkRr;5LX(O?ftDPlsg_Ni6UDACw5dnI6Q>p~H>{zDMp` zS^N*gXScCq(c^ZfQBR{)VL15K!!13G(?z52rRokKtnJ^=ECyqZZo)OI0OwjdB^9Ul z9x-{Q_BFH1&ULe zsw2~dEcKj4D#r)nFj10FNs{#c}Ps4tLT$m#~Mg@9M7aK=oSmC8xo|z6X zjXHLG#8+Qr1e?P6$!!EPP`xu+Ls9@}Y>nHo)i>QNZ!W;j@gtpew4jH7O(euDpq$CG zv~1GK)p#C~3r@9_ zq3N9&Em-f3Cc?_rlit)~I_&rBuRxB*Q?II1ZE2DOOl^T&#O4XPDXLi=ho$?^Q%#%Q zBuvqEX?vOL9`DN+ify(=A$exD=S=qYWPUBZJh&Sf-l~7?9jE$mt}A`Z;#*JEo1qS1 z1tP$@YXk|91;nX6=u%5{rkVbvx~;n&Z%^|$xWl@owj3IQ zrwA(?Iv^K&EjwwvlT>W2xq*!&`P#+Uv>A+{-Q8x^pU;Rv2Vhw;e?OAf9Gx&EFYK|2 z7u{2Et@b+J+7AY(mfWo=U`@7HQd5Jco8p}}eJP#!D_3JtLt;?{`J%$0CFr>M)za1c zyf^H-gT2da>qYJSn{@K|YUMJ?YFHU!1dE1Xf5|(~#U?$bQldiUC8 zje%a#(>Yp;9gzKk6=!{5!}@NvX!WEPP1-Q=>`i8X^mc{P%iIZU+-UhenkmEkUHtrC zEgjh9Jaqd?rhNY*{9Fdh#rI0hcSyX~M!2#J98@PaaeUg`8+k4}ax`mZhzAmtxZdf@xplw0}y8pArk`8J&$zHpMLXS2tY)G5Al8NP^W`*!i1 z6PQu|De{WPDHZR-rz>n=A1F8i1?Cxily1 zA6Q`*uKQ^DKuKV&O0c8ntih8@(id?AMLkMabGTC%~V~5un(e=sV z{&Hm;SH__g^Q@9yVP4BOLH{J@Vd~*S`6hMzy=i?depm0y6Wj;&bO|=E0WSZ>>yN;P zZ%kf5t7Yu;2I+|y0s9e4zTWXSoA^m51-;+5{VL zs4QNssx|1yZMxv(AmY6W7Tr#J)KH58S({LU2gw6s*lJRnY$jvp@IgkZpmqDZV z`CoR!=ctmCrGd8OOnln)AGK2PFR1G zrvRpu9t%CzrTxGkU7T|a^G0Q1_p)ENeSS&cVZK=5%ewH4*p{c}C?J(LWy;8v7iG%H z9;T+s4UkagCUMzhZMo%5$I9_Ip>Y0<|E#>R8y5EBh1}5Qo2B`%2(Oiw7!bdE_L55O z>{jwtx3cTB&6e1IhL6ZxDI@e8X!SoWN+``PKQHoDw3+r~Rkr8zzys=_PT}hr+mPU^ z#^X9dv7SiT+GD`!9Xhe6AN}1~A>oi(J-i%asx(pS>(DJ~H+M?s*-QUzApP@Q^&Dy- z^V+l0wi7?kD~d{d|fzVY%G)=sdbxKfSKq;yGM4jtl@CA~DXC z@VI2#T1Zif%i%QDX#UoAO*>8c zJN|n`$D z8|UW)KjX(}Q=(PqNzf#zW=TZ%fES%KOti? zo<=^S6SIlvLyN%|EuizcC1^qGnJ*t_Na3#T>VkC} z+}+(82`&jPL4sRwch}(VPH=ZZaCZoS#=UU~(pYeJr-9r59cSEgpL@Uc9&6XC`fAR4 zCPH^$mEGrM)l|*ZW2<_ndyH_?Gn)f1<=mepTam3-2-4(C$iwCMb#!#$E_xAUtbrz@IY zj3%KRy#Eoa%1ZJZGkN(QF;})(Bulmepeg$kQKDrNNwT@-w@)6xGUmO|m0W=>--Bwc z=a%?!7q^muvfo7hB>HnxRp1e0`UK6hHDR75U%AJ}jpql+_?`BA?L#rZ=b?rXJ9`CA0 z3EvHD=GdZ3VVQ;$yEr@a{0I?kpAn*eEgs5i{i#RwtuE27;Cl}Yv!O%BrY+hDG6x^% zj!P}z;*opXyLeBJ7r;sljN1G;8Y$;&njMv533u&dJARO$Eq8>|Z{=;sBRIAk914R9 zL}}3z@v*{|05jtnNK%Im^e9g=fSNPBm#{Ac=e$N==oY{6&ZY7 zIWW}}2et)>4|t-b&a5tK7q7JA1)a>?FFA_vwev4Kk>%$vw*{XG0F$Z|(z}`O&M(L2 zTB`zhYNkF~TWvmg5xNWJE7Ggq8qJvI1h0%4KW#9q%CUgvyEUhLc^mRBGo5Ezaz0%F z-qn07ooXJam7LS$>0HMT7bLL13$;YUXXwH81bj=1P0`bu@VyGw&x3C5%i(H|@YFDm z(XZPOKZ!QaV5M_l5v<@3>UGe(j(p_Hrhp4_NI8l z#NLfUg3@w+A`29y1|ww5e`8?`q zhp|fUhs``kT<^r_p{u*$XuH;S14oVOfb{**{%$^jjW%-QmXe#y4CdEA-#FHv86PsW zrpdIN$3EfxsQKp^fgHb9uu{RAB9#*yuf*h)?$a&(HXbDs|tsyz38Bq$Bew;ht0I&K5IF3T8kouCKxeNxaJsihYrI15 zJwPO)lA?On;4YuE*HpFG1L_7^g`n#EwbK=?H-2k+(ZyXok3HojisggMKb{qGQ*R}Y z2pTbn@BuS(zbhY~9#BlDt}XBAR&snk1C6FiaeB$^dfkXBHL1V#ozQXny-r=R|4S%j zA;nuJ+w^!1X^4lLheR*CZu=MQ`N#3|CG{#5?)hTP$I@l(SP6(x_t6o$2^laMzru() zMr$G=bkKfm4d)+zVUK~FtK^9?rxNlABp=`>q&JgLlB3VH{;&jOsqlTYRL?-y98>sJ zptv^|wYeV{wR@jSd+uG-a>fzxqbH7aHIG1gXoD^UEOyW?CieN@Fn`4+@l_`A>PD*VQ6X&zEr7QV`^jp)gl2MFKNej>o2 z33G(eeSZF4yuSy1g05pEhPq$Y7bIu5Dep{U{F#xQQcrH${!HYk&a4B4q{U~WxQ<9} zpNp0`AY%Ljdi5eAHO6s7pd|$7-{N5NR28LHE^COp$7N})j?EF>4ZJQkz8@wsv0f(t zM>N+!a5r)WaeH01OC_m5S={WVmhsJFuqQbV_~MwMZjmhdjon%7<*rlTCL{nfgo@D@ zAzzAInI=d)o>9kU0v=SQ7*czgQ{L-L6ZW6TYRi8_8#oNH=sdQ3q^V3iBTPd_$=YZt z);d4+p9%}^8383VVTYt@^!9z>Cyzmc!V#WgH1%cGcBWFCAAgt^mqN}x3g%ZDM~3&Y zr$GRoRH>{oD=i+yk36{+MhsUr$|>VrHv5xRMu(PP)z^2pFL1X5^0Rnkg)q&P!@NUUE}|o{ zbxl5C7yHYLGKBg^3a$7veo;EYw-k#6JSpYvo3PfMi|W%Gdzd#3vIw&xy^nl9QLW@M zZo4Ie^hLK5@U5BUjh5_FxO7Bz+qS0zKgLQIh=x)~hsGV!p?@mY0-vpIPqUA5RWXHH z9vOBxEJ+|oW+`0E(_>Z>=`HgIKW&-gq_G~8*B_Z<5g}m_zFL+wZpg}cZby6hF>EE7Ir((B^jC_|P3z@mi zv!;!dbh1w(^QS`oq}m6Ol=-jIw{;ZMThdfrdUbPt-u-1VtqZI~exL}ejv3>(H&5|$ zs02wEHvCI}(YEf7_(ec1`mvR9TSvRFqxxMe2CH>bz{`Oa_62i4E4^u1ze1^o7Ps@W zxkn5^B4?}?S8t`A(&zD80Z`|w;P1Rg?IhaK&sE&$BE~WQ&>#DC`;s)UjSBWAS&c_O z!X0xICe+gRjmX);#8iMsr3;y~)WL-#6JsVG>W#4s1bKf(zMx=wP{%^2JB&cO3QE-(>dHc|Q}L?O3J zpRnhKKHZ3(yVE;}E|OHXKgrEctd}o(N7046B2mP*nqnhvy!Bab;fR9vjyAwuwaALp zQj()jk@Ai8x@nX}xCXTZEHGnlpH&c#V1Ia;(B>ry4uF1o<-%raMiC+OrHS+lv44!z zG#@`8{PDIXW`#vtwRZNHYAS;*%LI@hT%HGUabXPRTcC4qZ0jAmODRO=z~yrt2oB)&4e zPv9U<@^$w|4+rji5T_k;WrdiCLg9D!j75@-3E9aFa|SNo9wSo5jCPPBKZnphdE1{T zyM(!kz#-zTbW*-+WT#u(vDt%+AkmMEn4?cP2BzTfj@m3Lw&UXLZa9e*ZvQIQRBJ@c zSUa&wN&&VaPuvhx0;84?U;L-P`CLZ=f+tB0+12hH2tqjLM+053kRm zyK-HOTK0}c!BVOmW>+rH@X)q{aE>e^33A_0BcT_GJ~0B#F=w0xM`Q2LsoN=I25MyA zo}YFNEX5i-IP^Ezn%AF(G0S7DQnh73Q7UW&_tBo?DH}YQ0A|0-ngV4jHIzpVRN|+r zFVKF+7g*)N0g^|_loG~B?rikjNzN6AgSNMYKF`aDUy1kXeg0eDLU#BWKxOj+8AVH6 zl}RaBq}*=|is097d!W1;dKk;iN4(EpG? zv%c2UuZ+zwE1XaRl@0xHnc@_!gT&~wtxQ>emgqv? zedBsa(`R(Ym5j!-o&F#;{pt5W$Z~iaF;{tMJDVYxp&B;{ zQJ!m^r{6x$C^vWJL=EwV%_Lme>DyckQ29@@8uO$zw*+rF!(a}7YFhalOQxoGWzCsi zaS+Kj%q}@fm)}0aq&v5ot79x_z`B3wWmOM$`#;UlnfF}OEd6$t1^Vd2$=q7(5>(@i zJR!Enm5CMQta#92Y&vO^08>S4+NapgdyyqI+WbFpJR`EcJrIc<64$u}W_*Cv>a8`gSxmAt{Pv|9o5Mk9E(r#qsf1C-qre~Q_;Wn2;;K_*=HXd| zkZt;Lv$-*|E6PT4K@tLHTfBV_e1IP@J}V|D)TO9pYTOjFh9q|UZIfNU31O{~1Gqpe zm@l9KRGopLa!%%od-CF%=kj?LrU!So8MkL;8R?*><~ONSw}JHz?9BZD6!W?bdaPPM zp6vQ6GAD)~yl}N>36%!%`b;>n(T9G0e2Vlk3}VdY;jJPPOBH(fxI~Jpe_2_jS=u=A zoPqlQos$=p8b12R&eLo(g7edJTe3Gk*^#)gGA>pZU_1@1uoh1C5!D7S@=wW@4w4rS zr#Jp10JL9MJFwU9$}}OVcm#ggmJv(pxzzQ2+0)GMi%&0*Spe2q4>eW%Z8|=j z4c=8vz%3CgT3svJ^F53SxHBK7CLr{aL&UJM@gYP3?AB9CA)1|z1WU%tXdQ57{XEZs z^S+jGJCh`u7;e?-TR^=ZK2Ti5mO0YfoMuefph)03#U^LoZh(6ixs)j0`(TLe8=x@Vog03zCP# zv3f-$_=!o9+sd>AHWuMU0x1n8wv~s)y*JGhg0LiEKivWMH9|0dY1MB)lFj4y^42{k8x7QX$)YyV?PaaESpEtq zDoOmZnC1y<-Kjoa@;RgPk?335hgZQWHROvd- zbUh+08GvBAjayfq7;!hR%sz{0eHv-f?%p$yzgRrgvEFm`%S=7)(n=)ojA1R~<4d>3 z9LhE-w7M_mH3O+!4t`|1lldcMy~RwDH% z{s1G1(q$`bn(X4#2r>)b<3DW1bghrge*Z+8@c+FO*wnC$X4G3!!Z~uKMPGVVk1%zS z#q)tPs!88rN~c7vlPPasof4@6Hd1QkuuBX% zQJgZQD7K^X!+a7v9cou&5k9XXMhLt+ZL;z7O>%D3O`Y55-H#u2x-X!G6}6(Z{3&%- zAR4Fq5N3**Mv*K8i#Q~q70Gq`G&t}u$8F;J7)d#Rlz3C>N(paJn@%?Q{H6G>asB;Y zW(=E?Md{+dHQJ;L<3sWYDJ~(Ra5xeofZJcn6vF>4gqNtJyl|I5eL$GfgZ6J*PeO9< z=f?f(@8cU!93!ccMXkoo&%94$8y@V$uC?T@b*sA&G8Ddh&A7m$Ek9Z}w&@+MX=WBk z(vXv%AYfEc8XsN~`}&tou(BZA7t(|)x_*$H!x4irIDz9VP2><}70Xm)L*J^TQgbl5g96%S5O zdY0lw)NqrEWD8d|-g%~r!7AtA`+qQ_-y60}=hl^;z0~;4!h@a6!6!$gd%9!1& zM}FIHjZ(jBg~wy`&Ywl}wfQ$c&7**v;j^6<702!f5<|hi$MDge#E?mf250_oD04vg zzm$U_1I&Fy3mU9dZx^<|dL=95Y;p!leB-&r)cpXVC-fHIMJP~qH8H^E#9t=DZ4i;; z;i8jskK$?lc-t4zrll+0vMBq&A7rXi4)=&Rl`u`vbU^Alu*YM(VNzFa_o;5=MR z^OWXfM1N5zz_<6$jw;~5h%5a#`x8PvzYm68H&;JfA50|e6K6F!>&_hheEg7_Kf)C< zdcRq`=1w3kw^6Ph&5olzS*Ecj<^d+*`mlhah(OwOH0xMhoq<@b z`?l#vAu+U#o7NpU7O&BZY>=_%YfQNFYwXjy*t$XulqQzWRzCi6D=eLd)w0d^HsJPP zbsz(11Et3atA=8PYG&;G%eeVRb0DN;wrY&fs=^j$5+3S0M>{g__%E`jWKewZRK$>#G>J-}AJ9#_DpEwUB5RmG1E9mC?nmreryDVysbzflxs`MtWYAqGxUk%~Hmw z(-~>3oB?;ZLc)rSWxqfRXDi1qJQcQuDKBD*(ByEM2n{5PN&p+t*Ogr8SY86mYbY zJ02nbuiQF4P_)(cG;S3IODi$^n~10HUO19Hqo{^ppw`px?D*7ut;9k@p+M!=L4j60 zt3=T)3C-X{rao&nyG?!AGS~!Znp6ac>f2lg$j^v|tF4j%k)(r0cv+1oa3J=z6jbOX z0)-S)y4I|odWV*fzQ{d)dHST-xKgn0aU=DCkuGBn0L;P#dJ+mmDywZQ_FFd4HMJT>hU(Tu1) z*GL{nr;xd{I3!?ucG5oo{94Ebso3)lsc7#WK)t;^%~PGi4;-F)s2 z3dfj~)n>!8Bu~|Jue)Xes(U9_aBgS0<_7H@dHl*_m6PTL{TQasOg6zuNcNGoAbCDS zitb4Hd3n3sFkS&vbSYOnVqBTKubSKSCa75(8ypa-YbXEEhJlI-UlhuHG;x7y75l4= z{$(zIdXoUXc;~1&yfUs`HWRh8_1ux=zEtI#YGqto!IV>(jw)TMXG5%~Y?U79p2=0Zrumtu;#PmWSkcI^_>xoOClKs>_#CJn|LVzsw zK(sLbyKf{Lw|qqUxC;XnF@NR7+Fnde-^b75wwHE>Vi3=#8jpUGL;`ki@;PBf0-wgb zDLWiTB&gX5?;{sJNNV5`Z8TU(yxJ4F%}WYA7l9dWq7SUJz}$A`hAC*!2g>dToO2_urImt4roJ+d-d1d}(lFw~G0Hac(OlpX%;Ne49 zFkR8*|7!|Ip^Lm$?QBVf^2yw-zZw0@NCdUNmHLCCitB+gS)Ai|&%3oNrrad`N{5utv8e7x9@cf!(Ov2KFYG<&D>eQTrD%sIOpR`Jgkyxt_+F``3wo= zIO}D>&^@s%RpVqI_Qg{8gc*NFne%t#IV?u@K()@%_%Jw_W-AmLw?#O9eIQJ87}@-e ze42}lxYbUC^;|p?8J4UAz`-2axw7abLW$a$wP7YzmK;q8Perkz8(EZ~k z8Le-snfFuUHZxCjdxfT;W%TP3S(jp!wuMvnNxMXGS%-w>vW_XuE<#q;o$no}R|%@6 zInDj4-FAb4 z1C7j{>Q4?wiFy(y% zo*V@U?k#xw0NTKZ<()Phb;Y2uYf+%)E+-j1%&pQ!@B?5ylvO-*ifIv^U`ukay zu*t@iYp0d`x>%@BcN(PSuxc#OU6b{y+n6D0TkG{XCd2cfB!y||SEc{kM!m$1tPM=a zCYj%Ov47w%lTXDfzr3e*L=>X!agKDQT7=$l;w_7l9e%kWzn_zGS^ z0uQ%R2zRxw5LQtn^nmG<4!|^zo$y^>fz(#WfA6R4ma~H87(jds0kHQTqO0epvXH4i_(yOs+X0{BQd!0cXtHjJx6{a>^Q~I|=q?a@%lo* zDb-j|K*{9w z%`TQNXgesz`}}FOGjOyu%VIw;XNolDjQx9;{Mt=S5R403Nhi}6KUU-2-bt|0LPz+6 zVa6;%{IKO-1(E@X&H3OyzF_ZcF3wY#DQ4IN(39vaen9(+;B3Y$rS_s4U%FJm57jH^ zL|1&RI{Jn#CGR%*=bgBgRiLljwAr?i;&wVvr;>FvUbSp%{YzehOLH8*c4|SX)W&;z z$_69V=f#es-x+w*bc1&(1V)l6{vxQ9_L+vHgpaWm&OdxPus-~CNGGSNFI8-=UcmaaX`U}73xb+IqOQkt=mx;XqXJAiDXS$$5_fVqy7 z{CDaJmFbW8&sBHHb}nY&-d*g#dT@AZP13FjH*O=ShRT@>LW!-3<$VUls`e zCc2pNdz@c`j(Bw~e3pWMSe-r| z8H?|(s-Rj8%_s~>)S77A$Uy2`x8u|-2fu8#m2Tt%!@cWXKV5!w5$4%_QUJ{})G546 z6pq4+ZYUaTZ4pHo)VHk%t?W9qXAe1kvxoX9k&hPpAMQC3N1XzDdgx4?`N@eA{g$67 z<@UW6B*$fRoUoPM?nYRns&@J0Q#Lc^~<#9bzyZ9C=#V^EHq%>XUyUQ9_X0*XyJZuI5AJj zy5*|>WC`Tsdb7Y{dJ;*Y_h@K1Rq!i zuWqajqeS*~W-vvSUB{!PLKWzBWm_nk8658Bc$#UCbe6kkn2>M;D3uv$Jt0b-+s@>X z2w^xqV#xOq*DnKoUjVq#v&x*PZq)EdVWvUU__q-QriWgLeqo-FA^12*)_T%lOAHecjb$$ zW-Y{p8!9hU- zBoDaa`m;e35_S+Pqqu|Bxf%4X>IsL^utTRH=RLRC0e2o# z@EUu<#?m&K-K8;u`McN}$E6-HC9|54u5~w@316AEdhb8N0&sg|(=8e{yv9Z5A5Aog zpshy-1i99l6I@Nb3DzY{H%cdPE%Ru38`1R@)|)39oEF$e6w)0zaFtzPBfZqMCjz(~2P&a+Nw?B!lm~zIjn5Nn4!IHm&3XwO zk2}(rT)2LZ_R3Uo3BoH^gmML(xqLYjQ`+zuxKQ|?0APzowwJ_~ny^?XPrUlxXxoGu z5r&U2j47{Sx+Qjm`i^18WbZpJJbC~zx??cqbNrmXjU+5)_^Xj~S6k;Nt02{yX_bR7 zm{5bHQBcXl44Fh9{;e(YeUyI#dROEKln09gr3^?ie61I6Qdr(cU!nq^51lPbq?Gu< z)~@N{?#poV@QEv;u+h)T91iSD{E&U`P=L-hTDdCmLX7RUyNV~OpgSbhR-E)c=^L%^Q*CahRThVJ}{;1HP)+^UnEFnvpbaL{#dHekMOH21oC6a?%V zTHI}3Y9-W?@dbVWqbB&UEe+(-T*+8|uj*rdU1KY}7nbYta#XHq5?z*oo7vV^u7V2dx)%o*L(MhNvtgt2hedroGzXt%Uwsk8@2RqV^|6YCXbODc4 z)(~3zq0RSrur8nKI0z782$|?|)Uf z_4Y4PjbqrNZt05_=aliPPc zA&4NRo?*ptI;~ZD?>STQB>I61*FOzjdoIjBK)a?E9}#sgFdz|aaWR}~xkr^klNGB% z+IcU-%N`V6^RNfn>Da!%_E!_J#vHhWt+v)tayp%uA)_0uGQtl3rMk`)G)>fJ3FwLg zWF$k5ux<a?lVhXaXaJk2*x znDwl7n)@!J?QFSo2&l6l97B6&z3Tr(+cptX*ky3DrM_SX#(Ufb9Bas&lk7tHl0IFOQxE5gCKw zYOHL&DzPx%c1Uk&{HZ5~*yR!nCPfOhr&9(x;Z8Q?r#7FKD7?I1jCPhrWZF26e}_Y}+mY2l zY{p3f&3)(r(!DSOu5YnMk^H-{7{*ce%UhJq`_dicW&uvL5PTN-;q(R(w?EkBYQBR^ zRC)8Z{SYGvx_j*TQ~?%{-WjS2>?c18)y5mjr~OvuHek&LVzzA~#X;;>e{SBb=FPY@T(=Zw z7P9-jFj9s5(c8gKF~Vc~K?5eC3kh-VHl&$HvRElvt|WH%L&KIc;AD5w(%GGF7mS$2 zHns@!X;=eU|<=JE+cd71<+pPlcDbPLF`_1-bJZh&V_q-2s+LgK%UZ)!0*U%S40Df1T z=`Y-~w^4Ih$8mM3Ij=!N?CD&(BuKF!`Na>(Wh3q#d084Vi9O$4SkZXXH3aI(=p*?Q z4r*O$7?1OUD&>I&l&plLCj3fl74nqhQ++=-lr_@DH(2RPQ_wS0;kA-`i*IqF-s$MQ z@|3X9C8^@b-m$$mVD2KSe|+ z+soZ3E(w=r$Jd1c`%LjqU}lnzQ*i@)x~wM>7W zNK=CBDlLKvCtb?Eomvms4K5eG|7njud3P{)|F_k8A&uwzNnLJ=u`(V&qY`(5r>-lK zo-9y={KMiYC2++aYK={)c6Fr(5vKUpy6(KJtrs4YIjCJYBjiTOjRzZ%r+!1cwyqgh z1gGUcJGx%C4~Ov4t8s&f<^%+Y$HITDs4`KYGQx#aVC@n`J5`z>lanLNB1*c{b8Mqo zhoNv0Pr`?yh%Ev>TQU(x5k6C!9}dV_6B*Fi8IP5Zu6 z%erxDIx}K}^VtYayGSu*Bsq_6qK+1Oz0v@!7 zrBqmTbP4T=R7&xC19aK(&GS^DkjlD{6obNss*4|VZc$B?~jd{C2Jg66z!srQ&d9mXJV!?l)S z(O7J(y0t{K2=#}``e|18%GH-#zhJ=NY@@;FrY!zP+gi;}aEZct2=v-_TN7nO6U-+^XS-t`CGKZm51@#h!D|Zrgds_V`uvIZCCnYE zHSs(Xa~4~%{-qyU>W%()paFPum50T9SKM=)&yS2{`VZFV>eS>p+c94U@0( zceAb@Be!PTFDy7Ez8#{fb(oiz53h3D-u|g4V>Ab=gzc}oBt>O*rD~00`fH-yv5!WK zWm-$_q}JlS9dW&XJ&)hd6i~Vc-EMa+B_EddQ2G8d~b#|ZlKza6Z%*$_zE65%DwcU&u*-*rPu zd%w)K$Jjr$ci?3VtLPo;s*LyOXxS>7V0N#dy#F<}5h|ojxRYFy0693=6Q}zZWh_;1 zi?%a&8rkfh5JdMZhIDr~mLC6}IAReF&teqLtB*T{UICUyTmxQ^%Ki6!y7q!<_6(f` z3dyQAudA3pT8_>}__yj*?7x$B{(KKYfH)tw!cd66d3Xim=aIUF-k@yjE+$OTKdlVT zx2a!y3}o09mc z90d0D&*=dz4*;Vhh09mY`B#_R4QeRv&`7%EwO+|;&_Qca#qiX>j>0<|zT;b$&c9Om zu9o%}`t6)&0k;X^i*%~rMzkGQ<@=AMVowT7zS|<_KGS0yos{j{eBG&&wgP`;=_54c zqers6N4iy^@XF+SBTHxiEEtVF&eu;u)S2@q|P$cdD~ML|*)6$K|cBOLcLD!el0h|`yh6`%9k z{6A>V2N*C4F)gS1dv*|;%OqsDqY8>y@Kmz(F{XiRy_F6J+bPRV*!gWT{LvWG&MB1_ z6WaIojSA1+j7}9}QF)8wLbR;4k@&JOUem}{GQ7nil~SeG{j>5ba#NBP&m?eo{Z3r^ z7*9N~tE%$z7`bes@Z>A0OFhY)z2C|3&k8~sYNwSa2P9+JYDHyP`Xh`uZrkVEzYdWg zOK65wa;1LtBgyl?@R_icPKJAyEn3#Oe;XFr4^q$!@p?=@Do45mRZ2k=c+7F?hof73 z5Z$bDj=rt(1ck2Q-QVOnyuMSK0fugWRY!4c5e}r4jr(y)%Dk)wW}Gu*0_W-1d;5RGL4ffnRGeH^Ml3 z9~3}%`!3P@P6aDk&t{T|_I9G2Ao%x=|B6bGP3Q`~F37lt<3mroA_LLwP=9>!dbQkT z(*d!vsSe#F@-^l`i8T%%@XiW+mp~8{3s6I?zn4zb-6>e`WV7$oYAHPejQ_>!|KeS= z_Y5a@7GOM0^COP~`<(9tIyN2IbK3*vd9?$nlE0h2e=5FF6R|!dw-je8BEmO z3eIGBlSq1AsNQt1qdt$4n|9dtACTX;HxfobSyn7P)P;?KCo3h|AC{yLpWCvhnmk~Ix;o5v4;yUQlq0)g;|D}jvAxe?(c}kXQWA3&{ z@1(F*$P=%;k6R1}kr^+RwCJbC>{!Gjimr~-1wF~vtt}>H<+|D`|6+_<25-wTwhu!vXZc^!fkbRk>|W_pxX} z(I<4-<<# zwFDYjDR_tOh+TrrGWkE0_N1#kYXCqe6xZoJMCA zO~-`acXT62gHJHumQoyYQt$lRx--25zOftpBC`M5EbWZ@ku1qToy6rbrTjG!=K!ST z@sA;bPf&$S!`5~02QbPgUGd65dW)YvU!CP4K<+~HRSkTF?;?^N-zz#NReZt!H^6t=MZ1nD3ysgV^aLe5)xqQ`iNdL z^=4glJSD3G|BDA;yUdWZpSq=o>mNrZokND$0%?6{|=;lzK_50qtRIjKnWN8E8ECin%4=D$$h1NCbfN1n< zaitC*Ux}+gRj>QS_-$ygGkDa`FlXXYS6SKmCI;xFWrgQSI5tU3=lm#1OT!S4GC_{} zXY*Q2;!vp|SH{&$Bz?B^WRsn>Y0iL#HNlJu@a&R0NAfj^d5%*1t2XX5x0-L3_v;F} zd1N!a`MB!Q=q8U^{K`J752hR$7*(ErOdCpo%|BT!xEti^F%&Tm6fCuH)e|22DnY_o zy+n5XbZAIJv*elLgw4&axQgI*no+>Hm<@-EVCsd-DY93UYJU_VecvigaW|*`{R`D< zP!LK+sGOe&^bE^FmpODSWI@v_b=x2O)gi??)ac4P$*|qAuobeZhO2Ng`+ec$Y|XRN zZH5Bje|m55k)kB&W0rGAih=_giG>*?J|#AaX&@mB=FapQcn2}%VrHk`QB$chN2|ys zFGR8@gGE;yq%Cw~QWcFj;~f*!5`o4d`{H}Qb%z_VC@(747f9t7>S>~OUb27Gt8x!7(nVE&u4v4@vy|B zH|vm%<+%-Uj2|=k%QeJ_3`66dkm!L$7n!{+b&@;R%1x#??h&dCmNTuVJ-xRHkEeM; zD1;OkuyeBaHs056MtTQDoxy+o-RCmtcGO(PxIu>_ z5wBf<#chmbg2Pzx{W{|GiNhXgEvG(?B{X1CVr(Hc(&(i}6cO?sLJ52CT@hsHR1P9Y zu;S@7%rE#(*tzpGaevTQjD__C*J#5)uE;q=LAMx&H>^CXlYn=forb@gJKooWWX(8H zSb`=N4`?M1Kr9_h^OCaxGx{Z3jK7rVXwH)Ac2lRfD>T88G)|1hyL&v?#2q}rGG^-7 z_KMy2FS~&7ASV3m79IY8Da%{Oft9Af!2r`h4ZThFbV8rXJX3|$)_Ol}oN4A?Mh|+l zmL;yzk>6ndrHzo?KI6dz^)HzptiJpD6A9*1OtnB~Jy~!RvMrwW7j|u4+)<$eOBCXc z9cW1S6X9dB#8z`Z!uq-w`k?jMNn#F|neHLp(M_(bL#SsmTel-4&T4?`5y609I(<3p z;>LRNT1ENp{tM*p&i(SA2Q+qx{2!UeWFCrO&y}-GIbtooU%!kD+*}&6-6U4$TPzDm zb)tlF1NsXx6MQaNB0qb7vu=i6R!~>Ev@x(*U*O!5U|xVA1*U6 zpO$cY(Qqp~UVpdC^A&>==bdC!%j4hfwTTu>BV$@<`he6P}R4dQ`N^tY1X&Kq637qc~;S57ZBXc zQM3%*iEEb@VQqS>cgfj{)`mhkj^8BXDyVk;kzKDk-xr)*1H?Z&YRB^Z+T~Gw*`AVh zEoXn(9fQVSP&92n1~5hob8t(18q?X5~8RY{P(CzT1+@;yz(==cMfI(Jl_`sN=cnl=ShBk zwtL^4_`|v@v@y}g8gx>0qVH*TILB*Cw}J^s`ncmJ2pKB~goFvHq#Y2^3ca5S?sRKF zez*7y1hXBxU%t##5{ttbuL39vE6ludl;MyqZ+0~!Jy1pWCEPIUiPuT6mZqY z2fsgG48lU5y#5!MioPA%*lU?cFJA&790~$V8Zz2RZ%wO8@Hj0T%hwZ`M#lRF18Tpt z(owk5Cgo{~f$#aA4CRlQw9`B6cORvH82!OVgjhPBd<(rRA|Jm{eEjltB748`vF)UE z(f^`z&A0PXbayy$ogGfj*Wjt8rJ*%^H4CpiD7Es29eBqHF;#u0Zd{P7LXhU^ z^O)8Wt1Jaix{QhdxiBp^hzMlyKqr)*E^e|W8cGQsJV{&5Y&js!&}uSZs5Dxa*2z^> z&@hIsJ)T;&@{nFqYObDc#DhT2BL8ASP$s|NO~yX}-RM7I_E7zKjq1SIr5gdxFGDq9 zd9)GWhu~X0Onf!1X9KH6L^G$(kbl$IXnDC4F+3o%Z`<+wpoHb#Ftt8pM&MCghL@915uXY(-Kz5%2=I0rC#oLKl%j5o=q$h=lxY?PC(FG$I{1-Y-KJ)mDUL(fD(Q$@!FSt@_W1 zF4>HMJ3jzlBu%`@LX}p`v`7A zovRAaQkx&}Knqj-M}?*zDh6DDz6A$;} zlwuu|0|qb`Uk1oBPhAf8{k^^hF4sOaJedWrV!$DFkGFbalIwL%+Az=eJbJ_PloulW zock{=2wP&b(hqnTvBG)B8&U%w_lYZtt~( zlZUy{7cI_9VD52@lA$_U?4Fy&zxBt=GGCM>bbGD>wXWD2de=H|qh#`oB3JdEJEggQ zqrWl9Gs=QXGl&VKF>=61vE5CcM#6AoL#;wi^TFFjr4373({2PM)M^Dh@-$qyviHus zoZHd*3Zy>iC&wNeIgXSa{lpe8D_6Y3AIQ=n_Kwdu(}AO{23paM?3Y5!E!Ap(e|#^% zKwG1cvxE4PP~&GD@t?EIlb+ujd705ECBp^~p=jmy{vJF{A1^*=3bPYoE%hXh={DZ$6C-d#qh z9o<+x%vF6LNk){)2(UeVZ7QeAZX4T->SZ`;#H<}VCNwxTWA*zpv+Zxa~}px|pR1k?^he+KTLY(l(jlGT%M74~tIKBfrzwW~%_{ zBiG1!V#Qp-X?mOdQsr1XlGhGr5cD{(zDxH_m5G(*r=ac% zIB)t76E~inJ=(#=eacQHYo?5)^ytgbout&Qjuaf6@0$a@ZRaM&YmX7%YvJkyl*13> z*JC`~9{Tb1y6o0>b2d;3VL#RZ?@;f;sXpaR$)WLQ{F2&O6oqqIXs@D;ng6^eeSA_h z!^C;GWy*Ta-S);GzQRMtC^ZgnWG5_SW&nriS6`cUOQr}vwVG7wls1+)Y`>hf9$Vv! z*r~j{IC2qmZs?SlIr<7E4@sJeQLkjf7mpLX7(V8b6cX!ysIV-$O@r>5-8QBYty$C% zIm+Gpu+Arb_DnNH<-)96pu{K0SQKLdIrfCw9cKUzMa^3Kb*_IM;y~;KlhII3zePfS zYBLWh<$mwBqLwkG$FSXmbVE}y>8z!hl%|zewt2KysO9}tM7ZB38`vyNacRuDi>9RwZ{UQCit8Ngwp>}gJ;0*IKmk#x6XNAOl+S0|7&us;LpPmLX0h=b^!{S(JU zNiF$5&{RbBSZJlcSA799+D#m;)D*F|ZFa<1?pll6!T`Zpaxv{SL71uglgO)+r)cp|5NXFBsM)g;{Pz6fTOAFK+ZU_vJ7r&-W2%@6pwg@6MjhT4j*q6`P&W& z$P`Ix-;de~js&DN^fcBmTmD70J>~_mCE@;#Sy)sOAhWPg>NB|!{2UNYWz>kOwkukp zx2AaV{I+d+_v-u6&5yf7DX_25s#Q1Xe&Q+dSWau-uH5&coC4qYmSTfr>66We=eE_i z^vK0_dgxmzj(OhKUr zRet|Ozn$RKClBsC7~T*WJ4=1g{YI}5yIT8G@X&Ijijq02#&frCXb0PeJJSs^0=iUA zt$O38Fyd6GmSOH)5SnE*aVrQ<$zZFxb-9y%cK)fx73VUI>I-Zpra`kbwNbgcR?7(= z=azrm;_}i|Qa3Bv^z^vpq)Jh0gkaFq@VV*9m~W9Zz_Nti;6m}rASHY3$u->7H4T}w?y?sIA$T`auKok%8@r6_@qyIHyZag1YM`=gousN(WEm>0Od^z9%a9?YN5kc4B6~GdCzBFQmLL^fA5v=%oybZi#fjR)=lvR zWVP>44{l~l*7Q&<1(Ec%;`0}7U2X?~hQ7jCLi%0e!` zRHm9Q==XM&S@;(&rflDschOfCO*|Xf<00~l+{HV${AB@qMvwJ6)GitZJD^!AB}!KAN+&1j)X}h}+Cw%j6G-#3b7;5YASo|U)Jk`| zxGA#5L1y4|^V$Z>ixYn|axlSi1HK1U`;l1lH8q03sPof;(H^JH<#o23pqy6f?6SEl zzj(x7T(#6$8@IbDk%XOZgLB9ceFhe`T_&{)K`u52gEcw9lq+w0hJ-HG7SistY9;HK z+PVif9Cj@|MEj4T7wiqnMosG;3zAp1R@laluZ5b}SM59&kH{k1J=*$Bu0uF+5;w6L z(q_diwM8rnM4B#%AR=rDo}%Mcn5AkyRQJc5BH`LcW2|*TeD{Qwh2}NQ6&9sI0hwGT zwO&-cz^w=Xm)`xDrj0NdWhC6@X?syup=;?6?Y-8J@ITeCA{!emoNB=-1-xvf?%>n8 zzJ5ovo+YP4m-@PMPofHs%qa4Z;R88f zZr-KpY4NgVJ@Oo>_FIuSVhtTpTu}U7tX?LVjEhWsX~Opwm%fLGXtqK-5t{j_eOWPj zuxdlSc4VXi1uzl~gF_86^F}rqxuphJe>$LjtHe2LpRod)BTnRO$x*kEEfSbVb*GMRX*bJa34+Et=jo6H}zUZ;2~GvQ=Q}%KJKUiWT0o`n`1LJS^zN zskBq;OGd2JZ&63exjU88)9RkCPlHRCG8CzUJQwMzylP|SL&+CLt=u#>*kzxcWFRw! ztZ_KCEY=rj8#9SqYH<*54=e7sBqE6$yyYci4KS?TC$p&Jk%X2!%p3$3P^DGZgCT3!G8 zk5Rn^npUDhfJYHPEfw4$9c1Y=d@_nD_2u)h6t)D&uv#F9yJ*;KdhYvhr;Yk{@f~=f z6kn0di~-|vrp!X8b7g-0^5US32KYi#=cD^l!H~Wr{k^Dc#<8%W`}?JAIk0f&`pmyr z#E;IVQzD%$KEH1IbQ5&^mp9{SuzNz+vLwF!h3xCpZ7=yHd)Wr#6mGJyw+mE z7!FgYqJKygYTXS_EFNLPKWI^9;<%pQ2=cI5-FX0#(@tOOck0mda+z@Gr>SO@?akTG ziEw8v-*f9QgL`GyL%bg>(_UP`-iP5Ry&Ha9Ii!XLLr+pHrRAob!KW3KrjU!qP*rFa z;v~Hp&k0X=#TiCKM%r2W<WC@o+Ec32$u z^)b$QSsRvQBc;W2yWy45{ZoEbOaZ@!n(L(*4DZ{iHU73ltSgkeOn}82S;@U3?mt;9 z_Q+?ZRj1MTV2uof%6IDF)}6n+Qft%S6Fv)|GMyq~D!U$R*a?~WGe41(@>a($;rH6U zTPeZ@o9!AI0Dq#hV_*C=5pzFuTdF3r^y%HClyr@Kg4{*^nnc0C{;ZLbz2)j71NZ7; z(P5_NPK=-MHYdMxe~xir=}Is%kc|<>flI-&QJB#a0$I9}f+1TMF@LhswG64i#s}K> zw?#nS#L2%(t4iA~QG%Kc<7H?XhIKk^R_j6!8TK~)l015kylGmp=;i8|EDq?u+JBV_ zF~)HRbmqdBOw&!kD+l#UCF7=tbN-Oeejo7c=w{tBqT_0ONymPy>^Ciy7?l-E{sWQN zaTAS7nCoRn&$|=b^!~<|_Rk5kH;@I-=zD11N7nMB&8XrY$M0cK9XfzLP71~NP zZ0+%Ni{`uI1||ICsJ_0xIzu_SZuR57YHn8yc=($TY`ktMeFh3adFoafqQyD?rHN(d zV7RV$y$~-K?p`L;icmn$^W46OnfdoAfkz&0*MFClAE*i$eGpDgk_Df{FQ}y#@4d`S z0_M>SE{c_FDJ^8rwTs*ahaa%g_VvKcKYJ58_RNE?e+SMYeJ3w00ms7`*G?a7M6s-x z$6^R`M|4%djiZ}O6udj)?L*$g-U5d1NCQ)d2<39x+f_5QAvZK}omx$u6?%(BaRiXw zH8XUa$OdGN>5(cE+E5}g(`)f6MhzcE@an=1l)ZDnjVbI}4))-FJQ8%O3MpmApE@A= zvN35!jTHCv6r-2>cMYq1}CYa%~a0rtSr?tG(<|zramk=*@kcMU|4&E z>q=QuE6DUFKX4Pcmyr2pKlxe{E7{9^*}*=+$m!eVN8e=OHl;;DN@c~_TYB{p z2zxn2uTF|>Cey#Jg zHfn*-qf9Zyes)bd4gSBSI4d8HxdG-V@Gv;;PG+m2LoyES-pC=^(BaWq?;-$ z0uge~@iacS39i90nWEgn5W+dnD_fyfN1<365OuHa!G!e#uWL>tuNtuwSZj#a_~OWDmNBE}k{9jYO)03hBj$ zf@wX9ynm;N_A{$4iA>!Yc3A+yXt4?uvZJB-h`zLaepYv-cDMe`9> zZx@--$K>NF{iE(#Vewvsenv9q)epG$|Lo)<5YH%*DcPEQAEY9GnC|_GtJ1XfZI~+E+)OBalE&s1R}P6N&AL-grMz+#g04j+L>(8 zJKg7g*@Nmbu0;!oSR#*Wucjc7RRJ!2n``1zYwDE&qK3L1Tu}R?#9sul*OI6Ba5D*H zr})2kAq2$Tu-rc=ajsw&J*M@>wh5)P`iy!1Flc)y##@660Fi;!p8C`4?NW_Q^;%h3Dp=^XGsP>cmC;#;k;>gzB_R5i_5hWUO}1 zLDxVEk#*%qv7*4N6t42XX433ScjhDvDaua)IZ^bVka};nkb$Nc0L#+lKtMWZp2@a^j!}U7uNNFXWr@6WD!RO>r~Pb;X#SD_R2#USwQ+uK1Ga*<*r6bWWMIN zS;k?ih6ndscx^}pL4#yeK8H=$wu*KP)Ng$z551H|i_CAfS_s=r>c)HSrZqp)Rp9eg z%TJKFOhvQb-uABDzXlx`S^3yu-s_`k0mz9BY8o+~kzDGcMIK6YcJ;siIkyEm$(xf2 zAPVng(H76sg;7u@xj7Jvb2^yWbMZX~gb~lDj;`WLbmTKu^~@W5d(wDWv?Nc{b#>{I zL09eP5$|=QG?WtR?GpA~mzbyKj=I!#4s$e|9v?7A=PH+k-AM{?Xc@W?4;S9+&c=L^$ZW(9W~q7V(}uilyDVZ>(08-eQ^iddk0V!SzpD zIIZ0hS?fE!Q*GWne2F6a6`E+m$W&DK^J@- z@j)0C-LsGN7s8m=_mYM@cNbN;Y7uUX->mr)q9q&N3gcn$$ihLpuU<~$r#{6^%YwCr zJ40;BRaVM}sj3VdIaT+!-BZGZpNa4JMkHUw6xLR31CJjqhuYdrPd0qNOk>Zi)PA<; zv`xM-)lZF)rKK_Ie2<5Sh!{D@2XD)ZqSW>6B@1F0OVW!t)Ks?~0Wf=Yo!RHcyA!SY z*G-Fi4GZ@==iUq3y|W5Y4Qdp{U+r8oK-yMD-iHx9JOiR14in*B0^MR_cgFf`6MF2s1NyS!rKZeVS$DhKJU) z(z-=G?aHW^0ul3o5?zKU4uW`Nuh+nzx)@OYf9i^QwbTX(Eq}HOXX{DDr^>U z`S1SZmF0aQnrdU#SQDo&Fl%XtVBcOzq5dW!wMp)VobW@~-#o7_lA+kOaBNF?vnqQ;tvaf64MP*|jwuOBrsBY)%<1Jd@flrLwvV_sP!D;mu)v@=x%707xSY@DiZ*U=W16VMNf+|* z(#n>5AA>iM#*F=*9_#SFX#)R1nP|sQNbL(5k?trh7o3QLZT*0CdIxFEG#xHDx48Y0 zIVY$$o99A$D6w+Uu2@WNe(AU~ewpdRksLTfa;fBv&@^y0Xpv2C#Gq_;{~tic#Z#v{ zAwueo>5-V_Co2f(ul zx(BnPs}(h+%IuZb9b17N*`Mukp}n1QvQ(cWrZ$n0K-ONsNB+-aX$sFaOBRhn{z1d3 zt`D`Wj*+#U!(&|az4E;&fxVPxW!#QC~de-*V(9m%gNOis2(YNA_G7_QZJ5rmL&8wial?W|6~8bVfa z#BBH+;7?OWToDjBLGaEi+n|c=Bj#lRN4Q@?ymE!%W1cAnSEU*=iMAbu-04$Uy?#|z z=MsnMK)s!Glj6Sa6zlUkOJKQn#}*IFM^o6tLX`|8WSrnEzy!a6IKfrs+()YI2C9SH zD>U^lg(}uWh+1;iI649%@nl7E`ywW%NAh5ETbncS5aT6;;KReP&z3-qeEPoDaA zl80Pjhz$EHV1vm*X9&0nz>&=u5|@z|m_dZk)M_3XVBTN-bG9llQ&T9j!Yss&kwW5c zf)V>jne{KwFa%Ez_@A>NSh%`x^H^YDGMaLNWS5@O8)Nc-*18J*pY8KxVZpq zIDiTYg|7bayZg9c^*qxW=_JcFuShX=7SBSYV$;zn_%7N#T&U1&;V^%%#r&{pa;o(T zJTmJ@H)goT_e`42@ilqY?qF(vWM5=#*x+9G;Q`v}gIVBW&+UG`uV7J-6I4|(Y*p}E zowG5<7p}i-kpvPXOI}OKV%E`_ zR`;SH+6n@=t4B zuQ~^G0!3VNMoIJa^2+5Ipu^JN`0SmAM6^2oouk$J)c3jV0_N{R<@fk7oB&7>hig$MC73&VBA*l6oVqDP2DVZ+xAFXF~QCtlfZgcnv&h z_D|msdfM5W=?(@MCh7Lezd`@QyU&LuS?Q;?v^z6^99TVSns*#%%o%M*&3hp(bLn3Q z=@zg4Lz?-51?TN=`Mlc2YYF4y%w#e@a4}QTA#K~{x}O-%e+%5y>%o2m=8}a^;VnGd z%@Nq?^f$m+VxwND$BB4JMsydpi)EDggv(IV9N+K~jZATP5?XpN-+Iad z+F%auzRL!#)zp>de+n!LTJ4_>wOk~YyDT!x5w1|;2$E-8QacrfqpX0ACNfButJ zc1n1ti!ZW)=>G!CrnKmmX1A&XH5!< z=8TF*|3ZU`IFFn+{JPFS!3e5ma(=)fq#3r|JZkr_#`t;(uCHgr(ck??UT~8B1wn=8 zzdrJWc^}Un($9O`Q%{?YK%d#N@WUNXnObu!HGMPClRcND@_G$D_OITg+Ubt_0XlL_ zY9@I>N%W z&$a{t9DCB&cw7LkhL;3z$DEmQ8{atwO)d>)Cf<-J?4E%S88+kIGmxbQan?b~z>HDBcH{oJGo%jjS?t%gw z5kefLYjjf9g}er^4guVMYXW7aV%$GlS5k`#b)NFR#!IS3h5V(0ibnQ@eCNcA>KD(` zrYAgq+$&JJhChD&3@1%@WBym8h6-M?Vc%m(C|&o)7ygG5K1w2d_Ga35Wb5PSMx(u( zsw|T5(S6cZ8vtL0$@%7Kb-GkQ`}Y~@a)dMZeWC6bYAof{AB|ic1 zGFn$}4>p$#|%P{SwSgtohQ);&ot{M>zko(eR2k_!cqw$>(!wm zY;~1~FO_mf+ulEj;DcgsSBXEM#TFEi!2jki4h97NZ92e#KRFhhmoE}GJtwBe1}^CH zkFZ~tR$}Rt*AVm?26S06!9|xP1+<^hGh}OSp1H8upTht1Ag*ROYY9Dtjxmczw_VOm zS0uUi5x&p0I7KR7f^@H`j1u!&^P%hG=U#YOf1OtQzV_n&%O*@Qy!-In8!{C_sU9pf zdaEo|a7eB2-fU7OBGqBFl*>bsjH19i;0Ej4+VgEi_@K0}~uRv6i$B;gp@6?u= z@&=C)hqA?fC9YnV)#f z4Jm`LY(k+U+LgnR+F#!IT)XHNhNBOTkMy5^y*KHjQ)KTLxv;E?d=1%?nUv;yiduXi zz`GQ~*Z%IfJkk<*t$&dQJBSV0@WXMW{x-`;9NSOnTrq)QDu!6xOR)bVIwqauGk;X7 zKB`+ml(ekXR8fo4At9H3BC_uwDoH3t& zbsvqVfh^X$SuZ9oz4lm&pqx=G#;#s2{i~+hBUfeV4lH%coJoXms)7f+dQE@p60q@$ zLgQGP09>Y9MBWoHJ#H38mrkWw2lM}v2`Feq4A@M!%R|K`E9^)NEikPk^+eq2R2-8X@4f<3x8pjeb5Cn^fUL$} zo9~65{pCGD`I)^3(=Wy|B=n~jES*nhDZHyASJpor6SkpoQzU-F+ID@#0a_Esm}hJu zn=HF_0ax6cx22_;d_HWcm*S7_o*6Q0Dq7_%t!1M_F0m0%9gI|fUK%0VeGB` zt%ngu)R^UD3p2Nt44YA}h!TQ_;b@M(->uDkfKiv0;}mz0Y*>Cpe+){WvID#Pa|~Kl zo{;-9!*=W%;DsDDx+9lsY=cbF?3CSq0{+|5kZUuU(x+^`lHmh+DAhW*g%l``(B%fn zZl|DE29jLWD&mYOTYsqy@N^_ItPmz3hyx&i!N0u9u?)dUwdI`E6YSnd^dsKSz zy5f{G{c`?EU=kYFZH$#v%)P_tLpsZv*nLb8n~v9deK??Cm=YkU@I1;63fKa%(7!pv z@W~Vc0^kWp=8kEPN_!m-N*j?K&-T;N$RCG#af5MLls`b&e?BOmZ4cjchwd_H3shj~X)kM-Csa?mJu z!)AsAnB2iM9}wTU;lF+Wy~zG`9BZ}k2V7UyGCiML#*tMh~Jwc9yE4Vw0WW97?_ z4f7-}u+I&)tEJg>3*EO}ocEuWzi`EXzwzAGOBGJHQ zDe&B8l%+2^jO?I#>9a>$l+xEzX9qUmx9z3f+m?<5H|s<(wqjH8nnKgGf`s4z z)NO|+S=DZ0oQgSFoA{xlGdIqpbM5r<4a83sa(#KuO@yaw|B%pva%CKX-Whkm&cnEq zmm4?a74+AgU)sJ9>8+m&0s@)4bQrrt!--8a1YfPNFcv;Ny{Ap=-Y#~=jdS-Yh(IZ( z+dh-Esn`j7}1$!CJ$E`t`% z+J3bnX?1VpG|ONn*AQR1^v~--RA~)&)KNSO=J~(P3w-)q8xHdIb#GwvVCbI#y!D4*SZ-uI@x5?VroGCoSr9TFYaV7ZZ3*^ zUjf$q(Lk{ctbjB@=FAcYV-QJF&Ngsve}z52>>Q@%j{su&l@Dh=m%f>L^lRhhEK z51r4IkjVIw;XlUr!J*R`XTIxh6X=tXf=5r)l21kt+<&*%4Fw*axG@4fiy}U2Hk{u0 zzPjQFvbgbHL58L}h?h0Ey~zgq;{J`9 zt|L^dtYJ_(&4$JOjwoXGuB^d|{VN(j9qmgDDnFfynP(|l+UDAq!#n31uQZKKX1o@b z{@$td5BR0NGknCYd-<9UO(XoO!3|>au4lTIHEAWkFFOh#j=NA`{-QORL0a^nSyq@$ zdcN(bMIu6cX~M2{vGoWubw##DBU1y2e4U%NGztNyR=vT~IZz${e%S2@()8GJMJBJB z1HK3Zk$-pHq68(ReD(YalOBdr(&7i8NG1--vZ=i7z`SGXa&T34(}4CAQ6A?G$k|Fw ztL+!L7hQvENBPcmN4eWZ|2zVpb|>p=ZC-0lDoh_G{ealBbQiHE&PLmJosowkVk?fy zbS7zMSxj|*(gQwQOd&uLXw*VeYDuL1mkJMp&^UHc#Vc&xzPu!_2{d-c6Y)=n6;^wcvl7G?UZrHB z!32u$XY~U%UW;AYa4CmRpfu=s-S1261C4D{J$Q7$+q=`YeFD06&cWA36%J+W8P!Ki~9$n`;_rKk9l?qE-bW>vhR?r_K zZrpr|hUUiJWE&D1b0~lqQ#mTs8YQE5cs!o$47FqRM?cO~iv0U4UvW{sm_kXaX9qrV zB_2%?S3wPBoA!dy!va+WnN(5yGt=Wo2&d37aupMCA=ulB5N(3$>mjcyEyU)c4Ifik zhqQ$d3%qGe@d6s>DrMoLdld< z+5hT1`|nP~0;ym?>`a@o-CkK%;0SFA?~Gh~)|KY=PfZ6CN8?Ad>tkB=;t#lV*U5c6 zmmSygWf=?vA!3pPzt9+xKmPut^Q$;f9Ynd$?|1+cVbc;5{yS|*Cx9YO|cL>?z-b(xq80`+b7Y<^d{YDy37hh#-gD`T7J zP<#tb3G-%1#z}PRm($79{HQAdX`+m?HKV&pp}o~A`k;>Z2Xr2(Z)_HGWK(4AI=*PQ ziZLYSW6qk|G_*@^l1Q{%p9*1XQs9igTQ4U}nWo_Qgza^7BNasEqkYGI6f?c~@k%wj zl3=QXr=^MZdTv`&=LjM@>(`p3Fx?-e-L-qo5A+~`XUEKzlb&i+Y!SccaPJ4^Vax+`M( zE@NAVIiRakz0eeu0{T(8n97NrZ8>IhVn5Y&bF)mJmpP)gK$@T~T%E*asdfLC|AK^}tRYau?SJ?Tn4>jWwSj>Tg zq0dy~$X#$@Tq7i;A1XgkJ|H2nqbvVFMnaNAMrTJt>Z1RO{00e$5S6|Q2??nH36&5D z$p|+W=@k-EG|K<|=>In2|19GFUXA}hF#>@)NTw(#2q^k*MWemz`rij6cG@rhfe>PK zrTsucz}A23|GxNt+w*@$@qe!ZVh8?zM+(2wqYmILa@&U^s4X11?PC3>>lC`ZlqB#xSaE$k5oxqNuJOM$5V|_50p2>m>OFF17Q9tm`v)sX z>3Etz+As$({{tiY%=20Mx3(}$%_dUAY{AaPH{!qMr$ez$0u^L?Q*lNF1X_!%|A54zc<1^6s_YvqGP5Q6GIN? zacHyceXvL`T8Bi2MRn@K)5dq=X@v^*1xDq->NfV8RQxj5x@7YCAr%rU3v1~*Ebe_x zD?jJlgj2_zN8kvkl%JPF!5}e*7+_nM@E-i){@sL4u6ZFtJY6o`p4?+0v&SmeF)Lq2 zeIb#Is%uxq?2+25DYFMT+n$d1lojL2JYul)foRlqB-<&`h_zZuWcYNY=Pw{W6cxOAI=?d{YEqQ=B27d_vd`?#H_ZhWjp0X6~2{PV8bwQrCta-`EC~T z0!^3z3DAIx#JySmO4+Ji`!}|U4{|e&dGnV>{vjnAnEPtOzOYuQ-@QEyb$&G!{oR9m5<{Z4Y{OJQCYH z)27;vd-`T%Dvs%HkJ!PE{q_%ypk=x2?8f6YZyPu(4CAO-;WQR@Gu6ON#S&hm^(X>U z%9o5fP8Y24%=~l0u$cjiK27JW^u1_4iFj2Zv)M{_#rO7+c*)5G5VM~yT}X)xaT<8gY$MK;?bHK6z~@*rdTJ#4KIf1s4VVuJEk(9!w*H5AILNDS*h*= z_gil$_EU+nO!{l#6*;O2e2gSoy^jDrtl#d`AzLvTl8|K)W$_XO8ZO(T6< zVMF!Aak*jgd9EC`M2CEyiM69s$n7m}-!<=>l7gFtvF1+NhM%2ppeceYG3Om_r!$hC zbx;Ya$5vaBQS^pY&E$-^ed~+4m6C~Q|CQO!lObtajR)%m=IMCAm*=Y=GyT`#^Z zVmF@$H#4cS|IBSUD^%EXb@G_|*-KlbW1 ze3^|*M<*Y;RxQ#QoFCRXm`;DA z5~m{)V&gxmajZ_+0ElolCf$U^=7$)rO(Tj`YnM0m3;CEyK;R(fg^YnyW`M*N}{ zD-#4s=!rdP#{FGQHQX@s*M**Z?|sj?lOR#TYhCp$NsEv|4;()f_mPx*N|!Qqk68iyXBlCjQo; zNWf7;O$Qk2{A8uQyyAw$NKUEMHB>d?23CUWA7O!p(IjC2%{096DhuuxHQOw};OD(w?$Ix#ubz<$yfIZhx= z#1+=*J8OpQeA>u*Bo3yc?Q;1*e9r4xTM1o$9^%A=+8HmYF~med!EY%JK@W@atf`}u z`P39th147@gOrBhpP%gqy+uVt4s7T$2j#FwC9;E0dU*l52@$+HlxLo4>5S;UVef>OD;iOC zA-`I;46Xgm^#mRw)^+(cU*0+Qq|Tf8m_jAn6+!BrAF<&Nl{y<8Cw@>MeR+EPP^LQWmLcx{<;1>cH-1TORq@vDvVMvDd&5f|- z1TI0;(zs#M9g<~s5*J*uU2^P7I~UKZ^p*JkK9@5q=K00g-aCw^FHilVU1Ip^eW_kW zXJEWYtg9O^$uq|rcwcIk zNoJrtC9{y`mZhj6W~rcJQb|%1|F+18Tgj3gM{RWsvZ4(hSg}@U{FS*6b47nLI-Py# zv5~iCp=3$xbAV>qxHuunaX&has4HOm=bjP5PqQ;Ob2%xk7L-^XDmBLJ?WzF2Vgnb} z$x+=L`BGZ=Pi2&O@MVqhQE_30{=>7?Wx9xSeO->M=bS3lmcK7H>r4j>ieWhZ>t+W& z?Wx74!f(ET4LBnfM*F268#k2a@rF~c>|VxmH{t(EUtO{1-6jQEi1+Snl4{JntZ6Ai?;|VBYNY%=s>qnN#OINX=D%=r0dBoDhjL}f zq&U8VR~#w7=?!&K-CXMhG$u-ZTM4N$nYm;#3-tQQrVDi$!Oa`)$E>-`m6+?svY>zX ziIv}8YGbkQwN^;ei|s1Wk1wO!d-L#xmWJ6U58})xb@i!ChbI*_PevXlH8)qyN`1vv>F?FNfFU>O5vO>F( zjjckgkR!uQjHK2Xqs(J)tLq@iSnPhkL9s56#aK3qWBkPE%bkit=8<+7Ph3;7-+Va} zY|Zs%{o(IZY?v{3u8(pR@A}-Ugipqa*bO*AY|Z_i?pXX;p@xQ3c9fI0X6U&?Q^l2b z%j~9$yQI5V&(D@0Kz}zrW>x5G-9)5nRBP5==BTy#{~~W#pes*%Q*61Pa9T}SWbg;4 z!kz?>Unu`d2zJWzAzpYj_}k4aPceK7^QL!UcR}f=2QfTXk~GBcC8~84ex2V8fSoa1 zx^$*b6Fg{3N2`i^U0J4X6#qCxM@J#7oISG)5Nn&9Sv-@uk`o>qshw{qrkWaB`eitk z&SI{&cjNa!darmWYm8Tni3%|Bdy}N_x+#CDImyi8}=2ai^@`d2vRl)2iK1EQu>tC8TD zq4$G{S2&$mn_GB!HZdMWj_P;{_As;(LmAACevqMFd=O2EQlM5=sta}-H3hS&5hGD= zv(5DOfh5A zv$A;x(b+rnZ~C%WF6rb-D1lFbg=lq9{2^y&i!pKie8SbaO>KT-`}?kyT9Sjkf;8@k zP(fQmKwrgJOn3KOfDuC1w}zdyOjKTX0#2bZR|R%xsN5uVi~n}uqh zZzVXoIVsk#?MmMueU?+{&d37yplpJ;TXj53qr17vhm^^V6PQ!}sOId(r z!~D1XEKl|qd)w0VY%G)0%aR#&B?Ivjlf_ zF3Frc&?Dfiq+R{Cn>YLGO8`ec=`A@|UCS9{_-wOqaf*>`DEzzM(Brg5ZeT%6`AOvs ztu{ziZcBaO>D4X?7b&i)TpbA|jbSZPJOctNlS&XPcu@ne(*x zmp3)=K?Xxks!~t;R}9e|R`p*7SAA1G=p#eXnZVy)~QUMig) z{`NI;k*8A|Z(1b9tn&U#>)zumYSMb2HRhwFHTwNG4fEUYV_Nzp9jG+{X+!`br;nPn zHT%Tpp-CM<$jstg5u=S7R;@Jqa%29ziLX@Pq%*w_@?mOoZ&SdRZ28|^dkUgPs{?QL z?bYx=iHyP@dNL`42eWdnnJXb8`?He3=VOqO21hjJzgcft6&+U9xqtd<#Usy@MG7*= zMNL5yp(0pu*A?g+lwErLm=5cf_f>->$6?a>S1`Cy4x36_Rlr5pPmG7|n$@_g`4ILW3&?jz z1>sM6ZKNWkW4_#q2QsX+4Jwxww4C?{jl^l$!?eU`-pgw8MK3g9iX z{RWPxl_XHqPJ$xzzRZXai@5SL=KbI>rGlQ#y_3fcbxW@BO;A&{9ZDxQ+y-CQux&o$ zEo3s&&w@{eIkwo_%+yyook~GwRboAbC6vCcPYOmQ0G3W+-dT(0;{hUu58_xh+h9=H z#sN<8T2sGDmh+FHtV>q`=Nw&4n)RZz18zT;_eVhk=v0eU=$2MykS)G3)qjwaXbE4( zHIo>hE>+kajM=SyK!&{Ft9!XO0BL0tJhFgHK61*MKnfe&5|q1cxj&7xQBz%whyI*+?E@aC1FFJoxQ8dm-l-V?qY z2h2TJobt-d*95ztK!O7a7R4+$$TD_QWf`5|QD1PZgnZ+2M~143D1{M-QRDt1yLR@Jn9S zSp}Eus)t-wBn`A@5?B$!hYi-1>)ddQX_Pw4c{l800eMk~To^?X_h7^%7@;0!=YZe+ z7h7%D-DB=STwSflc8+@RYt$aT73b1=lKeP=CjNt%X&%VDe6RVlym*7fi7RhmR1Luw z1kF4AH=P?3wohR#u44$VV?9dUS`-FZ6JADXm_&(&!6}Tjk`Ywg&tMv@=9@Hki*D%SOC#h0(+v%EEMz7J3kkMbEG_A--4E=Doc58QI z&IzB#V7Jxl$6J&YKuS$SSrArT2UWNUaWfA32@j!VV3>h!_w;pX31fk`V`p>c#CN=rkwvmq3=NcMS?oinFCgjYKB zq9t<~W81$*C#zZIW^R%KF+ES>qz-Z_YBpr{YFcO}wS>kPN?tSOcwAm+cD}-+*Wm^( z{i-FX&8VT2BRX=(9=p_5FwDx|bzrT-oVW ziRc%}mG-edQo>(YwY=rjvylHC{H>7g8&0*MA8-2F`h@Y|BK=qY$4a>YNM4m8zgt#T zw~MbqnKGeCUuaET#57KoLxl~#x8$~Z%^EBlj1Z( z(cUh)*$I2Sgmj~C8NQ)LzP6sM4UZ#k&2wIp1<;+IaY0A8Z$&@;bWizNy4F@GEOZl; zpeNp`m5PqK6(_e!fb{67)NsnQ*352wUv|^4(?p-^b`~$$HK`Xd&hNO^ZmhK8T^>ytbkK zeo*M4RupNtiL?Ve1tCZ~PiGsoF|U97^*Etqv3Tv{zPbwrRca`HH(34H*b1X8HX~hS zRk98?^sBwS3Fg2l;&9af@d09{m#4fJ@w=CMtCv|-Gb88D_I)-ejVR;S)Zv9B)rxnj z#O~D$&Iw{}&rjFSO1x=r#rj*}ecXG*+a2;p#Y8x8ps`Vqlt*BFaR}1k&(~+3=Q!vD zNTu0dq4{=;mytpfxdz-dnO;qmRw z0>i@{e!1W;8Gfjmi?Q%QnHsztjqr!^^n#A=rXz`~u!Rddx~8bIOromlv0MZ({s4cZ z1tMARAsj4oxFG$9cKv>lQKf{=D2j`HX^7=0rfLpX8G8ZLBn&)_D`jt(Rq$wc{2lDu z6S`El*Zr_F48k?d?T-D`bi3Ou6P90$kFJtoh_1zN$!$jdXsP{FL!Op~{CV5yG&_U5 zkPZGI7mSjx4T$#=L|NqIv0VrU_Lj|UMge3z0(9KvYq^m&pf_a*l;@wfvtPoyzbvSh zh7>w{jb3T7LyHgluiH&xMx@)GzEpARHp6$Xt(Wht7tDIlYI|#)TEIg$%dE-+E&qbN zb#Im*{+_3K{Fzq-X{`K@_Po(nN^5#M!m~%OgnH zw8nHFVcB9)Em=8XTK+OCCHb}B{2!5tcIbjL)Y;if+M_5j{_sVXlFpd(B8->?yU2nO zT~Ggoujg|W>7Aj-=wdGQWmG=Yfl6VQRDqhue*RH2JU_xLM^=B}7A~_r&Zl z|F)m9-3+?LBhS7rwUjDxZ@4!#*LQqf>7czO$WzqdN*Nm2lw>DTaQDqc$e*!Ki${FS z$7GxRZ%;g9`F8|^gRcVYk!BWzv9eE#FX?_HKNtK7oi8=*W7V{<>{Niy*r$)V)~7Bx z2$1h}nV}uXzol*Sy~w3r9Oc(7rKSu+;fMVnYXVpp%10e4Y)X3F;0@CGa&E;p`Jaz< z-5azLWZsl)pXRT1+cSy#KB1g1ll|Seign#v6z6J-isjYMVf6zp?mxFGcVW+zdwjQ9 zxg4RX0$IjLZ(F)azR;`2S=D0oA#Ey;5n}EB8{?-|B(zg?vngSzdd5aXn$DuHJ;5Gn z`g`QBm2@poz0JAa&X(?^bywSW2kXN7 zGQe&rz?vb>e^v%%g@I!%mO^5QWSGN()8wdo3L82#8y_(2L2(MOZf0!VF+ma6a<~~J zrcbXy&NpEBUjJ_E&ovIX;hmu+t<$5vw*Tgl+q|6c-;)St3BQel`N5si5Dn7C8z?^M9wR(M096JW1 zv1#(AytTb@N*VY3!?OwbuZi%1kX7Gu3GTaIr_TFItlx{5FwG8Gv(mMTIle$H?A3nE zwXY)-AN+H6$~9H009-$|H5S$|BN)5j*li>?><{4BR|`6Mf{vUysZ2x>A4n_o`!TBn zcSe0&Zq@=<8DhyF^x`ujoB~rvTfY%aoU$9Ie7eC`phyNzmh+|fIqV^$pu#s$(YmwP zdD=XI$TbG)xK%4Yt2GcjDU)tw>pgg)OT8nZpsS}Sq0m<*Y2Yj^VSN;NLEKrliNonr z6|l3Uyo-k60ea^1tFe0qmZkQA)}x?tX;2@^Nx%j}`Sq5PaXt8agabW2I$ELh3!_u# zWq8Jh*;XRQtaAh#PM?`_EnGG~_Q=V93Fw-f%!D2_4?SjGsSW0nE|6stfCT|I+mLoG z_QCP&+;*pYv^*6ZN}Kv7XYL0{kCC&W_z5F>J{-C)ow#x~n2XaN1<4L@=a%77ljd*h zljZk^GXEGddht4|THO2fKZ__pc97Ap>f&`8w{A@If}cWO{DDdbYfl;s{kZ-ULCNX5 z>S)9iE~4sw0RX4iBQw3uwXIK_fGl%r`z1CJIuksxUx+}uACRkIGgHsrSm1BYpE)jd z>+xw*-*#Bg!&g(SOaIUoqRK)0LLOYHbVR`B-vn;som06WJhY|-0J!#dZ5q{sPYox- zd=KvyD1B0VNxvpPKFTLM8wFd~dpLWwGu& z`x+6_*1Yd_2#f{NX{AjKwW7w`XG%QC>ZLno)I$+OS8Vi#R@}P`^~%SrDoD(f<4t+U zt6{~QuE+r>NMd;HxZ}B4h!J07x)cokFfDbZgeR^=J4#(gvtoQ4%&>MM3Ns=YiyQqp z`u-{+)GWJSx!>IWGAFc$Z?HY0i?OB8dB916-D2RgYm8ILj8Tk*RHZU=vUw+(@c_LI%d(L1sP^4(jUqs24>P6f_o%c%9;r{ z@gK7ak{be&7c{Ag+&bElxn0{hrkG1;jWfiQOu|Dcu0t5=yVVB>sz%?J3q#{s`dnunpd^ZZ!Z?;C*k1J+EbiAxjElG z3LSB+!*MFMgWmg6B@zdQoQGWe$ zrOwQ=DFdy_dlws7KSa4TXBC#w2~%YU!hf!#pq0QjgD8(m!K&SAtSAGg?dpz3sDpfa z!br%K6*-jp^cPzHm!09Wl1pd2*BqQIM#Se?O&?@4&rhOKBGAYoDgQZws$ZNHBuM>i08In1rpj*BcZYxr2=J3Gay} z10vwnA$dVW${6%l>@TnR{TYQf`AQzB@ZX`cbr(3Vcs+sBweg;{ayzAd2}yd@P3igI zGh_Von2beY+;*4vt>>~iBL>KZm;Q$15kc?wp0lv#r~wBn0ipRR}Z6(HODzaD!?b^ z)j^Mggc$~BO!(#~kNS3Z9(HTCVp{1xA5e?27OYQe&&u4_W_ z6zNbIIWoIdX_|BB<(PQD0gZ4B))(7PoP|($pM-ATm@=B29I`Z#3o{`NNUdPaa6{HM zvJQ@WhiNNVQ=BfCR<&$#)Fibxj{#(2M-`>toO~hVbN4Czu29!aS*C;MJ2he~-JN_B zI!->`(91VDov@^;)>^H9T%rx6&DzHz^a|aJWuPzle!|DPJ}}J5TWmTk|Ltp*xI8VpY~!0yZZXr&>*Ce=0YjPoqmj zD_qYgPgu6&@@rl-7hWH`4>_qNn{G-9m{hAIG!SuWvRO4nc9o`H>A*fi)@ler|1;cA zlMOg;n7vZn+R279# zy;qAPcu7+@7!dR8Uec%8p$ZI??1_uuG@6heTnW$DOcH;}P@Zrm7V7o#`0prqZI-JMimMN0rk^0Oi32lQm78^LOmof8Ntp3b5_XWutZl z&qdp40YjdiV*xw+c9R4zc`pj?L`XuZdbh5r)WLFAOvo8cjy)fk1eont_eC88+xcrE zRpyqsS!z#4nsc+#{j~e#qb6b(9;#ADzAU(^Qg@%uXCJTEC0)7an3=&0a4cD4Ub9@q zoO#t?ZJ~n(5zkOXkib+=8?m-t8TiJ$VZ5Gy+i}>+DLJK=Qz>S^`N%42d$MVJQV-x(|liOVXmUvQ1^;L6*~`!?xIBmkJ4fo*6O( zg%yBU$+MIrJj-l#Crt#|qrH>j1svjMF+psMj*OTm0yxK*IWBrx;78#fmv%cpUb?m0Ud;6T$F)a{o`Em}k%GcR=MVekJ(0k1O0O zG^r?})!~5?igEfqpE+^e%IBJ<;<;k`$3atWc%gDjnyA#W2cqdbMiC1_RGVDwbeL!7 zCYrB8XX3KF?j8s)Mf-S*E^|L3ZALK+i=8hC)h8uj0#p8*1ZJ~pku=K<`pmS>pdF5G zXTS6m8q$5v2H;awsZ2)73i-)MSyT`OEJcBnOxWi0@b4@PH8pOev_^|QfQ2P!Mm~=L_c_Z}V9f%ng zS9noSLOrpavJ2UxC|c%rtKc?c(8?eTF}s}K<0OYjA92X1t+4Eag~@n z-nl=cpk5T06Y>e*AVFm{gyth`+-_&4VU3Wa*p&|GEGDQ&#_La;p_mX&^7GejoL;4* z)l9|Oy^4+=c4p};U_N)>h?>bb;k8y84|pGr=w#X#j75etrl$m53UAjro_?OVdrKA5 zR1EHmSg8c##6-;e`dK<3m13-BXU>Hfe=zgvNJI}{F2-f`3Zb{q{J~%O*$s<(Qs(Xf z%`n;(>K7oJmluo=4-)a7?z zX5-wxWBc>gVH>D+_v{sY$&l87ECvEv(&?!#$jM)m(IjNd%dL!=W$x|iPQtmZL_&v8 zKX*h_pMN-Qwa?xH`kxr~>5RIAj)Y!!UyF5B0y8b;!o1P;Uq{ei2aiVz z%XVJ*JBX@+ImXU|VLO5~5o+!3iHt@sCqZJ`H!Y}d@P~5rBGW?LeyTkAT3tq|;f^s> z%QwqjmoEK{zH<5g-w<{Au-o%Tf;>jOfpS@2UcS(bV?TXnxITEX9{Vf)VP|Wod&xy3 z?A&CPN`6qdQ`dp)d4nXodgeIn8zuJZod<MCgl_nb|Es72^5<;uZ6|l~xQ6^Q-yo22G~H{0z-#yFa@yvsL4~ z5&+`E^t41O4hwDth1=C0#8IsEzar;VSFVZ8yDQWb^+Lk zeixTuvpu2N2hV(7xE2V;;tO(z7+D1sB}G-++*%9@b2VNy7?;&)CZ$=57%lU?PWELF zhw+7f;1kOmMj}Ob{0svqDRlA$*`C|Ux+@Jp3sy35m>t-oclQaKiwNXnHO}Szw0OU& zW`6alhQ}6=b(5?dVK?()|D+)57&Ryt(RiU%YIe15yH?1zSHcIfrd?yeKR(_Gl5$H^ zYoB%*&t9=tJ%6?H8;0As+o1^;9?dYU$BkyA!oFJznwBRVYAMv>`X5^bH>=H5zc9mZ z^wZANpskw>ZkEw%`w6HC4xOrmEmuhd44L-5Re1_AS3UFx??IBbOpOiM)Zut_IrEX& zjxkxEv)s`#w!w;N2N!1<{ir5A(DfUHG|{b(&wk(I55<~DhDAWzF(#8mJI{-8R>JO_ z;myeZd(AYSX|O-zU{LQU)c^1PVRR^0K@Z^H$)iE=Y5L0LZk7J4fJw`)fI-m5jIamj zvy^oyaJ1(M&Ue;n7y?=QV7IA;j%x^au%h0#3F>EKZpV5vBMhgYXjp%eSq@u5;^k%KkPdv?lOWa|%?y;^waYgMX# z&j}4G{Pr0tcgkzVBS(eYSR7IUv7Psxlig}*fmn@>D&hFXrjO z33iAwWA03e-MfH6oO)impFF@Sz97VTq9B3U{)*qs*_vnv3(nTBVzfpT3;h&-HfQ*yBVIhTwH#or8NM!u}|PnX8YU+V$mXGc>2( zVp-aN5o@GD7t*RPVq2N-lDpr8pIlX1y+)#!rz;ZO{}@d$6-Xzj{B#9ON_L60Z)YlP zq2La!Ef92&2Ih6+f*8t1jqysv+x*3HbLeKBcb>9faq%B=Nk=mt^i$#~==2Z*IX^?b zuL;gh7NC;r7he>}A>`iO?uhrwjS4tvh0gV#GD?HZH#x!j+WpLH5EVPi z_nXl(&7%Mhd7}P+jNewXAF7nWs?-ZNe2zP0#wzMI|G|MqrIiL5S?s>5<@H1*4DW4k zcdunF+zoa++1C+m{1ZPM83`-t)UF~dLFJ>^atw4s=p1gl6 zr%mmuyI|_7E8W)Dt6QL9hv`p7S~JNR4VV1W#piuiZM#GJGyU&kLDQpQBdep93_sun z^ZC8UW^U(-VJRWOy#7$V_061dH<-Khjm_$xPV)iP?`>!Uej~=5%e>34!0%S9{Yhyt zx9Q=$zaP!zy2=XP_jlWHjodFW4AX2;syE%NH3`w`(#-0xYbm_;*RW7|&%`~|squ_Q z&w{S`?C)48llK$wx=4Bt3%z2bvS7cSq*ZEHF8>>Gd)xj2E9XfLRWPJCHH32_U#-2_ zV8>nR)T;VDSL|LGo>|~yxlIJ8dBfE1wfQR^uVc3Xyf%j~ku7HBV_@L1H2-~5E0SgN zY5#_2U+0F?MpAnfB7K-u^BNa4E=$?J9|@*WU(EV{kPNF$AZ`E-X~9%i^#C8-G>*U3 ze}IY$>^slU&~|8wvn*E7UUL0*%>goi>lVe41734QHN(o1+<4}Ktp(kt=SHd7pAc1A z+hEFglg1es|K9aYF>bOdU{aInzgj{I#t0ta`u@);Wqt=FsdZcyz7%4^RW|F|Y&G*j z47v>x_IlRzsq3uzNAriEjJ@W6Q8D9JSn~%z z_^#)b0dfbg_~L((g`_i{I5!Mvqg2DKB$-$1#* zsq1>4^W}N5U-e&FXc)Ig=+4g&B}MTzS+=Ka3E+}Imj?1c;};o}`&{Hk4$Ev$w6p_^b%PN)Q45n^ew*4~*Ek~ZG1xjd(9@5C1t zAum?!fN^g0HNg8*SI5MqccaU?Xg$~ZLR>nf(qYTL{$H80N2Z_F*g3Sr&I9Rs5VQ5Vitq*f+|eCpbJLdt^A5Dyz%MK=o!AS_(R| zcyi4PmCoOU?0#5EaaN}SBtoaeLi-PQ#3{)S=S8_hxG>*>1^|&U(QoM6&+h0>5Pe!q z^kv2j4Oi}FaQ~a#WBF=euhSmx`0z;?)=&=gWp?EdEs>5Gn)(xhLlHY`Q~FZIu7J|+ z&Y$HJe#^xoFJZqF5TIIIb0|P2_ajXT#DjM5#rgm1%n0*H<2;^ia_f(=okQ@`s}9#A z4KA^YhBrvQeckA+S(#IG@yn^Y*48yW77e}gM)}Gg-|UGqG>ZDO&d!G5)J#{?v=ut)W?O#5xqagn~d8?k4T71 z3L|c$=8fsz_XwuPdpy>{l9gRM3oq*Ilk_F;(HR{v{L{vj*t6~3G82%|PuM-N&l-_- zekpq17!mVG)Tk)L=C2N;T3@TGTdO)pNJw@x4=hg)*sN@@_UTE@nP+bwSN+CZO!pjm zA3drlnsKq~X(wH2eUx1puEB%xZ9+P??ey%uFSb9ArUc)n=MIQi@7{^898Y;TNbnmZ zX0GjDRnJ*%PH4o#Q~{Im3w?Rp>z8Fh4nox2C>7h!X`Qy^1;QHhC)ZZ~Y{@1*)1;_z zP}DX-vB)DvnWH$Fh}3nZ|7K=DV+#LG9WQD4&%X^Ce(UI!Ei-pw(1wU;qrB4yo7Zrs zx;S87I#AynKir$^yw@HbazihEE6m~Wuf>9jbJT5=jL(Qop-nh9H#Khjq?qYrPbn`~ zLH66w;4#szUOv9-5$rRo52%-$TN$%fWZ$G8e+eoq7+@rZ`v>PpEH1%x^*5;&_Pfrs z?+i;Bz6p2>KBCH+iUWQt(tRu}^9`OVMC zP;P@aKt2|`Wm;F%MH0CJFcEn04N4-6;QXg{-`z0qZLqX)$ZgK~2~xc+(o7N(AM*V4G<%o=MV zV|VuTI--Xebroc-LI2s#3Y!1vDwrA))eE3!Qt0@nR-yN??%SImJ?%#LbPf$cQ#&lz z+d7`PaoPRgyNycm^B&3J(hK7-oafWs?Pqd*sN|~ov=PFCQ7QTEP}N}7(B>?O3H#Po z8`7h7b*-Jlvg_pG2yRrc#NjU=jA$++)(@e^=N`DZ%)5V_y>TioPYFOjL&RE4Hm*B* zsUj)d?4X3|ok?!(U%D@66@Q47=uA?KmVgpxWG!KefKf^|#<#740@t zuAA|?%1?@XDJ5qyBFRzLD*5+qIWdKyB0-CJZg4+oJExY^ABnbxeQ;Dw zZ;*|xJ=NLm=&eAX)S9!kXE1uXI zTD+HjSVA}apn*)TK0%%KjwR@M-xy;$D(Qqai zkbZW4FmH=djHiYm7Bub+?IrwhB#mIx?BS`R zdstq!%87ekB%gM}zo4tQ{ilg&c(!y2PgUfCT9y}gDb38pt=C9drK$$mLLdyymbi&1 zg;vAQCS_A{mlMBJH(CEy1Lm@Y>AIPrAwk6IV@VhBzG1hs(v=Z=R~}VIh>DnA$F$yO zGEKOP?iy3B#f=_CmX<~+^dy^yw z&@IDm`#rN3Nt|TU(^ZrTKh8L9&RPScKik@HdG>fMgf1<{upTrg_aF2^3+6vSzNclq zTPHf20i@k_ht+FGc+41d>{Tve(O*m(I^d$$4&-@^vO>!-`s}|mPWRz^F*C;Gb=jY3 z+SHp8?en9;uAAureu8qREQjioVxn(;B52hhI=R?aJK?6!Y*T$1#3ng~J=x}nW{Vbj z>O`)gYP%M!8F@+Apz5ZwY|75yo1A20TIkZXKATm5VEpQiK+(&|{fAPrM#HO?KFF+I z$rWwEoShW9kM+}u%POTZHI%18+y~W8F!%r_HquF;3y)@vs8_zPAY$@|7o215sUtjI zR>}u>@<5&sw0-a+Y@j`RDqU=n1AWsB?aUuGQ?&fI-pBDcG>ol9{D~NOd83ID(4V#N z(8(Nbj@y$FoYzdx?y8UpeZ&(c&9gBmR#sr&&xrm2_KN93aS z&&Ssi1JCUup03irbiMy?_tp2`=ef(=X|B@K`^Bs=B_9d{|GOWjHnx{}KY=dhQ?9nt zKZXC%J+Y{GBQ7OtHN0lk`x1y)Q{5Nn9)5`^S+W{B>p!u261ni#=i=QamwjoE#c7Rb zY_v|?ZzILlGaN}=BJ`KG1kI`;yB#$pw4~sA^g^F{AN0gTXYS3_{z7=1ZSQC?-Hwf7 ziZM6TxM?q^Bb=ul<7)T6x|?Q?Va!{}#l)VDQI|~W%on9cs>R8vNsSZZsY|{xXL(99 zndsFUKO5SADX)3PlDG{rPY(`D9N<88trXahuB>T(pr6yC!Pwu0u}=lRKSsx0jb7p7 z%Hoet?6y}nT;#iB$arDA=q%vI+E21B2Tva@&-im^b2)ZC5exq zSBW6y61a|>TBcvTBtgUO3Wi^gY!SR$Hda#b&60vbjMD^UF%+VDga_9g6lQD%YY2na4qu2>ul} zqY5_lfRwrHTm<($_ps_>64{#CAeO~Ap`Z;2k30K}4V`q^el}*6n1=9wT!#z1S7R1P ztkx$AOA17)rJaU?*Yh`yi84Q8Ts~37Dsm!bJ!QJU(Ve;~e2G7j)|Z(apl8@!USD+Q z6;R7#sld>Kj7B<8j)m+Q^6%}8&ua=h);(Lbb?d5(A&Y|^XfCY@ANf4fmesJ>$SA}> zn{oL(`S`VehF=lCBS%-?^ZIc4k3nvk>_3&24x%|2_n4s5T+Yy)R^!HV@)r4p|)4R!Gt+J18>m zbK1!A_^56zweEB<>sLJGA(2u19j~J^WLTzN;wql-vVvJ;+ftsAbaKndvhcnc|6H07 z@^`X;@o-FsTMN-SYg zobm`4(eK=G;xN|oo>q_RF{$;zH`>qZ>AC#C_v|3kx<4G~8wf|Bgy_yEwmPDqZ6a-$ z1!}`Se;oZ-MlluZ1ZOS?!`%}WEe6jvPwaIV4evPqbgc63^(Az_Z|NKVEcr0(w_g?) zJ`FH?6|i*mjBg~$U(#4_yS3u+kUbnM62l4|GAVmm5>9x>xk5h6Xpc%8X#YSqYxr$> z;h5{sBq8LGm8E&{yOl=HKGRC@iKzL*tuxv2u_x7wGTxR%pcbvqFV8oytO;a=iNbzq z?Y*?U6H9pW@b2&kUV7!vTOM{gz7Oxse{eHgTN*z@e;(#6Wj$0^B!0!Sd@Y=08sq&6 zbTH`qaRt%zh2>WpKeKARMqZO~@-H`W3%ZLF%N|;PfDJ2(V6^iKd`-fLDRTQ>%l0+z zhE_6DPc9}P?B|2c)I)od3?d%V;S7CB7wEi_o38PmA>$kM0&1 za=Zh6XYG2`!|Zu7a`umerxOZ;Y_F|fu4RqMa;lsHS09`9lYHva`&oex42hag#$H_* zExuI9kMDAk-3xq_rNQ$Am@&9|K&FMFdfEC~LS?OpEJ)eHK{PeB)}WSAX@k4+YFOKsKeaf>gf{<6xUQ>CZwO zmATA{4vlw)Sx}8j5i=nZRu@m=xdT>u?Ph6d8^A%0L;ZwDhduoJJo$sUNCssiAqFx1 zto&Jh;Q9n2Yrb3hX+MWk;LA}^lLZx36k^Zc4nquC=W87J9%w4RP~6*zSTQ&FU2?M5 zn9`ehe;BqS3k~|Hq$lbax#F?NAj(&P=jrgy+8N16GLjX`Tktmf?WTg#5Kg;;z4?mO zM_KcpmQg>O;!4plZfAFJ`jm0{G;tc0D0hkhMh!Rl{^uWfiKzdKT|Bg6{bCFkR^N%CHDnXl}95h4DSHOy$x4F|^BoaV!4by2AHU;na=LJPXn>_AKKYa`!sUcPlv@b;a z$DJm-U3Bm!R(bED?I1=^+g+LlJ1kGP-AAtUkMp08)maGR`dZXv^{Ocp=$$Ha9*u~^ z7ZEf{rR?}NhWQ;JptW=1ElOF8>m8=(zc+z7?6s!~p@SIgD*;aJ2C11;x(r)l6>)`^xMhpvTJ92<`O{)T;Xn*c1j_=tRTz`kQ{UM zf1PWeF!HL|wi0a=9!OdMxge>B6`Hf{s4iE1Sk(H6KF)w;CG7(C? zT}#>aE$#*`4N-o%t0(CMM=*LXZLe>)|*h)CEeADf(CqS!$KP1mZXvJ zD~Tz6;n+ZLlxnw`m>);U2d#Vg9gVYR28uU`Fw0p_DvL?O3ui(wdiA$DfF5_Go)%U(E#J|_=|8czkO1`zB%ZQyHDKhT6MEwqx0>s8KHA5t4E`dX1 zFnS1F8JN7ah|GsO=GNk~Qf1O*y82*9l}j~sV9`$C9z{#aUcJG$BmT=ru) z(CQH)yY#7b|KNQFWSV@cMT|iJ-HQzi#P%|SV|v_9i}obj_j3P(^)#8K09eseK0~wJ zJ0~f-7b~PNwboSD<cD$xOJRw5mD#8=GEN? zwqjAbQg^?C4pnu=RclNGR@eZ(Ykd7?1*3{LSWU;hpvIh9J!D#8O_dp|j(4ETJLOiP zAbC!4S0O2z;dQi@!c<028PAy0-QN&pg}yn0ByF@gvWT-8>F~)dJ9<%1-<96t@`hh? zk3}_&0~HF)R<;9cv($uStLQpW3`JYujY%{+tyezcHy2;Hb;WdBW#j zp1(qw%prDeXz~`*fsUf8HEUYUdPNW_ZyS5|W&&^yYa! zTE~a9qMY7q(p^7YT9*$_fFljA$%oK;_#f-%t#)(i?|XjYFVK7OF`(;`cV^2Xz`!4q zbv3*mFSp&b_>yCz#|!II0LR;igham3^l8De?}qReSzU-K(i!Tvea6lKnyS_EU5*2qn- z58(6=uN})QO(9sNQRvq>rCn$@FG|3Y6R|Pf$&rDI$-E{X;xKFa*!AK}?`1W*4+MA2 zKyl(+3d`M0*O`U&-pY(uC{WG|X!Kl24irybQ7mk|{CxP+)|B2uY4B|If%?J5qq)ct#)+5%bZ|+cCAg!LT*f?tVLReye zQ!JB7$7jtwrgWQvgFMdWNvn_UNWf-WMKW(V?JHL%(IgH$^xW6iAA*nCKvvGT=+<-e zFLFWNqCp3KQB@)+(--bF8o%0^Zj$x8qb!9qzx|4#FU-NKiZY)jsu=@_*am$%C(L^Y zB%jn*Mqbcv;nY&dpYA~_Lc~g6|Im-O;n;nOCC8USFP259K{b>W%7~c}W8qRipY>={ zQ@*=v5hW{G&}m0fxb(7;3LJs^ZeHn@|Gh+Dr3WSTp3FQ=Y%v?&^*FDthfIjfGjG~^ zXFnaH%D=pkW7NLf_m1C@ih=?kBjS_7MB{&Tl$;q;t;RaJxn8V2+z~xhuHIC++J9R!}%NIcrqYm@F+Pm@+dnyA2oCl1e*oq_uup25f!2fS#ti zw*q8F%FG7ZRdq9USA{i;pB1IPf{6zjgedjDht)9(gPXeWW!+{oe149DtX!CKIqPLs z>J`=@W;sGWxTI4d+@)0$&FMB1cBd4em@eHkJ$7d{)COl9 z<8l^^1os7^Qn^Ge7GjrIBKd1vK-Fk= zM7LZ>G;pR@*$uHDgk6$w90S~@BVJQ}7GHemkT#}^HC<(DjA^7oKErx?T;qP}piZcP zsf%YID@Srl2!;A_y30bx72#UYZT34FzQ2dCe540Z(cv%?j=KckGw<{=+p~wfm!`a! zEvn}DuG4;iJ=M^%E~4f1q!TuN3SzGfP;Y;=I9+SIv)dwa-8)U~o;j43S9j2TzoBwi zAM`_$JU%lP!azVh`NQVS`WK>)1xrx3PTqoS2g?5mI&d2@^iBPkfs8$6bBV>^!-(Bm zB_sr>M=Eq$IWY`vB_Fm4tR6af{8vE*%xg2@}om+Xx77%MlgS+^TLHJw&fGV^1eb>0}upXcwyK&;2nU9%*K7e)!_acIGMm;@egmsK)oy zWM{}SoR7tFW`by{O!YBT=72q&?fecO^uRRqvIRlrlg9JZ{uwL*hEX@he_Sd;?xLRX zQH9HyKukg{`Ss7D8$qL*5Lc5mj6qK}1vy7!_j8TPJyr|NkltOOBm<%+61L0Avrt4= zS=i9>r{SReNEGzYQzId-pq8#nRs*hG{MiuRDh(DbyMNK*5|-!j(W0Emv9zaY{PIrk zwJ0psl(jlCrfIrAHZJ;ptoxlqnd0eh#jja<>)e#?FyD8#L|nQnVyug2vcxS}Hi0IE zy!9P57vG5+;-YkNpE^6KCS~>xwf6I+&sN83>BPgt!1F4FKw8`tFRQcb=L)yG)tbJu z2rH9;HsXI9m%5?a=e}uezirB$|1d3s^p*v6D-`zInr4g!rmP?TwF*^bd@GG& zl1ZkJbzZc*WHGKky(9@YT-iERtF;S*y68ZvMy3|4t8zx_=nJMoNjmu9o`!MKzHoSz z9E9F4ls*uiXr(!ElhP!;yijvB73 zWMGrwaBumuVSeyI5F~y3D?j#Ji9$_*BaMpV(UF$Jxl^5Z07VGe!A4$x4XmBW1JPE( zte<0z#mYt{`OAi4AOR*-)(i!X=&;aY8&(caQ}t8mNn4j<9bsi1ZK4}z0r}Z(56R6H z%rYcAICxr~Jq?3G*FVgi6uVah8xkq*lXxhd2K{xqfda+Qgg@6TICJMfVX%M2m*sH( zD+^v_aF1h}*#uL0CG{VFb9oiHP_@()q?zm@>puNlrsJ&SY&k6M5Orz#8AeURT5u?H zOx7r)^Eeln&7R!I<11Ww?Y}b)YY}bssnFWh&;r-cp&MB7dVVdet7(<1ta5&hsLW*# zP)9@+i?b12R|EVG=P z>|8JgLzXHuH0ZG(4q640%t*_%QVU8l$w9X-YI1x2;xQuG7TKP5C+!rzHD{^!1Jald zNadN^XDrEI9^9W1o~us1Z6dRS!HN&Z<5`Z2@buvh=)5an*+t_CQ+MpvKjw)T=r;_D zSnoU6VeBCL`CtyUF{H=TLC_ADg=?2v0vX@D-fdW^al_|o%qEYQa%Wl*7qrh`uvP2Y zfc))u#%DgL3Y#xuNl2tg3}(`(ih0DS`TQ6T!mJYo=?MGm9Gfv4rOsNb6;5rSu-8tq zyMt9?6Hbf$-`oz}S$YIH z(gcNt=p6H3O{UBE+Wa2Acc9DL>`!cd8t*#ZEL`bTcsNUx!+@!nja6u1kUfT>whD)` zKr7xCnmXnxLv+nccXL*ao|A2m${wI^$W8?3=p|0ENSy7D3A`H zJYdnJ&(pzO6{!_b?q?{l8;9P>;v)e38%=QRwT;|xr2YPUWTAuMEp{5 zt#fG_XwdnKk1_7V4I)XLp%N1h>s9vyI5(;VA*MH3JpzA48Se&09qsk8q; zGvCht&h$FWqthOFBvkGFEb;Qk3MswY+OPxE;hyVx%{atp#DV4)%y3S5RNVafjd3L# z&(9TpbJ=4W$>c24-iNoanNfR9mF}EM+v_km(u=S6a2L1bjnzt;F;}51nSetc6548k zAexLIn$#O^Z9c8F`Jj%%0m4yUzm+f1TW*&wZQPPmHgt5>d>L|^N&FW-oJx8Lfp^89 z3JHEZ)|2mM-ZDG*#8q&!$VfPXKROGtiaIusBlM5G!>AMCGjIycb@PTljhx(?_@PEgJA~gN z>=3d`Q{gfcsm{xLk7CLbJ_ORi&R7S3iePzJgGde*1l5o%1$X5ILW4KPWy3Z0V2i>1 z%W{iW4G$v5JSSA?IH9OrpFA+MMrqaGbBDC_tc7B*OG4PfCxzETsz#AIk1!k<+=%&e zNcbB3xEJ$xIN6H@iKH+y+itBJ<8PQgZ+mO!1J)wna4_TFJLt{`yN~>n`MQ|?S|~*A z;N+VFImmQ;cjW11Iez=j6RbwN;EvT+16V^3`GVzs@Q3*3a4n=HYG||FZ?g_KjK+1W z&Nz~yi%JLyfMCp%UOya4E#8@2f93v^Ucc+S=>gn-%wp6&3u4NgqoVor9rpel;XB2S zGezE{Yg+5dLGDAq%EP1630fIaP6%LiZPPOFhw-hfey&c>`}s6IQ8hNL(yc+8 zt%vNB!_n)7l;ezoV_S85z%st%1gDaZRyXjWcq$GK5lutSe25RYXUnz4&J z{hF`db+LD{_x2q*-*iF{?7B1>uM$gMtwA}2MDFF1+MT)9WAE{$qr++q#6x+9VV{^P zUkNJC0%omF$xJ8(1Z&oxgmX_N}(a>tO(jB&bEWxaMkC70ng*5!=t2RIV}} zM$8S73{6WuUKBq~o2@=4Dp)vy2y5^MG^=E$?sUJaR<6-k+{Fq>{N0=05EiwhxuYe$ zR8~AYENiY748nNmPNw@==0oJ?Zj(uY6EW;l^9`#(dDq4Kn z5TuX(UP;WKdC9NC9LwuU*WT?&gs8%q;$V_w7m5kNmFqKBsSgQmm6 zVtOF#gcPbrz+KPHp=}T;LVmP%t7qqenQr4U%kscHe|A_L;Eh^V_dcA%ov_0ESyct7l1sgU_ zOkUXQ1-(` zM`^S3#b7JMIW5sORIenJ92^qA4clDD%UMVEJ}Q&>%Canu{_pZFH8W(C{zqQKD6QVU z#-Q@olFnW;uir^LG7YBE2{DzcHCe(*tJKyWdS9kJNEqUUG)aB1xWN?vHdcxm6E3-_ zSgmKxX0TI8DMdG!;;?Q>8{P83(lZsw`ZoAM3>0OQ0UYDNR}RiwzON+1au(thV{oy$ z3hX>{e&HUQGKuY@oweWH_G-lEi{F;n)j8L!mFVfeD$a+5+l4NtPF+r(1!%wb=SLmW z36ofql6%rH2;Fx2pJ`Mc8jlec>j*c3GJhd0E?a%3HnEGK`rx$)DN|gYlzMp}!%MKr zT(6e<4_Lj9Qhv^Osg{kpEXfv4FzM9o?h(^i_B<2j3+^0=9$0+H$6T;mqKOB=+BQgT zb!OXx+O?D`K&-RMi4K(xDiowtNBLRt6?WO}>Nz&Flw%x3(NiuPE2UiAcNia#&G$6& z&r|JCz-6K@dQuH$mzp9Pf1FNJ{|juVm1mYNsq=^S?Sk?6*-w}*0qF>Yc<(i-Uq?YnmlxlZq+^E`bPJa6bPFEnKNd%9f;F3s;4$uu zJMAwtfYw_d|HS6zF4y{vxmu^FR#;+c>o@tq$)x%y#A*RY;0P z;sL-i%ZZTWZEDs>q^2VdJ!h!py@AE=hsa=o=)r?Db)%-cW*K~H^wXga28K$WnM|jf zgoFP%pGpLhnQ$+r6X$c{_bmmtP}3D!7WlPm^(CN0hHaewX_X&JCH{9<Raq#WTa_?WJ&N^ud0jvvKQz z3e05plt#9_L#|ydFSp63+iYlY@`5)LDCY}R$8pE>ci-rzzdYePuHsg7gp&nlC)ARo z4nQ`$!8wNv-^xqZJDNL>7ym1yg)S~c#va$KeHuIio|O7_O*4l9pO23{0oXsZyrkEy zIBvHj$rQ2&&7=i$5)!?Ce@EkrB!$mZjl=7=_rZF9naVcfzF)P7A`4FK*FI~p^@-ao3pE&FSbo1&CtpS8N==tXfWJhcLE7c@=_xWWW=cHoJH*&D>9+}e5Vzt? zrIt7O=Zd4P>jjre8++eYs0P^&RG=(*y!xa4bY-wXK5Rr}ulHJe24_Ea>nJ0Bk~X31 zbm$r;TY;m2Dp3KmuOq`R#G7QKI;J#;Cm6UX-Rp?ceEP>D1P_G{I14*aVoJB^YaRCR z6{>c68><6bmnOMX?K9H=otG?I0pnPTNBTg`iU}bosx2N2ImO~wf=BjLTKCv}VP5Gs zER&n)?;G|ZC?#}V)pfHZ>u7$G!XI_%g7v=GVubbbc^`2pZa)0EX2=`UX^1Io|JB%` z_d$M81l+`PF=(EoM6Gs(u1J%rM63Q7F=Gy&7xEt;aCoZvMijh0pT?{N6M>_H(pP&4 z7O4CRRBAhEv-SHefCWgdXoF z)MmdGxV5w8sXzTk$Ol_4#1^XGaRSveF{AR2i8mAPtMv*g(+9B(s?b?2FKL0%NdDQs zV$bWp4*w*;WDL+pq35A2bSf-yzd_ZSDBUG;70*`8=~q}5Befa~Ew&|aUmeJ7w1(~; z(%5OyAM<0JaXVG&nI4gXUv4_G7E+5~TMa$Ey|+#Nn!<bwyt_UaQ8o24*VK>q1in6k%XdNt1v*}_s0tW|>djdasXPKq=yE;)+tHLD=RDa2bGGMIWSiAuju@EX4{Cz3EJMO+uO z?M+FEwij=dr4#LiOOgIM{8L0JN$}?@LN{AhTdU=L5EU%ME4^G-)Q{rQ@L1^Y6QC2F z$OQy1g%{?Y4`P%2AalE9F!vO0#A5QAjDv{5e)m&V{F0FeUCW@6u$=RZyEw!r{p+Vv7^Ve)!B(*?u-! zeuG*F{lhvSe(~M+Aw{2av|FX)u67SVx28$^0ljBPmr7-FC6)ATXko57k>5#rUq1(S z&28*3b3&v{n|w6!`mU{+Fk@)<#_T&#`~KT1zT3{j zZMQaC#9+sbBM}dAgny2p9W6UJG>!_3+1-1tOUWl{*ZH8x49w2>ow$6a}1Kfl3>U2oCH} zq55QBSTwmV%-`XYdDg|?j^8o5m~1nu({yLagcc{5Ycr96J12*96YZ9^*cg{yxr)4Ols$lwy`^G>&@xJ*^HP zoJ<%N1g>1V2K$9T;hG9`gk%>qzK}N_4xuFCRDrvUGc6octJCsUJs9^K{ z*^LX8KPt|u1MWpz%+QBhP_QVTOFY0Kp|j@%7dnq=NV86j$yDTw=&BO6Zk=jvhVE23 z<@OED8h33*;Z!xRiePNL9qMi+nkKbEEtbW~l4-Rbq;7`PTGNIs_@gOE?Q}4kUhSDu zf%zVDE-H}Ieds9~_pu)W0#L(@7953Qh>Mt|RI(Sv$L7q3g$Ns_GPqZvU}ANU^&ns< z5t5&935r)MrYT=H9Ch$3Y~3tb;*x6Xx;KCW6oj=oA z05jXcEjJ6oM2$eR_@n5!kzF>1KtjpYkGKoY+_jyP z#ru^=Wy3FUN`dIqMpBb`D@h9C%f5|wlqu^yXf-k+Z3&09Mxl}27=o@2BdVue>2F8C z1tkBdMn+ZNGCL`=i~{#$pv=DG6BiHQlhIsq?CZ~kvJSR&9Rze7e-J;3NFK}dD6XLG z9Vn?@&bvu!cBb!Fv#k&1wbMMKz-@116Jd3NhW_sPR4@>Fl+4o79dNB@3L`<+KOpb> zwH)X#4rW8tiS=kQZy7AQ96Vf;Tv%4SbQzeG6j7zai);^(Ba=d%e%?YgSWTrsy&ib$(GHL+&?VCZSi?Wz$!YI+WrcPACQvj1 zu)kWGS;vmY?Rf?J<(klsJjm1Ru6ln;LCn=3=QTJG5$tKEfpga+;NGYVc_RwBPANNh zI_>V^y8a-a>q*e!wISt|a*FR6tx4Ggy@|$ikx#=`!ey$~8JKl+^AgvqT@7?C=Wtjx zc+P_SMS!`vhv1PI@gcAZO_UE$c8K^^kvhqEuqeu0i2eUz|Doz)$plG*i8EroDwgx! z>};|a-ZiXY;wvr?*m1I`&a4^&Y-fK{h|s3Y`l+YyV>f?04qL^UT?a9xbR7x!0dJ{^ zk6?-LGvIy=M?0hkO3S(s;OhY7WYroVnHZqa8RO)aVLUx@x-Z~JXU{})&N$fMR;H*B zks?_jJ|F5GSeJs!e1q}e9yCN~tUZzdA6`7dwV1!0DAp&TwV$IB+HO%*SR{)88legl zOUGuRD@JM|hcID}Q;>41^%v_E|yfA_xwV-mD7l;I4(5l{o%W<7Zn>d?x69 z6e!Kcl#;zdg-$ii&LyAdjKzx5%~qP&bdE(zOnKd>!CY*eDLqg#C_5Bfb!`N%vHFV5 z41jwb8y2JE7OPW-f=)M97%HzSaq$hHZL*`SvIA6oJ{WF*COfO{o?_MYx$+BR4KEH` z!R$MdmXmv}Ll!LuKvPE(aBHdj3?}mHgQMkxqt?oW)}f=;VXb8Vpt~nVYhAU@-VP_y zlu6rMp4a}jA4ISwF3vxJD>i#}2}_q(&bVP`4r6JGL~AZAG?OtZ^>BW@ZubCh5~TOw zQGG+jC`t2T`+t5*{^)kM$0G`3Ov8M%)qgSSNK+HP>e5c`S{9GGjkY;s4>x6^q9^Qc zDE6f5g`g+aKgQc1iZfY7=>gVTg08{ooNg+{xZvsI2NwHh%U1s;eGtK@U>iNFWQ)9Z zWTPGrDms0fkLN(1u2z|i0Y@wx^~@=886@dKih`P{z{D`(cM`9NyS}>XRByy`CrBhF zvA~S|SFI7(ec)d(lcRgJ)H2lIIUl*@CB5Oh=aJ9;2LLSEOV5T5i&wObwV%Cm%j6gV zC|*mU;oo=8VwBUfk~q;)&Xb%JC$S?*zoXdCj|`1T@am`8I%N*F4V3$zNjxR%Hy#DH0<7$gO7eY#x(foGndvo~9-V~V1Uk4IHtO`Dd z{!b3T@P9Y}gfS@sUozB%2Xx`W;}m1IY$;;2DM6l@RF)+^7J}9;0hvx-32dK4wBTw;KjKJid0uHR)DqK}Evndvr(s*H8UOli*=(%!yLLs!ul!j4+u zkCH1ruMC%^nQSs_>c~-*i_onm6+7)1&y0Rj!k)A~4wGRc6+ilRB>W*2-Hsj$vt)^6 z1RovYSGT#fAwBI7ETCBe8+_)1W<7A0q}m-7rK7-D@CUL6V)?NIt0nLuWuSW&gnvd& z6lQ;%c9Bu=d-b0@%CZv2k#^{umQdV$xtR(|;df*ycY(}KIs z&IjHFIV&sKLVmWB$}htjExFpHh+CmGQoh|p&ID%h>KVg(LR&Kf#gm>W%Z5l=vW=M& zJ{s~b>|27jXYB0i%s22JQMJw6zh6H; z-4@_qsPl`8uluNLDG>rSMj4H&*^RP!;7E}gqs*?cj5$`NHREaf@#Ob}H#{ z`Cnfh4DB90X3$puVJI$ZN-yDJ2HMAA>rC0K^$pYyeIuk>(T&3x3T()L?weEL!$N}n z)SpsJ5&0Q7iacL#xc7suOGR+oE+_ukfu?Bc4JL z))Kg*x51OE6KjB_YeEj*&6Ek%lMM22*^n*fDZH~TbtVr+-){goatfgwdhc^TbO*|1X42~+;MOrCiF2r=mm zQKJvqlM_Vn)ohKpl8c&+q>aocH+bji#h^|vglRSkO_Eo!r4Q9Tm;ovnI)@nU;$*!FXm|bSaZr1V)ljf&Za-sfaG`6ZXSp51?bNQq5ZcYZ9 zxRhIdj6f_RX~;D~z0(K{dO9GkE4}KnoB%t%If7y zZwC6Sn*ZaW>O-KR$hPegp<{L~eTc?o6LVaZ5neh9LK|oF+fJxYD2lj7T5q^@6%B>RfN^(;bY-a}R;&=;4u10?h z-DE~TVoCr7d;UO#W;8Tyhdpw0mt^cUO>Lh4$$3RpsT1tGl7BfNGVe-s~l zXfM3jOuXm~yl73l$WdN>op|^|v!&0x<(3U~+1hnV^vhp_CptRB62}KbmZ>Z zBzrY=(g=giaHMaDz%?Bm^MRA9Km!>J=_}c{yi2QG|{vFahqYtR$4 zt8qV(dul#-wyC;XNIO(A8oPA!8wNp^Pu8DAY}%wo9H*+f+Ss*r@d5>ZlwZiCJXnnT zsXmeL`!SU*aaWZdcAl%X6?uT~RT_Uor*7raUk)tZ7gqXlq;`KZBQ9}Aw3SvH4|eX- zz(~cKSjVw4r(@1cRMX{8#4MH-A+*AlrAdV%#ryWP#9JQ2HPjl>RD7{B6AaolS*U0N zeS3dnK3P1R-=kF-W>Ij_o+s{S=xg@)j1BokYEie2gw3nRnBaaeF*j`%KEC6ACstHX zTM`dwVbQ&W&C+y}v}lrF?!f6Yu1Ri^N%+25K3-^kj#1=E$GLCadZW_b3MjrC`-ibW zTDlsLBrL_=VbQ{}-)hR}H+LZ1z6NRkwmo>|_M^83=i{A;^Bq~=+fsa<)^BH^XXL5+noejF!t}49qVg2n8YHf1YfYtE#9=LS6F_CNsJcjKIdKa8 zmkj1F-%{@_Yh03+hhluhS-^y|t|2CF1b+(nIWxH_6s&&KmV6VG9gnIeok6(9!Hy}x zmW5ipo9ZAnUcp5=FOG*LqZXc22Y+n(U<~D&YXiP5teMEd;VBKNScPjRY=LyiygGat7v(| zL|@WwkTsTU`ZLA%TZ2S&Zn(;*Wu^kukw9*A|8|rP4zx2vBFr-^N}P-t^o*GPe*%bC z&0qiRLMHHNp(N*FK#`ceZe__TmQD2D59D-mq&_N7Y3S<@|G$J zR9ieJm91m+jpz86GTqrDH|>O2rTR9h3uOi|dYvH?`UrF!T6#SxPk9totD7g zC4)TEfK)FqVgpr|_>azKOR&1LCq~i1M~kb=PA^YWab`U2;W+JZ_3Mb*py;jL`LAO= z51GUjYD3uQe>}7st}cW)thmEO96Pzm1sWvyaDN7aPbe-d*FS}jQ8@N&8$1`fY$yPu zT;?pXT>L#q{P!javyGJomlsjUdO<$A$wBT7`PV1Ci<0XIg+>-IhoY;wC=a0Gk5~!3iCF7wOZ{Sr8-ci|1i-Vu@(GSsALu0YF|_tE9Af!hO9W!~^U_Z3a=>$_vbN^$`8}0zns=GmWOKwl0~q;YaTvMC zIAD9H+AVD6x^z8<$S6sa;HA!Zg$?fM>Ax}W^_u%S(->i;I?z^h&>d8Kn6>3U@$&YC zJ3#c|g^<~(>EHvEtbIrK?E#s|wep$YISd|7v|Z z{U?j*Z4e479Nn|p9fztpU4MS@l)Ag+A4)Ez7Kk^$|J`#4);=qy{aOjXKB7SEd*Exb z&iQRQoTV$lYLLYu@I+4fgaFdF2$)to{2ADc2-$#FZ(($7o?os=7=k_Q?wvCVa_y zIG6xwum-L4fV=X;rwJ-J<^>k#f4?%v=z)MWM3ZT}~Evlf3>B}#Ab zlIOEexw=~$%D>qBa{Kyi=|r>I5O;L-2(y`V6(m=2R;}S#Xcsw}-bs7d(!!R zLiw-p;Wr})yVkeYc{B2<_>1MF$u$fBn(l1@YFmfv+?YYcy}N~FFjS&sqR$vTtzdTk z4!84jF%$ztuNVABrqqc-9B;A1@M+`0f`d>RG*h7&D1=E#L`bk5huov0YmWR3d>GmD z){-UW%T=Vg&f3vP8kSE_mO|O)Nm3Z%bY?9}!cW~3ySqlYm7|)E!hdBGkuf zf|KG~7PK+|LQZ)vIf4B*W5`>GA|P;2oP-hC#R+%ye8uPJdc?&{wQBk{dHfVb^gFZs zA#Y}N_nwc1@WoX#PZLIICK~rNyyDD$Uw8d70Lf$WYk!5}M@pKN?81lA#Ea_0hu_4_ zJ&h@Dqf)-OJnZnQ1UCL()f@`smt+D=9O!x?gY(j)f4J$KA@v)s2@)avI~b8%l3`N@ zCwW8Lm523qE7!N7rdu5EP%fyY29clVDN_5r<<8mD2(eD*QwCChP8;-cK^&Be>N`yr zOV+M-oWz4$5|(LZF~foiP3Bbm`HG0zSd+pO)(9%kOJdwz2yxhcs^<&oPgI#Z3@9|3w=8+R55j@f8gAe7E(nK%R zzh3&scbXt*3&34^ymw!H2Qy=QQ;HpjzaaHG)8XX*s8_x5{|m`P{FV4ain8B$_VNK9 z`ZKH=E(=qjWdT5|z4N^%&nNrG`aAvB(@)QtHZZf%xR-x^$3yz;YpIpuM8;N81ZlPf zljRZn)6Ft{e2ah6^L0LL-q}ddicTwg7tGkhyPklFo&m`pJ0H!eg-v*(m2UXE#8f)h zYpbZvKqR5{XeP(-nI|p**vr~qLa#P@MDXVFh|+=Cy><^D|KRrn3y(9q$2uWspm|Zi zQ;sFH+$k}JDPKv_sokTOU>inn@W%Et@+8k0K|f}OfL&k6!8`Gzf4t8qwB#!1YBGTk zY0VmkGI{ofuSmQkJH(5O*?9+#Xt$Gg;Z|fj??!I4lT+oC_=Ue9ptCot=W}aFRXTSj zFq%i!1s%Xbt6(PkVJph>+MtS;%?TY!TsMS!h||rkHR-j^W0@&B&|8@MjPf7ghuj7@ z#!7DBMx65sRZax#Q5yK8eAyiNTZ>ZTkh=@-dGq(<^}uTdr$Ic*`(0kFd!;Uo{e#fz zC723{M&BHca_INOyhx!LipMqKm}@+BeDDr`*7~?e>zKfUkK-tL*E~H@E|vE5;yov; zfwIC^!$;DBRk?lLIH9XEq0e)7uv$|>{fvW+L{w?Hch0w8&i|~^UHhHcaN%h?F-}dH z@mVfPuNzG`vP0CVq@iwiyR@XzLI~Lk+?CsrxK+?x>Ek@(N=SN!F1D<>NYO?NNYNs9 zgG!B59x!kOI^f*B{zWud?YI;n&U&L<7?XiCC=%spLei4iw>Na4Q222B=?f_N9{EGl zA)((w6Yc+#o6V>7<}^e8W_DQJl`^Gghyg&*VzU<*o%xQJ>Li)*0oV2jJ#7bGq&Z(b zCIqdDy*wv)o4#|7OxkUSbKPuf)(tU9f)P1)4eGjP&KF}N4$Y+egKxv?*rYxoE zhGdH7TWCK z-&9qy`uVdS>yZPc4%N8={QdjhdcS90ay?Bm2}x43;FTt%q^{Rba3TB*APw3S5~xa#$|S& zSR}!U?~D-9X^$`F4iU!7S+9@whk=J!$3uU2INN~@Zdw8mnYr2HOzs842Iru%WD}ds zzgi=9jgW=v{xd9JT6&*350}$lm47=B+jmlLxrY4)CXL%zrF) zUmTFL898fBr_HzV4d(3qo`#BUjQdJv_%(`F^{y2?)g;m_w4%~yfes>nU2Cj${Ihyd zUwWxJS};#r=#u}mK(M-&Bzg(+Zms^}f*sghe1772eVT_zvHybgRQE=HHHzLJxc^eP z^HRN|yK?DcoBUCwNRq8$J^UpkMijMc>r!TP-PVVfTWG5EGCr-Y_h5DQi*?IuzT;Kk z->cfohZ@knpIz~nD{r%b7kG+)4h+OD`L>>TXQ2)U!NH1>jCyRGe*be=KjI~YX6wU3 zixdv8?)Jkz9`9UaZ6kX9!Ne60q?PVX zU?G#6hBu;TB}!!U18SzYZ(kIPH)slU&+oAScoz%;L(tYaHP~VB-~U1(mzPVu5RmIS zuIdw5yoLh<@53H?g5X8ONbFv^0Uw{bEAR(|_r3b!My{EMT>0D{t#k4Y(bsogm8;gE zc|b2c@P^&x?qx2L$M+u(rCp-`ojhiTL?Q(%9!_yYvZzhVuNL`#8vw~FOb9m4>xE|i zG%9Mt$r!%h9ZYg1u>#9{5AA##;ivjN-`}FLds3Yq6*{hOj%s?J0Id*?6{+7gE`7DC zZM5o)J=|=)7T0*)o;g*;X1`Cjes#+RNi)wZppvP%tic5?W`?d+=?oC~37*!=ep+}) zLNDQiu^qQterG;Z{OtMSjCOb%W>i)QFQ?a*MFge(vjo*0nEO6@V)iipneWNW9*?799#dH}QjMbP?Q{$MT!G>-a z4^D@u68oj(%%P;&!td_mX27L&wgT%>fsgZS`b|kNQkEf=OkGJv(g zN?`^&t zeOYK6YR>!^Ig4TIHlvg=eHOzgy7EpumX5-`jI`=^xD%6^qw#d*W^ssqlxv)fr}{qq z@qWr_VfQ9^MYJ$-L=eo)ZWBa4AYXbemkZD-+$aXLX~k!z>4pf27WDDf#L>PR(>N}c zIhvL^Zd-Cu#M|F8IeCkmUq0fg@`}tHbvK$a+A3jiCuz1{1Tb1LpL85_Z}hCpnMRpV zr;QM`i6PQu2NHS9B zv4tDE#OZ}Qa7QMJ(-87!?$iHb>n)q&47asg+$Fd}aCe7B0>RzgEd+OW4VK{U4#6$B zySvl4yL+GBtJbbMRr~!5&xh`N&UuY-sh&=xhFEo7q~EkZ;bHqB<5c@(m!GF^eBKUt zFUX9X9Eg8bXE|F{NDm}fEdVkIh))zRhgW9Tx9Ru?aKdy)Z~fY@aW+DC z%aXCN?jW~y)@O1$zb4aAfBqio$rQ!jOK;@OulveiJ3Zy8JRIZ~)3t@ltWklVjJUaoGEDJt1{3yY@XDVtWhnC>o}zOa7R%G>s# zb6a00ce{#d6Q5Y2bUVIZVd_{^zQ@5P3s9kuWiJ>^@)$9=m$fBzFW#j!b)Ii2+3`X0 zqJ8u^;)P;u3M~iH{LQv#orvT%twc`yl?iazZKv>Bfi7AZ92Y`#vAW#V3aR`9V*U<% zch9<-#H4tKQ@&?l)#7W#NqbH@t~+5-YBa1)^n7AK#P{samDhgz(Q=PO?f|Xt9QzLAbjZin zvdT7R(m&9+`rr)&(Pq72;ee_>S)zcg?l5$Kr{PeG#d#f>S$`ZYexjvw3=)by4FPjj z-Mp0i;2V`+s46FD#NH3F3Qfr+?8gyM_4mi67g^X6YaDMLZ>1^vEjoj($*R+Kiv1xN5qocyY(fTfEj_{M|-KjpUdGUCXLEpE(Jt^ z_;hTwat+Gi;t&&~>>u308;}S%(|GMmO+4xt(ZAQd?Ryfq+}zd)H+7)ASxObZjoYy2 zot-D#`mM6Pr+FPm7Am(jR|9THJ~Vi1L;q{DH4eP?-w9WeG<<7oo2<*qJE{_^TS;eh zEl(zCbjcmk`|%mBTEi20F)ILeRE1W1SwM?a$=B0aNmn9R_X_S&1 ztaCQW`a{%j=R^N=U6bSS1%l0E&n0*%7E3r+|B z5`zlML3OMwMxBwA_g8iZ;ELx1Wc#uO#P%$OmA>*T67d6dQ=;2lGmCB=>4{?8rZ?cz z&|V9S8$-Z(me7;p~2# z{;-_5Y1Kgmt)+lQEj=-&Ac*y1Ovja7O_1M0!IL;Eg*R$Nj*woRRDz(QG&JUY;lMoe zS*W$9UxcZ(cKMtZ-TmQtRCdjZJF0jM;)Wi_J%H}Iy8vJR%HKc@RNN8RM)r~4Hh&VX z!;B{=cvkrHVW)L1uDpF2b|*Iql^OIlB2u@Ld7TN$7lHD0bD9Uq&Nuic9>u0SY3v(x z&w}LUmw{JyTtb0e3j5Yv?`1fiEjz6T=)iLksE@eimhJ;k=Ls+rFquLIegu&Xe%SNG z*mt}*vRIu-ZgG)tfdZDHeb=my4^rXJL99>^#KWIPfzTZgE5$%#sV|@1{dh>F)Q2;} zb9Zb`UMbwFPj(5Uc9BYrX;MVDNs_O1lE#|z?Fi|2U)WEdVK~2w_`|Um?-;~FaS|?= zp?>ZtY!rU%Nbz_b?uy`!$Ym^)iT`3^9pM=~7@1StkdZS*=jl}94q+vqt|ID0e}Xr3 zc*&LAr>^;>mo0l8UY5d;K<4)I|u?$bNLxLSBPn$g` z#f-xL@Ry6;@h4oENM_p^fFx#OaX-sod6PeXD~t&*56=@SRB|~aCS^<<9#E2%mQYR; zf*(XkB5`x`%BBoR!b`Uk4qhpgCWzqkTy&AEN+r#~Y-@ui#*C-Me0$MB`sP*kFAtvE ziHoO>)mz`#IXNEcp)A@T{d{Ir{O>RyTUR?i*MNwSN=h}|UngZ|s`G-CJATNp!eYOC zM3nZQFpHvW-EVZ6C64BVB2Jh6i8F!#HBG)o+$OU3h0HfS>y)AK(zVvAR~BnxD(Yx29A9#)fey672x zeZ*m&mVKTK0lDwJkPd25oc2V}=TFsoGhCQsgFiF#mm1`rp31;-#R>-7f7G=$0^dr& zMymwHp+IL)WhS2DAi`fK3e?iw6*rWJOgY;gzPxI(m3*VrplsM4@;sm?A2IqX^tH(u z*3ts4L;ddnDj5QjAaD&T9NW-jU*K@w3O@=PahO_?M)n*!Z6fkpVEOT>&{Wwsl z2Lqo61&Wuyhy5M?$!8rJOgMm{N80~Fj~Jd&N7fl*Xx@SOR1M#0vQ?yrvjevArrT1U z_EJ17x}Qcl|5fcx(Y$VGU40IC$D`UvMn24@4WPN z3k&G(ovNYH;>d%tqeHFMjauJ)!-_}T%1&n!^_~6n4c~n)g3-#1jg@)VLRA)zwI0N6J_=vxn7IRPG&Oj|J=^n?{2{HMc)$W-uzX3m8&kR(~<^3gWE(I0z{sV{1qFvgXCPdGg}jt)WjE zur8nJ5lNP^-S^BTk&PUw-24KL`6$rgN~u@$Gd!3iJ1dp1=!|H6iw@Hzy>bX8Sm8p- zFeQ>S)KqA2Szn8z{n=#a+K@B8T)DV4HH9Jcu%l_A|6YwJj_{&9AN>cEI~li3xMf8o zoMm2l0;UhZ!?;lk{q?jzYpp%<3n4~tK6!%WumR+&*MNfqSp_i%q}21 zoVXl_RXXQg&pH*Et1zv_d4(~1dPgWSX!&lbr~euhSH^Jb3FrE>2@>gJ>w!cI8Y10h zo2kKqLIvIJ@g0jw(IxCO^UL^cuOG`U)Im5To*9Kg8ME3;x5l;Ty&I-YFZ?-%d)t1E_NDT$`=BzXwzO(CM*`MW(W{h-%kO?Ig^N z_8^tbeuQ%BUmh){<25sDwlT^`hHavww+Nn_A2j$&B!a;D{T6B9%G-&F8B%0;_3|B9 zg892}0V%2byc|Z&Qd&dRhRaIoS3&LDL*GaDTZ%EL)C1>sTB;AAF5u(;b*#7nQ~WG2 zX|*v;!#jDidan9|=~Ei3DD;mJx2t}e?4SO8#rLY>ahji9DZb{30pvN*hPo&QDbRoR zYNk3)iyK$q_sfs7AujSm2W(w}UVn(#qu6Qgq?;dpeaq7LEFz79V4s61`;5bG@0WPT znmnc`0=_~fu7~fSKuHUCCwiTg&b~-zgzpPgk2v$|jyQqwR5|hrMwf}K5&2NO1aLNf z;_7K)aA)2T=d(^^ppKSm&XwZZu-W&Gn@O|+dVDmK-37IqA8^{d7?Xjf_io1c3lc_J z20ZR0Blf6VfwFwA`5v5Ndlo%t1Ez7Xlu@&I@H`RyDt}jqh|yj`5WCvnk3G_H${Yt@ z8#m@L5G1ylu*rLtztiD|H+q%VJc)xLE<+nG@T3%5;U?ZDHS&A(UfF>aRYKJUsY-hNd+{04bL!6dxU6T zbx@)iKdDxbR*$2qBzj}0c)V6^;u5(w+jYLnm`2&ak5k8bz27$a9+$a!cwiQ1mqb+m zD9LB3%E_QNM7;MN&a64#6-!6FGZ>y-V=<{QgZp)*FK1b|-O%pRJ+}Fc`_*t6`>{*7 z<-%IltudWQwJdh*pq;ui0{Ei)mgTnA)$$|R?!i~974TP|K$q?j`_66KFs93Wm6}eQhZtZgZv2Kzl>0pjnomKqPY6)nu z1eE@HJWuyQHTG9Bevz&jDQvmmm;u`;q_0xt*m~b+?#izO*x;OabyDJC-TegKSz7cz z$@jMu5g5$LGTEKlr~q-s!H#H!IcLl2tZ@w_uQW&q$EItMYY371nSdXU0c8QfCc_^@ z0|dDwH*f5(rkH3!sEg#XBNkFwX)AAJy+UNM#C$@v!1Fk0D=G9eyo~@$^BU_}b}i~V zpLl21$ph{|%_auMENXHvr+LhqHO9V8eP=?%3~W+GF)7St#E=%Kpg4uMJre z^)$&um3D6)1y9#$GlQB>T<(0VdMiOVw*~qsu~B)r+U(O#JVvXAz#Cu75fh!}o=8|l zI%Qw-!uv?dxczVv9J5LtU92L^8g=}-@2i3?W~|Z|o=t5)lG{rU$95pYm3M={WftFh zIyG#tiuTfO5r9&YqbP+OTb6+4w;*Tdut7W}E)80C|G7KfvWI#w1G}o>pUNoBodwLw zrSDC$h}o-Smt*!?ob4ME=e~@scd_q=9x^_gRaZ`w2J3Q;%?fi!g2tOyVal04xRj4M-BeJu+4(m39I&qw+kvM0ruEg50aH;QbJ~tfi8KBSLEQFYGopMm zz72^2|4UhI>{hMvwe zPXh?J*x~Jh6K$rQf0j;P8$)`lNv^IVTKjm4Nfb>c$m5Nf2w_ST!ux0NS#jKP(f9sX zx5&|DB%QXWGz-Sgw~6%{Q$a;aAau>>Bm%P36HPusdtUs5K15sm(FlqnOmo1+p$^5o)lEY$EX*} zL{Skys^#XNc}oR8q4CXdwJS9I)P2Y@XgRMh z(P>M8UQ{O%ofoXk96!Es+kk%PK!WvJyy0#LTcnL-%j zspVtyt-@+qiS`(OH8VcU_DPbC2lDJHrowZlk^r#xN$lY=rfP2)O~= zJbQBKa1&HX0qw+gLR(+iU1Ii*j+6{G+%~Y*(kLg_mR`n=osZJI{q%b;=iG(#jJ9H` zq=vA%m(g1XnSL)u_cOOk)p^Ue&o?u85rMPFEeQ>fs)+b2Gj}P5NwHIXhN-av4*pMu zcn9N@Ri;|Sa|X=N5FQ6E+?>EfqIxZ-`<VAna_27uV>)I zj?}&+aXGq>{*~YaseA2GD=p_E;Bdk%{}(8(BkrA*8|sSJuZBxt%K^sl3cY(1N#vhO z`;Oe%afC}D+USALon}5nBBNHtAZCfnu!aHSJNgm?2!s)6V4>rBH>oqs4QCt(ol9~* zh&fPRd+pv6gs|A@aCyu7oK6RT4@)hGOQvfigw@RJNu>J+Gni61VZPvZwY)kOcS1FO zBN;wb?xU~M@j-NqnH}S-mN7SF<@PLggRS7gFkZ=3>0}yrc<-7rJ@-?Rgx=lL%x{^GB}HSk;%hFwW8&P2qt!m&Dt*n4)3QYfQWwJXV>#UfBY7uygU63=J<1lNnIN>fst;dMsOsr;qry zN_3DN5-WcwI#+~1wgeQoJ!LVWKjc5SQs4w}rvn8B@DWR?n~!?VN7P2@tcs;T*#us) zJlzUhh{u^|&`Y}?5xYBI9?Qi}_$&>8f3XVA&(&d`k<8E9!Zu~$f7IFFZ7*?eeEGq) zy5Q5y{qCF2d*^}9(0er+dP!!Z)A7`{yBC%gAtefR#1 z=T7Pt&ReGp&yKexX7JUN@k=nz_^xh&mNxb^w+Z$^kY1m$ks2V&;jE&QVzKfTO%gaV zp4^&s&DDzos9LpM?#7VZwl1FJ zb%JO~AxL%`@jGH65tnWteRLjqL^OkqL9TXwuC=C3o=b#~G3Rq^gZOQ0eqT__z?M`O?(JXuf$BHS z?CU7E(dbq=LsGZVwf7aQg$;y7I-8R02FB}7NQk{iv-ZD{;q@25N*k3&FZI^9hFka2 z{lB`!D;ia(&?qd)5aB)y$z9%VWgG3kg+4N$&qm%hX+*?MN_8K9H6UgCBnz~#c-U!F z#WJMkjU#BDsIa6lcEv);y(Zpe%vux8xxAVs@(x&iVgwP{E=dB16+p`)p4aQIQ&z{` z!d3^4`!4y)!N{pdi>8p4a43Llws+qBX$arv9q44x^zG4?A6g{G5&`e*GZ6XsTVHYJ z!CMsJT-Lzs9zNW)t22afj(g40x+l=NAKHr+x33G)7QX^0m?o)fu&O7RrVeHnE3@+{ zAie4e|5u4jaI7V&_r%Q_Dp}|e`4wl?LT2Ran|{ETQe;d1SWE1TAXQ2oemMjJ7hmf> zxm9pj4f~e)C+t#Q568g7w|m6#=dF+*^Z+Yu$9&ROQ1qL3IPZ`)^2LM&h6IOIDBBBP0T0Lgw|gM+yXHw&Cgt z1j6EUS%1u4h`(81zgte!4OF*)*>(XooZ^(PJE7$=0<XiDxuhteFMVYnEDoo%L6Uur_$HP z=$U^O0G!xX>#V(5gc64AoXFgAi(B!qk996vx}Ia--HE%8UxbeTEH3huWl`8*9H@TZDea^h?imI0z4QTHH zpNtq9>52X?fd ztBA8xN0p#NrWueO#HUE!({!-)*BHM}`}YP$;ZhX1;2_Z~0vAFbV0Xtf^dR?pbjD{o zD)9T?-92f5x3@lV&W=kwA5Ih%!oLq~`ee%=sxph{rPUu}Af_naxU%S!p*^H&1VByw zUpU!%if=O5#ijRk6F1Qb^B7w?F;`W%7bK zwoO_@Md&jzykP^B7Vaq~l3{~9(g>jtD7cWe$Ny^=L%Q!*0f^V4$Ry~X^iauQz`!@I zwCe4&JyF=GY@;qXElS-F=0=XI+*^OI7M&;pidl7&VYxC{SEP?6FgZ=8~0u@tMHJ#A-}Q_t~0b^E>+ z)%m*~ehcP{UbA48{wiB^+o`vOf_z$I&mShU7~|$nQT~0h??=5%4!zM`bQMuaYB*oD z)f&buyDX1>h@w zfod3LxZB0I!D=&)o7z1ucQ3HVn8AH^dC-_ngI0Ad?B@??@u0rvxY%#mW;pR$ZdC)( z{I_XRrn$Q!kT9uSyX_*P;QPk-+n{X3uu=fe1D9zKbzEkNMsv-QBo1Ao=v?!|otB5l zwcY0m{NmbUiB388*?}|b7w7U7y-JkcGJR50jiDrly?LLFr5pPO==_!X6z2()*7{1# zwJ|!hlZx2CZm|_hl4U=sTb4B*#YLV|9j^UWZo8MS0~YSP*RKO+5ITUzp6v&j&9hn0 zYSV~Mhhv=;e}B{`@S-Yby;b~yAM~pIq}9m-5kb6zLX5lhxMDm+ojHslp9I)vR$z%C z=E=w-NNBh2Dv#**_2y!f)&@47vfauWR3B|@XHT!9R*hbHM3c=bIv^dw6Wmd@+Ucu58Sl1TcKFrYiPfU z1D!5JtG+iY#!o<093OXL*jr_SrX*d2u&L6!kYM3~A6WkR&qHGTiRqRFOo4Kh?crIC z`Ka*~I$LF2$zC}*n6lgkgaLB7K9smP)iMU|HGh}}7?Vx~WqBgeAF)W6S_Erq3r|P8 z2eZ`fv0_|(ZnZ#p3I7Nakt(k=j6w8+$>+$LWTaMMp7$kBUJq&85eE`^Vim41UzLnS zV65?2`v6yF$Af2XT#|GaVt(^Qn~WzZkD>6Zcx~fdfjM#9m+FtMz5SK~tBw=F520zh zZl4@G_l~go7GGjsk10%924Xqg)-St9L+ zR3?g9$^|@s+0jPfGX_$xKnASb?9zz$YS%*huRx^pP2khv9WTTRxW9KoMB#t`W*{Fn zyOC$X%bx<>_KzmaLyS=S;OsTXV7Mj`5_x@Q!aC8-F2aGGzn?k_j*YNOnJ<*21EMal z9^bT%5^n~t1P{&qyMRCeNxn27K2FSnE()ZGHT0`y!vwthA}AuMh#d;){WDG0uKwzkiDoRWETPgc39yx2S@Y2`m zB}-#swFd6;eeezq$yRKuRIknj*fQS+)M8|j9gPh3kw&@QIIs(rmRGB{IjV&JKNDX7d|*fq0R~D>V0#=8?=8UZtFmWjuPD=|50%H$ z@6S>Up#ZavW|4Fg1X(2vU8;mwNw#V8d8!TD=M>5?Z2(SGQDQ4e5vLY44G&}{G#Ld5 z3zRaYpFR~UM{5lS^!$mdn;>}^c$O^K^-JE)tF#HAmbo=dlp~`sy`Yd|MVOomq9NOWV&_S#c)ftTP2fFX>3YiE>yWEMPA`X+XYO}iHKUdn}LXbMzu+n#`a#^LdQ&fSxYg7NqlI*0(Pza>rcLzD?t= zqm9R->1d26e(_K&L)I0de3X3yQVKom;g=dodWsZLqXKtRJiNNEu=WGcF!QvFj8W~eBV@CC)SVuyyfba4OAg18c!bD3r2ofg=V{C* zduSb$Psb6>X%1YcfswfQ79d|Ep0gYe zk}#iq{dKmr&M}su>Y2%yj`93A|K~!{DNuQ;-%gEq{p)hz8`6 zgG!l!s_MBnjKGE(1cXw)YQ=9Z5Zad3!ikpoa?`~&l1lFuBcT>Lt*H|nT)j|AQ3d3Y zgfc{rqb@gqJh;84-b}wfmb2ZtAibxBuh9Le+unXVf6risAW)>wiQ6tr z!mn|@nHhTq+D)vpag^2#69P#fPM06k%zE5aA3@zW+4gYN2 z67wp{PYvs2r8JZf{r=tkjsJ1G59w77H#JvW;MAeX$V(XFyE~?h5$yWC(ZVi_ z3`^$V3c;edwQmr!JXXQUTT~sS?{HqpV*A6UYyBPlLld8D zJ!KxIpLN!(l$Dtt%u9FMwRGPV4H(zECoUrOHe#FL$dX~lm7#O;kM!?VyMd)^#U{WL z%Ek^y63aIysQ4lG$ZhGCPO;+MX`cCT$6#g`F9hBM|8U#g z_2}*>KZ|g=4kfi8&FUFDu)Svy?ZXo5#ubnwA50Pl-vQ~pBGZ~o*9xU`RP{Afmsp8O z0TV?*K(BC*D^!(69Bil_z&nf}F%gQ5#2FE`@J?u^2#Q1aj0$uuoc%HJ0{(9@I7WkN z@utH^ffxV3^mN65EHN9~D1T)DWFP_DR)H#4e6d7B?Nf2nTi1m_VAK)QuFq40BgbO3 zzFiTwIjh@MW)q{)RnFFNyL+ed+8EDxi3Zls_2entK338EBV|Vo#(F}Fl)Nnh5#;_y zXjv|Ey?Le}jXAMS*Il!v7)uhvBD7A$WS4NAapdlMyI)wy9Kja#Fu!OTFPrsVZ zvp(RlB#IFJoK}yxQZ4MFoy9pph4uTy1}=MPZqEF~l)Ynv-n;Jte?zzDF>`?`2CO*m zScc}pLqQ3SIydV$&-e&7wK-k9sayDv&2LB#YuQk|Z*fC66N-FXaqcD7;BOKYI3yd+ zQQxSbA|Q@i*l#|L6kF?zo{IIY96hEtH>WA z+C2j|%p(|Q1YxWN86K4GM6~>I_Akyri_QLOhPA5zs}|M87$JknPA|8%!tpI)NP+B_ z*FUNKV}6Sy1^2)`=WEVmzNP7oAvUkatSDI<89we zUedV2W;L{m<(K3Y98cb90teKb(SHa7=~u-=$9POFJX)>&=|2# zlXV@s7OM2b7dopoY5%6ucBFTo@erqhNsS$D2L0vDvhC(f#RyF+kv zA&Kbv1M2lKC`h5u+};U-`24yZa>o9g6Ao72HZJTDewRLuz~A#^pP-M&%41-AX->5$ zo#OtxZWQ%1LU^?_Uy5uA0)zOln)+8vtMl0~Gulk8u5rWr6vt7bvB9$oESljW(kytn z8PC=~CS3TqWgMd#!AsS*!n_r<6PSu=H8~y1f=+e*YYB7KcZ*J?k_|MlF-X60s^%%t zYxL`v{yppL3|Q@@wi8RmdoDG1ur~QtW|Wsl3f|FIJoRvyn9C4Y^_Vs?QBybA zGARz3e&-5LR1Xu&`xO)$00(G+hB@oAllqFpvjD9v@Eir5wn><3yZKnX!{bhmr`s0G zi**t(vO1`qYS|lz`4cRJb~A`{JHZ?K=1@ED+2gnvW_Wzb6W-v6gHQI8cxJH6Xqmjj zi^h1Hy2~j|h7A`bbdNsAav^EXELXVHd9ZS~nbrbOK7S0%h=;x^s`~Nf*C+lVf<+jc>7e>q@e^tuDHuvyI*Ok| zB!}2p{l($8=9I3wu$}$oKK}#f5zeD9>o?;9HMes}TrcMF3frq3N~K&?!4t%aH@5ux zOaXHdzvHf-eu7K*V+-yV{Ga*kcud&g=ZT7SqRTx6J-O*9VZ7oLq3RhjrPc|3$WD;T z54J?HHMYoS)Zbo%?U&_yntNp(ktZP+YnP?0VwHnS>K)0@HZce%B zy5oFRRvE7^!xEENdx;`{3ed=Elu2)N8tDfz?kSDVaVW?UvyK-~N5Rq3W&Kc1!lt}C z-CBMSes2IvsIupP9zeWZaMN(ns%@03cwvKexU|VM+csT%eo$zTVm9JknhIoiJ{4?E z{B^wvt}(u#Go+sZ+YrioX`0nC1Vp&^GXFUb1avAm>xuldU?$NBzI!Z4trpG-TwCFtW&V9*^Answ02kXwm z(K41F3M^0nC?M1qBVA||Vzxk7Xqnm$Vb-3S<>F?KK|590M$t(9{pZIBf?Q%AgAJNd zNr(d%02sQp0>R>d_x)-yivX6WX0duIB$2^XmRM*1q9buxzOEWl{K^J$K< z{WFSvJ~WFOHDPw|6o2ok64^!hlT0L`@V8t%gM>8-^zVFTX)nW(hT!bO^T-Yb(<8lx zqrbm%9(3g6I(n z=h5a~nCHaj`k%#A^ZUX73Tx_nq~Ap`nW843eAt;rRf*%lgKqn6%kTm%54Bd|%smZ% zH%{)XJTpvta?Wo|HNvF{%bPj82|Li#D1Sa*+gXg)+Cq36QZlQqq@$OND)!(|`_vep zN@7J?RD%mTJ`;5jB*?k&jYWS<@ZBm`1fPh2TnZYhZ^?3|Q+bqHREyh5y8Leq_r7G=)8roqf3 z^~LmFI+HR9us{fE^})KMW&8Jxj5xp@I1hd!@Y95xA)AXnXDO0ta0VdLBgm*=Ng!%e zYtUVX+*wVudNaQ(%eEm)=Tu;?*~d&zDj3CkRlT3%=GSMB=y+CNy$vVlU}+rFtH}D} z%H6dFErN{^A2&mWZ!@*HqB+bXh(X(Zu>PlewPm_cOe;?Nw80BC0(^0e_~*mOoAW&A zTL63?GDp?fmg*ZC%VINwRs>OD9GyIf|DEghFL2)gc;7vDE4+N=N8puf?P+LJqM5g= zgB-i|*!mjz(~gS&@%&m2ga!e_a%%NkZ_I(ScjE@r>CB~duVlSaWtec^uW1mh?o=yf z9JHIymKwtKL)N+vSiSNoJ_+bL@hLg*sQ}x>5gTr+wy=Fj>E%>D?oXUeVyZL3tMy9k ze^%r(^z`I;^^aJef^+V`KOkxdG!GcZr6b6vn&~5`+6K%ObSo%CAH9=|-@%OW9x{}l zd<7*16Y5F+1F=%P` z2D4btp~z4Ueq6C^eNghk79Zvvi&0HW44!SF<2))h@W-)6s^;u5hc6YVlM9?PU4+*R zzC`*$QIm@9s~1g+-_qX&zdHuABjQ3=Q61q=$*SU5JQ2l4@WL&9od)~KQb$@a%ay+R zr$$4QtbR`zPhgmYQeS7XN+h6{EQ>#_+fV@A^FxyCem+sn5y~KD{b&Ge>HaENOk?{l zY?IE1UTg&}LJ=A83BbEt%>!Kfoa7ko_miUDP-5vYE1WRFlTAuvnM%#c#{hu;j*cd5 zlYC(;uFj?bL47VNt}GlhPd4DaJGMN`=7J_#y8vSlJ0kQk)T98RhngP0{URiUyB&NC zySqSyr#K}YxHF>#Mgi^IAha;vU_D=x_zvKSWEg!_JNANPH6%{`TqdCd8T1MekYo_x`-Z z`&tOw$dZqefHGlmvuO|&=)9l3aIJ8G?z?r{Id?5DzC~X(LO7uGuCxB~uACN(40!6JDPUQ~95A5vBiTJqu*B7#yQ|u9+0(sfJaV`b{I~w|I(aJx*4(>_K zYkP72>gF0S(H~R_E|}R*Uz(m28w%;RX$i0qdA?epM4jI7zq+g1MjrWhWYCA-Ul#?T zEsabsHZugOIx1P7l**(eeGaO*N>NDQqIBw@hz@63C$4cyzNpaZm|zuq-n?urxoQfx z+AN8~)5~d!ogs{YAEdm(N=`61XMluT7fR5_9L`bhetNr?-S9Y0)f+an90|p{T zpIu@Xf}($rDs0mvafCS}&W4(g+!WuLI|+MgPQ1Dg{4iduTIP8DE6HvG^$Agpcw}}Z z`c_f$STkjI5nOu|w>8dxyK#*DYb|v0PdZ_m#RGgZa=FwKBkk)U@|pgcdTdH=?tU(rY5mDbKc9G=j^_?AQw2$ zr#YN#X@>wgUu`p_MY&U*d)z&#K-M(Jd1Ym+Oa@Fo`sHGZ?;FAOP~A79LJ&>hWh5VyiYe4 zOy`eq(m@4&U_*5&Fxmam>To!B4%6N0W%w=UK}KM7!>o^SB;nHrZ2wsZ&b(ihhdXHW zcQ>EtwT?cSM;io<9Wlkk$0 zNrKUMA0c)MUg4*wP*4k8U>&gF7sow;-RtM@`Xqx~Oz>Lf-QiVlndDUg+q>okpdkeI?Br456V zC`si9z-=v_pe4@-@CrL-N&6|AJf|5;egHHTP;DKjp z0fMq00Q=XQAk;VOBG2I?5|?<;h2-{l3Me(@DR;8RVFcveZDqmuwgPARUQ9sPWrb)o zyAc$ytq1Bz0X39dMJsd}>E&Dz<>%^cS@C+Ne%*bX=G=s**)?a%a7_WZ^Qp|>2O9jR zI1Q|S$oXHB4LnCdJz$GFvVbn3L^=y5y4XU-bkgvwNQbIJtt$>n?ei;(uT=Xs_uXyF zF#c%ySt>NrJG5P zp0v~`Y1rt7NjHNLqq)5GJny~tU)b~P{66P=Ptf1~#26hqdA@?YU+wvMxmvoZp4xd` z?chYN2i>#c%{bs1Yc0^HGhn6Qp2$oP=HLnF6au{Jm{DPsPGgy^owbSNXAB+4YG6Ek z_UbJatI=-6lfc`}?l4T1WG6n8Apid*ZyeolddpPZ5VV_4Bo z6!Zb@JBF-J{qwgY_^V5DNd-KXL(`49DW_N;CSKjVXZroivsTFUDs2)oExRK>@6*e@)O^9&BSid+`_N`R<#_}|!LX!7HY1b0tl|xYg z^joz+s-<26`YegX*S|oMKH%(RYHq=Q5lZ+o)AZ+znBOuuz97)h0S_jfu#49Wn+4l1 zVH#(u4$@(*TRI!F)K!2-%asT4@=B)*e-Q0*V^XWP28xz^>lEZ8T=2A|covD<6Z_NI zc#6Lo&R@QkGkYRB@mA6KilLF&9j~+ZWsnJv!U?Hana@){>z708!R9(KA>4>hO?3on zK4km4KPI_Vw-UfT!#{Kdl7=vC%V9#k30yC;6yAB>wX0ssTAi1ls*&`m7*Xd%5VXe< zh$&P`)XRwygHj|^-chf<_rbF?k*s|R<*>*krG?QN80G)jR(O;r^rsxsse1cq-Y1ui zG=7^kW@xBo?L5?Q7Y z%HS<#AyBfX0EHngN%PfG_Ors!&|B(w3-fjD^%<}XNx>m&s<4k;p@(Xdmuy4O+tcR0 zNdmUOXTy1oLb-L^dZL7imi;DgJ1yG5h38e$15M2sM}f&LfnmKISSy!#!IUvIGMlbb zDNJn9DO9+0+0PR^MY@zrS*qAo#5;P^*1rA+36?w9!hPDPRYT|z& zE@CY+5Zs-n^f<%eivo*P*oD0e`qPJCeix;kTfg@*nW4Z`tvFK$-nL-#Oz&P z_~jQZd)(;b#_d0fP5;WRm!`CdUBZ$y{JPtB)p)q`C~xsD&ycbCM*L-IxoT;`wbNQ?c?#-UCMwJ!h7 zHD~^ZYvRNHy!m09J!)0|MN=4Kc(87CKX2Am<>%Q?6x*iJBG7mm6Xsk@Qk2m0G@Yf= zoUHP38`<0^!cKtfP1{$#cAqWISP!pRr*yWeIe3 zP>N7W3ePj)v&Kc(GyoFzZ0g{h|KEE$4bCxjQ5~iR zI%2(qO(7`pm}I(%P2ap?$q?9(|LS0s^eVO5F2N??Xm5HjwxpUPA-;{<7nNwt5X&oa ztx(`@BEhnw&~g0@Xgfnxq_DELepn6ngMWcw54P0-1uP-6Z5fWJP4|_S08n0~K}&_% zjM;YU3h2m7YQ4s}o!T@*-}IRJ@GGjkq*}A6rlOQ4i{{s#dPn?|YV?Npen!WXtSb(a zVc5$OjP<1bA}?N|GaGrzxEzo!zKQo*Zl|FA5}k3&b+YgoO=!Hb#Q+l>C4{3E*LLir z0;O4Kw8{J%aU9~2`W)jSO zLO;3fFiPoHX_J^p>7sMUGJT4H9JRZKJ_`HZo$YPRfmHNW^6$QFZZ_Xz1!06Lbw&@9 zh@~M``5_~`6DnlS!h2%LdrlRmPp!8H@;}9kDv7w)cD%}LA-cg-Q)v`}8K+V=nb_?P zb%TQ6K11VJCCojMIu8eNfrN>~%p5OMt$PMUN&ejCsABcOr7sj_U+d=$TIjxlybLMP z3)#`so3@lA5;P)y+Q)s2yhEV{CVfKiB?ey8v} zfG{U^^jcOV&?~b%T&QruM@T`IwA6wE`JVr!^XSOxwQIv~hVn${rnj=AKey|Y%IOwk zCz_=YF3xq&0&7O`aDNDHD&i3}PAa>wH7qGUIc*nNE_b^OR(6=MSG|BHas`+ODajOi zw7UKFJU~5JNKK;4Tz5*7X<#HYia@{qsZ+flhOyK2BrhfCx*Ub&@ET+}F`tUQY79io zd3h<5Mw0xF>vZ+8WYh(S+*v83OM8;&Z0Pb6`WdD76|u!MAX_GDEwV3H?uTVlU(0Y$ zydYIjPx9d-8KCKl}%-A@k^Xpp95RH{wld$m?vng$0lb0q?)M(3jmQ$}OT zhea>hfPZ&>jnn3YvvmH3>01LILOmy%ZeepWka!|l55|8Qv9|2I|XyZ z^RjinHTdxm^OdrfLX#hdxJ-}n{P5Q9PJhEaWt+KgSr(w|6|r6N-E`wR+ae&LwlO8xqRs#NV!G` zI4sM@0#f=8xxev`Qq{*wx_+0WK6oM)TXvml51K?oe=77y zJs8QW`|}`ce&t9ld9B=aqWav=0d*KOoF~WESU$tm;JXal#nXibwY z{SA7uRDHK9Gd@Yg5l7j|rO{l`{ZsncU)Z&ip8|8>GfL=VTNe;7E@>g*%X9{FG=?Jc z@`eBDqRK{uh)BNbFWN{jRWBK4U!a-t^7i5J8`wo&C$nWd0_`eJLtsM37QPS9Kp9?C z2tVYSW@3BNiLZGuk4X)=*3lwTj&5G;@q0?^>UEe4`9P&R#`9z&p$IaY`i1^K z2TdL)M9v+zAc@0JFWj&HF}@t-=GaWi*x=#V{EVV?qgT;zu0f)6LY49DcT8{Np7{#I zs&gxdc59RVw1c`z+40Fomx-SLPep&S8SP@_vS7M{Bfwlhu!-a6cz?X#u1VfhlFO(e zvTHzyh{v&Y-hvj`@}b0UBdRv>;EOeQKHQfDVs*y2oRoN1O!o_ zVV@s-=WGfIMeoj3eLvMaP$J4*{I5Dg; zNm0N++Yg7_b(gX?@lWtywa52*U zM=TJ&Uw5%&baQw@i#Ygj5Tg1CzxyAC2jQWt*gi3|2+o_rJyf zPJ7lw_NbYBV81vNyqVUc1qZYpO!BvJ`1u}297t1o>9CgE1adlXQFQxX-bfU;&b%pt z844hs3FLf>Vbrk$+z+bB4FlMM`x9!Qv56?^J?zq6JX1k@QGJ>4IFOWh4a47pH%~L@ zb74I;PB{uuxs4TwO(qAYfM4k7%%j~A#===+y9ZP~*4~5zViF1ioiQzx@+5#fdCpfq zrnhFo2^xe=ad*-1+d$uxk<6;QTM3*ty?Rft44?DNF#6@zi}iFL6&IZ{zDAUbV~;+C z6U{-aUN56UDU;sY7Ki&KtJXKrZw`QFR(17dr>x}5`li$w3FD1wrG60e*u*Z|{QKq$3uF3+~6>w(5~waP{$aPX}N z^u2ee;ZnrTNUIisd*CJCfNq;f&9Pre`^XE=9fi$24mIzbU&lU4tsB0`tNK(|(B$=J z!Y3iVVAoe-?4S?tsb$^{VDiD)o2qSTY;@wR{#N+Yp-;JxMX(ghuC$z1OrhyP#Kd+c z?PdXnRk#xbVr>RIs6K+FV+OVw6EsorKXt4wKMV%?CE}0hM;6}t9XI=)oZuF=7UtDn%c`htMiwJjZJ|y^H0=* zMMoyX{wXpfI#RKuZ8TvQIb)mJPt+vTO%WKAO3qSYDaFPpFUQZ4t{1_VzGA!6>gJVU zrfnfm;whJQlr`K`C@480Uy+*OlG{Y{MhI3}XVFz9`IW$jNHa+(Uqe^gO`<586~`ZH zmHAdXW^_F-WfFSR;X#u#s?MWKJytqJD0OBV5JSW5qhOY7I;odkAkdEWxw5Wmh*N05 zfWo=HsvjB2_>V(|&{OF%FPk-))Ci-2!1Tgwd|?3~WVg>)3p@7ZgM*^GYj~Db{ zo(mDHh9%9ahiuzV@jIuh7V_(lts=|p&LukoyNyTFH7nDT0&N0Snxqp()OrV`Q-T&A z9ipwXjh>B7)eSRp{f*9BhzG+31UtlEti_t6sL8RMr>fH4gQVe*OrtWPVc0x%{rwD8 z1xe%@P2U z(VzW_u=z#1e-;q{L3^q{l;?c!`s3v|&mvw-n9zZm*}L{)?)Nbf=914{=!d28g;NXO zlgFutd|5_dJqz@B5lR{ITP*W6L!T}mNlZ)uBSFjrr5V?IOMti@kpBhklcU^>_Lte= z$yF6ljtPfKFSr!`}Z!%43lQnLuc8E^eVW%?Z>AU~> znq+0O^mHd>aNK*}71TUxtp%#59wV}`a7XuEiV`%6etGj`FmG9-tiV_`TmHlTNB?g= z(TyBOI=|T8nnbO#-dUFxQB`Js&&s4DxSI=5K?p7kqgd%^Uoh*%fU7SYzi(`mG6w+@ zDLl+=H|`JBgF9wr@SIFtyx{*RAfhMfVtasZQl|`wo54ucy&p2K67{tfE(gEaOIJEW z2X{ZA-@E5jf3D8DOU=<)h(RILQ%EjNffK=B~<_@G#vh&AmmuKy_$`*$J(b zPB8*OmbQ`Aq}4DQT5*zAlbb_F>ZU(t#pPz}w9&>tDtgTtY{w@%?8 zvprHxDmXGsf|Zu5#pAiezW&|am9+*yD$p~Litqe$@}-WCAsYqu3vtb!wX&%X;-1+s_go)}6OHIs zp%dTT_ljH?$+o)`?IZ5nX&N_`L%$w}-{MY0x{fAl_De-YodnYpq*r=CNcD%!DZe6(3{|h^Y$SGKB2l5=SQT3`{1c?qT}9I~-99lW z=0_%fwv)r-&&QG4@)7Q}u8qV$_n3}eN?g*)6p9`$_(9Qm$e=(L$E4u><F;c*;&K9g(rnfLm^vi zrJ(JHwGhd0Ve3>2fNa#$TXcr$S8LvsqBnZ(gt_siG0%wYkb7BAu)RU>M|Qg}_fwaD zawH?xYnSquWT9!zxnfI9q#}JJ>xDYJTg$dg`;n{VYbG*0k$9$PS9M>}Ud{g-C9`d| zlUh<$G`hd5Y#&Hb2H{65TGofXs(IjSmG{xEIumJv-i{YPPRKpgq6Mlb`%q;MiqgfQ zgQv&5KqAu5{-^@89)}AT<$`}Y?c^T%s$b=k(DKE?jhuU$C3(7zvUZGfO+&>H=GG$}^bL9`qX>v$f1!2B2Bs zC z(AE9QU@f9ASVYc1tL5Sznd{86-y45f<>lX?N$%|6AFvb2n?KN+PZEJDf0KPfWt^hV zrFYX;IR<|CJ)1kVpJP;$FykdE=-qrGymTSV8ZcLLwj%MhR+2h>lSWQTlnWldk!K53U{OZW~U7$8z?4^{)^_Er8YyhU%Kpc}QmEoSj5` zzi##9(1McXv3F$fH3%6G+^o5l?AR{qnxXd12cBTz1?mw7|1O?v_I&J;?hQM`z_8kj zIPEd>W>h)N`x!8OL=f=I7`E1Jfv zH^Fyl1Jh}tCC&Ea%Yom?(+pc|824V_J_AIF_zP1Fcd&cEn?JY^km!DDNgE~2G_OwN zAf$ZjroMHqz?~N`@^RKzRxpc0Owx9i$t-(KJ9PTPyS1;TkgwR*wm4l6KjE;m-qbSm z+!~O-fyr8uHS?NjoO3vNvEz1+U{-xC`FBBdz`!ewZyxnFH2a3HvaF;T^VCIH)ly5C zfUnvglGQXKsN8@&QB$m*TkKnyB_^0P(C=(JF7i2Q$GMYC&8|4&+aN6Ev9%uu;Rc|T z7a|_~iTaZo8r&6s&2vo!1vQ9E{tkLqZ80j?rf3%{``FS+!N|9gR?9m!E~n&{7R0?v zHA=-fW^6yv8^5*!l+DvMDby`Hhf@g6ad zQW>^gR_f&OJl>Sd5++Lgl_OcWCbG+zSfdFL z_rB&t3t0GohVJ09ozQrbEj6O9C~^HCs8TeNrO8|w=QtO2RZyyl-! zI9>d>P=h#a>>g;H6bxoR46=fJ2I!F49S25xVOZA7!6gVL{H5-wl5}aFdn?~iAN%EJ zP3IH!p|fjx#;^wFL48_gHOX>D1=jep`g(4~jNsM;qF>+G#kZ^PV%#&Qk+bMGir1)@ zUuG~njxbxfpGbR@cZE5m(%8ymhu0t>DgvNjG4q9Dp)zxnrz()H!wABjJ_rocH-E9M1f0dx7S+V2}lR;Ys{d_?h(d5=6K zL7kJCT1>bjSS0ir)u%NC6JnSfQ7Gb^9BsQIeNW><29~g3JcvpJ0^*s({g5uUqjY~b zCt=-z@HOqZZLT>lW;_a(Y7s1&5vw$`dz#^NGPyEN=F54`*0-(Gx~kI(A!%D}zOH^F z!f_~pV7myeo)T;3=4lq;oU0g>LwMQV7_S-4Ua&pkeI2ykBM;?m1n-nObX5avHo3VM zdAUnJ{u>SH&f2|-dRw2yNP|;h5ndfIZ+4$+`+NB8k4oy zPHnrQb?0GvbGLnHhiUTz#ksuUK43}w(*n@=KM($OvBZ>{qQ{GRYVThJ&ge{Xw} z1L~?{(s_Qym-;j803#(&{B&OM%c97ur8lF(m>oeL9<4n?DI1I+8HNP6JQnEaBP)gK zTokPQ*-ZIiE94LNN{LfF2cMS&lchp-O|X!$yog3bT`s`02eeeshTT4GG*bO1Lcc%n z>*C(SN`Hg>v4k~-uX$Pg(&(ODg(`ZsoW}m&JPBX7Kn15mVJtC^{MgvrpZVT_$jt)t z>!2Os zNele(7Z)+i9PidRVTY~BLfJ8Hva)Br75lj6Uwm*~c>*SaSlTb@+>K?UO!O88iH0rZ z1!lBc77O0yax@HIxccX4G#ES_UAMiy*YF9xc~V~rXWkdx&GHL)o4Po)3G#~}zAy4V zo^*T&4c_+)?ueXp_=ie)&}q%jnSHv7iY4u;u8RMh2Bi(#R11!jIqNT?vip_ndlbTV z);vQp1ZHl5MOV$~e4jX(o`!yb6boS8L+~{JFv2yYJqG>vjuBp`tE2mbn|}amvTM8L zfz3Vs`kIS+!>Bxdy5e5i=|eLtjqBw_&B?uG&+T8KNA7@(4~Quv_^{LZwr7bs5IVL3 z(6QNZ7d}yNog3>oUWr`hG-T&pE+>yXi?9J{74xyDnIX%0i%Yt>P zEiE{v2jx`NwC$C>(X#HcH%MQL3V(F&@joDgzd;NRK=OsRvEiqPE2<4NiU76u@J^6$ zgOasrP_0lEo4_x`c5puxalXu_j!K)kaFpwse?SjvQOJ%WNI4Z3A_C9UamK-U3*A4+ znXrp%AP;L~GV~+D0t9zmx~L1p50tlz<$Hn_<9@UaI&Pe)aqV1c zmU6j#z{mXC6QJ7QKKyyWym_5UZItOV;iju8Dp}2`ulnqrz3zJLvT3ONGU;B4R4O{t z7oq5kbsCT)gE5k^65ttY;7zWgvwW!u%%4<@aXAiPcGA(b+4HF1TNf=ZUjp5ZR|7t0 za&upNiD;TKk|zSu^XD9QSF2`v96*^GDT-IFonQwB%45YbOW&Jj^CtI?&syC}vBp4x znl^Ydw!@ABKajS)>cX@PM4}vXwj0FWM3^D;!}rSglwwPTh(CK>PY6TS3t;D7E=Yz>y2U)_OhnV@D+eWWDS@o|H3*|=yKUkU zLrZbr6rC$Xc-ih-qoMs9dH(FZX&AS4mEf`tG$*DsASmzyvzt85z_$``K&SgkcGm{Y zmFe8=8NY-KBv;_FxwgnyKL5%e9*yvG%0797u-t3e$SkMwy~=@isQG^JStVKB`Si4} zVxN1N?JGpT<9+X4a|)?b_EXq}cgjJD?YI2u1y!BTiAF9$S9rFm$_(#D&3L%KB9s=2~q=`gzvBzJiW z5bkod<0ejV(@Vl}hK_G%vL&h^wSM-+>Xf597_>g* zr|ngQQoMvCZEXiqstrM=844wS)vD`ks^^Y`$VRvFJ&TR^OfyPj$#vJ; zADc_4H)v~ZK{Vc;w6PIz(zeTvzR$cjF`PIp1{|;^tT9a|Y2;v3XfV;LXU~};zfZF) z8$t9|Pp7yKFzdXX^}M9;y4r-889i0`=uUNKAeNisHO#qe$Mb=H>Qk3-u?B5LO3lVJF!#P>BS{-Vp@XI5=Lw3V_ zb7)*c(zAs`HtR!H@Juzf+USB$B_4bP8idGvnTd&V9FU!=%A&seQTva2Q*XLT)6)r3 z`9RRydz$Qx7y1ko0HS8)*fOrXcq6Fz_N=XV(hMo~cBH0jA?tvtck29J7b(wW&M{lDV#7@b8hOp(T{@v;!c%cnnKE)et(JeFuyJ`nPRj!rXZ*~p)j z_i|Lp5-8nETOAcU$rAqZE!v-h+aU6gTKCn>ypbDyogMG=?azBajtDRnw8p(?{> z%f@VOuK}!%bkFSlbp9|3B4)U%(>k81#&_NSsSU*J!rxk=0}>rp^=0`oF9?xl`ngkC z*$`z9A3H|n;b@$z4kM!>$bGx&Iab%<|YL{wwVwe^Xu^0x`@2hvX8 zJQPuqzhPi9ab{X)9gj=bww#&Z7L+ zf{mwZe0#~3n~Z^a74alZqIF1neY(T6<-`(_N+QU)PMfn;hoF8D=Q3G2Z%NJ^;o6N zf)qffkSQ(K=<_)ZkRcbmjnx=XYnf@;!Dsx??|Y2}kLJL8UVkRm$Stn9o>w(_`-7UwAZ5D22jgJ_wITFvAw0OGWBw;8f-$Ayl>`CwmT{D z?m@sn)hlXZh)e|P>sRMOlxxE9g-PII^GbJQI>(N|goM2@!5k|$*9bIq__jVY~AMtRi?Z3ytl2_@cq|m^* z;PHirs8)S09*H@{lo+$Es`hdN!Z6$H=CcUHf}bqun3gero8&%bDXc;lUQvI%@5A-& zp0w{6SY28jAX;=o3fs9sw9Z-4EX>E#o%kk8kDQfI|1X(7uopVSTdZT4MKRN&MnMm|%ff7+7(5o}E z9tP}%rWM*6<<~z54H3FB5vV74vZ(F65_fV9tp$V*XSuYB_bGKc^=y8%5@pOLSM}#N zt5GRv$5a9_2S7Q+HW_Z(SwI9k(Tvd3$q&_oMPt)%l$eZzT#2BC(9CKBA1g-E#9~JD zD0J6+A-2x>Y6IZ$-#oc{Rad~y-t+`i14dND;6_p{MzzG&5OFLa->z8@r3rd1&y>N< zAZ3WU8Ljmgj;@=IR6+M*c9Ii_`rKwDc<+Md73nzTrtYc(f|g6{Fxg;SmpL%tyZ_09 zc4+2G=9R|fDJl<`Lg$t9WD?DxwPVrY-0=)d=dGmk@sQ(F*a3^Mr#lDH!;&1Q1DV!X%3M)1;-%?I&H!f_0e}xLhH~0A?Ru)skarb<(%`Xq^!g@x(@1h{@A1 zO~8Lt?eu3p7d5$BnUVzVoaM-f8s8%TX-O;cV|p|DUqHdoe}mFtv}ZYi?#$IKTh$46 zRHrr%T_tPRG6wMWI%LYsC4UXijdzU~al?)E+>Q0jtu@~wH}hDFygwi?M*|?{_bccs ztWheFFTtmwaksX=FbWX0QUZO1=r7Re0B3B*M)`NL4DK3S#=lvDrnWAd>>RmET3S|D zvtuR}KEazUAw23exXp`2dC(`NL${1WRR%Te=We3Ocvu;!LuXJ#)bg(xl>2NHq&s$#5CY@ic8s!^R zFPLQB7SgxO7rq7T^Vejcj98ESRqUc-duZW_4gsM}b7RBXv8g?>EdrXXID>ZF-h<%e z%5c$v*(`4Q>^IG;I`JcECMclHaq<7fGPAB&rSU|(6KbPgF$y`h^<@x&zgXoSsqcKPV?KG zNicxS!KM^IsE9k4h^qngwf>Ts-&402b^(m~^7O2t!Ghh$tReQ6JWhIneWxKbO^Kc+ zfZLRzO?k%{r}ismZBXaj#+lEZXz(~Sj(X~anl?^^xNAaPfN4Fqi^3!7RF$pI5}2Vs znce-4-T=26D$(%_vrIT8KTPI7WDEJuT<2?5YmT&c5oX@<5d_X%E#n@Hgm2X%wy&;@ zYr1WIYOlpQOVK>&e(2g067Tl3`<2G=g~&@e7WOr`;Et)_OGz zYc~sEb`I5fbNaK<-G=`N4|sG)%%A{x1Ao)nj?k#hr9xKXB@R#cB!{v*A%=!+V+}w5 za5ITb#F~$Kv<$sI50NF4=Tyo~QG=W-|HU?4Izmryq7=5zawcDNkDw=2w2Uu@ z&-ZMvT)wb%Kl#G9iqX@g1PQYEGTLdaW5tB>P+>Hx1hs4>dLqo)bA4A+WmT$e&BIbd zKTPWlCLaL9y^yWp?3oqBJoD&1;x%yfu-&A6skmlC|t`tyZ_9!%;6V zS(Vs$(;xHnNo&$LXhX%V2&lE_5FTfaau8p3^cXYiEBx)+;~1vB-LZ3Lp7~fL3=TYD zqiv23$(geO0LvSiDlVPXs8?IZJAWpF8c`Q{2eWajEZL8NfqeW+WXrXPU9EUP3(Emj z0i3p>EuvO8Q+cPzJLa-rq%LqNZKNpe#w7?UN=tI55*YFRZp;31x8`Cy&d*-Dh&zXQ zYtg4#uxNs!dhRy1FS{ar)c29st?zbUJ;{(p9rfC_R+g2xmR?++1|JnY?`Fn|SM(I} za%^x`XWnko<#9W+&f21BQKUlnICa<$?`d77-Nw9pHp-{EFZ9JD_cCw>BtQcEqtXwa4zjc`8SE1rTzrF^nJZ8vy zOKE9ZOQ@){geUNkk5EH{aOXiXvNSW&N=+e~Sx;lmEz+s7qQC-MzOSN!JjTpUXl6Q2 zelKIaCVT6B`@_3kiU{LHf!KlYy>fTLFsv3DHfExjCNr`a^#>T5VZ*sJli2j~e?ISO zAzRB!Qh6+e6FQIQ!Q|hWur$2V;vR;H#U?xbiXmGnsbGrqg{xo7#A4MBf#Ip-NPngl zWXRaV2PqTf?ytiQ1r3wS#lIK6-lWUyFFmq$PQlJ{g2mFSkxJ=|S3(2a8WNrELoARh zZXo(7h(P1%f-&?~=JZMkC|y=CQ*9pbVjNwj&*ov&(RdNB`YlfHvLEhrl52j40A zNr3xm<5{rcaH2sDRoT%=>~eDrAF5^YSKXY}#R@F>NDb46P+o9MXaK*Bl#43;dQ??Z zAU)$I@J&mL?_)k_i#P!APBHSn;XC4uV)qjD&*DWLmNuaIk$+&&qe1+=6Z4UOglJPG z|K_SsI*mJcR=LpO-(-FpY}U>e=acKbzf z5^kQQZ&^vbgKF(rJmYeQDa4Dg*BrS82=Ob1qyRIVd3Y%UH=tv{T&!)Xp%nL;LPb(umNUISb~KKRv`fTZ zHxVrx97q808^XoUy^Y{}`_oVymG_2C?sIDzLb*!xrJ3fNq*=W$0Q{1la?j?e-h{kxd8tfObCbyA{LTE`CBs&eNcZBUPv9BtFnO>6Kj04aeE%aW zfB|=;{q!6Ma9>T>j;pjT*U5Z3=hJ;LuY7|ch0HQJ+j_-}$g zQ~mlC5Iw?HE8A##F(cngzWQ-|Ouj~+GbTN|bQjMHC#3Z5;MDbnS54#=mSi<$0zdox z2iJ+tX>Mb1{&m1!=2l;d4hihu{9zuX;BQO_!`1*me8FOexPLbvjw)`A(-v-T7LydP zK%yFdb}27%#q4(=wp3>g9UCz8H}Q!c+vg+b-O7`t<73a}Mbxt~>5XfyLcc=TIL8XS z);3R+8#k!Pg^x*%S<*k8awPMg?9<3c`>1cU(NMhm!z%d#EH6XPx``{ zUffQS(vxc2yKop6C?CZ>mEb=sd>el*2xi%VB#GSPPS$d~j}psXy4ZB7c!#69pM!an ziodc;Rp3IN5S6E31|SkO%^8RZQ$v}W+wGwSLbo%U4i<*X!3r5c>4N(ucXJ%09uTs4 z%d0yBJ%!RbIlcISNjG@YHM&zR)W@i0Q{F9*)M7Ra9;>*c= z%ktqNRbRT^(8+Kz&bgh{t20(8oo=v@G_Qax*`#&I2GqJ0-M)x5%DNiXWFuSwYJ1N(g;UP(8Hv5J#ykI&SokK8XwR3Py z-90ICn*r3+(IvcWG{3a(yx?s{ z>4D099@&d+e9Vz(Nxq2x<+Y%4jKkFyM9W|`8XJw3f#3EBaAFQ;fbXUbR=C-A&K){Z z55O5Ri!`_(r>4%iK+7R2lfMVulM|JQAEF%KFk>wjLBdal4+>8PJ$$Em00y?tF*TQagODX0G zKD6;Bko-Xy_Eq6il_egmc+-LPE2Z>wJWf-M{z1#ri_W#{KM;ouu?T_Sy6xjcC$=GA zO^@{u(AqMFi{sZ@TRvgLUTYKTY)xLUOkNaYaK&XRoIr_vDwWSjFj;?Fc+=J8nrQ9x zr0W9DSLaG_(hMYb#!Uw5OLSSYhVay(2 zfZS5Ulet~y_U5IeMYkPI~1O33V75-+C-)fb?#L+v zV17L+!mwU_&zVOid+by{uvod|tL0OILjFK2Ffn#3EnvVDRu_HM_c&~F16$srP6gk& zYzu<5_yp$%u^V#Cl_1brmwN75yZB2_E6s!Zh2iH%JFxL3JYVC@MBl$vP?n6v88VR( z)s{tOLi@T!b%?(`Qf|#x*~j_L`!W>ePV&I}IMUh3{xq9B!E$(NbQAzmKjt9HDb455 zEs?LaBK2i%()%S&BvH@XH?1`?e)ZeJXr{5M)C!xvh>28d%xS_*c6esQS*3jT<-n4I(^ZJ=WorGX>5rpUMWu&&9Zgl z%LT4#adsegb-S%zwv&B!^+9CvOue3lmIIps+f#`V23g5}AmA&LmqMU42PI`m^R-`z z<{W2!OaaKX@sHKzlqmAY6q@_OL5GW?OtE*d@*zJ;58Z++YF|T`8#MA?OC!rIga~0) z{g4tPWYUgr0UTvp(ING{&glc0rcAYz$}6gOry1cFaGe*zqQm96PIE;!=U?ZbofLj6 zJ-7h{rlWk9Eqtde1*;5kdCDl?!k`w|4HR6GGwYWQK6_NQ0Q=LL#aUUfCGl|OQM!Zf z6TPzrS8Ca3Iu#hp=)PI0=A{U3OFeGToMw*)1;}t#S=D=eNL7N}=DYUEbeD(=-EwqC zFi1sqQtmIYG;B$ASBjoNK5WWFH2nMdlLKF&v}0AafwS`=?6!jB5fC&ax&CRbMgIzX z?8go~U!J=F}lMO3~KMHekCH`TZ%{} zpGztt{WliIf|=nB(6b*Wt73cprjsbuj2P5IjYr!cRV&v-r-VT108P0j*G9s9>rPv1 zSl3#)`*#p~)s`VfY@un~DlOHE?FLp*6=XhPM>50X9vpiDeqcicgcxU)SG<&Bv6$3n z{wNbR;bLxZy&ddlLO4YJNJM$-#aSvx%ALO_D)sO(1Zj@QuXGaI`MpMH=-} zWg0l??*f?Nz$8|>equXe@iXlDIQR9PT)2*Msh5xr3^%LAin)LuCBTA|!Mo}hSJAbc zCDCmuwIw7-1DACM!BC|?oo_O@9{s1Qtq8>-9@|~^1kSHv5=M_|o$&~yf-mZfb_s}g zwLSLaCI}J3JZKlw4Kz5_+lAI%LOZ~W6O*gRyddAJInpc7#9(7FoWuMZg=d+#K{z+) zPOp^vY4$8e@A1&kt|q_yw_P$0TQi5Z&5&~vKTsx`ZhgWhu5+3^@Bk&xfH0*ZT_1ZI@oZWQJ_{gyoq~pqb zUSX4>W@3Q4lQVguWwIPomh6zabh3q>vIbiIuyS@mg^*TVs!LtuSF_RI+*b8|UGX;^ zG$-vpovviQMa3Q+DMOEZq%G&prjMY5!Pc%9$i(WBZ~cxy8*WtU2-a=|O==Cxj|U5s=;?v;cB*&Tp-|?w@c!oc(D& z?e)I1_RO9=^E@+FrTfeh)~BB@Ky#L(2G*a81MwoLXUeX&Czan*bEPZD<5Rse&$a_` zGgI}pw=uruEi8?qD(Sbq)l=TKd$%da7bOq0rH-jF4v2Yk3jJIi5Wpy zh{xz!=%P1NoIuDuv)Ws_#bI#x}9P^G)?ETSwyb ze0xX4!8r)n^t_WvPsK1cm0s9Pu6Rh_AJ-In`kuZP6E-!tPEw~EGp!zZwZ3izVYZZc zlWMGu{5f0{%Xarw#k&>1p8;aQZGikzsU+k8|9(hi%2c3k>V^aVbuyU8$ z(uQx>?o*BThMf5%OE&T*$qQB*k{7vp)W7i=J>DEo3eHhZ7s-R`Q>^%bNeCrFNj^8R85%*!v$Dl;1u)n?Ag0&l|38b=H8}1 z7SS=X8*l0s4g-M620|mI7arfEw@=N9viea3@}T14W=}&n(J)lRdxD41GX48Q0qMgd zwud&|O9javWiGzXH-zuciiOm@6dVt|YeRNj+6$+ZIPD9DkDoT`D2q$N&rJmMTQQP< z^3109`enJYs}Fr z@u|4Cd|tiXc|;>w`{MRWjsOMUr2WbPBG0?LAKcC!RCtGiayok9Qm3{n&uO4l=oJKw z;4=#O^LnVgZ9lDR>QLA3J1*q)--QK5{VyluHH+S=AYC?}%pSjUPTiTByhi!S!wCP{ z{aFUop7VwKGbKXovA4;xJrQ`kdvEll%Ne-k`qhK%`_~I;REPU}3T}huf4GgJhm#f_ zrXSt9A-|(;zFG~YzaT1e&i(Y)fxy6y)qbmGDiYH>*ySsBD)RXr%}H(FWb^{f%mR1P zaPqM7O2+du*L6ddR;bIkQ6lQl-j6aJ!e@BOuXBWO5*dhjJN0m)n5%jJUH;$l;`<2# zY;@qShH|BdYd-fzhxoo`kurOH)E;>7PZRU&Y7szE!qc;{;XYE;vo_wk=n*mH(geaa( zizf7xfa-1bkr#AI0wj#a$tO6r!LLba%8k*y)V)>_Q@iqd1k3^#&vI5mVfz4mN}oax zw7SRCbEugK-tT#nDCf`^}xYw2h{Zt^Zwe4r@h-H$* zIruQgTZOLU)hYM&M{#;J{z`XJD{dUh3)cNOk@)juy!z#k4?nZ5*D1%y^P%d3o3OiY zD$Yx4UR>1ZPb79a83?C~$6JK&n$EM>ePpMOnaOi;wCP`k4Kz%+4;Yg)+9db_)R$Dj z8zV_O5_5t&N_tT-xFaZq7m1@imV^m9bs^1Nc0zBtx62fAJfc!8n?l_Z&y5X@rYe=pmJG@vI8Qc zT$v02%%AcvfJKUZgLdoQ4UXm|1Woi`0AygXiN{P$?&F=yPxD3<$ zI6~(wx z1K&>QHjuW-K%b#4lKc)C6p|#|^L<96^vQk2`XJImW}Gs_lV!K8acptjzO$M0(S_i- zArW-G7&WGY53n}n`)Sm2D0R`)b`eu?QRPcq*O=$IV2fS(;J+WPW#;jJ?pMLAYVyXcVZsh0wGMD7*S6sDMqZhM30obUJE2!+%e zlESwY!i@{23*ceIw;M+>f?k^Zv(;WJRcH8IVbAODZfYEBvEa-#JlTh?+5et8s-pfm z828OTm|pYP*m?OciviGAYp=?9O=zh$(u$Y%Y~7Pnb3&eLH&AI}4z)iX^7ppd$&FSM z(FK$648PcjMua|oo=*9te8rD!zO+Z=E2~U%i?cK&2ho))bklc8`!ki{>ySc8b;En_ z>V@#bt7pa(5OXSYHgWK@Nk4#}GucXW$9bF9({er7kgL@eI_Y6zE?c9o%p1Qxs{ju( z*RXikW>@#LCf1D;>IImXDhN61oLPm%-v!atlT43 zr4@QCy{Cs@DK>_Lw8pd66__6GKAt$3JdLx`IBIe zyiYq#3!RG6e%-+b`+r6newC9OJdbiOmUbpQpTF#jJsXs5w9n3R%*I>i+nILiS%rn4 zMRk$~LaPIpE*O#fTI(M#xD`)pKUZJKdv_(kd~+Np%_|D55+==K`jt+)Pg(;SCH*bh zYc}l5k(HC?nFUbSLq4BG>&9P*&YKKJ!?n#Ow>B@xy4lDxQs1_1^0R>Gc`o4GKn;5h&wFrUpYzWxB;IGykb6;b}_KkrF43TX^I`w-fx ztQ558>2xcke0r*DaNj)m&(5XvM84L>KoVQ|g2fT)_~~-T=;lJshRRm=;pei+P8FH` z4(-k(1$A*!8J00x{*Tmags$+=$pk$2V%V`7=bt*0Wpqb7QIA9>K1)j-UVW;-ZREOn zA~@S>w~|3i4zDT*J>ENH>4j;FVZi$_N4|-VWnYf8OWl};+SVr_A4<{z@D6*V0_P#X zz-NSJwJV>8S=HH0J+p6Wftc`bEX%J|-qUCyeJN0u({Jg@?ELFxkPtITMEUe*%h;xx zij?GO78{XHh5izG)PCL;Nx;AO%b$F;z#rW$6jbOcLjT&-U>S7oH9Uy3w4~V1c>311 z%&K^|(>>DANV)O#x)H`Jzi>fb)Qn#@V@L1t1jZe{mwf)VVCjY=z}Yk_e{quf(XEM@ zt(_-gw|0Wnh#9l%NzRC|G}?p>AE#339xu>!rd{`lNaZ9sc7yEppR)c+_#cNF!Kb-5 z>K>u%7nxbLh6y&b4ORm`phgQt6m&_92THzETMB2KCWjbWTMP`IfS;InRYyuD7_@s6 z`HtNj7pR{GzY0t4XxsgC|Gj&7x%XTX;t5n#!)ZG%m1r~CFe@Xgte;!!DQ|=R&CO`c z7z<5#nV5gAv+x~a6zdVH;a(YcfFj8HDN5DtpPW!?8sQ6S&$ebhYusi19fEgG(<;c< zQM)-){P~nSdU}Ac>7CdzoVj#};qX(&G@d{7_KL5vjL(Yxu+>n{J{WNc+>w79KOSY7 zQ|IikaNdSZ8CO4DIG{Psu%1kLmCJM7VmF%e+3KWB`!NIa_zgZMkDv_PP7Gc=?0v<+ z{z>~KzT3G9lE_B%fNL*cktTwawyTMcPz8tE;SQ?Um?EX;>LP7%R4p%Y#yU4=N?SIW z5#;BsNwS@Ue7Qc69<4$;T)BrBHxrFeq3s0uZO^6g8%Isu;Ge(+35h}@hg+RV{6RKeV}2p3&WWYnI#C3xv=r~Kx!3A3VimXQ z>w{MIdh(Ctn1V}!j?dL;ryt*%j<`@;h(9+hWZW>nHwMA^#sp69&ODAH=g_w(O7UES z9-}R!*mZ;{g_K`gop=3EY?`(+WDJSqbF)>%nhCQyaoEL z((|tuH!kNxbmsFlsy}9M;6J+hsn^~Ns*u1oJVmNC>9a_9T6Gs8yE9fl8k{K2Kd07~ z;p;GUbq46}z0!~OLqX6fX@H-ZDIa?wce4u^G0zZV($ilykf;S813;+HSE!k0;ctT6 zacwEdZN zd>x6C;#yy_;>%tfkEeA-rTFncgrY_psUuh_{> zPs831GI3$y$f_(m(?`6LktuW>=9zr~Kx3;ax`JdK_xjj5)OE9AS{SU<_jQe_k7$4_ zzXK{CjRIG$STU6r-Ed1ftMJ^Dj@1qjF4HXpDvWF7T!djx37x?WEs?oP z3{#oI$glvPzi?09dlwRnS~c(Kyos)X?CDTHD`|zm6mS1njTCfv;a> zW2lpLGAKhaZct;uuy(rG5N2Ckx@TJh{JA8L?&ULf)L$liFB2XWR)#dXie)isLtaxY zTfru_(c*Ncp?50T<0pETnB9jNCnYhex4)k1 z-M{Z?{Iy|Ql91Ops~v~j-MQav+^isDb-@Ff@?Fxmoc$VD=)w^O4-K1Sh5csMIKtipe3iHp~V#&|`~ z94W;K7Zz1Te$sH&FSyVQsCktRad4wX1F z9pI`G6An0IN4MrD-&$3U5jW)4cus)NF!)PNJ5!$$K|DLE?I|-5jafwX5n5+RjrcYaVw^SK-doF2g03Zv+H#M>iO4A@)VKfx{e=TZ~F|LKsIk zFhOtie*bC8K(SD4xaFj_vazD-Nv^Hd8^A`PUjWK(8 z{X@~9VQNvI)M5<+{K)f?Ox}y0N9hn&FBocOMMIh$Ef*0~cw_2(n0gfZuw`EHuvay0 zZ0<%5@QgfE)@Rd&Z~&b;1&qj}mRijR^#^L$y-+lyMxUsq50MxxxVw#ymLo?`3Gex^ zWDCRA+z5{&jK0RSd(YstIE9_Aho2Y31l~^1%4{qi)?3#3`%_8+(G9Svg!mx`nhs!MWr=)#b0=hqvrkeigVL} z7uwUIzrSD@Z@y5IK4_0bMfvbzl-GV*x`r|<&WYqDc#Zp3343xcDv@2${A{>U$OCr9 zHzmw7`XajC1Vc(@PF=A>=OpWXmHz8kwltqjVGMV<8k(e&u0=5W z00}05+7EXBfWr%)Jmdbu5H7I9xamEop2tSIC5rsjYp)4`SR3Ne8X90NDOa#L;yHl$ zy^8xCJs}xD`VO!$K`q^5ua;LbDPEk@5Fy_MZJOj29BT`w7UXr&jw|>`M}H88b7jrn zc2uReTi1@9zNEDl@15}o%@i1tSzrTRLJz2ZVXr^dpFglf#5M^mssR;6ve6x8;y)6= z$q^sgMAA#QR2%3k@z8`noq}D8DZPMna?#iCor@6iBK0dk7N~PdVFEU+AKCaj(i@x5 zB#h*C-9}#5#b#IC@l?dy&iGEsC@W_85ncP{a;opwQjp3Ul})|P{xwOGb<4h}=ep6f z#UN3=GqSPy5{re~-$tr7L?AoZwPAk^EtF;CvvOecec>!U@-p*FOo}e>?18SG!YZd> zFC+qf$j*G-$6g;(!^m7QWJ=h1Av%g}8~*s*crJNV`idb?x|6bFPXW6!009{@6HB#q z4}uTTz!kIpDpiZ~egnpr-w^8wXpZf-m@Q~blM8hFsio}9DOPS?lbuSL7UYS*raUwA z?ys80a+FBx`4zg234{9F84y>mqhII9}DSxd;5Wb8KmHVRaEGbDhXzKz$DWB zLcz!T_VO3(^P}jDPL@Gz0|U09*JirBeiLg59SB`-nCR{{;;opvtkXY*9+KGWXKI7VWaer?b)QYiM8GpqjP! zliO>?OoYeBp^nX2xpvuXDrO6jPIL0%D|28?+L|hTWlieYr8wieONDEIkzr}A*Rs5G zfCq&y5t>^d5Bob?~4$P6DE^03sdqvt&{MH ztBkWB+zq@D@0}BC)|1U1yO(BO^wu6iKXiP@Oss#X`b)lXezV4%*me$00AGSZHqa5+ z19Np#zXQaSv6r}jGkZ2)J1cm_rEm#L(|da;7W#^#*kCh*F23r?KFzUUC95fBP01jE z0pEe)2Ft`^ORHW8UL2&P=N_PJAmfacijhr;ksDT&shWz+K??yuUM7CBIoTkiZ`u9a z5HwZh5XS^4%c8iy@X&M_i7j?j-n^}poV<9Zt7II#CsK~*mJx4g&o&(o%uAg#){`*J z@9)~Xlx%GZN=;=2R;G9_32}fj4e+ZCBQ+gHs@bJGM0|~44flvhf4)V^jn&MEeotD1 z)t0VI@C+mlgraX*B2_eaS_7Qxr7(KvC+48uOyMItVu5SJld;}FvW6KwXJ}f9al~d> z`l%cKo0Dsa8IK!hC%IILTvko<%;(L`Gv}=2?^DF3sn1J_SxjFx(*K+4Qu&svC+M%>C4wz#Rv{3RNZiI_@ zIZU^5$Vl5z6gH7k&e`{TAdkQm8n7a$!Oy5mctJdVzFmTR`xN?4ADEN6K>BX!mZgjY z1#L5;pRON3Dr}`sc~1VAhn3jy77qbT9P=_g&HJ=6XKwd%zZhHL_E89ZE+m)@8A{pU zomO3a#L2Xme0$&?A3FaYUL;m;;A>g1JqEfQvlWu%?UmJ#aI#^wtYMtBS|NEb2r%*Q zVM(B+!@h|1vFGwvD|>OhFuwQPN6&V8n(C;bo_l=yYNtawf+QaTVP3~T?|dj0`c4cl zr3DTJl->O`04Mm!XJkT*KGYnL*o z@~i#VJ2g?xnL%!K{B^XjwZ7ancQ=YPRvhp3gqS%7!3ECvLO%~a!h?nb7wn0Y0j^*T z_3o<{E{VY^0>JLa8B5U1=vWP59z*;p8$LP=H0VhK+}c*xOWmhASL^znpf7BcYt(`$ zgf~pEhJxM6MOc!Tcn0azJb#vJ>%PRAYVxXoqDHO}X3#U6U8x8Qor9~M9m$Y&Lf0R<-el}#AyJCULt4}vlXkOYnU*Io_M6n3QbTM%Z)rb|Yah3JAMZyfXW=`7#ZA=`L^t!fxiEo1|9?C zGyo;6-q~a*|1B{;fD&3A6P&{~o?4scm-`<@$o**Gu4uwEcTFoUcRi`Ad;GjKH>%nl zuuRXe{=xk%oZH!^PQTUIG#NRp;{t9$vwAHnj8lS8CkhY2AZHDk~!e=^jWXY%Ey|36FV(j~6{NkM=8_27RJ>GI_#|C3heDB;n+ zYmkkSd-zw%6QT3~_*eSd=)VpBQN({b{P#HgcWnMu!+)0YuNwYy4*#m*Kj-j2HT?hJ e9QbZ`T+o6O#;3&{kh1@Lmae9uMupmwi2noPTMTIc literal 0 HcmV?d00001 From 05b446dbef8bdd757b17adfac038656d369644a7 Mon Sep 17 00:00:00 2001 From: Verkehrsrot Date: Sun, 23 Sep 2018 17:36:27 +0200 Subject: [PATCH 067/105] Delete OTA-Screenshot.png --- img/OTA-Screenshot.png | Bin 165813 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 img/OTA-Screenshot.png diff --git a/img/OTA-Screenshot.png b/img/OTA-Screenshot.png deleted file mode 100644 index a81177470585ab362e2026e6c25200d4b7ee6515..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165813 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 From 25d307e820a73765c60affcdfb4d8b81d75d7900 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 23 Sep 2018 18:07:40 +0200 Subject: [PATCH 068/105] code sanitization --- platformio.ini | 2 +- src/button.cpp | 6 ++++- src/ota.cpp | 68 +++++++++++++++++++++++--------------------------- src/ota.h | 4 +-- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/platformio.ini b/platformio.ini index 518aaf2b..4973a892 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.3 +release_version = 1.5.4 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 diff --git a/src/button.cpp b/src/button.cpp index 3f75674a..4a816c14 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -6,7 +6,11 @@ // Local logging tag static const char TAG[] = "main"; -void IRAM_ATTR ButtonIRQ() { ButtonPressedIRQ++; } +void IRAM_ATTR ButtonIRQ() { + portENTER_CRITICAL(&timerMux); + ButtonPressedIRQ++; + portEXIT_CRITICAL(&timerMux); +} void readButton() { if (ButtonPressedIRQ) { diff --git a/src/ota.cpp b/src/ota.cpp index c4f64c90..fdda780d 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -35,23 +35,9 @@ volatile bool isValidContentType = false; // Local logging tag static const char TAG[] = "main"; -void display(const uint8_t row, std::string status, std::string msg) { -#ifdef HAS_DISPLAY - u8x8.setCursor(14, row); - u8x8.print((status.substr(0, 2)).c_str()); - if (!msg.empty()) { - u8x8.clearLine(7); - u8x8.setCursor(0, 7); - u8x8.print(msg.substr(0, 16).c_str()); - } -#endif -} - -// callback function to show download progress while streaming data -void show_progress(size_t current, size_t size) { - char buf[17]; - snprintf(buf, 17, "%-9lu (%3lu%%)", current, current*100 / size); - display(4, "**", buf); +// helper function to extract header value from header +inline String getHeaderValue(String header, String headerName) { + return header.substring(strlen(headerName.c_str())); } void start_ota_update() { @@ -99,7 +85,7 @@ void start_ota_update() { if (i >= 0) { ESP_LOGI(TAG, "Connected to %s", WIFI_SSID); display(1, "OK", "WiFi connected"); - checkFirmwareUpdates(); // gets and flashes new firmware + do_ota_update(); // gets and flashes new firmware } else { ESP_LOGI(TAG, "Could not connect to %s, rebooting.", WIFI_SSID); display(1, " E", "no WiFi connect"); @@ -121,7 +107,10 @@ void start_ota_update() { } // start_ota_update -void checkFirmwareUpdates() { + +void do_ota_update() { + char buf[17]; + // Fetch the latest firmware version ESP_LOGI(TAG, "Checking latest firmware version on server..."); display(2, "**", "checking version"); @@ -140,24 +129,10 @@ void checkFirmwareUpdates() { } ESP_LOGI(TAG, "New firmware version v%s available. Downloading...", latest.c_str()); - display(2, "OK", ""); - - processOTAUpdate(latest); -} - -// helper function to extract header value from header -inline String getHeaderValue(String header, String headerName) { - return header.substring(strlen(headerName.c_str())); -} - -/** - * OTA update processing - */ -void processOTAUpdate(const String &version) { - - char buf[17]; - display(3, "**", "requesting file"); - String firmwarePath = bintray.getBinaryPath(version); + display(2, "OK", latest.c_str()); + + display(3, "**", ""); + String firmwarePath = bintray.getBinaryPath(latest); if (!firmwarePath.endsWith(".bin")) { ESP_LOGI(TAG, "Unsupported binary format, OTA update cancelled."); display(3, " E", "file type error"); @@ -327,6 +302,25 @@ void processOTAUpdate(const String &version) { ESP_LOGI(TAG, "OTA update failed. Rebooting to runmode with current version."); client.stop(); +} // do_ota_update + +void display(const uint8_t row, std::string status, std::string msg) { +#ifdef HAS_DISPLAY + u8x8.setCursor(14, row); + u8x8.print((status.substr(0, 2)).c_str()); + if (!msg.empty()) { + u8x8.clearLine(7); + u8x8.setCursor(0, 7); + u8x8.print(msg.substr(0, 16).c_str()); + } +#endif +} + +// callback function to show download progress while streaming data +void show_progress(size_t current, size_t size) { + char buf[17]; + snprintf(buf, 17, "%-9lu (%3lu%%)", current, current*100 / size); + display(4, "**", buf); } // helper function to compare two versions. Returns 1 if v2 is diff --git a/src/ota.h b/src/ota.h index bda9fbe1..e426bb15 100644 --- a/src/ota.h +++ b/src/ota.h @@ -8,10 +8,10 @@ #include #include -void checkFirmwareUpdates(); -void processOTAUpdate(const String &version); +void do_ota_update(); void start_ota_update(); int version_compare(const String v1, const String v2); void show_progress(size_t current, size_t size); +void display(const uint8_t row, std::string status, std::string msg); #endif // OTA_H From d4d6a7ea07f257e0f2cb3bac0b0d9fa0e1359115 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 23 Sep 2018 18:45:46 +0200 Subject: [PATCH 069/105] battery check before ota --- src/ota.cpp | 13 ++++++++++--- src/paxcounter.conf | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ota.cpp b/src/ota.cpp index fdda780d..b2da167d 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -42,6 +42,14 @@ inline String getHeaderValue(String header, String headerName) { void start_ota_update() { +// check battery status if we can before doing ota +#ifdef HAS_BATTERY_PROBE + if (batt_voltage < OTA_MIN_BATT) { + ESP_LOGW(TAG, "Battery voltage %dmV too low for OTA", batt_voltage); + return; + } +#endif + // turn on LED #if (HAS_LED != NOT_A_PIN) #ifdef LED_ACTIVE_LOW @@ -107,7 +115,6 @@ void start_ota_update() { } // start_ota_update - void do_ota_update() { char buf[17]; @@ -130,7 +137,7 @@ void do_ota_update() { ESP_LOGI(TAG, "New firmware version v%s available. Downloading...", latest.c_str()); display(2, "OK", latest.c_str()); - + display(3, "**", ""); String firmwarePath = bintray.getBinaryPath(latest); if (!firmwarePath.endsWith(".bin")) { @@ -319,7 +326,7 @@ void display(const uint8_t row, std::string status, std::string msg) { // callback function to show download progress while streaming data void show_progress(size_t current, size_t size) { char buf[17]; - snprintf(buf, 17, "%-9lu (%3lu%%)", current, current*100 / size); + snprintf(buf, 17, "%-9lu (%3lu%%)", current, current * 100 / size); display(4, "**", buf); } diff --git a/src/paxcounter.conf b/src/paxcounter.conf index 1c6c6948..7e8f5a5e 100644 --- a/src/paxcounter.conf +++ b/src/paxcounter.conf @@ -68,6 +68,7 @@ // OTA settings #define WIFI_MAX_TRY 20 // maximum number of wifi connect attempts for OTA update [default = 20] #define FLASH_MAX_TRY 3 // maximum number of attempts for writing update binary to flash [default = 3] +#define OTA_MIN_BATT 3700 // minimum battery level vor OTA [millivolt] // LMIC settings // define hardware independent LMIC settings here, settings of standard library in /lmic/config.h will be ignored From 8985ee3603a0cd658ac1881e3bf6083ad7ecf293 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 23 Sep 2018 22:12:10 +0200 Subject: [PATCH 070/105] code sanitization (volatiles check) --- src/cyclic.cpp | 2 +- src/display.cpp | 2 +- src/display.h | 2 +- src/globals.h | 9 +++++---- src/macsniff.cpp | 9 ++++----- src/macsniff.h | 2 +- src/main.cpp | 14 +++++++------- src/ota.cpp | 4 ++-- src/rcommand.cpp | 2 +- src/senddata.cpp | 2 +- src/wifiscan.cpp | 18 +++++++++--------- src/wifiscan.h | 2 +- 12 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/cyclic.cpp b/src/cyclic.cpp index b0c1a0b5..0d5d1a3d 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -48,7 +48,7 @@ void doHousekeeping() { esp_get_minimum_free_heap_size(), ESP.getFreeHeap()); SendData(COUNTERPORT); // send data before clearing counters reset_counters(); // clear macs container and reset all counters - reset_salt(); // get new salt for salting hashes + get_salt(); // get new salt for salting hashes if (esp_get_minimum_free_heap_size() <= MEM_LOW) // check again esp_restart(); // memory leak, reset device diff --git a/src/display.cpp b/src/display.cpp index ba71a544..b3ed4ee4 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -13,7 +13,7 @@ const char lora_datarate[] = {"1211100908077BFSNA"}; const char lora_datarate[] = {"100908078CNA121110090807"}; #endif -uint8_t DisplayState = 0; +uint8_t volatile DisplayState = 0; // helper function, prints a hex key on display void DisplayKey(const uint8_t *key, uint8_t len, bool lsb) { diff --git a/src/display.h b/src/display.h index e85b9ee1..e009216d 100644 --- a/src/display.h +++ b/src/display.h @@ -3,7 +3,7 @@ #include -extern uint8_t DisplayState; +extern uint8_t volatile DisplayState; extern HAS_DISPLAY u8x8; void init_display(const char *Productname, const char *Version); diff --git a/src/globals.h b/src/globals.h index 9d79575d..28d9fde7 100644 --- a/src/globals.h +++ b/src/globals.h @@ -39,14 +39,15 @@ typedef struct { } MessageBuffer_t; // global variables -extern configData_t cfg; // current device configuration +extern configData_t cfg; // current device configuration extern char display_line6[], display_line7[]; // screen buffers -extern uint8_t channel; // wifi channel rotation counter -extern uint16_t macs_total, macs_wifi, macs_ble, batt_voltage; // display values +extern uint8_t volatile channel; // wifi channel rotation counter +extern uint16_t volatile macs_total, macs_wifi, macs_ble, + batt_voltage; // display values extern std::set macs; // temp storage for MACs extern hw_timer_t *channelSwitch, *sendCycle; extern portMUX_TYPE timerMux; -extern volatile int SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, +extern volatile uint8_t SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, ChannelTimerIRQ, ButtonPressedIRQ; extern std::array::iterator it; diff --git a/src/macsniff.cpp b/src/macsniff.cpp index ac037e28..52056753 100644 --- a/src/macsniff.cpp +++ b/src/macsniff.cpp @@ -11,8 +11,8 @@ static const char TAG[] = "main"; uint16_t salt; -uint16_t reset_salt(void) { - salt = random(65536); // get new 16bit random for salting hashes +uint16_t get_salt(void) { + salt = (uint16_t)random(65536); // get new 16bit random for salting hashes return salt; } @@ -71,8 +71,8 @@ bool mac_add(uint8_t *paddr, int8_t rssi, bool sniff_type) { // https://en.wikipedia.org/wiki/MAC_Address_Anonymization snprintf(buff, sizeof(buff), "%08X", - addr2int + (uint32_t)salt); // convert usigned 32-bit salted MAC to - // 8 digit hex string + addr2int + (uint32_t)salt); // convert usigned 32-bit salted MAC + // to 8 digit hex string hashedmac = rokkit(&buff[3], 5); // hash MAC last string value, use 5 chars // to fit hash in uint16_t container auto newmac = macs.insert(hashedmac); // add hashed MAC, if new unique @@ -81,7 +81,6 @@ bool mac_add(uint8_t *paddr, int8_t rssi, bool sniff_type) { // Count only if MAC was not yet seen if (added) { - // increment counter and one blink led if (sniff_type == MAC_SNIFF_WIFI) { macs_wifi++; // increment Wifi MACs counter diff --git a/src/macsniff.h b/src/macsniff.h index 40adb6cf..81e5a2ed 100644 --- a/src/macsniff.h +++ b/src/macsniff.h @@ -12,7 +12,7 @@ #define MAC_SNIFF_WIFI 0 #define MAC_SNIFF_BLE 1 -uint16_t reset_salt(void); +uint16_t get_salt(void); uint64_t macConvert(uint8_t *paddr); bool mac_add(uint8_t *paddr, int8_t rssi, bool sniff_type); void printKey(const char *name, const uint8_t *key, uint8_t len, bool lsb); diff --git a/src/main.cpp b/src/main.cpp index 8f4bd99c..3022f63c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -47,16 +47,16 @@ ESP32 hardware timers configData_t cfg; // struct holds current device configuration char display_line6[16], display_line7[16]; // display buffers -uint8_t channel = 0; // channel rotation counter -uint16_t macs_total = 0, macs_wifi = 0, macs_ble = 0, - batt_voltage = 0; // globals for display +uint8_t volatile channel = 0; // channel rotation counter +uint16_t volatile macs_total = 0, macs_wifi = 0, macs_ble = 0, + batt_voltage = 0; // globals for display // hardware timer for cyclic tasks hw_timer_t *channelSwitch, *displaytimer, *sendCycle, *homeCycle; // this variables will be changed in the ISR, and read in main loop -volatile int ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, SendCycleTimerIRQ = 0, - DisplayTimerIRQ = 0, HomeCycleIRQ = 0; +uint8_t volatile ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, + SendCycleTimerIRQ = 0, DisplayTimerIRQ = 0, HomeCycleIRQ = 0; TaskHandle_t StateTask = NULL; @@ -96,7 +96,7 @@ void setup() { // disable brownout detection #ifdef DISABLE_BROWNOUT // register with brownout is at address DR_REG_RTCCNTL_BASE + 0xd4 - (*((volatile uint32_t *)ETS_UNCACHED_ADDR((DR_REG_RTCCNTL_BASE + 0xd4)))) = 0; + (*((uint32_t volatile *)ETS_UNCACHED_ADDR((DR_REG_RTCCNTL_BASE + 0xd4)))) = 0; #endif // setup debug output or silence device @@ -329,7 +329,7 @@ void setup() { // initialize salt value using esp_random() called by random() in // arduino-esp32 core. Note: do this *after* wifi has started, since // function gets it's seed from RF noise - reset_salt(); // get new 16bit for salting hashes + get_salt(); // get new 16bit for salting hashes // start state machine ESP_LOGI(TAG, "Starting Statemachine..."); diff --git a/src/ota.cpp b/src/ota.cpp index b2da167d..6c0a9471 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -29,8 +29,8 @@ const int port = 443; const uint32_t RESPONSE_TIMEOUT_MS = 5000; // Variables to validate firmware content -volatile int contentLength = 0; -volatile bool isValidContentType = false; +int volatile contentLength = 0; +bool volatile isValidContentType = false; // Local logging tag static const char TAG[] = "main"; diff --git a/src/rcommand.cpp b/src/rcommand.cpp index 7f984cdb..994b0f44 100644 --- a/src/rcommand.cpp +++ b/src/rcommand.cpp @@ -18,7 +18,7 @@ void set_reset(uint8_t val[]) { case 1: // reset MAC counter ESP_LOGI(TAG, "Remote command: reset MAC counter"); reset_counters(); // clear macs - reset_salt(); // get new salt + get_salt(); // get new salt sprintf(display_line6, "Reset counter"); break; case 2: // reset device to factory settings diff --git a/src/senddata.cpp b/src/senddata.cpp index f738ee6b..2ca7973a 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -29,7 +29,7 @@ void SendData(uint8_t port) { // clear counter if not in cumulative counter mode if ((port == COUNTERPORT) && (cfg.countermode != 1)) { reset_counters(); // clear macs container and reset all counters - reset_salt(); // get new salt for salting hashes + get_salt(); // get new salt for salting hashes ESP_LOGI(TAG, "Counter cleared"); } } // SendData diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 55c690ff..d6012afa 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -44,15 +44,15 @@ void wifi_sniffer_init(void) { } // Wifi channel rotation -void switchWifiChannel(uint8_t &ch) { - portENTER_CRITICAL(&timerMux); - ChannelTimerIRQ = 0; - portEXIT_CRITICAL(&timerMux); - // rotates variable channel 1..WIFI_CHANNEL_MAX - ch = (ch % WIFI_CHANNEL_MAX) + 1; - esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE); - ESP_LOGD(TAG, "Wifi set channel %d", &ch); - } +void switchWifiChannel(uint8_t volatile &ch) { + portENTER_CRITICAL(&timerMux); + ChannelTimerIRQ = 0; + portEXIT_CRITICAL(&timerMux); + // rotates variable channel 1..WIFI_CHANNEL_MAX + ch = (ch % WIFI_CHANNEL_MAX) + 1; + esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE); + ESP_LOGD(TAG, "Wifi set channel %d", ch); +} // IRQ handler void IRAM_ATTR ChannelSwitchIRQ() { diff --git a/src/wifiscan.h b/src/wifiscan.h index 5e8aae9a..581d7b0d 100644 --- a/src/wifiscan.h +++ b/src/wifiscan.h @@ -28,6 +28,6 @@ typedef struct { void wifi_sniffer_init(void); void wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type); void ChannelSwitchIRQ(void); -void switchWifiChannel(uint8_t &ch); +void switchWifiChannel(uint8_t volatile &ch); #endif \ No newline at end of file From c713b0a0e0b4c303562831696aa4c84fbc5ed43e Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 23 Sep 2018 22:13:31 +0200 Subject: [PATCH 071/105] v1.5.5 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 4973a892..6712dd22 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.4 +release_version = 1.5.5 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 From c7445f0a1ef0c67bc2268b1b2b3fade39e4b8666 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Mon, 24 Sep 2018 16:36:11 +0200 Subject: [PATCH 072/105] make OTA selectable --- build.py | 3 +-- platformio.ini | 2 +- src/main.cpp | 3 +++ src/ota.cpp | 12 +++++++----- src/ota.h | 6 +++++- src/paxcounter.conf | 1 + src/rcommand.cpp | 7 ++++++- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/build.py b/build.py index ed222a04..7f2c3a28 100644 --- a/build.py +++ b/build.py @@ -90,5 +90,4 @@ def publish_bintray(source, target, env): # put build file name and upload command to platformio environment env.Replace( PROGNAME="firmware_" + package + "_v%s" % version, - UPLOADCMD=publish_bintray -) \ No newline at end of file + UPLOADCMD=publish_bintray) \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 6712dd22..b795bf33 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.5 +release_version = 1.5.6 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 diff --git a/src/main.cpp b/src/main.cpp index 3022f63c..26ba304b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -143,12 +143,15 @@ void setup() { batt_voltage = read_voltage(); #endif +#ifdef USE_OTA + strcat_P(features, " OTA"); // reboot to firmware update mode if ota trigger switch is set if (cfg.runmode == 1) { cfg.runmode = 0; saveConfig(); start_ota_update(); } +#endif // initialize button #ifdef HAS_BUTTON diff --git a/src/ota.cpp b/src/ota.cpp index 6c0a9471..15ef87b5 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -1,3 +1,5 @@ +#ifdef USE_OTA + /* Parts of this code: Copyright (c) 2014-present PlatformIO @@ -17,7 +19,6 @@ #include "ota.h" -#include using namespace std; const BintrayClient bintray(BINTRAY_USER, BINTRAY_REPO, BINTRAY_PACKAGE); @@ -250,10 +251,10 @@ void do_ota_update() { size_t written, current, size; if (Update.begin(contentLength)) { - +#ifdef HAS_DISPLAY // register callback function for showing progress while streaming data Update.onProgress(&show_progress); - +#endif int i = FLASH_MAX_TRY; while ((i--) && (written != contentLength)) { @@ -311,7 +312,7 @@ void do_ota_update() { client.stop(); } // do_ota_update -void display(const uint8_t row, std::string status, std::string msg) { +void display(const uint8_t row, const std::string status, const std::string msg) { #ifdef HAS_DISPLAY u8x8.setCursor(14, row); u8x8.print((status.substr(0, 2)).c_str()); @@ -320,7 +321,6 @@ void display(const uint8_t row, std::string status, std::string msg) { u8x8.setCursor(0, 7); u8x8.print(msg.substr(0, 16).c_str()); } -#endif } // callback function to show download progress while streaming data @@ -328,6 +328,7 @@ void show_progress(size_t current, size_t size) { char buf[17]; snprintf(buf, 17, "%-9lu (%3lu%%)", current, current * 100 / size); display(4, "**", buf); +#endif } // helper function to compare two versions. Returns 1 if v2 is @@ -364,3 +365,4 @@ int version_compare(const String v1, const String v2) { } return 0; } +#endif // USE_OTA \ No newline at end of file diff --git a/src/ota.h b/src/ota.h index e426bb15..59c1e464 100644 --- a/src/ota.h +++ b/src/ota.h @@ -1,6 +1,8 @@ #ifndef OTA_H #define OTA_H +#ifdef USE_OTA + #include "globals.h" #include "update.h" #include @@ -12,6 +14,8 @@ void do_ota_update(); void start_ota_update(); int version_compare(const String v1, const String v2); void show_progress(size_t current, size_t size); -void display(const uint8_t row, std::string status, std::string msg); +void display(const uint8_t row, const std::string status, const std::string msg); + +#endif // USE_OTA #endif // OTA_H diff --git a/src/paxcounter.conf b/src/paxcounter.conf index 7e8f5a5e..28264cb8 100644 --- a/src/paxcounter.conf +++ b/src/paxcounter.conf @@ -66,6 +66,7 @@ #define HOMECYCLE 30 // house keeping cycle in seconds [default = 30 secs] // OTA settings +#define USE_OTA 1 // Comment out to disable OTA update #define WIFI_MAX_TRY 20 // maximum number of wifi connect attempts for OTA update [default = 20] #define FLASH_MAX_TRY 3 // maximum number of attempts for writing update binary to flash [default = 3] #define OTA_MIN_BATT 3700 // minimum battery level vor OTA [millivolt] diff --git a/src/rcommand.cpp b/src/rcommand.cpp index 994b0f44..42e855a9 100644 --- a/src/rcommand.cpp +++ b/src/rcommand.cpp @@ -18,7 +18,7 @@ void set_reset(uint8_t val[]) { case 1: // reset MAC counter ESP_LOGI(TAG, "Remote command: reset MAC counter"); reset_counters(); // clear macs - get_salt(); // get new salt + get_salt(); // get new salt sprintf(display_line6, "Reset counter"); break; case 2: // reset device to factory settings @@ -33,9 +33,14 @@ void set_reset(uint8_t val[]) { break; case 9: // reset and ask for software update via Wifi OTA ESP_LOGI(TAG, "Remote command: software update via Wifi"); +#ifdef USE_OTA sprintf(display_line6, "Software update"); cfg.runmode = 1; +#else + sprintf(display_line6, "Software update not implemented"); +#endif // USE_OTA break; + default: ESP_LOGW(TAG, "Remote command: reset called with invalid parameter(s)"); } From 5d883e12f6bc9a029269dfa4cdf5b46922b30390 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Mon, 24 Sep 2018 22:49:55 +0200 Subject: [PATCH 073/105] improvements to LoPy/LoPy4 HAL files (button+LED) --- src/hal/lopy.h | 6 ++++-- src/hal/lopy4.h | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/hal/lopy.h b/src/hal/lopy.h index 2caeb308..d8d678cb 100644 --- a/src/hal/lopy.h +++ b/src/hal/lopy.h @@ -3,7 +3,9 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define HAS_SPI 1 // comment out if device shall not send data via SPI #define CFG_sx1272_radio 1 -#define HAS_LED NOT_A_PIN // LoPy has no on board LED, so we use RGB LED on LoPy +//#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED +#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED +#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED #define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 // Hardware pin definitions for Pycom LoPy board @@ -28,7 +30,7 @@ // uncomment this only if your LoPy runs on a expansion board 3.0 //#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 //#define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm on board -//#define HAS_BUTTON GPIO_NUM_37 // (P14) +//#define HAS_BUTTON GPIO_NUM_13 // (P14) //#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown // uncomment this only if your LoPy runs on a expansion board 2.0 diff --git a/src/hal/lopy4.h b/src/hal/lopy4.h index cf988324..47f37f7f 100644 --- a/src/hal/lopy4.h +++ b/src/hal/lopy4.h @@ -3,7 +3,9 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define HAS_SPI 1 // comment out if device shall not send data via SPI #define CFG_sx1276_radio 1 -#define HAS_LED NOT_A_PIN // LoPy4 has no on board LED, so we use RGB LED on LoPy4 +//#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED +#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED +#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED #define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 #define BOARD_HAS_PSRAM // use extra 4MB extern RAM @@ -29,7 +31,7 @@ // uncomment this only if your LoPy runs on a expansion board 3.0 #define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 #define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm on board -#define HAS_BUTTON GPIO_NUM_37 // (P14) +#define HAS_BUTTON GPIO_NUM_13 // (P14) #define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown // uncomment this only if your LoPy runs on a expansion board 2.0 From cb4870bce56a59dfdb1d4673ed6afd0c85808f74 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 27 Sep 2018 14:01:23 +0200 Subject: [PATCH 074/105] testing --- LICENSE | 4 ++-- platformio.ini | 8 ++++---- src/button.cpp | 2 -- src/display.cpp | 16 ++++++---------- src/display.h | 1 - src/globals.h | 2 ++ src/lorawan.cpp | 4 +++- src/main.cpp | 38 +++++++++++++++++++++++++++----------- src/statemachine.cpp | 9 ++++----- src/statemachine.h | 1 + src/wifiscan.cpp | 38 ++++++++++++++++++++++---------------- src/wifiscan.h | 2 +- 12 files changed, 72 insertions(+), 53 deletions(-) diff --git a/LICENSE b/LICENSE index aa52acab..9288a85b 100644 --- a/LICENSE +++ b/LICENSE @@ -20,9 +20,9 @@ Parts of the source files in this repository are made available under different listed below. Refer to each individual source file for more details. ------------------------------------------------------------------------------------------------ -wifisniffer.cpp +wifiscan.cpp -Parts of wifisniffer.cpp were derived or taken from +Prior art was used for wifiscan.cpp and taken from * Copyright (c) 2017, Łukasz Marcin Podkalicki * ESP32/016 WiFi Sniffer diff --git a/platformio.ini b/platformio.ini index b795bf33..028ecd13 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,13 +6,13 @@ ; ---> SELECT TARGET PLATFORM HERE! <--- [platformio] -env_default = generic +;env_default = generic ;env_default = ebox ;env_default = heltec ;env_default = ttgov1 ;env_default = ttgov2 ;env_default = ttgov21old -;env_default = ttgov21new +env_default = ttgov21new ;env_default = ttgobeam ;env_default = lopy ;env_default = lopy4 @@ -26,10 +26,10 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.6 +release_version = 1.5.7 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose -debug_level = 0 +debug_level = 3 ; UPLOAD MODE: select esptool to flash via USB/UART, select custom to upload to cloud for OTA upload_protocol = esptool ;upload_protocol = custom diff --git a/src/button.cpp b/src/button.cpp index 4a816c14..1cca9f4e 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -13,7 +13,6 @@ void IRAM_ATTR ButtonIRQ() { } void readButton() { - if (ButtonPressedIRQ) { portENTER_CRITICAL(&timerMux); ButtonPressedIRQ = 0; portEXIT_CRITICAL(&timerMux); @@ -21,6 +20,5 @@ void readButton() { payload.reset(); payload.addButton(0x01); SendData(BUTTONPORT); - } } #endif \ No newline at end of file diff --git a/src/display.cpp b/src/display.cpp index b3ed4ee4..e7c168bb 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -91,6 +91,10 @@ void init_display(const char *Productname, const char *Version) { void refreshtheDisplay() { + portENTER_CRITICAL(&timerMux); + DisplayTimerIRQ = 0; + portEXIT_CRITICAL(&timerMux); + // set display on/off according to current device configuration if (DisplayState != cfg.screenon) { DisplayState = cfg.screenon; @@ -114,7 +118,8 @@ void refreshtheDisplay() { // update Battery status (line 2) #ifdef HAS_BATTERY_PROBE u8x8.setCursor(0, 2); - u8x8.printf(batt_voltage > 4000 ? "B:USB " : "B:%.1fV", batt_voltage / 1000.0); + u8x8.printf(batt_voltage > 4000 ? "B:USB " : "B:%.1fV", + batt_voltage / 1000.0); #endif // update GPS status (line 2) @@ -191,13 +196,4 @@ void IRAM_ATTR DisplayIRQ() { portEXIT_CRITICAL_ISR(&timerMux); } -void updateDisplay() { - if (DisplayTimerIRQ) { - portENTER_CRITICAL(&timerMux); - DisplayTimerIRQ = 0; - portEXIT_CRITICAL(&timerMux); - refreshtheDisplay(); - } -} - #endif // HAS_DISPLAY \ No newline at end of file diff --git a/src/display.h b/src/display.h index e009216d..39a3128a 100644 --- a/src/display.h +++ b/src/display.h @@ -9,7 +9,6 @@ extern HAS_DISPLAY u8x8; void init_display(const char *Productname, const char *Version); void refreshtheDisplay(void); void DisplayKey(const uint8_t *key, uint8_t len, bool lsb); -void updateDisplay(void); void DisplayIRQ(void); #endif \ No newline at end of file diff --git a/src/globals.h b/src/globals.h index 28d9fde7..e9412403 100644 --- a/src/globals.h +++ b/src/globals.h @@ -53,6 +53,8 @@ extern volatile uint8_t SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, extern std::array::iterator it; extern std::array beacons; +extern SemaphoreHandle_t xWifiChannelSwitchSemaphore; + #ifdef HAS_GPS extern TaskHandle_t GpsTask; #include "gps.h" diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 95babc37..26ef93a3 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -247,7 +247,9 @@ void lorawan_loop(void *pvParameters) { configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check while (1) { - os_runloop_once(); // execute LMIC jobs + //vTaskSuspendAll(); + os_runloop_once(); // execute LMIC jobs + //xTaskResumeAll(); vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU } } diff --git a/src/main.cpp b/src/main.cpp index 26ba304b..27c5b8c5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,6 +32,7 @@ gpsloop 0 2 read data from GPS over serial or i2c IDLE 1 0 Arduino loop() -> used for LED switching loraloop 1 1 runs the LMIC stack statemachine 1 3 switches application process logic +wifiloop 0 4 rotates wifi channels ESP32 hardware timers ========================== @@ -58,7 +59,9 @@ hw_timer_t *channelSwitch, *displaytimer, *sendCycle, *homeCycle; uint8_t volatile ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, SendCycleTimerIRQ = 0, DisplayTimerIRQ = 0, HomeCycleIRQ = 0; -TaskHandle_t StateTask = NULL; +TaskHandle_t stateMachineTask = NULL; + +SemaphoreHandle_t xWifiChannelSwitchSemaphore; // RTos send queues for payload transmit #ifdef HAS_LORA @@ -245,11 +248,6 @@ void setup() { timerAlarmEnable(displaytimer); #endif - // setup channel rotation trigger IRQ using esp32 hardware timer 1 - channelSwitch = timerBegin(1, 800, true); - timerAttachInterrupt(channelSwitch, &ChannelSwitchIRQ, true); - timerAlarmWrite(channelSwitch, cfg.wifichancycle * 1000, true); - // setup send cycle trigger IRQ using esp32 hardware timer 2 sendCycle = timerBegin(2, 8000, true); timerAttachInterrupt(sendCycle, &SendCycleIRQ, true); @@ -260,6 +258,12 @@ void setup() { timerAttachInterrupt(homeCycle, &homeCycleIRQ, true); timerAlarmWrite(homeCycle, HOMECYCLE * 10000, true); + // setup channel rotation trigger IRQ using esp32 hardware timer 1 + xWifiChannelSwitchSemaphore = xSemaphoreCreateBinary(); + channelSwitch = timerBegin(1, 800, true); + timerAttachInterrupt(channelSwitch, &ChannelSwitchIRQ, true); + timerAlarmWrite(channelSwitch, cfg.wifichancycle * 1000, true); + // enable timers // caution, see: https://github.com/espressif/arduino-esp32/issues/1313 yield(); @@ -326,18 +330,30 @@ void setup() { // start wifi in monitor mode and start channel rotation task on core 0 ESP_LOGI(TAG, "Starting Wifi..."); - // esp_event_loop_init(NULL, NULL); - // ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); wifi_sniffer_init(); // initialize salt value using esp_random() called by random() in // arduino-esp32 core. Note: do this *after* wifi has started, since // function gets it's seed from RF noise get_salt(); // get new 16bit for salting hashes + // start wifi channel rotation task + xTaskCreatePinnedToCore(switchWifiChannel, /* task function */ + "wifiloop", /* name of task */ + 3048, /* stack size of task */ + NULL, /* parameter of the task */ + 4, /* priority of the task */ + NULL, /* task handle*/ + 0); /* CPU core */ + // start state machine ESP_LOGI(TAG, "Starting Statemachine..."); - xTaskCreatePinnedToCore(stateMachine, "stateloop", 2048, (void *)1, 3, - &StateTask, 1); + xTaskCreatePinnedToCore(stateMachine, /* task function */ + "stateloop", /* name of task */ + 2048, /* stack size of task */ + (void *)1, /* parameter of the task */ + 3, /* priority of the task */ + &stateMachineTask, /* task handle */ + 1); /* CPU core */ } // setup() @@ -350,4 +366,4 @@ void loop() { // give yield to CPU vTaskDelay(2 / portTICK_PERIOD_MS); -} +} \ No newline at end of file diff --git a/src/statemachine.cpp b/src/statemachine.cpp index 9dfac811..c9c5ed07 100644 --- a/src/statemachine.cpp +++ b/src/statemachine.cpp @@ -10,16 +10,15 @@ void stateMachine(void *pvParameters) { while (1) { #ifdef HAS_BUTTON - readButton(); + if (ButtonPressedIRQ) + readButton(); #endif #ifdef HAS_DISPLAY - updateDisplay(); + if (DisplayTimerIRQ) + refreshtheDisplay(); #endif - // check wifi scan cycle and if due rotate channel - if (ChannelTimerIRQ) - switchWifiChannel(channel); // check housekeeping cycle and if due do the work if (HomeCycleIRQ) doHousekeeping(); diff --git a/src/statemachine.h b/src/statemachine.h index 7390e5c9..ec9966a0 100644 --- a/src/statemachine.h +++ b/src/statemachine.h @@ -8,5 +8,6 @@ #include "cyclic.h" void stateMachine(void *pvParameters); +void stateMachineInit(); #endif diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index d6012afa..04ebb3a7 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -30,7 +30,11 @@ void wifi_sniffer_init(void) { cfg.nvs_enable = 0; // we don't need any wifi settings from NVRAM wifi_promiscuous_filter_t filter = { .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; // we need only MGMT frames - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); // configure Wifi with cfg + + // esp_event_loop_init(NULL, NULL); + // ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); // configure Wifi with cfg ESP_ERROR_CHECK( esp_wifi_set_country(&wifi_country)); // set locales for RF and channels ESP_ERROR_CHECK( @@ -43,20 +47,22 @@ void wifi_sniffer_init(void) { ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); // now switch on monitor mode } -// Wifi channel rotation -void switchWifiChannel(uint8_t volatile &ch) { - portENTER_CRITICAL(&timerMux); - ChannelTimerIRQ = 0; - portEXIT_CRITICAL(&timerMux); - // rotates variable channel 1..WIFI_CHANNEL_MAX - ch = (ch % WIFI_CHANNEL_MAX) + 1; - esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE); - ESP_LOGD(TAG, "Wifi set channel %d", ch); +// IRQ Handler +void ChannelSwitchIRQ() { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + // unblock wifi channel rotation task + xSemaphoreGiveFromISR(xWifiChannelSwitchSemaphore, &xHigherPriorityTaskWoken); } -// IRQ handler -void IRAM_ATTR ChannelSwitchIRQ() { - portENTER_CRITICAL(&timerMux); - ChannelTimerIRQ++; - portEXIT_CRITICAL(&timerMux); -} \ No newline at end of file +// Wifi channel rotation task +void switchWifiChannel(void * parameter) { + while (1) { + // task in block state to wait for channel switch timer interrupt event + xSemaphoreTake(xWifiChannelSwitchSemaphore, portMAX_DELAY); + // rotates variable channel 1..WIFI_CHANNEL_MAX + channel = (channel % WIFI_CHANNEL_MAX) + 1; + esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); + ESP_LOGD(TAG, "Wifi set channel %d", channel); + } + vTaskDelete(NULL); +} diff --git a/src/wifiscan.h b/src/wifiscan.h index 581d7b0d..930178f2 100644 --- a/src/wifiscan.h +++ b/src/wifiscan.h @@ -28,6 +28,6 @@ typedef struct { void wifi_sniffer_init(void); void wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type); void ChannelSwitchIRQ(void); -void switchWifiChannel(uint8_t volatile &ch); +void switchWifiChannel(void * parameter); #endif \ No newline at end of file From b22fd808b89b80121b025009644c821afe4acea9 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 27 Sep 2018 15:13:15 +0200 Subject: [PATCH 075/105] task stack sizes tailored; semaphore controlled wifi task --- platformio.ini | 6 +++--- src/cyclic.cpp | 15 ++++++++++++++- src/globals.h | 1 + src/main.cpp | 23 +++++++++++++++++------ src/wifiscan.cpp | 2 +- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/platformio.ini b/platformio.ini index 028ecd13..8e3d7397 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,13 +6,13 @@ ; ---> SELECT TARGET PLATFORM HERE! <--- [platformio] -;env_default = generic +env_default = generic ;env_default = ebox ;env_default = heltec ;env_default = ttgov1 ;env_default = ttgov2 ;env_default = ttgov21old -env_default = ttgov21new +;env_default = ttgov21new ;env_default = ttgobeam ;env_default = lopy ;env_default = lopy4 @@ -29,7 +29,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng release_version = 1.5.7 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose -debug_level = 3 +debug_level = 0 ; UPLOAD MODE: select esptool to flash via USB/UART, select custom to upload to cloud for OTA upload_protocol = esptool ;upload_protocol = custom diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 0d5d1a3d..756c1a42 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -23,6 +23,19 @@ void doHousekeeping() { if (cfg.runmode == 1) ESP.restart(); +// task storage debugging // +#ifdef HAS_LORA + ESP_LOGD(TAG, "Loraloop %d bytes left", + uxTaskGetStackHighWaterMark(LoraTask)); +#endif + ESP_LOGD(TAG, "Wifiloop %d bytes left", + uxTaskGetStackHighWaterMark(wifiSwitchTask)); + ESP_LOGD(TAG, "Statemachine %d bytes left", + uxTaskGetStackHighWaterMark(stateMachineTask)); +#ifdef HAS_GPS + ESP_LOGD(TAG, "Gpsloop %d bytes left", uxTaskGetStackHighWaterMark(GpsTask)); +#endif + // read battery voltage into global variable #ifdef HAS_BATTERY_PROBE batt_voltage = read_voltage(); @@ -48,7 +61,7 @@ void doHousekeeping() { esp_get_minimum_free_heap_size(), ESP.getFreeHeap()); SendData(COUNTERPORT); // send data before clearing counters reset_counters(); // clear macs container and reset all counters - get_salt(); // get new salt for salting hashes + get_salt(); // get new salt for salting hashes if (esp_get_minimum_free_heap_size() <= MEM_LOW) // check again esp_restart(); // memory leak, reset device diff --git a/src/globals.h b/src/globals.h index e9412403..a63859e1 100644 --- a/src/globals.h +++ b/src/globals.h @@ -54,6 +54,7 @@ extern std::array::iterator it; extern std::array beacons; extern SemaphoreHandle_t xWifiChannelSwitchSemaphore; +extern TaskHandle_t stateMachineTask, wifiSwitchTask; #ifdef HAS_GPS extern TaskHandle_t GpsTask; diff --git a/src/main.cpp b/src/main.cpp index 27c5b8c5..15363f54 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -59,7 +59,7 @@ hw_timer_t *channelSwitch, *displaytimer, *sendCycle, *homeCycle; uint8_t volatile ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, SendCycleTimerIRQ = 0, DisplayTimerIRQ = 0, HomeCycleIRQ = 0; -TaskHandle_t stateMachineTask = NULL; +TaskHandle_t stateMachineTask, wifiSwitchTask; SemaphoreHandle_t xWifiChannelSwitchSemaphore; @@ -308,8 +308,13 @@ void setup() { // https://techtutorialsx.com/2017/05/09/esp32-get-task-execution-core/ ESP_LOGI(TAG, "Starting Lora..."); - xTaskCreatePinnedToCore(lorawan_loop, "loraloop", 2048, (void *)1, 1, - &LoraTask, 1); + xTaskCreatePinnedToCore(lorawan_loop, /* task function */ + "loraloop", /* name of task */ + 2560, /* stack size of task */ + (void *)1, /* parameter of the task */ + 1, /* priority of the task */ + &LoraTask, /* task handle*/ + 1); /* CPU core */ #endif // if device has GPS and it is enabled, start GPS reader task on core 0 with @@ -317,7 +322,13 @@ void setup() { // streaming NMEA data #ifdef HAS_GPS ESP_LOGI(TAG, "Starting GPS..."); - xTaskCreatePinnedToCore(gps_loop, "gpsloop", 2048, (void *)1, 2, &GpsTask, 0); + xTaskCreatePinnedToCore(gps_loop, /* task function */ + "gpsloop", /* name of task */ + 1024, /* stack size of task */ + (void *)1, /* parameter of the task */ + 2, /* priority of the task */ + &GpsTask, /* task handle*/ + 0); /* CPU core */ #endif // start BLE scan callback if BLE function is enabled in NVRAM configuration @@ -339,10 +350,10 @@ void setup() { // start wifi channel rotation task xTaskCreatePinnedToCore(switchWifiChannel, /* task function */ "wifiloop", /* name of task */ - 3048, /* stack size of task */ + 1024, /* stack size of task */ NULL, /* parameter of the task */ 4, /* priority of the task */ - NULL, /* task handle*/ + &wifiSwitchTask, /* task handle*/ 0); /* CPU core */ // start state machine diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 04ebb3a7..21a5a95e 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -57,7 +57,7 @@ void ChannelSwitchIRQ() { // Wifi channel rotation task void switchWifiChannel(void * parameter) { while (1) { - // task in block state to wait for channel switch timer interrupt event + // task is remaining in block state waiting for channel switch timer interrupt event xSemaphoreTake(xWifiChannelSwitchSemaphore, portMAX_DELAY); // rotates variable channel 1..WIFI_CHANNEL_MAX channel = (channel % WIFI_CHANNEL_MAX) + 1; From 8125662753b8b8969eca53c689ee74c90aa1bff2 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 27 Sep 2018 21:13:18 +0200 Subject: [PATCH 076/105] ota battery check modified --- src/battery.cpp | 5 +++++ src/battery.h | 1 + src/ota.cpp | 2 +- src/ota.h | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/battery.cpp b/src/battery.cpp index 11371748..0e894965 100644 --- a/src/battery.cpp +++ b/src/battery.cpp @@ -49,4 +49,9 @@ uint16_t read_voltage() { return voltage; } +bool batt_sufficient() { + uint16_t volts = read_voltage(); + return (( volts < 1000 ) || (volts > OTA_MIN_BATT)); // no battery or battery sufficient +} + #endif // HAS_BATTERY_PROBE \ No newline at end of file diff --git a/src/battery.h b/src/battery.h index 93696fb5..b4a83676 100644 --- a/src/battery.h +++ b/src/battery.h @@ -9,5 +9,6 @@ uint16_t read_voltage(void); void calibrate_voltage(void); +bool batt_sufficient(void); #endif diff --git a/src/ota.cpp b/src/ota.cpp index 15ef87b5..ece73e28 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -45,7 +45,7 @@ void start_ota_update() { // check battery status if we can before doing ota #ifdef HAS_BATTERY_PROBE - if (batt_voltage < OTA_MIN_BATT) { + if (!batt_sufficient()) { ESP_LOGW(TAG, "Battery voltage %dmV too low for OTA", batt_voltage); return; } diff --git a/src/ota.h b/src/ota.h index 59c1e464..135591ea 100644 --- a/src/ota.h +++ b/src/ota.h @@ -5,6 +5,7 @@ #include "globals.h" #include "update.h" +#include "battery.h" #include #include #include From a3f35a6ecae505f8b069daaa6e2f89730402304e Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 27 Sep 2018 22:04:51 +0200 Subject: [PATCH 077/105] modified DEVEUI Generator & added Lmic PR #197 --- lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/lmic.c | 2 +- src/lorawan.cpp | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/lmic.c b/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/lmic.c index c0c1bbc3..a6e42933 100644 --- a/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/lmic.c +++ b/lib/arduino-lmic-1.5.0-arduino-2-tweaked/src/lmic/lmic.c @@ -907,7 +907,7 @@ static ostime_t nextJoinState (void) { } else { LMIC.txChnl = os_getRndU1() & 0x3F; s1_t dr = DR_SF7 - ++LMIC.txCnt; - if( dr < DR_SF10 ) { + if( LMIC.txCnt > DR_SF7 ) { dr = DR_SF10; failed = 1; // All DR exhausted - signal failed } diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 26ef93a3..7f93c1e6 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -45,8 +45,8 @@ void gen_lora_deveui(uint8_t *pdeveui) { *p++ = dmac[5]; *p++ = dmac[4]; *p++ = dmac[3]; - *p++ = 0xff; *p++ = 0xfe; + *p++ = 0xff; *p++ = dmac[2]; *p++ = dmac[1]; *p++ = dmac[0]; @@ -247,9 +247,7 @@ void lorawan_loop(void *pvParameters) { configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check while (1) { - //vTaskSuspendAll(); os_runloop_once(); // execute LMIC jobs - //xTaskResumeAll(); vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU } } From e18a78c36025efd869dc77b8ced406829efa078a Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Fri, 28 Sep 2018 08:26:37 +0200 Subject: [PATCH 078/105] edit hal/lopy4.h --- src/hal/lopy4.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hal/lopy4.h b/src/hal/lopy4.h index 47f37f7f..97cf5b40 100644 --- a/src/hal/lopy4.h +++ b/src/hal/lopy4.h @@ -1,4 +1,4 @@ -// Hardware related definitions for Pycom LoPy Board (not: LoPy4) +// Hardware related definitions for Pycom LoPy4 Board #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define HAS_SPI 1 // comment out if device shall not send data via SPI From 6ed536809814150693e0163f5453c84c62732dd4 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 29 Sep 2018 19:26:24 +0200 Subject: [PATCH 079/105] edit HAL files lopy.h / lopy4.h --- src/hal/lopy.h | 29 ++++++++++++----------------- src/hal/lopy4.h | 23 +++++++++-------------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/src/hal/lopy.h b/src/hal/lopy.h index d8d678cb..545ac8ab 100644 --- a/src/hal/lopy.h +++ b/src/hal/lopy.h @@ -3,9 +3,7 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define HAS_SPI 1 // comment out if device shall not send data via SPI #define CFG_sx1272_radio 1 -//#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED -#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED -#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED +#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED #define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 // Hardware pin definitions for Pycom LoPy board @@ -22,19 +20,16 @@ #define HAS_ANTENNA_SWITCH 16 // pin for switching wifi antenna #define WIFI_ANTENNA 0 // 0 = internal, 1 = external -// uncomment this only if your LoPy runs on a Pytrack expansion board with GPS -#define HAS_GPS 1 -#define GPS_QUECTEL_L76 GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21) -#define GPS_ADDR 0x10 +// uncomment this only if your LoPy runs on a PYTRACK BOARD +//#define HAS_GPS 1 +//#define GPS_QUECTEL_L76 GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21) +//#define GPS_ADDR 0x10 -// uncomment this only if your LoPy runs on a expansion board 3.0 +// uncomment this only if your LoPy runs on a EXPANSION BOARD +//#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED +//#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED +//#define HAS_BUTTON GPIO_NUM_13 // user button on expansion board +//define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown //#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 -//#define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm on board -//#define HAS_BUTTON GPIO_NUM_13 // (P14) -//#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown - -// uncomment this only if your LoPy runs on a expansion board 2.0 -//#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 -//#define BATT_FACTOR 4 // voltage divider 115kOhm/56kOhm on board -//#define HAS_BUTTON GPIO_NUM_13 // (P10) -//#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown +//#define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm -> expansion board 3.0 +//#define BATT_FACTOR 4 // voltage divider 115kOhm/56kOhm -> expansion board 2.0 diff --git a/src/hal/lopy4.h b/src/hal/lopy4.h index 97cf5b40..cef619f0 100644 --- a/src/hal/lopy4.h +++ b/src/hal/lopy4.h @@ -3,9 +3,7 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define HAS_SPI 1 // comment out if device shall not send data via SPI #define CFG_sx1276_radio 1 -//#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED -#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED -#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED +#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED #define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 #define BOARD_HAS_PSRAM // use extra 4MB extern RAM @@ -23,19 +21,16 @@ #define HAS_ANTENNA_SWITCH 21 // pin for switching wifi antenna #define WIFI_ANTENNA 0 // 0 = internal, 1 = external -// uncomment this only if your LoPy runs on a Pytrack expansion board with GPS +// uncomment this only if your LoPy runs on a PYTRACK BOARD //#define HAS_GPS 1 //#define GPS_QUECTEL_L76 GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21) //#define GPS_ADDR 0x10 -// uncomment this only if your LoPy runs on a expansion board 3.0 -#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 -#define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm on board -#define HAS_BUTTON GPIO_NUM_13 // (P14) -#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown - -// uncomment this only if your LoPy runs on a expansion board 2.0 +// uncomment this only if your LoPy runs on a EXPANSION BOARD +//#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED +//#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED +//#define HAS_BUTTON GPIO_NUM_13 // user button on expansion board +//define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown //#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 -//#define BATT_FACTOR 4 // voltage divider 115kOhm/56kOhm on board -//#define HAS_BUTTON GPIO_NUM_13 // (P10) -//#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown \ No newline at end of file +//#define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm -> expansion board 3.0 +//#define BATT_FACTOR 4 // voltage divider 115kOhm/56kOhm -> expansion board 2.0 \ No newline at end of file From bad2331a6b2e44d22fbe56b783cc947b8221c103 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 11:54:45 +0200 Subject: [PATCH 080/105] ota battery check disabled; v1.5.8 --- platformio.ini | 2 +- src/ota.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 8e3d7397..014772e3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.7 +release_version = 1.5.8 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 diff --git a/src/ota.cpp b/src/ota.cpp index ece73e28..bf826953 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -43,6 +43,7 @@ inline String getHeaderValue(String header, String headerName) { void start_ota_update() { +/* // check battery status if we can before doing ota #ifdef HAS_BATTERY_PROBE if (!batt_sufficient()) { @@ -50,6 +51,7 @@ void start_ota_update() { return; } #endif +*/ // turn on LED #if (HAS_LED != NOT_A_PIN) From 43b4946252e4668f0263a2db92212f6d959e3612 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 15:08:00 +0200 Subject: [PATCH 081/105] timing improvements (separate mutexes for IRQ; Loraloop higher prio) --- platformio.ini | 2 +- src/button.cpp | 8 ++++---- src/cyclic.cpp | 8 ++++---- src/cyclic.h | 2 +- src/display.cpp | 8 ++++---- src/display.h | 2 +- src/globals.h | 2 +- src/hash.cpp | 4 ++-- src/hash.h | 7 +++++-- src/main.cpp | 22 ++++++++++++---------- src/ota.cpp | 7 +++++-- src/ota.h | 5 ++++- src/senddata.cpp | 8 ++++---- src/senddata.h | 2 +- src/statemachine.cpp | 1 + src/wifiscan.cpp | 3 ++- src/wifiscan.h | 2 +- 17 files changed, 53 insertions(+), 40 deletions(-) diff --git a/platformio.ini b/platformio.ini index 014772e3..936d5fa7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.8 +release_version = 1.5.9 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 diff --git a/src/button.cpp b/src/button.cpp index 1cca9f4e..42f4180f 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -7,15 +7,15 @@ static const char TAG[] = "main"; void IRAM_ATTR ButtonIRQ() { - portENTER_CRITICAL(&timerMux); + portENTER_CRITICAL(&mutexButton); ButtonPressedIRQ++; - portEXIT_CRITICAL(&timerMux); + portEXIT_CRITICAL(&mutexButton); } void readButton() { - portENTER_CRITICAL(&timerMux); + portENTER_CRITICAL(&mutexButton); ButtonPressedIRQ = 0; - portEXIT_CRITICAL(&timerMux); + portEXIT_CRITICAL(&mutexButton); ESP_LOGI(TAG, "Button pressed"); payload.reset(); payload.addButton(0x01); diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 756c1a42..0e49726a 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -12,9 +12,9 @@ static const char TAG[] = "main"; // do all housekeeping void doHousekeeping() { - portENTER_CRITICAL(&timerMux); + portENTER_CRITICAL(&mutexHomeCycle); HomeCycleIRQ = 0; - portEXIT_CRITICAL(&timerMux); + portEXIT_CRITICAL(&mutexHomeCycle); // update uptime counter uptime(); @@ -69,9 +69,9 @@ void doHousekeeping() { } // doHousekeeping() void IRAM_ATTR homeCycleIRQ() { - portENTER_CRITICAL(&timerMux); + portENTER_CRITICAL(&mutexHomeCycle); HomeCycleIRQ++; - portEXIT_CRITICAL(&timerMux); + portEXIT_CRITICAL(&mutexHomeCycle); } // uptime counter 64bit to prevent millis() rollover after 49 days diff --git a/src/cyclic.h b/src/cyclic.h index 0a169e4e..a006f137 100644 --- a/src/cyclic.h +++ b/src/cyclic.h @@ -2,7 +2,7 @@ #define _CYCLIC_H void doHousekeeping(void); -void homeCycleIRQ(void); +void IRAM_ATTR homeCycleIRQ(void); uint64_t uptime(void); void reset_counters(void); int redirect_log(const char *fmt, va_list args); diff --git a/src/display.cpp b/src/display.cpp index e7c168bb..9e2ffcb2 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -91,9 +91,9 @@ void init_display(const char *Productname, const char *Version) { void refreshtheDisplay() { - portENTER_CRITICAL(&timerMux); + portENTER_CRITICAL(&mutexDisplay); DisplayTimerIRQ = 0; - portEXIT_CRITICAL(&timerMux); + portEXIT_CRITICAL(&mutexDisplay); // set display on/off according to current device configuration if (DisplayState != cfg.screenon) { @@ -191,9 +191,9 @@ void refreshtheDisplay() { } // refreshDisplay() void IRAM_ATTR DisplayIRQ() { - portENTER_CRITICAL_ISR(&timerMux); + portENTER_CRITICAL_ISR(&mutexDisplay); DisplayTimerIRQ++; - portEXIT_CRITICAL_ISR(&timerMux); + portEXIT_CRITICAL_ISR(&mutexDisplay); } #endif // HAS_DISPLAY \ No newline at end of file diff --git a/src/display.h b/src/display.h index 39a3128a..84ff9b7a 100644 --- a/src/display.h +++ b/src/display.h @@ -9,6 +9,6 @@ extern HAS_DISPLAY u8x8; void init_display(const char *Productname, const char *Version); void refreshtheDisplay(void); void DisplayKey(const uint8_t *key, uint8_t len, bool lsb); -void DisplayIRQ(void); +void IRAM_ATTR DisplayIRQ(void); #endif \ No newline at end of file diff --git a/src/globals.h b/src/globals.h index a63859e1..a4fffd4d 100644 --- a/src/globals.h +++ b/src/globals.h @@ -46,7 +46,7 @@ extern uint16_t volatile macs_total, macs_wifi, macs_ble, batt_voltage; // display values extern std::set macs; // temp storage for MACs extern hw_timer_t *channelSwitch, *sendCycle; -extern portMUX_TYPE timerMux; +extern portMUX_TYPE mutexButton, mutexDisplay, mutexHomeCycle, mutexSendCycle; extern volatile uint8_t SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, ChannelTimerIRQ, ButtonPressedIRQ; diff --git a/src/hash.cpp b/src/hash.cpp index 62e12656..e5d268f9 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -34,9 +34,9 @@ * SOFTWARE. */ -#include +#include "hash.h" -uint32_t rokkit(const char *data, int len) { +uint32_t IRAM_ATTR rokkit(const char *data, int len) { uint32_t hash, tmp; int rem; diff --git a/src/hash.h b/src/hash.h index 010fd6b8..b86e7b2e 100644 --- a/src/hash.h +++ b/src/hash.h @@ -1,6 +1,9 @@ #ifndef _HASH_H #define _HASH_H -uint32_t rokkit(const char *data, int len); +#include +#include -#endif +uint32_t IRAM_ATTR rokkit(const char *data, int len); + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 15363f54..d150928a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,12 +26,12 @@ licenses. Refer to LICENSE.txt file in repository for more details. Uused tasks and timers: Task Core Prio Purpose -==================================================================== -IDLE 0 0 ESP32 arduino scheduler +==================================================================================== +IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer task gpsloop 0 2 read data from GPS over serial or i2c IDLE 1 0 Arduino loop() -> used for LED switching -loraloop 1 1 runs the LMIC stack -statemachine 1 3 switches application process logic +loraloop 1 3 runs the LMIC stack +statemachine 1 1 switches application process logic wifiloop 0 4 rotates wifi channels ESP32 hardware timers @@ -77,9 +77,11 @@ QueueHandle_t SPISendQueue; TaskHandle_t GpsTask = NULL; #endif -portMUX_TYPE timerMux = - portMUX_INITIALIZER_UNLOCKED; // sync main loop and ISR when modifying IRQ - // handler shared variables +// sync main loop and ISR when modifying IRQ handler shared variables +portMUX_TYPE mutexButton = portMUX_INITIALIZER_UNLOCKED; +portMUX_TYPE mutexDisplay = portMUX_INITIALIZER_UNLOCKED; +portMUX_TYPE mutexHomeCycle = portMUX_INITIALIZER_UNLOCKED; +portMUX_TYPE mutexSendCycle = portMUX_INITIALIZER_UNLOCKED; std::set macs; // container holding unique MAC adress hashes @@ -312,7 +314,7 @@ void setup() { "loraloop", /* name of task */ 2560, /* stack size of task */ (void *)1, /* parameter of the task */ - 1, /* priority of the task */ + 3, /* priority of the task */ &LoraTask, /* task handle*/ 1); /* CPU core */ #endif @@ -350,7 +352,7 @@ void setup() { // start wifi channel rotation task xTaskCreatePinnedToCore(switchWifiChannel, /* task function */ "wifiloop", /* name of task */ - 1024, /* stack size of task */ + 1536, /* stack size of task */ NULL, /* parameter of the task */ 4, /* priority of the task */ &wifiSwitchTask, /* task handle*/ @@ -362,7 +364,7 @@ void setup() { "stateloop", /* name of task */ 2048, /* stack size of task */ (void *)1, /* parameter of the task */ - 3, /* priority of the task */ + 1, /* priority of the task */ &stateMachineTask, /* task handle */ 1); /* CPU core */ diff --git a/src/ota.cpp b/src/ota.cpp index bf826953..5e123dff 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -314,7 +314,8 @@ void do_ota_update() { client.stop(); } // do_ota_update -void display(const uint8_t row, const std::string status, const std::string msg) { +void display(const uint8_t row, const std::string status, + const std::string msg) { #ifdef HAS_DISPLAY u8x8.setCursor(14, row); u8x8.print((status.substr(0, 2)).c_str()); @@ -323,15 +324,17 @@ void display(const uint8_t row, const std::string status, const std::string msg) u8x8.setCursor(0, 7); u8x8.print(msg.substr(0, 16).c_str()); } +#endif } +#ifdef HAS_DISPLAY // callback function to show download progress while streaming data void show_progress(size_t current, size_t size) { char buf[17]; snprintf(buf, 17, "%-9lu (%3lu%%)", current, current * 100 / size); display(4, "**", buf); -#endif } +#endif // helper function to compare two versions. Returns 1 if v2 is // smaller, -1 if v1 is smaller, 0 if equal diff --git a/src/ota.h b/src/ota.h index 135591ea..560ee144 100644 --- a/src/ota.h +++ b/src/ota.h @@ -14,8 +14,11 @@ void do_ota_update(); void start_ota_update(); int version_compare(const String v1, const String v2); +void display(const uint8_t row, const std::string status, + const std::string msg); +#ifdef HAS_DISPLAY void show_progress(size_t current, size_t size); -void display(const uint8_t row, const std::string status, const std::string msg); +#endif #endif // USE_OTA diff --git a/src/senddata.cpp b/src/senddata.cpp index 2ca7973a..616df8fb 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -37,9 +37,9 @@ void SendData(uint8_t port) { // interrupt triggered function to prepare payload to send void sendPayload() { - portENTER_CRITICAL(&timerMux); + portENTER_CRITICAL(&mutexSendCycle); SendCycleTimerIRQ = 0; - portEXIT_CRITICAL(&timerMux); + portEXIT_CRITICAL(&mutexSendCycle); // append counter data to payload payload.reset(); @@ -68,9 +68,9 @@ void sendPayload() { // interrupt handler used for payload send cycle timer void IRAM_ATTR SendCycleIRQ() { - portENTER_CRITICAL(&timerMux); + portENTER_CRITICAL(&mutexSendCycle); SendCycleTimerIRQ++; - portEXIT_CRITICAL(&timerMux); + portEXIT_CRITICAL(&mutexSendCycle); } // interrupt triggered function to eat data from send queues and transmit it diff --git a/src/senddata.h b/src/senddata.h index df7b53f5..fc236b5f 100644 --- a/src/senddata.h +++ b/src/senddata.h @@ -3,7 +3,7 @@ void SendData(uint8_t port); void sendPayload(void); -void SendCycleIRQ(void); +void IRAM_ATTR SendCycleIRQ(void); void checkSendQueues(void); void flushQueues(); diff --git a/src/statemachine.cpp b/src/statemachine.cpp index c9c5ed07..76d02801 100644 --- a/src/statemachine.cpp +++ b/src/statemachine.cpp @@ -31,4 +31,5 @@ void stateMachine(void *pvParameters) { // give yield to CPU vTaskDelay(2 / portTICK_PERIOD_MS); } + vTaskDelete(NULL); // shoud never be reached } \ No newline at end of file diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 21a5a95e..fca54e2a 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -28,6 +28,7 @@ IRAM_ATTR void wifi_sniffer_packet_handler(void *buff, void wifi_sniffer_init(void) { wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); cfg.nvs_enable = 0; // we don't need any wifi settings from NVRAM + cfg.wifi_task_core_id = 0; // we want wifi task running on core 0 wifi_promiscuous_filter_t filter = { .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; // we need only MGMT frames @@ -64,5 +65,5 @@ void switchWifiChannel(void * parameter) { esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); ESP_LOGD(TAG, "Wifi set channel %d", channel); } - vTaskDelete(NULL); + vTaskDelete(NULL); // shoud never be reached } diff --git a/src/wifiscan.h b/src/wifiscan.h index 930178f2..ea507cd7 100644 --- a/src/wifiscan.h +++ b/src/wifiscan.h @@ -26,7 +26,7 @@ typedef struct { } wifi_ieee80211_packet_t; void wifi_sniffer_init(void); -void wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type); +void IRAM_ATTR wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type); void ChannelSwitchIRQ(void); void switchWifiChannel(void * parameter); From afc464d3f7711a7d8c4aa9f94a8b75cc83a3c436 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 15:54:17 +0200 Subject: [PATCH 082/105] improved wifi sniffing (use all frames not only mgmt) --- src/wifiscan.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index fca54e2a..621c8494 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -27,10 +27,11 @@ IRAM_ATTR void wifi_sniffer_packet_handler(void *buff, void wifi_sniffer_init(void) { wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - cfg.nvs_enable = 0; // we don't need any wifi settings from NVRAM + cfg.nvs_enable = 0; // we don't need any wifi settings from NVRAM cfg.wifi_task_core_id = 0; // we want wifi task running on core 0 wifi_promiscuous_filter_t filter = { - .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; // we need only MGMT frames + // .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; // only MGMT frames + .filter_mask = WIFI_PROMIS_FILTER_MASK_ALL}; // we use all frames // esp_event_loop_init(NULL, NULL); // ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); @@ -56,9 +57,10 @@ void ChannelSwitchIRQ() { } // Wifi channel rotation task -void switchWifiChannel(void * parameter) { +void switchWifiChannel(void *parameter) { while (1) { - // task is remaining in block state waiting for channel switch timer interrupt event + // task is remaining in block state waiting for channel switch timer + // interrupt event xSemaphoreTake(xWifiChannelSwitchSemaphore, portMAX_DELAY); // rotates variable channel 1..WIFI_CHANNEL_MAX channel = (channel % WIFI_CHANNEL_MAX) + 1; From 4b80667f036222b5b40ca084248a00bbfcf75501 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 16:20:19 +0200 Subject: [PATCH 083/105] edit HAL ttgo21old.h (removied display flip) --- src/hal/ttgov21old.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hal/ttgov21old.h b/src/hal/ttgov21old.h index c423ca67..ebb76e4d 100644 --- a/src/hal/ttgov21old.h +++ b/src/hal/ttgov21old.h @@ -9,7 +9,7 @@ #define HAS_LED NOT_A_PIN // no usable LED on board #define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C -#define DISPLAY_FLIP 1 // rotated display +//#define DISPLAY_FLIP 1 // rotated display #define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // uses GPIO7 #define BATT_FACTOR 2 // voltage divider 100k/100k on board From 240212afb9afe6c7ba1f29ea5a2d2fdeef71ecc7 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 16:49:31 +0200 Subject: [PATCH 084/105] edit HAL file ttgov21old.h --- src/hal/ttgov21old.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hal/ttgov21old.h b/src/hal/ttgov21old.h index ebb76e4d..ce92a8a4 100644 --- a/src/hal/ttgov21old.h +++ b/src/hal/ttgov21old.h @@ -10,8 +10,8 @@ #define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C //#define DISPLAY_FLIP 1 // rotated display -#define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // uses GPIO7 -#define BATT_FACTOR 2 // voltage divider 100k/100k on board +//#define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // uses GPIO7 +//#define BATT_FACTOR 2 // voltage divider 100k/100k on board // re-define pin definitions of pins_arduino.h #define PIN_SPI_SS GPIO_NUM_18 // ESP32 GPIO18 (Pin18) -- HPD13A NSS/SEL (Pin4) SPI Chip Select Input From 52e4dbca28acfab852b4ff418ffc4ffa02119bd8 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 17:48:51 +0200 Subject: [PATCH 085/105] edit lopy4.h --- src/hal/lopy4.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hal/lopy4.h b/src/hal/lopy4.h index cef619f0..5a4fffa0 100644 --- a/src/hal/lopy4.h +++ b/src/hal/lopy4.h @@ -27,10 +27,10 @@ //#define GPS_ADDR 0x10 // uncomment this only if your LoPy runs on a EXPANSION BOARD -//#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED -//#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED -//#define HAS_BUTTON GPIO_NUM_13 // user button on expansion board -//define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown -//#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 -//#define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm -> expansion board 3.0 +#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED +#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED +#define HAS_BUTTON GPIO_NUM_13 // user button on expansion board +#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown +#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 +#define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm -> expansion board 3.0 //#define BATT_FACTOR 4 // voltage divider 115kOhm/56kOhm -> expansion board 2.0 \ No newline at end of file From 7858270d9405d34b9e24f5a9db819b2d46870b29 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 17:50:45 +0200 Subject: [PATCH 086/105] edit lopy.h --- src/hal/lopy.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hal/lopy.h b/src/hal/lopy.h index 545ac8ab..97f3119c 100644 --- a/src/hal/lopy.h +++ b/src/hal/lopy.h @@ -29,7 +29,7 @@ //#define HAS_LED GPIO_NUM_12 // use if LoPy is on Expansion Board, this has a user LED //#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED //#define HAS_BUTTON GPIO_NUM_13 // user button on expansion board -//define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown +//#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown //#define HAS_BATTERY_PROBE ADC1_GPIO39_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 //#define BATT_FACTOR 2 // voltage divider 1MOhm/1MOhm -> expansion board 3.0 //#define BATT_FACTOR 4 // voltage divider 115kOhm/56kOhm -> expansion board 2.0 From 1c2fca392bab28d106643724204f540518051943 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 18:02:05 +0200 Subject: [PATCH 087/105] edit lopy4.h --- src/hal/lopy4.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hal/lopy4.h b/src/hal/lopy4.h index 5a4fffa0..fc0be422 100644 --- a/src/hal/lopy4.h +++ b/src/hal/lopy4.h @@ -3,7 +3,7 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define HAS_SPI 1 // comment out if device shall not send data via SPI #define CFG_sx1276_radio 1 -#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED +//#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED #define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 #define BOARD_HAS_PSRAM // use extra 4MB extern RAM From 16efbb0c901f62d3eb337d336fc1692a85bcc744 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 19:39:58 +0200 Subject: [PATCH 088/105] edit ttgov21old.h --- src/hal/ttgov21old.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hal/ttgov21old.h b/src/hal/ttgov21old.h index ce92a8a4..25271b80 100644 --- a/src/hal/ttgov21old.h +++ b/src/hal/ttgov21old.h @@ -7,6 +7,7 @@ #define HAS_SPI 1 // comment out if device shall not send data via SPI #define CFG_sx1276_radio 1 // HPD13A LoRa SoC #define HAS_LED NOT_A_PIN // no usable LED on board +#define DISABLE_BROWNOUT 1 // comment out if you want to keep brownout feature #define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C //#define DISPLAY_FLIP 1 // rotated display From 15f7f2fd85a5a0f3c17413183206fbd4c59e233d Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 22:06:10 +0200 Subject: [PATCH 089/105] v1.5.13 --- platformio.ini | 2 +- src/button.cpp | 2 ++ src/cyclic.cpp | 2 ++ src/display.cpp | 2 ++ src/globals.h | 1 - src/main.cpp | 14 ++++---------- src/senddata.cpp | 2 ++ src/wifiscan.cpp | 5 +---- src/wifiscan.h | 2 +- 9 files changed, 15 insertions(+), 17 deletions(-) diff --git a/platformio.ini b/platformio.ini index 936d5fa7..5fd9ef9e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.9 +release_version = 1.5.13 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 diff --git a/src/button.cpp b/src/button.cpp index 42f4180f..37ae91fe 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -6,6 +6,8 @@ // Local logging tag static const char TAG[] = "main"; +portMUX_TYPE mutexButton = portMUX_INITIALIZER_UNLOCKED; + void IRAM_ATTR ButtonIRQ() { portENTER_CRITICAL(&mutexButton); ButtonPressedIRQ++; diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 0e49726a..1e864ba5 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -9,6 +9,8 @@ // Local logging tag static const char TAG[] = "main"; +portMUX_TYPE mutexHomeCycle = portMUX_INITIALIZER_UNLOCKED; + // do all housekeeping void doHousekeeping() { diff --git a/src/display.cpp b/src/display.cpp index 9e2ffcb2..773dbf94 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -15,6 +15,8 @@ const char lora_datarate[] = {"100908078CNA121110090807"}; uint8_t volatile DisplayState = 0; +portMUX_TYPE mutexDisplay = portMUX_INITIALIZER_UNLOCKED; + // helper function, prints a hex key on display void DisplayKey(const uint8_t *key, uint8_t len, bool lsb) { const uint8_t *p; diff --git a/src/globals.h b/src/globals.h index a4fffd4d..739512f1 100644 --- a/src/globals.h +++ b/src/globals.h @@ -46,7 +46,6 @@ extern uint16_t volatile macs_total, macs_wifi, macs_ble, batt_voltage; // display values extern std::set macs; // temp storage for MACs extern hw_timer_t *channelSwitch, *sendCycle; -extern portMUX_TYPE mutexButton, mutexDisplay, mutexHomeCycle, mutexSendCycle; extern volatile uint8_t SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, ChannelTimerIRQ, ButtonPressedIRQ; diff --git a/src/main.cpp b/src/main.cpp index d150928a..f758a795 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,7 +30,7 @@ Task Core Prio Purpose IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer task gpsloop 0 2 read data from GPS over serial or i2c IDLE 1 0 Arduino loop() -> used for LED switching -loraloop 1 3 runs the LMIC stack +loraloop 1 2 runs the LMIC stack statemachine 1 1 switches application process logic wifiloop 0 4 rotates wifi channels @@ -77,12 +77,6 @@ QueueHandle_t SPISendQueue; TaskHandle_t GpsTask = NULL; #endif -// sync main loop and ISR when modifying IRQ handler shared variables -portMUX_TYPE mutexButton = portMUX_INITIALIZER_UNLOCKED; -portMUX_TYPE mutexDisplay = portMUX_INITIALIZER_UNLOCKED; -portMUX_TYPE mutexHomeCycle = portMUX_INITIALIZER_UNLOCKED; -portMUX_TYPE mutexSendCycle = portMUX_INITIALIZER_UNLOCKED; - std::set macs; // container holding unique MAC adress hashes // initialize payload encoder @@ -312,9 +306,9 @@ void setup() { ESP_LOGI(TAG, "Starting Lora..."); xTaskCreatePinnedToCore(lorawan_loop, /* task function */ "loraloop", /* name of task */ - 2560, /* stack size of task */ + 3048, /* stack size of task */ (void *)1, /* parameter of the task */ - 3, /* priority of the task */ + 2, /* priority of the task */ &LoraTask, /* task handle*/ 1); /* CPU core */ #endif @@ -352,7 +346,7 @@ void setup() { // start wifi channel rotation task xTaskCreatePinnedToCore(switchWifiChannel, /* task function */ "wifiloop", /* name of task */ - 1536, /* stack size of task */ + 2048, /* stack size of task */ NULL, /* parameter of the task */ 4, /* priority of the task */ &wifiSwitchTask, /* task handle*/ diff --git a/src/senddata.cpp b/src/senddata.cpp index 616df8fb..d4f89d62 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -1,6 +1,8 @@ // Basic Config #include "globals.h" +portMUX_TYPE mutexSendCycle = portMUX_INITIALIZER_UNLOCKED; + // put data to send in RTos Queues used for transmit over channels Lora and SPI void SendData(uint8_t port) { diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 621c8494..4e2500b6 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -33,9 +33,6 @@ void wifi_sniffer_init(void) { // .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; // only MGMT frames .filter_mask = WIFI_PROMIS_FILTER_MASK_ALL}; // we use all frames - // esp_event_loop_init(NULL, NULL); - // ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); // configure Wifi with cfg ESP_ERROR_CHECK( esp_wifi_set_country(&wifi_country)); // set locales for RF and channels @@ -50,7 +47,7 @@ void wifi_sniffer_init(void) { } // IRQ Handler -void ChannelSwitchIRQ() { +void IRAM_ATTR ChannelSwitchIRQ() { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // unblock wifi channel rotation task xSemaphoreGiveFromISR(xWifiChannelSwitchSemaphore, &xHigherPriorityTaskWoken); diff --git a/src/wifiscan.h b/src/wifiscan.h index ea507cd7..48c218a1 100644 --- a/src/wifiscan.h +++ b/src/wifiscan.h @@ -27,7 +27,7 @@ typedef struct { void wifi_sniffer_init(void); void IRAM_ATTR wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type); -void ChannelSwitchIRQ(void); +void IRAM_ATTR ChannelSwitchIRQ(void); void switchWifiChannel(void * parameter); #endif \ No newline at end of file From 62d6d865b3b27ed76e346c2fdff8a00345c15821 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sun, 30 Sep 2018 23:11:25 +0200 Subject: [PATCH 090/105] ota.cpp: text in display mask modified --- src/ota.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ota.cpp b/src/ota.cpp index 5e123dff..65ff97c4 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -74,8 +74,8 @@ void start_ota_update() { u8x8.setInverseFont(0); u8x8.print("WiFi connect ..\n"); u8x8.print("Has Update? ..\n"); + u8x8.print("Fetching ..\n"); u8x8.print("Downloading ..\n"); - u8x8.print("Flashing ..\n"); u8x8.print("Rebooting .."); #endif From 126a2b1326150c5b52a5a18fc297fb3bd0ea6e45 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Mon, 1 Oct 2018 09:48:29 +0200 Subject: [PATCH 091/105] v1.5.14 (esp32 d4c38ab) --- platformio.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 5fd9ef9e..f43f40a0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.13 +release_version = 1.5.14 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 @@ -35,7 +35,8 @@ upload_protocol = esptool ;upload_protocol = custom extra_scripts = pre:build.py keyfile = ota.conf -platform_espressif32 = espressif32@1.3.0 +;platform_espressif32 = espressif32@1.3.0 +platform_espressif32 = https://github.com/platformio/platform-espressif32.git#d4c38ab board_build.partitions = min_spiffs.csv monitor_speed = 115200 lib_deps_all = From f9ab110289c25d36178a4249825377f59872e009 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Mon, 1 Oct 2018 22:04:56 +0200 Subject: [PATCH 092/105] v1.5.16 (upgrade to ESP IDF 3.1) --- platformio.ini | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index f43f40a0..1b43cf4f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.14 +release_version = 1.5.16 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 @@ -35,8 +35,7 @@ upload_protocol = esptool ;upload_protocol = custom extra_scripts = pre:build.py keyfile = ota.conf -;platform_espressif32 = espressif32@1.3.0 -platform_espressif32 = https://github.com/platformio/platform-espressif32.git#d4c38ab +platform_espressif32 = espressif32@1.4.0 board_build.partitions = min_spiffs.csv monitor_speed = 115200 lib_deps_all = From e5df1013b303be7ec9eb9db987dcaeae8328a6a1 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 3 Oct 2018 00:25:05 +0200 Subject: [PATCH 093/105] v1.5.18 (improved tasking, lmic has now core1 exclusive) --- platformio.ini | 6 +- src/cyclic.cpp | 9 +-- src/cyclic.h | 4 ++ src/display.cpp | 18 +++++- src/globals.h | 6 +- src/gps.h | 1 + src/led.cpp | 104 ++++++++++++++++--------------- src/led.h | 2 +- src/lorawan.cpp | 23 +++---- src/lorawan.h | 4 +- src/main.cpp | 143 ++++++++++++++++++------------------------- src/statemachine.cpp | 5 ++ 12 files changed, 164 insertions(+), 161 deletions(-) diff --git a/platformio.ini b/platformio.ini index 1b43cf4f..9683cda7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,13 +26,13 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.16 +release_version = 1.5.18 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 ; UPLOAD MODE: select esptool to flash via USB/UART, select custom to upload to cloud for OTA -upload_protocol = esptool -;upload_protocol = custom +;upload_protocol = esptool +upload_protocol = custom extra_scripts = pre:build.py keyfile = ota.conf platform_espressif32 = espressif32@1.4.0 diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 1e864ba5..4dbff221 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -3,8 +3,6 @@ // Basic config #include "globals.h" -#include "senddata.h" -#include "ota.h" // Local logging tag static const char TAG[] = "main"; @@ -26,10 +24,6 @@ void doHousekeeping() { ESP.restart(); // task storage debugging // -#ifdef HAS_LORA - ESP_LOGD(TAG, "Loraloop %d bytes left", - uxTaskGetStackHighWaterMark(LoraTask)); -#endif ESP_LOGD(TAG, "Wifiloop %d bytes left", uxTaskGetStackHighWaterMark(wifiSwitchTask)); ESP_LOGD(TAG, "Statemachine %d bytes left", @@ -37,6 +31,9 @@ void doHousekeeping() { #ifdef HAS_GPS ESP_LOGD(TAG, "Gpsloop %d bytes left", uxTaskGetStackHighWaterMark(GpsTask)); #endif +#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) + ESP_LOGD(TAG, "LEDloop %d bytes left", uxTaskGetStackHighWaterMark(ledLoopTask)); +#endif // read battery voltage into global variable #ifdef HAS_BATTERY_PROBE diff --git a/src/cyclic.h b/src/cyclic.h index a006f137..9aad61d4 100644 --- a/src/cyclic.h +++ b/src/cyclic.h @@ -1,6 +1,10 @@ #ifndef _CYCLIC_H #define _CYCLIC_H +#include "senddata.h" +#include "ota.h" +#include "led.h" + void doHousekeeping(void); void IRAM_ATTR homeCycleIRQ(void); uint64_t uptime(void); diff --git a/src/display.cpp b/src/display.cpp index 773dbf94..1eaef6c5 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -15,6 +15,8 @@ const char lora_datarate[] = {"100908078CNA121110090807"}; uint8_t volatile DisplayState = 0; +hw_timer_t *displaytimer; + portMUX_TYPE mutexDisplay = portMUX_INITIALIZER_UNLOCKED; // helper function, prints a hex key on display @@ -27,8 +29,20 @@ void DisplayKey(const uint8_t *key, uint8_t len, bool lsb) { u8x8.printf("\n"); } -// show startup screen void init_display(const char *Productname, const char *Version) { + + // setup display refresh trigger IRQ using esp32 hardware timer + // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ + // prescaler 80 -> divides 80 MHz CPU freq to 1 MHz, timer 0, count up + displaytimer = timerBegin(0, 80, true); + // interrupt handler DisplayIRQ, triggered by edge + timerAttachInterrupt(displaytimer, &DisplayIRQ, true); + // reload interrupt after each trigger of display refresh cycle + timerAlarmWrite(displaytimer, DISPLAYREFRESH_MS * 1000, true); + // enable display interrupt + timerAlarmEnable(displaytimer); + + // show startup screen uint8_t buf[32]; u8x8.begin(); u8x8.setFont(u8x8_font_chroma48medium8_r); @@ -107,7 +121,7 @@ void refreshtheDisplay() { if (!DisplayState) return; - uint8_t msgWaiting = 0; + uint8_t msgWaiting; char buff[16]; // 16 chars line buffer // update counter (lines 0-1) diff --git a/src/globals.h b/src/globals.h index 739512f1..eb0adc11 100644 --- a/src/globals.h +++ b/src/globals.h @@ -56,19 +56,17 @@ extern SemaphoreHandle_t xWifiChannelSwitchSemaphore; extern TaskHandle_t stateMachineTask, wifiSwitchTask; #ifdef HAS_GPS -extern TaskHandle_t GpsTask; #include "gps.h" #endif -#ifdef HAS_LED +#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) #include "led.h" +extern TaskHandle_t ledLoopTask; #endif #include "payload.h" #ifdef HAS_LORA -extern QueueHandle_t LoraSendQueue; -extern TaskHandle_t LoraTask; #include "lorawan.h" #endif diff --git a/src/gps.h b/src/gps.h index 5121528e..b5cbff88 100644 --- a/src/gps.h +++ b/src/gps.h @@ -19,6 +19,7 @@ typedef struct { extern TinyGPSPlus gps; // Make TinyGPS++ instance globally availabe extern gpsStatus_t gps_status; // Make struct for storing gps data globally available +extern TaskHandle_t GpsTask; void gps_read(void); void gps_loop(void *pvParameters); diff --git a/src/led.cpp b/src/led.cpp index 0ade583a..9379d9e4 100644 --- a/src/led.cpp +++ b/src/led.cpp @@ -94,69 +94,75 @@ void blink_LED(uint16_t set_color, uint16_t set_blinkduration) { LEDState = LED_ON; // Let main set LED on } -void led_loop() { - // Custom blink running always have priority other LoRaWAN led management - if (LEDBlinkStarted && LEDBlinkDuration) { - // Custom blink is finished, let this order, avoid millis() overflow - if ((millis() - LEDBlinkStarted) >= LEDBlinkDuration) { - // Led becomes off, and stop blink - LEDState = LED_OFF; - LEDBlinkStarted = 0; - LEDBlinkDuration = 0; - LEDColor = COLOR_NONE; +void ledLoop(void *parameter) { + while (1) { + // Custom blink running always have priority other LoRaWAN led management + if (LEDBlinkStarted && LEDBlinkDuration) { + // Custom blink is finished, let this order, avoid millis() overflow + if ((millis() - LEDBlinkStarted) >= LEDBlinkDuration) { + // Led becomes off, and stop blink + LEDState = LED_OFF; + LEDBlinkStarted = 0; + LEDBlinkDuration = 0; + LEDColor = COLOR_NONE; + } else { + // In case of LoRaWAN led management blinked off + LEDState = LED_ON; + } + // No custom blink, check LoRaWAN state } else { - // In case of LoRaWAN led management blinked off - LEDState = LED_ON; - } - // No custom blink, check LoRaWAN state - } else { #ifdef HAS_LORA - // LED indicators for viusalizing LoRaWAN state - if (LMIC.opmode & (OP_JOINING | OP_REJOIN)) { - LEDColor = COLOR_YELLOW; - // quick blink 20ms on each 1/5 second - LEDState = ((millis() % 200) < 20) ? LED_ON : LED_OFF; // TX data pending - } else if (LMIC.opmode & (OP_TXDATA | OP_TXRXPEND)) { - LEDColor = COLOR_BLUE; - // small blink 10ms on each 1/2sec (not when joining) - LEDState = ((millis() % 500) < 10) ? LED_ON : LED_OFF; - // This should not happen so indicate a problem - } else if (LMIC.opmode & - ((OP_TXDATA | OP_TXRXPEND | OP_JOINING | OP_REJOIN) == 0)) { - LEDColor = COLOR_RED; - // heartbeat long blink 200ms on each 2 seconds - LEDState = ((millis() % 2000) < 200) ? LED_ON : LED_OFF; - } else + // LED indicators for viusalizing LoRaWAN state + if (LMIC.opmode & (OP_JOINING | OP_REJOIN)) { + LEDColor = COLOR_YELLOW; + // quick blink 20ms on each 1/5 second + LEDState = + ((millis() % 200) < 20) ? LED_ON : LED_OFF; // TX data pending + } else if (LMIC.opmode & (OP_TXDATA | OP_TXRXPEND)) { + LEDColor = COLOR_BLUE; + // small blink 10ms on each 1/2sec (not when joining) + LEDState = ((millis() % 500) < 10) ? LED_ON : LED_OFF; + // This should not happen so indicate a problem + } else if (LMIC.opmode & + ((OP_TXDATA | OP_TXRXPEND | OP_JOINING | OP_REJOIN) == 0)) { + LEDColor = COLOR_RED; + // heartbeat long blink 200ms on each 2 seconds + LEDState = ((millis() % 2000) < 200) ? LED_ON : LED_OFF; + } else #endif // HAS_LORA - { - // led off - LEDColor = COLOR_NONE; - LEDState = LED_OFF; + { + // led off + LEDColor = COLOR_NONE; + LEDState = LED_OFF; + } } - } - // led need to change state? avoid digitalWrite() for nothing - if (LEDState != previousLEDState) { - if (LEDState == LED_ON) { - rgb_set_color(LEDColor); + // led need to change state? avoid digitalWrite() for nothing + if (LEDState != previousLEDState) { + if (LEDState == LED_ON) { + rgb_set_color(LEDColor); #ifdef LED_ACTIVE_LOW - digitalWrite(HAS_LED, LOW); + digitalWrite(HAS_LED, LOW); #else - digitalWrite(HAS_LED, HIGH); + digitalWrite(HAS_LED, HIGH); #endif - } else { - rgb_set_color(COLOR_NONE); + } else { + rgb_set_color(COLOR_NONE); #ifdef LED_ACTIVE_LOW - digitalWrite(HAS_LED, HIGH); + digitalWrite(HAS_LED, HIGH); #else - digitalWrite(HAS_LED, LOW); + digitalWrite(HAS_LED, LOW); #endif + } + previousLEDState = LEDState; } - previousLEDState = LEDState; - } -}; // led_loop() + // give yield to CPU + vTaskDelay(2 / portTICK_PERIOD_MS); + } // while(1) + vTaskDelete(NULL); // shoud never be reached +}; // ledloop() #endif // #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) diff --git a/src/led.h b/src/led.h index 8dd4716c..68f5bd88 100644 --- a/src/led.h +++ b/src/led.h @@ -34,6 +34,6 @@ enum led_states { LED_OFF, LED_ON }; // Exported Functions void rgb_set_color(uint16_t hue); void blink_LED(uint16_t set_color, uint16_t set_blinkduration); -void led_loop(); +void ledLoop(void *parameter); #endif \ No newline at end of file diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 7f93c1e6..49a6b861 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -63,6 +63,18 @@ void RevBytes(unsigned char *b, size_t c) { } } +// initial lmic job +void initlmic(osjob_t *j) { + // reset MAC state + LMIC_reset(); + // This tells LMIC to make the receive windows bigger, in case your clock is + // 1% faster or slower. + LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100); + // start joining + LMIC_startJoining(); + // init done - onEvent() callback will be invoked... +} + // LMIC callback functions void os_getDevKey(u1_t *buf) { memcpy(buf, APPKEY, 16); } @@ -241,17 +253,6 @@ void onEvent(ev_t ev) { } // onEvent() -// LMIC FreeRTos Task -void lorawan_loop(void *pvParameters) { - - configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check - - while (1) { - os_runloop_once(); // execute LMIC jobs - vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU - } -} - // helper function to assign LoRa datarates to numeric spreadfactor values void switch_lora(uint8_t sf, uint8_t tx) { if (tx > 20) diff --git a/src/lorawan.h b/src/lorawan.h index 32220496..162caa25 100644 --- a/src/lorawan.h +++ b/src/lorawan.h @@ -14,6 +14,8 @@ #include #endif +extern QueueHandle_t LoraSendQueue; + void onEvent(ev_t ev); void gen_lora_deveui(uint8_t *pdeveui); void RevBytes(unsigned char *b, size_t c); @@ -22,7 +24,7 @@ void os_getDevKey(u1_t *buf); void os_getArtEui(u1_t *buf); void os_getDevEui(u1_t *buf); void showLoraKeys(void); -void lorawan_loop(void *pvParameters); void switch_lora(uint8_t sf, uint8_t tx); +void initlmic(osjob_t *j); #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index f758a795..0961af1a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,12 +27,14 @@ Uused tasks and timers: Task Core Prio Purpose ==================================================================================== -IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer task -gpsloop 0 2 read data from GPS over serial or i2c -IDLE 1 0 Arduino loop() -> used for LED switching -loraloop 1 2 runs the LMIC stack -statemachine 1 1 switches application process logic wifiloop 0 4 rotates wifi channels +ledloop 0 3 blinks LEDs +gpsloop 0 2 read data from GPS over serial or i2c +statemachine 0 1 switches application process logic +IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer task + +looptask 1 1 arduino loop() -> runs the LMIC stack +IDLE 1 0 ESP32 arduino scheduler ESP32 hardware timers ========================== @@ -53,7 +55,7 @@ uint16_t volatile macs_total = 0, macs_wifi = 0, macs_ble = 0, batt_voltage = 0; // globals for display // hardware timer for cyclic tasks -hw_timer_t *channelSwitch, *displaytimer, *sendCycle, *homeCycle; +hw_timer_t *channelSwitch, *sendCycle, *homeCycle; // this variables will be changed in the ISR, and read in main loop uint8_t volatile ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, @@ -66,7 +68,6 @@ SemaphoreHandle_t xWifiChannelSwitchSemaphore; // RTos send queues for payload transmit #ifdef HAS_LORA QueueHandle_t LoraSendQueue; -TaskHandle_t LoraTask = NULL; #endif #ifdef HAS_SPI @@ -74,7 +75,11 @@ QueueHandle_t SPISendQueue; #endif #ifdef HAS_GPS -TaskHandle_t GpsTask = NULL; +TaskHandle_t GpsTask; +#endif + +#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) +TaskHandle_t ledLoopTask; #endif std::set macs; // container holding unique MAC adress hashes @@ -133,6 +138,7 @@ void setup() { strcat_P(features, " BLE"); #else bool btstop = btStop(); + //esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); #endif // initialize battery status @@ -228,45 +234,25 @@ void setup() { #ifdef HAS_DISPLAY strcat_P(features, " OLED"); DisplayState = cfg.screenon; - init_display(PRODUCTNAME, PROGVERSION); - - // setup display refresh trigger IRQ using esp32 hardware timer - // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ - - // prescaler 80 -> divides 80 MHz CPU freq to 1 MHz, timer 0, count up - displaytimer = timerBegin(0, 80, true); - // interrupt handler DisplayIRQ, triggered by edge - timerAttachInterrupt(displaytimer, &DisplayIRQ, true); - // reload interrupt after each trigger of display refresh cycle - timerAlarmWrite(displaytimer, DISPLAYREFRESH_MS * 1000, true); - // enable display interrupt - yield(); - timerAlarmEnable(displaytimer); #endif // setup send cycle trigger IRQ using esp32 hardware timer 2 sendCycle = timerBegin(2, 8000, true); timerAttachInterrupt(sendCycle, &SendCycleIRQ, true); timerAlarmWrite(sendCycle, cfg.sendcycle * 2 * 10000, true); + timerAlarmEnable(sendCycle); // setup house keeping cycle trigger IRQ using esp32 hardware timer 3 homeCycle = timerBegin(3, 8000, true); timerAttachInterrupt(homeCycle, &homeCycleIRQ, true); timerAlarmWrite(homeCycle, HOMECYCLE * 10000, true); + timerAlarmEnable(homeCycle); // setup channel rotation trigger IRQ using esp32 hardware timer 1 xWifiChannelSwitchSemaphore = xSemaphoreCreateBinary(); channelSwitch = timerBegin(1, 800, true); timerAttachInterrupt(channelSwitch, &ChannelSwitchIRQ, true); timerAlarmWrite(channelSwitch, cfg.wifichancycle * 1000, true); - - // enable timers - // caution, see: https://github.com/espressif/arduino-esp32/issues/1313 - yield(); - timerAlarmEnable(homeCycle); - yield(); - timerAlarmEnable(sendCycle); - yield(); timerAlarmEnable(channelSwitch); // show payload encoder @@ -288,43 +274,6 @@ void setup() { #ifdef VERBOSE showLoraKeys(); #endif - - // initialize LoRaWAN LMIC run-time environment - os_init(); - // reset LMIC MAC state - LMIC_reset(); - // This tells LMIC to make the receive windows bigger, in case your clock is - // 1% faster or slower. - LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100); - // join network - LMIC_startJoining(); - - // start lmic runloop in rtos task on core 1 - // (note: arduino main loop runs on core 1, too) - // https://techtutorialsx.com/2017/05/09/esp32-get-task-execution-core/ - - ESP_LOGI(TAG, "Starting Lora..."); - xTaskCreatePinnedToCore(lorawan_loop, /* task function */ - "loraloop", /* name of task */ - 3048, /* stack size of task */ - (void *)1, /* parameter of the task */ - 2, /* priority of the task */ - &LoraTask, /* task handle*/ - 1); /* CPU core */ -#endif - -// if device has GPS and it is enabled, start GPS reader task on core 0 with -// higher priority than wifi channel rotation task since we process serial -// streaming NMEA data -#ifdef HAS_GPS - ESP_LOGI(TAG, "Starting GPS..."); - xTaskCreatePinnedToCore(gps_loop, /* task function */ - "gpsloop", /* name of task */ - 1024, /* stack size of task */ - (void *)1, /* parameter of the task */ - 2, /* priority of the task */ - &GpsTask, /* task handle*/ - 0); /* CPU core */ #endif // start BLE scan callback if BLE function is enabled in NVRAM configuration @@ -343,14 +292,16 @@ void setup() { // function gets it's seed from RF noise get_salt(); // get new 16bit for salting hashes - // start wifi channel rotation task - xTaskCreatePinnedToCore(switchWifiChannel, /* task function */ - "wifiloop", /* name of task */ - 2048, /* stack size of task */ - NULL, /* parameter of the task */ - 4, /* priority of the task */ - &wifiSwitchTask, /* task handle*/ - 0); /* CPU core */ +#ifdef HAS_GPS + ESP_LOGI(TAG, "Starting GPS..."); + xTaskCreatePinnedToCore(gps_loop, /* task function */ + "gpsloop", /* name of task */ + 1024, /* stack size of task */ + (void *)1, /* parameter of the task */ + 2, /* priority of the task */ + &GpsTask, /* task handle*/ + 0); /* CPU core */ +#endif // start state machine ESP_LOGI(TAG, "Starting Statemachine..."); @@ -360,17 +311,41 @@ void setup() { (void *)1, /* parameter of the task */ 1, /* priority of the task */ &stateMachineTask, /* task handle */ - 1); /* CPU core */ + 0); /* CPU core */ + +#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) + // start led loop + ESP_LOGI(TAG, "Starting LEDloop..."); + xTaskCreatePinnedToCore(ledLoop, /* task function */ + "ledloop", /* name of task */ + 1024, /* stack size of task */ + (void *)1, /* parameter of the task */ + 3, /* priority of the task */ + &ledLoopTask, /* task handle */ + 0); /* CPU core */ +#endif + + // start wifi channel rotation task + ESP_LOGI(TAG, "Starting Wifi Channel rotation..."); + xTaskCreatePinnedToCore(switchWifiChannel, /* task function */ + "wifiloop", /* name of task */ + 2048, /* stack size of task */ + NULL, /* parameter of the task */ + 4, /* priority of the task */ + &wifiSwitchTask, /* task handle*/ + 0); /* CPU core */ } // setup() void loop() { - -// switch LED state if device has LED(s) -#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) - led_loop(); -#endif - - // give yield to CPU - vTaskDelay(2 / portTICK_PERIOD_MS); + osjob_t initjob; + // initialize run-time env + os_init(); + // setup initial job + os_setCallback(&initjob, initlmic); + // execute scheduled jobs and events + while (1) { + os_runloop_once(); // execute LMIC jobs + vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU + } } \ No newline at end of file diff --git a/src/statemachine.cpp b/src/statemachine.cpp index 76d02801..b0476ccb 100644 --- a/src/statemachine.cpp +++ b/src/statemachine.cpp @@ -7,6 +7,11 @@ void stateMachine(void *pvParameters) { configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check + // initialize display - caution: must be done on core 1 in arduino loop! +#ifdef HAS_DISPLAY + init_display(PRODUCTNAME, PROGVERSION); +#endif + while (1) { #ifdef HAS_BUTTON From 1eceea2686980004411ca3baae109465a2cbc654 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 3 Oct 2018 16:24:45 +0200 Subject: [PATCH 094/105] new lmic tasking --- platformio.ini | 6 ++--- src/cyclic.cpp | 5 +++- src/globals.h | 2 +- src/gps.cpp | 3 +++ src/led.cpp | 2 ++ src/lorawan.cpp | 39 +++++++++++++++++++--------- src/lorawan.h | 2 +- src/main.cpp | 61 +++++++++++++++++++++++--------------------- src/senddata.cpp | 27 -------------------- src/spi.cpp | 30 ++++++++++++++++++++++ src/spi.h | 9 +++++++ src/statemachine.cpp | 7 +++-- 12 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 src/spi.cpp create mode 100644 src/spi.h diff --git a/platformio.ini b/platformio.ini index 9683cda7..fe7ad5a5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,13 +26,13 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.5.18 +release_version = 1.6.0 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 0 ; UPLOAD MODE: select esptool to flash via USB/UART, select custom to upload to cloud for OTA -;upload_protocol = esptool -upload_protocol = custom +upload_protocol = esptool +;upload_protocol = custom extra_scripts = pre:build.py keyfile = ota.conf platform_espressif32 = espressif32@1.4.0 diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 4dbff221..39ace8e5 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -26,11 +26,14 @@ void doHousekeeping() { // task storage debugging // ESP_LOGD(TAG, "Wifiloop %d bytes left", uxTaskGetStackHighWaterMark(wifiSwitchTask)); - ESP_LOGD(TAG, "Statemachine %d bytes left", + ESP_LOGD(TAG, "Stateloop %d bytes left", uxTaskGetStackHighWaterMark(stateMachineTask)); #ifdef HAS_GPS ESP_LOGD(TAG, "Gpsloop %d bytes left", uxTaskGetStackHighWaterMark(GpsTask)); #endif +#ifdef HAS_SPI + ESP_LOGD(TAG, "Spiloop %d bytes left", uxTaskGetStackHighWaterMark(SpiTask)); +#endif #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) ESP_LOGD(TAG, "LEDloop %d bytes left", uxTaskGetStackHighWaterMark(ledLoopTask)); #endif diff --git a/src/globals.h b/src/globals.h index eb0adc11..1f863dfe 100644 --- a/src/globals.h +++ b/src/globals.h @@ -71,7 +71,7 @@ extern TaskHandle_t ledLoopTask; #endif #ifdef HAS_SPI -extern QueueHandle_t SPISendQueue; +#include "spi.h" #endif #ifdef HAS_DISPLAY diff --git a/src/gps.cpp b/src/gps.cpp index 849ca30e..d2958a8b 100644 --- a/src/gps.cpp +++ b/src/gps.cpp @@ -7,6 +7,7 @@ static const char TAG[] = "main"; TinyGPSPlus gps; gpsStatus_t gps_status; +TaskHandle_t GpsTask; // read GPS data and cast to global struct void gps_read() { @@ -67,6 +68,8 @@ void gps_loop(void *pvParameters) { } // end of infinite loop + vTaskDelete(NULL); // shoud never be reached + } // gps_loop() #endif // HAS_GPS \ No newline at end of file diff --git a/src/led.cpp b/src/led.cpp index 9379d9e4..59965dce 100644 --- a/src/led.cpp +++ b/src/led.cpp @@ -6,6 +6,8 @@ led_states LEDState = LED_OFF; // LED state global for state machine led_states previousLEDState = LED_ON; // This will force LED to be off at boot since State is OFF +TaskHandle_t ledLoopTask; + uint16_t LEDColor = COLOR_NONE, LEDBlinkDuration = 0; // state machine variables unsigned long LEDBlinkStarted = 0; // When (in millis() led blink started) diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 49a6b861..d7e85d36 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -6,6 +6,9 @@ // Local logging Tag static const char TAG[] = "lora"; +osjob_t sendjob; +QueueHandle_t LoraSendQueue; + // LMIC enhanced Pin mapping const lmic_pinmap lmic_pins = {.mosi = PIN_SPI_MOSI, .miso = PIN_SPI_MISO, @@ -63,18 +66,6 @@ void RevBytes(unsigned char *b, size_t c) { } } -// initial lmic job -void initlmic(osjob_t *j) { - // reset MAC state - LMIC_reset(); - // This tells LMIC to make the receive windows bigger, in case your clock is - // 1% faster or slower. - LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100); - // start joining - LMIC_startJoining(); - // init done - onEvent() callback will be invoked... -} - // LMIC callback functions void os_getDevKey(u1_t *buf) { memcpy(buf, APPKEY, 16); } @@ -216,6 +207,9 @@ void onEvent(ev_t ev) { // the library) switch_lora(cfg.lorasf, cfg.txpower); + // kickoff first send job + os_setCallback(&sendjob, lora_send); + // show effective LoRa parameters after join ESP_LOGI(TAG, "ADR=%d, SF=%d, TXPOWER=%d", cfg.adrmode, cfg.lorasf, cfg.txpower); @@ -300,4 +294,25 @@ void switch_lora(uint8_t sf, uint8_t tx) { } } +void lora_send(osjob_t *job) { + MessageBuffer_t SendBuffer; + // Check if there is a pending TX/RX job running, if yes don't eat data + // since it cannot be sent right now + if ((LMIC.opmode & (OP_JOINING | OP_REJOIN | OP_TXDATA | OP_POLL)) != 0) { + // waiting for LoRa getting ready + } else { + if (xQueueReceive(LoraSendQueue, &SendBuffer, (TickType_t)0) == pdTRUE) { + // SendBuffer gets struct MessageBuffer with next payload from queue + LMIC_setTxData2(SendBuffer.MessagePort, SendBuffer.Message, + SendBuffer.MessageSize, (cfg.countermode & 0x02)); + ESP_LOGI(TAG, "%d bytes sent to LoRa", SendBuffer.MessageSize); + sprintf(display_line7, "PACKET QUEUED"); + } + } + // reschedule job every 0,5 - 1 sec. including a bit of random to prevent + // systematic collisions + os_setTimedCallback(job, os_getTime() + 500 + ms2osticks(random(500)), + lora_send); +} + #endif // HAS_LORA \ No newline at end of file diff --git a/src/lorawan.h b/src/lorawan.h index 162caa25..66d77f5b 100644 --- a/src/lorawan.h +++ b/src/lorawan.h @@ -25,6 +25,6 @@ void os_getArtEui(u1_t *buf); void os_getDevEui(u1_t *buf); void showLoraKeys(void); void switch_lora(uint8_t sf, uint8_t tx); -void initlmic(osjob_t *j); +void lora_send(osjob_t *job); #endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 0961af1a..4b61abdf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,11 +29,12 @@ Task Core Prio Purpose ==================================================================================== wifiloop 0 4 rotates wifi channels ledloop 0 3 blinks LEDs -gpsloop 0 2 read data from GPS over serial or i2c +gpsloop 0 2 reads data from GPS over serial or i2c +spiloop 0 2 reads/writes data on spi interface statemachine 0 1 switches application process logic -IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer task +IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer -looptask 1 1 arduino loop() -> runs the LMIC stack +looptask 1 1 arduino core -> runs the LMIC LoRa stack IDLE 1 0 ESP32 arduino scheduler ESP32 hardware timers @@ -65,23 +66,6 @@ TaskHandle_t stateMachineTask, wifiSwitchTask; SemaphoreHandle_t xWifiChannelSwitchSemaphore; -// RTos send queues for payload transmit -#ifdef HAS_LORA -QueueHandle_t LoraSendQueue; -#endif - -#ifdef HAS_SPI -QueueHandle_t SPISendQueue; -#endif - -#ifdef HAS_GPS -TaskHandle_t GpsTask; -#endif - -#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) -TaskHandle_t ledLoopTask; -#endif - std::set macs; // container holding unique MAC adress hashes // initialize payload encoder @@ -138,7 +122,6 @@ void setup() { strcat_P(features, " BLE"); #else bool btstop = btStop(); - //esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); #endif // initialize battery status @@ -189,6 +172,16 @@ void setup() { } else ESP_LOGI(TAG, "LORA send queue created, size %d Bytes", SEND_QUEUE_SIZE * PAYLOAD_BUFFER_SIZE); + + ESP_LOGI(TAG, "Starting LMIC..."); + os_init(); // initialize lmic run-time environment on core 1 + LMIC_reset(); // initialize lmic MAC + LMIC_setClockError(MAX_CLOCK_ERROR * 1 / + 100); // This tells LMIC to make the receive windows + // bigger, in case your clock is 1% faster or slower. + + LMIC_startJoining(); // start joining + #endif // initialize SPI @@ -293,7 +286,7 @@ void setup() { get_salt(); // get new 16bit for salting hashes #ifdef HAS_GPS - ESP_LOGI(TAG, "Starting GPS..."); + ESP_LOGI(TAG, "Starting GPSloop..."); xTaskCreatePinnedToCore(gps_loop, /* task function */ "gpsloop", /* name of task */ 1024, /* stack size of task */ @@ -303,6 +296,17 @@ void setup() { 0); /* CPU core */ #endif +#ifdef HAS_SPI + ESP_LOGI(TAG, "Starting SPIloop..."); + xTaskCreatePinnedToCore(spi_loop, /* task function */ + "spiloop", /* name of task */ + 2048, /* stack size of task */ + (void *)1, /* parameter of the task */ + 2, /* priority of the task */ + &SpiTask, /* task handle*/ + 0); /* CPU core */ +#endif + // start state machine ESP_LOGI(TAG, "Starting Statemachine..."); xTaskCreatePinnedToCore(stateMachine, /* task function */ @@ -338,14 +342,13 @@ void setup() { } // setup() void loop() { - osjob_t initjob; - // initialize run-time env - os_init(); - // setup initial job - os_setCallback(&initjob, initlmic); - // execute scheduled jobs and events + while (1) { - os_runloop_once(); // execute LMIC jobs +#ifdef HAS_LORA + os_runloop_once(); // execute lmic scheduled jobs and events +#endif vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU } + + vTaskDelete(NULL); // shoud never be reached } \ No newline at end of file diff --git a/src/senddata.cpp b/src/senddata.cpp index d4f89d62..fa4eb946 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -75,33 +75,6 @@ void IRAM_ATTR SendCycleIRQ() { portEXIT_CRITICAL(&mutexSendCycle); } -// interrupt triggered function to eat data from send queues and transmit it -void checkSendQueues() { - MessageBuffer_t SendBuffer; - -#ifdef HAS_LORA - // Check if there is a pending TX/RX job running - if ((LMIC.opmode & (OP_JOINING | OP_REJOIN | OP_TXDATA | OP_POLL)) != 0) { - // LoRa Busy -> don't eat data from queue, since it cannot be sent - } else { - if (xQueueReceive(LoraSendQueue, &SendBuffer, (TickType_t)0) == pdTRUE) { - // SendBuffer gets struct MessageBuffer with next payload from queue - LMIC_setTxData2(SendBuffer.MessagePort, SendBuffer.Message, - SendBuffer.MessageSize, (cfg.countermode & 0x02)); - ESP_LOGI(TAG, "%d bytes sent to LoRa", SendBuffer.MessageSize); - sprintf(display_line7, "PACKET QUEUED"); - } - } -#endif - -#ifdef HAS_SPI - if (xQueueReceive(SPISendQueue, &SendBuffer, (TickType_t)0) == pdTRUE) { - ESP_LOGI(TAG, "%d bytes sent to SPI", SendBuffer.MessageSize); - } -#endif - -} // checkSendQueues - void flushQueues() { #ifdef HAS_LORA xQueueReset(LoraSendQueue); diff --git a/src/spi.cpp b/src/spi.cpp new file mode 100644 index 00000000..57da9287 --- /dev/null +++ b/src/spi.cpp @@ -0,0 +1,30 @@ +#ifdef HAS_SPI + +#include "globals.h" + +// Local logging tag +static const char TAG[] = "main"; + +MessageBuffer_t SendBuffer; + +QueueHandle_t SPISendQueue; +TaskHandle_t SpiTask; + +// SPI feed Task +void spi_loop(void *pvParameters) { + + configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check + + while (1) { + if (xQueueReceive(SPISendQueue, &SendBuffer, (TickType_t)0) == pdTRUE) { + ESP_LOGI(TAG, "%d bytes sent to SPI", SendBuffer.MessageSize); + } + vTaskDelay(2 / portTICK_PERIOD_MS); // yield to CPU + + } // end of infinite loop + + vTaskDelete(NULL); // shoud never be reached + +} // spi_loop() + +#endif // HAS_SPI \ No newline at end of file diff --git a/src/spi.h b/src/spi.h new file mode 100644 index 00000000..b258a2ce --- /dev/null +++ b/src/spi.h @@ -0,0 +1,9 @@ +#ifndef _SPI_H +#define _SPI_H + +extern TaskHandle_t SpiTask; +extern QueueHandle_t SPISendQueue; + +void spi_loop(void *pvParameters); + +#endif \ No newline at end of file diff --git a/src/statemachine.cpp b/src/statemachine.cpp index b0476ccb..2592aa07 100644 --- a/src/statemachine.cpp +++ b/src/statemachine.cpp @@ -27,12 +27,11 @@ void stateMachine(void *pvParameters) { // check housekeeping cycle and if due do the work if (HomeCycleIRQ) doHousekeeping(); - // check send cycle and if due enqueue payload to send + + // check send cycle and if due enqueue payload to send if (SendCycleTimerIRQ) sendPayload(); - // check send queues and process due payload to send - checkSendQueues(); - + // give yield to CPU vTaskDelay(2 / portTICK_PERIOD_MS); } From 219f2347da3b75b10965bc8ccad31be027888b01 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 3 Oct 2018 20:18:01 +0200 Subject: [PATCH 095/105] code sanitizations --- src/main.cpp | 36 ++++++++++++++++++------------------ src/statemachine.cpp | 4 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 4b61abdf..29d1b433 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,10 +39,10 @@ IDLE 1 0 ESP32 arduino scheduler ESP32 hardware timers ========================== - 0 Display-Refresh - 1 Wifi Channel Switch - 2 Send Cycle - 3 Housekeeping + 0 Trigger display refresh + 1 Trigger Wifi channel switch + 2 Trigger send payload cycle + 3 Trigger housekeeping cycle */ @@ -320,24 +320,24 @@ void setup() { #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) // start led loop ESP_LOGI(TAG, "Starting LEDloop..."); - xTaskCreatePinnedToCore(ledLoop, /* task function */ - "ledloop", /* name of task */ - 1024, /* stack size of task */ - (void *)1, /* parameter of the task */ - 3, /* priority of the task */ - &ledLoopTask, /* task handle */ - 0); /* CPU core */ + xTaskCreatePinnedToCore(ledLoop, // task function + "ledloop", // name of task + 1024, // stack size of task + (void *)1, // parameter of the task + 3, // priority of the task + &ledLoopTask, // task handle + 0); // CPU core #endif // start wifi channel rotation task ESP_LOGI(TAG, "Starting Wifi Channel rotation..."); - xTaskCreatePinnedToCore(switchWifiChannel, /* task function */ - "wifiloop", /* name of task */ - 2048, /* stack size of task */ - NULL, /* parameter of the task */ - 4, /* priority of the task */ - &wifiSwitchTask, /* task handle*/ - 0); /* CPU core */ + xTaskCreatePinnedToCore(switchWifiChannel, // task function + "wifiloop", // name of task + 2048, // stack size of task + NULL, // parameter of the task + 4, // priority of the task + &wifiSwitchTask, // task handle + 0); // CPU core } // setup() diff --git a/src/statemachine.cpp b/src/statemachine.cpp index 2592aa07..c1607fb6 100644 --- a/src/statemachine.cpp +++ b/src/statemachine.cpp @@ -28,10 +28,10 @@ void stateMachine(void *pvParameters) { if (HomeCycleIRQ) doHousekeeping(); - // check send cycle and if due enqueue payload to send + // check send cycle and if due enqueue payload to send if (SendCycleTimerIRQ) sendPayload(); - + // give yield to CPU vTaskDelay(2 / portTICK_PERIOD_MS); } From 6e47a60f3227a6184868dcaf8e009caa809d2c14 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Wed, 3 Oct 2018 23:07:22 +0200 Subject: [PATCH 096/105] code sanitization --- src/button.cpp | 1 - src/button.h | 2 ++ src/cyclic.cpp | 2 +- src/cyclic.h | 3 +-- src/globals.h | 1 - src/led.h | 2 ++ src/main.h | 1 - src/statemachine.h | 3 --- 8 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/button.cpp b/src/button.cpp index 37ae91fe..086c3231 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -1,7 +1,6 @@ #ifdef HAS_BUTTON #include "globals.h" -#include "senddata.h" // Local logging tag static const char TAG[] = "main"; diff --git a/src/button.h b/src/button.h index 1900ac6d..9cb6e7b4 100644 --- a/src/button.h +++ b/src/button.h @@ -1,6 +1,8 @@ #ifndef _BUTTON_H #define _BUTTON_H +#include "senddata.h" + void IRAM_ATTR ButtonIRQ(void); void readButton(void); diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 39ace8e5..5f7ca3dc 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -2,7 +2,7 @@ /* Interval can be set in paxcounter.conf (HOMECYCLE) */ // Basic config -#include "globals.h" +#include "cyclic.h" // Local logging tag static const char TAG[] = "main"; diff --git a/src/cyclic.h b/src/cyclic.h index 9aad61d4..d8ee73b2 100644 --- a/src/cyclic.h +++ b/src/cyclic.h @@ -1,9 +1,8 @@ #ifndef _CYCLIC_H #define _CYCLIC_H +#include "globals.h" #include "senddata.h" -#include "ota.h" -#include "led.h" void doHousekeeping(void); void IRAM_ATTR homeCycleIRQ(void); diff --git a/src/globals.h b/src/globals.h index 1f863dfe..38d9e4ee 100644 --- a/src/globals.h +++ b/src/globals.h @@ -61,7 +61,6 @@ extern TaskHandle_t stateMachineTask, wifiSwitchTask; #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) #include "led.h" -extern TaskHandle_t ledLoopTask; #endif #include "payload.h" diff --git a/src/led.h b/src/led.h index 68f5bd88..2631563c 100644 --- a/src/led.h +++ b/src/led.h @@ -31,6 +31,8 @@ struct RGBColor { enum led_states { LED_OFF, LED_ON }; +extern TaskHandle_t ledLoopTask; + // Exported Functions void rgb_set_color(uint16_t hue); void blink_LED(uint16_t set_color, uint16_t set_blinkduration); diff --git a/src/main.h b/src/main.h index 40e9c208..ff4ae180 100644 --- a/src/main.h +++ b/src/main.h @@ -6,7 +6,6 @@ #include // needed for timers #include "globals.h" -#include "led.h" #include "wifiscan.h" #include "configmanager.h" #include "cyclic.h" diff --git a/src/statemachine.h b/src/statemachine.h index ec9966a0..a2a87097 100644 --- a/src/statemachine.h +++ b/src/statemachine.h @@ -2,9 +2,6 @@ #define _STATEMACHINE_H #include "globals.h" -#include "led.h" -#include "wifiscan.h" -#include "senddata.h" #include "cyclic.h" void stateMachine(void *pvParameters); From 30d22aa896e6551d518808e03ffe6616036e5368 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 4 Oct 2018 22:08:54 +0200 Subject: [PATCH 097/105] irq handling reworked (using tasknotify instead of semaphores) --- src/button.cpp | 12 +------ src/button.h | 4 +-- src/cyclic.cpp | 16 ++------- src/display.cpp | 25 -------------- src/globals.h | 5 +-- src/irqhandler.cpp | 74 ++++++++++++++++++++++++++++++++++++++++ src/irqhandler.h | 18 ++++++++++ src/main.cpp | 81 ++++++++++++++++++++++++-------------------- src/main.h | 2 +- src/senddata.cpp | 13 ------- src/statemachine.cpp | 39 --------------------- src/statemachine.h | 10 ------ src/wifiscan.cpp | 15 ++------ 13 files changed, 146 insertions(+), 168 deletions(-) create mode 100644 src/irqhandler.cpp create mode 100644 src/irqhandler.h delete mode 100644 src/statemachine.cpp delete mode 100644 src/statemachine.h diff --git a/src/button.cpp b/src/button.cpp index 086c3231..fd96545c 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -1,22 +1,12 @@ #ifdef HAS_BUTTON #include "globals.h" +#include "button.h" // Local logging tag static const char TAG[] = "main"; -portMUX_TYPE mutexButton = portMUX_INITIALIZER_UNLOCKED; - -void IRAM_ATTR ButtonIRQ() { - portENTER_CRITICAL(&mutexButton); - ButtonPressedIRQ++; - portEXIT_CRITICAL(&mutexButton); -} - void readButton() { - portENTER_CRITICAL(&mutexButton); - ButtonPressedIRQ = 0; - portEXIT_CRITICAL(&mutexButton); ESP_LOGI(TAG, "Button pressed"); payload.reset(); payload.addButton(0x01); diff --git a/src/button.h b/src/button.h index 9cb6e7b4..bae8e6b5 100644 --- a/src/button.h +++ b/src/button.h @@ -3,7 +3,7 @@ #include "senddata.h" -void IRAM_ATTR ButtonIRQ(void); -void readButton(void); +void IRAM_ATTR ButtonIRQ(); +void readButton(); #endif \ No newline at end of file diff --git a/src/cyclic.cpp b/src/cyclic.cpp index 5f7ca3dc..06d45b62 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -7,15 +7,9 @@ // Local logging tag static const char TAG[] = "main"; -portMUX_TYPE mutexHomeCycle = portMUX_INITIALIZER_UNLOCKED; - // do all housekeeping void doHousekeeping() { - portENTER_CRITICAL(&mutexHomeCycle); - HomeCycleIRQ = 0; - portEXIT_CRITICAL(&mutexHomeCycle); - // update uptime counter uptime(); @@ -26,8 +20,8 @@ void doHousekeeping() { // task storage debugging // ESP_LOGD(TAG, "Wifiloop %d bytes left", uxTaskGetStackHighWaterMark(wifiSwitchTask)); - ESP_LOGD(TAG, "Stateloop %d bytes left", - uxTaskGetStackHighWaterMark(stateMachineTask)); + ESP_LOGD(TAG, "IRQhandler %d bytes left", + uxTaskGetStackHighWaterMark(irqHandlerTask)); #ifdef HAS_GPS ESP_LOGD(TAG, "Gpsloop %d bytes left", uxTaskGetStackHighWaterMark(GpsTask)); #endif @@ -70,12 +64,6 @@ void doHousekeeping() { } } // doHousekeeping() -void IRAM_ATTR homeCycleIRQ() { - portENTER_CRITICAL(&mutexHomeCycle); - HomeCycleIRQ++; - portEXIT_CRITICAL(&mutexHomeCycle); -} - // uptime counter 64bit to prevent millis() rollover after 49 days uint64_t uptime() { static uint32_t low32, high32; diff --git a/src/display.cpp b/src/display.cpp index 1eaef6c5..edcf8cdd 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -15,10 +15,6 @@ const char lora_datarate[] = {"100908078CNA121110090807"}; uint8_t volatile DisplayState = 0; -hw_timer_t *displaytimer; - -portMUX_TYPE mutexDisplay = portMUX_INITIALIZER_UNLOCKED; - // helper function, prints a hex key on display void DisplayKey(const uint8_t *key, uint8_t len, bool lsb) { const uint8_t *p; @@ -31,17 +27,6 @@ void DisplayKey(const uint8_t *key, uint8_t len, bool lsb) { void init_display(const char *Productname, const char *Version) { - // setup display refresh trigger IRQ using esp32 hardware timer - // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ - // prescaler 80 -> divides 80 MHz CPU freq to 1 MHz, timer 0, count up - displaytimer = timerBegin(0, 80, true); - // interrupt handler DisplayIRQ, triggered by edge - timerAttachInterrupt(displaytimer, &DisplayIRQ, true); - // reload interrupt after each trigger of display refresh cycle - timerAlarmWrite(displaytimer, DISPLAYREFRESH_MS * 1000, true); - // enable display interrupt - timerAlarmEnable(displaytimer); - // show startup screen uint8_t buf[32]; u8x8.begin(); @@ -107,10 +92,6 @@ void init_display(const char *Productname, const char *Version) { void refreshtheDisplay() { - portENTER_CRITICAL(&mutexDisplay); - DisplayTimerIRQ = 0; - portEXIT_CRITICAL(&mutexDisplay); - // set display on/off according to current device configuration if (DisplayState != cfg.screenon) { DisplayState = cfg.screenon; @@ -206,10 +187,4 @@ void refreshtheDisplay() { } // refreshDisplay() -void IRAM_ATTR DisplayIRQ() { - portENTER_CRITICAL_ISR(&mutexDisplay); - DisplayTimerIRQ++; - portEXIT_CRITICAL_ISR(&mutexDisplay); -} - #endif // HAS_DISPLAY \ No newline at end of file diff --git a/src/globals.h b/src/globals.h index 38d9e4ee..703b46d0 100644 --- a/src/globals.h +++ b/src/globals.h @@ -46,14 +46,11 @@ extern uint16_t volatile macs_total, macs_wifi, macs_ble, batt_voltage; // display values extern std::set macs; // temp storage for MACs extern hw_timer_t *channelSwitch, *sendCycle; -extern volatile uint8_t SendCycleTimerIRQ, HomeCycleIRQ, DisplayTimerIRQ, - ChannelTimerIRQ, ButtonPressedIRQ; extern std::array::iterator it; extern std::array beacons; -extern SemaphoreHandle_t xWifiChannelSwitchSemaphore; -extern TaskHandle_t stateMachineTask, wifiSwitchTask; +extern TaskHandle_t irqHandlerTask, wifiSwitchTask; #ifdef HAS_GPS #include "gps.h" diff --git a/src/irqhandler.cpp b/src/irqhandler.cpp new file mode 100644 index 00000000..61aa6d2e --- /dev/null +++ b/src/irqhandler.cpp @@ -0,0 +1,74 @@ +#include "irqhandler.h" + +// Local logging tag +static const char TAG[] = "main"; + +// irq handler task, handles all our application level interrupts +void irqHandler(void *pvParameters) { + + configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check + + uint32_t InterruptStatus; + + // task remains in blocked state until it is notified by an irq + for (;;) { + xTaskNotifyWait( + 0x00, // Don't clear any bits on entry + ULONG_MAX, // Clear all bits on exit + &InterruptStatus, // Receives the notification value + portMAX_DELAY); // wait forever (missing error handling here...) + +// button pressed? +#ifdef HAS_BUTTON + if (InterruptStatus & BUTTON_IRQ) + readButton(); +#endif + +// display needs refresh? +#ifdef HAS_DISPLAY + if (InterruptStatus & DISPLAY_IRQ) + refreshtheDisplay(); +#endif + + // are cyclic tasks due? + if (InterruptStatus & CYCLIC_IRQ) + doHousekeeping(); + + // is time to send the payload? + if (InterruptStatus & SENDPAYLOAD_IRQ) + sendPayload(); + } + vTaskDelete(NULL); // shoud never be reached +} + +// esp32 hardware timer triggered interrupt service routines +// they notify the irq handler task + +void IRAM_ATTR ChannelSwitchIRQ() { + xTaskNotifyGive(wifiSwitchTask); + portYIELD_FROM_ISR(); +} + +void IRAM_ATTR homeCycleIRQ() { + xTaskNotifyFromISR(irqHandlerTask, CYCLIC_IRQ, eSetBits, NULL); + portYIELD_FROM_ISR(); +} + +void IRAM_ATTR SendCycleIRQ() { + xTaskNotifyFromISR(irqHandlerTask, SENDPAYLOAD_IRQ, eSetBits, NULL); + portYIELD_FROM_ISR(); +} + +#ifdef HAS_DISPLAY +void IRAM_ATTR DisplayIRQ() { + xTaskNotifyFromISR(irqHandlerTask, DISPLAY_IRQ, eSetBits, NULL); + portYIELD_FROM_ISR(); +} +#endif + +#ifdef HAS_BUTTON +void IRAM_ATTR ButtonIRQ() { + xTaskNotifyFromISR(irqHandlerTask, BUTTON_IRQ, eSetBits, NULL); + portYIELD_FROM_ISR(); +} +#endif diff --git a/src/irqhandler.h b/src/irqhandler.h new file mode 100644 index 00000000..fab9bee1 --- /dev/null +++ b/src/irqhandler.h @@ -0,0 +1,18 @@ +#ifndef _IRQHANDLER_H +#define _IRQHANDLER_H + +#define DISPLAY_IRQ 0x01 +#define BUTTON_IRQ 0x02 +#define SENDPAYLOAD_IRQ 0x04 +#define CYCLIC_IRQ 0x08 + +#include "globals.h" +#include "cyclic.h" +#include "button.h" +#include "display.h" +#include "cyclic.h" +#include "senddata.h" + +void irqHandler(void *pvParameters); + +#endif diff --git a/src/main.cpp b/src/main.cpp index 29d1b433..abc94c9e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,10 +31,10 @@ wifiloop 0 4 rotates wifi channels ledloop 0 3 blinks LEDs gpsloop 0 2 reads data from GPS over serial or i2c spiloop 0 2 reads/writes data on spi interface -statemachine 0 1 switches application process logic IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer looptask 1 1 arduino core -> runs the LMIC LoRa stack +irqhandler 1 1 executes tasks triggered by irq IDLE 1 0 ESP32 arduino scheduler ESP32 hardware timers @@ -55,16 +55,8 @@ uint8_t volatile channel = 0; // channel rotation counter uint16_t volatile macs_total = 0, macs_wifi = 0, macs_ble = 0, batt_voltage = 0; // globals for display -// hardware timer for cyclic tasks -hw_timer_t *channelSwitch, *sendCycle, *homeCycle; - -// this variables will be changed in the ISR, and read in main loop -uint8_t volatile ButtonPressedIRQ = 0, ChannelTimerIRQ = 0, - SendCycleTimerIRQ = 0, DisplayTimerIRQ = 0, HomeCycleIRQ = 0; - -TaskHandle_t stateMachineTask, wifiSwitchTask; - -SemaphoreHandle_t xWifiChannelSwitchSemaphore; +hw_timer_t *channelSwitch, *sendCycle, *homeCycle, *displaytimer; // irq tasks +TaskHandle_t irqHandlerTask, wifiSwitchTask; std::set macs; // container holding unique MAC adress hashes @@ -227,26 +219,32 @@ void setup() { #ifdef HAS_DISPLAY strcat_P(features, " OLED"); DisplayState = cfg.screenon; + init_display(PRODUCTNAME, PROGVERSION); + + // setup display refresh trigger IRQ using esp32 hardware timer + // https://techtutorialsx.com/2017/10/07/esp32-arduino-timer-interrupts/ + // prescaler 80 -> divides 80 MHz CPU freq to 1 MHz, timer 0, count up + displaytimer = timerBegin(0, 80, true); + // interrupt handler DisplayIRQ, triggered by edge + timerAttachInterrupt(displaytimer, &DisplayIRQ, true); + // reload interrupt after each trigger of display refresh cycle + timerAlarmWrite(displaytimer, DISPLAYREFRESH_MS * 1000, true); #endif // setup send cycle trigger IRQ using esp32 hardware timer 2 sendCycle = timerBegin(2, 8000, true); timerAttachInterrupt(sendCycle, &SendCycleIRQ, true); timerAlarmWrite(sendCycle, cfg.sendcycle * 2 * 10000, true); - timerAlarmEnable(sendCycle); // setup house keeping cycle trigger IRQ using esp32 hardware timer 3 homeCycle = timerBegin(3, 8000, true); timerAttachInterrupt(homeCycle, &homeCycleIRQ, true); timerAlarmWrite(homeCycle, HOMECYCLE * 10000, true); - timerAlarmEnable(homeCycle); // setup channel rotation trigger IRQ using esp32 hardware timer 1 - xWifiChannelSwitchSemaphore = xSemaphoreCreateBinary(); channelSwitch = timerBegin(1, 800, true); timerAttachInterrupt(channelSwitch, &ChannelSwitchIRQ, true); timerAlarmWrite(channelSwitch, cfg.wifichancycle * 1000, true); - timerAlarmEnable(channelSwitch); // show payload encoder #if PAYLOAD_ENCODER == 1 @@ -287,35 +285,35 @@ void setup() { #ifdef HAS_GPS ESP_LOGI(TAG, "Starting GPSloop..."); - xTaskCreatePinnedToCore(gps_loop, /* task function */ - "gpsloop", /* name of task */ - 1024, /* stack size of task */ - (void *)1, /* parameter of the task */ - 2, /* priority of the task */ - &GpsTask, /* task handle*/ - 0); /* CPU core */ + xTaskCreatePinnedToCore(gps_loop, // task function + "gpsloop", // name of task + 1024, // stack size of task + (void *)1, // parameter of the task + 2, // priority of the task + &GpsTask, // task handle + 0); // CPU core #endif #ifdef HAS_SPI ESP_LOGI(TAG, "Starting SPIloop..."); - xTaskCreatePinnedToCore(spi_loop, /* task function */ - "spiloop", /* name of task */ - 2048, /* stack size of task */ - (void *)1, /* parameter of the task */ - 2, /* priority of the task */ - &SpiTask, /* task handle*/ - 0); /* CPU core */ + xTaskCreatePinnedToCore(spi_loop, // task function + "spiloop", // name of task + 2048, // stack size of task + (void *)1, // parameter of the task + 2, // priority of the task + &SpiTask, // task handle + 0); // CPU core #endif // start state machine - ESP_LOGI(TAG, "Starting Statemachine..."); - xTaskCreatePinnedToCore(stateMachine, /* task function */ - "stateloop", /* name of task */ - 2048, /* stack size of task */ - (void *)1, /* parameter of the task */ - 1, /* priority of the task */ - &stateMachineTask, /* task handle */ - 0); /* CPU core */ + ESP_LOGI(TAG, "Starting IRQ Handler..."); + xTaskCreatePinnedToCore(irqHandler, // task function + "irqhandler", // name of task + 2048, // stack size of task + (void *)1, // parameter of the task + 1, // priority of the task + &irqHandlerTask, // task handle + 1); // CPU core #if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED) // start led loop @@ -339,6 +337,15 @@ void setup() { &wifiSwitchTask, // task handle 0); // CPU core + // start timer triggered interrupts + ESP_LOGI(TAG, "Starting Interrupts..."); +#ifdef HAS_DISPLAY + timerAlarmEnable(displaytimer); +#endif + timerAlarmEnable(sendCycle); + timerAlarmEnable(homeCycle); + timerAlarmEnable(channelSwitch); + } // setup() void loop() { diff --git a/src/main.h b/src/main.h index ff4ae180..9c74aef5 100644 --- a/src/main.h +++ b/src/main.h @@ -11,6 +11,6 @@ #include "cyclic.h" #include "beacon_array.h" #include "ota.h" -#include "statemachine.h" +#include "irqhandler.h" #endif \ No newline at end of file diff --git a/src/senddata.cpp b/src/senddata.cpp index fa4eb946..ee293485 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -1,8 +1,6 @@ // Basic Config #include "globals.h" -portMUX_TYPE mutexSendCycle = portMUX_INITIALIZER_UNLOCKED; - // put data to send in RTos Queues used for transmit over channels Lora and SPI void SendData(uint8_t port) { @@ -39,10 +37,6 @@ void SendData(uint8_t port) { // interrupt triggered function to prepare payload to send void sendPayload() { - portENTER_CRITICAL(&mutexSendCycle); - SendCycleTimerIRQ = 0; - portEXIT_CRITICAL(&mutexSendCycle); - // append counter data to payload payload.reset(); payload.addCount(macs_wifi, cfg.blescan ? macs_ble : 0); @@ -68,13 +62,6 @@ void sendPayload() { SendData(COUNTERPORT); } // sendpayload() -// interrupt handler used for payload send cycle timer -void IRAM_ATTR SendCycleIRQ() { - portENTER_CRITICAL(&mutexSendCycle); - SendCycleTimerIRQ++; - portEXIT_CRITICAL(&mutexSendCycle); -} - void flushQueues() { #ifdef HAS_LORA xQueueReset(LoraSendQueue); diff --git a/src/statemachine.cpp b/src/statemachine.cpp deleted file mode 100644 index c1607fb6..00000000 --- a/src/statemachine.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "statemachine.h" - -// Local logging tag -static const char TAG[] = "main"; - -void stateMachine(void *pvParameters) { - - configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check - - // initialize display - caution: must be done on core 1 in arduino loop! -#ifdef HAS_DISPLAY - init_display(PRODUCTNAME, PROGVERSION); -#endif - - while (1) { - -#ifdef HAS_BUTTON - if (ButtonPressedIRQ) - readButton(); -#endif - -#ifdef HAS_DISPLAY - if (DisplayTimerIRQ) - refreshtheDisplay(); -#endif - - // check housekeeping cycle and if due do the work - if (HomeCycleIRQ) - doHousekeeping(); - - // check send cycle and if due enqueue payload to send - if (SendCycleTimerIRQ) - sendPayload(); - - // give yield to CPU - vTaskDelay(2 / portTICK_PERIOD_MS); - } - vTaskDelete(NULL); // shoud never be reached -} \ No newline at end of file diff --git a/src/statemachine.h b/src/statemachine.h deleted file mode 100644 index a2a87097..00000000 --- a/src/statemachine.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef _STATEMACHINE_H -#define _STATEMACHINE_H - -#include "globals.h" -#include "cyclic.h" - -void stateMachine(void *pvParameters); -void stateMachineInit(); - -#endif diff --git a/src/wifiscan.cpp b/src/wifiscan.cpp index 4e2500b6..5613a5ec 100644 --- a/src/wifiscan.cpp +++ b/src/wifiscan.cpp @@ -46,21 +46,12 @@ void wifi_sniffer_init(void) { ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); // now switch on monitor mode } -// IRQ Handler -void IRAM_ATTR ChannelSwitchIRQ() { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - // unblock wifi channel rotation task - xSemaphoreGiveFromISR(xWifiChannelSwitchSemaphore, &xHigherPriorityTaskWoken); -} - // Wifi channel rotation task void switchWifiChannel(void *parameter) { while (1) { - // task is remaining in block state waiting for channel switch timer - // interrupt event - xSemaphoreTake(xWifiChannelSwitchSemaphore, portMAX_DELAY); - // rotates variable channel 1..WIFI_CHANNEL_MAX - channel = (channel % WIFI_CHANNEL_MAX) + 1; + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // waiting for channel switch timer + channel = + (channel % WIFI_CHANNEL_MAX) + 1; // rotate channel 1..WIFI_CHANNEL_MAX esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); ESP_LOGD(TAG, "Wifi set channel %d", channel); } From ae92bf377d0ee12293e764eaef5b03c86e83178a Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 4 Oct 2018 22:59:02 +0200 Subject: [PATCH 098/105] code sanitization --- src/button.h | 1 - src/cyclic.h | 1 - src/display.h | 1 - src/irqhandler.h | 17 +++++++++++++---- src/main.cpp | 11 +++++++++-- src/senddata.h | 1 - src/wifiscan.h | 1 - 7 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/button.h b/src/button.h index bae8e6b5..a7555c0a 100644 --- a/src/button.h +++ b/src/button.h @@ -3,7 +3,6 @@ #include "senddata.h" -void IRAM_ATTR ButtonIRQ(); void readButton(); #endif \ No newline at end of file diff --git a/src/cyclic.h b/src/cyclic.h index d8ee73b2..6b1e8d73 100644 --- a/src/cyclic.h +++ b/src/cyclic.h @@ -5,7 +5,6 @@ #include "senddata.h" void doHousekeeping(void); -void IRAM_ATTR homeCycleIRQ(void); uint64_t uptime(void); void reset_counters(void); int redirect_log(const char *fmt, va_list args); diff --git a/src/display.h b/src/display.h index 84ff9b7a..492af565 100644 --- a/src/display.h +++ b/src/display.h @@ -9,6 +9,5 @@ extern HAS_DISPLAY u8x8; void init_display(const char *Productname, const char *Version); void refreshtheDisplay(void); void DisplayKey(const uint8_t *key, uint8_t len, bool lsb); -void IRAM_ATTR DisplayIRQ(void); #endif \ No newline at end of file diff --git a/src/irqhandler.h b/src/irqhandler.h index fab9bee1..674d825b 100644 --- a/src/irqhandler.h +++ b/src/irqhandler.h @@ -1,10 +1,10 @@ #ifndef _IRQHANDLER_H #define _IRQHANDLER_H -#define DISPLAY_IRQ 0x01 -#define BUTTON_IRQ 0x02 -#define SENDPAYLOAD_IRQ 0x04 -#define CYCLIC_IRQ 0x08 +#define DISPLAY_IRQ 0x01 +#define BUTTON_IRQ 0x02 +#define SENDPAYLOAD_IRQ 0x04 +#define CYCLIC_IRQ 0x08 #include "globals.h" #include "cyclic.h" @@ -14,5 +14,14 @@ #include "senddata.h" void irqHandler(void *pvParameters); +void IRAM_ATTR ChannelSwitchIRQ(); +void IRAM_ATTR homeCycleIRQ(); +void IRAM_ATTR SendCycleIRQ(); +#ifdef HAS_DISPLAY +void IRAM_ATTR DisplayIRQ(); +#endif +#ifdef HAS_BUTTON +void IRAM_ATTR ButtonIRQ(); +#endif #endif diff --git a/src/main.cpp b/src/main.cpp index abc94c9e..4268bb3a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -140,12 +140,10 @@ void setup() { strcat_P(features, "PU"); // install button interrupt (pullup mode) pinMode(HAS_BUTTON, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, RISING); #else strcat_P(features, "PD"); // install button interrupt (pulldown mode) pinMode(HAS_BUTTON, INPUT_PULLDOWN); - attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, FALLING); #endif // BUTTON_PULLUP #endif // HAS_BUTTON @@ -346,6 +344,15 @@ void setup() { timerAlarmEnable(homeCycle); timerAlarmEnable(channelSwitch); + // start button interrupt +#ifdef HAS_BUTTON +#ifdef BUTTON_PULLUP + attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, RISING); +#else + attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, FALLING); +#endif +#endif // HAS_BUTTON + } // setup() void loop() { diff --git a/src/senddata.h b/src/senddata.h index fc236b5f..07819609 100644 --- a/src/senddata.h +++ b/src/senddata.h @@ -3,7 +3,6 @@ void SendData(uint8_t port); void sendPayload(void); -void IRAM_ATTR SendCycleIRQ(void); void checkSendQueues(void); void flushQueues(); diff --git a/src/wifiscan.h b/src/wifiscan.h index 48c218a1..9ad06422 100644 --- a/src/wifiscan.h +++ b/src/wifiscan.h @@ -27,7 +27,6 @@ typedef struct { void wifi_sniffer_init(void); void IRAM_ATTR wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type); -void IRAM_ATTR ChannelSwitchIRQ(void); void switchWifiChannel(void * parameter); #endif \ No newline at end of file From a5797ad8c4ef5ab09107f38b8be936fd2750e865 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 4 Oct 2018 23:00:47 +0200 Subject: [PATCH 099/105] code sanitization --- src/irqhandler.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/irqhandler.h b/src/irqhandler.h index 674d825b..2950de84 100644 --- a/src/irqhandler.h +++ b/src/irqhandler.h @@ -10,7 +10,6 @@ #include "cyclic.h" #include "button.h" #include "display.h" -#include "cyclic.h" #include "senddata.h" void irqHandler(void *pvParameters); From d587a8b8742cc4a282724525edfd79031fee8180 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Thu, 4 Oct 2018 23:24:34 +0200 Subject: [PATCH 100/105] code sanitization --- src/irqhandler.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/irqhandler.h b/src/irqhandler.h index 2950de84..f21e484c 100644 --- a/src/irqhandler.h +++ b/src/irqhandler.h @@ -8,18 +8,20 @@ #include "globals.h" #include "cyclic.h" -#include "button.h" -#include "display.h" #include "senddata.h" void irqHandler(void *pvParameters); void IRAM_ATTR ChannelSwitchIRQ(); void IRAM_ATTR homeCycleIRQ(); void IRAM_ATTR SendCycleIRQ(); + #ifdef HAS_DISPLAY +#include "display.h" void IRAM_ATTR DisplayIRQ(); #endif + #ifdef HAS_BUTTON +#include "button.h" void IRAM_ATTR ButtonIRQ(); #endif From c1bc56bdf9bdcacafbd6c0572603344043729fb4 Mon Sep 17 00:00:00 2001 From: "Fab-Lab.eu" Date: Sat, 6 Oct 2018 20:55:17 +0200 Subject: [PATCH 101/105] Create octopus32.h With this config you an run the paxcounter on a #IoT Octopus32 or an Adafruit ESP32 Feather --- src/hal/octopus32.h | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/hal/octopus32.h diff --git a/src/hal/octopus32.h b/src/hal/octopus32.h new file mode 100644 index 00000000..a448916c --- /dev/null +++ b/src/hal/octopus32.h @@ -0,0 +1,41 @@ +// Hardware related definitions for #IoT Octopus32 with the Adafruit LoRaWAN Wing +// You can use this configuration also with the Adafruit ESP32 Feather + the LoRaWAN Wing +// In this config we use the Adafruit OLED Wing which is only 128x32 pixel, need to find a smaller font + +// disable brownout detection (avoid unexpected reset on some boards) +#define DISABLE_BROWNOUT 1 // comment out if you want to keep brownout feature + +#define HAS_LED 13 // ESP32 GPIO12 (pin22) On Board LED +#define LED_ACTIVE_LOW 1 // Onboard LED is active when pin is LOW +//#define HAS_RGB_LED 13 // ESP32 GPIO13 (pin13) On Board Shield WS2812B RGB LED +//#define HAS_BUTTON 15 // ESP32 GPIO15 (pin15) Button is on the LoraNode32 shield +//#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown + +#define HAS_LORA 1 // comment out if device shall not send data via LoRa +#define HAS_SPI 1 // comment out if device shall not send data via SPI +#define CFG_sx1276_radio 1 // RFM95 module + +// re-define pin definitions of pins_arduino.h +#define PIN_SPI_SS 14 //14 // ESP32 GPIO5 (Pin5) -- SX1276 NSS (Pin19) SPI Chip Select Input +#define PIN_SPI_MOSI 18 // ESP32 GPIO23 (Pin23) -- SX1276 MOSI (Pin18) SPI Data Input +#define PIN_SPI_MISO 19 // ESP32 GPIO19 (Pin19) -- SX1276 MISO (Pin17) SPI Data Output +#define PIN_SPI_SCK 5 // ESP32 GPIO18 (Pin18) -- SX1276 SCK (Pin16) SPI Clock Input + +//GPIO_NUM_ +// non arduino pin definitions +#define RST LMIC_UNUSED_PIN // ESP32 GPIO25 (Pin25) -- SX1276 NRESET (Pin7) Reset Trigger Input +#define DIO0 33 // ESP32 GPIO27 (Pin27) -- SX1276 DIO0 (Pin8) used by LMIC for detecting LoRa RX_Done & TX_Done +#define DIO1 33 // ESP32 GPIO26 (Pin26) -- SX1276 DIO1 (Pin9) used by LMIC for detecting LoRa RX_Timeout +#define DIO2 LMIC_UNUSED_PIN // 4 ESP32 GPIO4 (Pin4) -- SX1276 DIO2 (Pin10) not used by LMIC for LoRa (Timeout for FSK only) +#define DIO5 LMIC_UNUSED_PIN // 35 ESP32 GPIO35 (Pin35) -- SX1276 DIO5 not used by LMIC for LoRa (Timeout for FSK only) + +// Hardware pin definitions for LoRaNode32 Board with OLED I2C Display +#define OLED_RST U8X8_PIN_NONE // Not reset pin +#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C // U8X8_SSD1306_128X32_UNIVISION_SW_I2C // +//#define DISPLAY_FLIP 1 // uncomment this for rotated display +#define I2C_SDA 23 //21 // ESP32 GPIO14 (Pin14) -- OLED SDA +#define I2C_SCL 22 //22 // ESP32 GPIO12 (Pin12) -- OLED SCL + +// I2C config for Microchip 24AA02E64 DEVEUI unique address +//#define MCP_24AA02E64_I2C_ADDRESS 0x50 // I2C address for the 24AA02E64 +//#define MCP_24AA02E64_MAC_ADDRESS 0xF8 // Memory adress of unique deveui 64 bits From b149188ca8d78762048377a77b19cbdfde8f310d Mon Sep 17 00:00:00 2001 From: "Fab-Lab.eu" Date: Sat, 6 Oct 2018 20:58:32 +0200 Subject: [PATCH 102/105] Update platformio.ini added #IoT Octopus32 / Adafruit ESP32 Feather --- platformio.ini | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index fe7ad5a5..cfeb99df 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,6 +20,7 @@ env_default = generic ;env_default = lolin32litelora ;env_default = lolin32lora ;env_default = lolin32lite +;env_default = octopus32 ;env_default = ebox, heltec, ttgobeam, lopy4, lopy, ttgov21old, ttgov21new ; description = Paxcounter is a proof-of-concept ESP32 device for metering passenger flows in realtime. It counts how many mobile devices are around. @@ -255,6 +256,21 @@ upload_protocol = ${common.upload_protocol} extra_scripts = ${common.extra_scripts} monitor_speed = ${common.monitor_speed} +[env:octopus32] +platform = ${common.platform_espressif32} +framework = arduino +board = esp32dev +board_build.partitions = ${common.board_build.partitions} +upload_speed = 921600 +lib_deps = + ${common.lib_deps_all} + ${common.lib_deps_rgbled} +build_flags = + ${common.build_flags} +upload_protocol = ${common.upload_protocol} +extra_scripts = ${common.extra_scripts} +monitor_speed = ${common.monitor_speed} + [env:generic] platform = ${common.platform_espressif32} framework = arduino @@ -270,4 +286,4 @@ build_flags = ${common.build_flags} upload_protocol = ${common.upload_protocol} extra_scripts = ${common.extra_scripts} -monitor_speed = ${common.monitor_speed} \ No newline at end of file +monitor_speed = ${common.monitor_speed} From 464db72198c42b14e28b2b23740be5ab8a9fc2ce Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 6 Oct 2018 21:37:27 +0200 Subject: [PATCH 103/105] heltecv2.h added; edits in lopy.h and lopy4.h --- src/hal/heltecv2.h | 26 ++++++++++++++++++++++++++ src/hal/lopy.h | 2 +- src/hal/lopy4.h | 6 +++--- 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/hal/heltecv2.h diff --git a/src/hal/heltecv2.h b/src/hal/heltecv2.h new file mode 100644 index 00000000..99eb6ef9 --- /dev/null +++ b/src/hal/heltecv2.h @@ -0,0 +1,26 @@ +// Hardware related definitions for Heltec V2 LoRa-32 Board + +#define HAS_LORA 1 // comment out if device shall not send data via LoRa +#define HAS_SPI 1 // comment out if device shall not send data via SPI +#define CFG_sx1276_radio 1 + +#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C // OLED-Display on board +#define HAS_LED GPIO_NUM_25 // white LED on board +#define HAS_BUTTON GPIO_NUM_0 // button "PROG" on board + +// re-define pin definitions of pins_arduino.h +#define PIN_SPI_SS GPIO_NUM_18 // ESP32 GPIO18 -- SX1276 NSS (Pin19) SPI Chip Select Input +#define PIN_SPI_MOSI GPIO_NUM_27 // ESP32 GPIO27 -- SX1276 MOSI (Pin18) SPI Data Input +#define PIN_SPI_MISO GPIO_NUM_19 // ESP32 GPIO19 -- SX1276 MISO (Pin17) SPI Data Output +#define PIN_SPI_SCK GPIO_NUM_5 // ESP32 GPIO5 -- SX1276 SCK (Pin16) SPI Clock Input + +// non arduino pin definitions +#define RST GPIO_NUM_14 // ESP32 GPIO18 -- SX1276 NRESET (Pin7) Reset Trigger Input +#define DIO0 GPIO_NUM_26 // ESP32 GPIO26 -- SX1276 DIO0 (Pin8) used by LMIC for detecting LoRa RX_Done & TX_Done +#define DIO1 GPIO_NUM_34 // ESP32 GPIO33 -- SX1276 DIO1 (Pin9) used by LMIC for detecting LoRa RX_Timeout +#define DIO2 GPIO_NUM_35 // 32 ESP32 GPIO32 -- SX1276 DIO2 (Pin10) not used by LMIC for LoRa (Timeout for FSK only) + +// Hardware pin definitions for Heltec LoRa-32 Board with OLED SSD1306 I2C Display +#define OLED_RST GPIO_NUM_16 // ESP32 GPIO16 -- SD1306 RST +#define I2C_SDA GPIO_NUM_4 // ESP32 GPIO4 -- SD1306 D1+D2 +#define I2C_SCL GPIO_NUM_15 // ESP32 GPIO15 -- SD1306 D0 \ No newline at end of file diff --git a/src/hal/lopy.h b/src/hal/lopy.h index 97f3119c..2ea2a097 100644 --- a/src/hal/lopy.h +++ b/src/hal/lopy.h @@ -17,7 +17,7 @@ #define DIO2 GPIO_NUM_23 // Pin tied via diode to DIO0 // select WIFI antenna (internal = onboard / external = u.fl socket) -#define HAS_ANTENNA_SWITCH 16 // pin for switching wifi antenna +#define HAS_ANTENNA_SWITCH GPIO_NUM_16 // pin for switching wifi antenna #define WIFI_ANTENNA 0 // 0 = internal, 1 = external // uncomment this only if your LoPy runs on a PYTRACK BOARD diff --git a/src/hal/lopy4.h b/src/hal/lopy4.h index fc0be422..17ff92f8 100644 --- a/src/hal/lopy4.h +++ b/src/hal/lopy4.h @@ -4,7 +4,7 @@ #define HAS_SPI 1 // comment out if device shall not send data via SPI #define CFG_sx1276_radio 1 //#define HAS_LED NOT_A_PIN // LoPy4 has no on board mono LED, we use on board RGB LED -#define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 +#define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 (P2) #define BOARD_HAS_PSRAM // use extra 4MB extern RAM // Hardware pin definitions for Pycom LoPy4 board @@ -18,8 +18,8 @@ #define DIO2 GPIO_NUM_23 // Pin tied via diode to DIO0 // select WIFI antenna (internal = onboard / external = u.fl socket) -#define HAS_ANTENNA_SWITCH 21 // pin for switching wifi antenna -#define WIFI_ANTENNA 0 // 0 = internal, 1 = external +#define HAS_ANTENNA_SWITCH GPIO_NUM_21 // pin for switching wifi antenna (P12) +#define WIFI_ANTENNA 0 // 0 = internal, 1 = external // uncomment this only if your LoPy runs on a PYTRACK BOARD //#define HAS_GPS 1 From 7fde2029e73f5d227e12e8d404d968e9b461fdfe Mon Sep 17 00:00:00 2001 From: "Fab-Lab.eu" Date: Sat, 6 Oct 2018 21:40:56 +0200 Subject: [PATCH 104/105] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8f793552..726e35f6 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ This can all be done with a single small and cheap ESP32 board for less than $20 - Pycom: LoPy, LoPy4, FiPy - WeMos: LoLin32 + [LoraNode32 shield](https://github.com/hallard/LoLin32-Lora), LoLin32lite + [LoraNode32-Lite shield](https://github.com/hallard/LoLin32-Lite-Lora) +- Adafruit ESP32 Feather + LoRa Wing + OLED Wing, #IoT Octopus32 (Octopus + ESP32 Feather) *SPI only*: (code yet to come) From 7b39044dc126a64b0ee337087cc225601a05fb20 Mon Sep 17 00:00:00 2001 From: Klaus K Wilting Date: Sat, 6 Oct 2018 22:45:35 +0200 Subject: [PATCH 105/105] fix in GPS Sats display format --- src/display.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display.cpp b/src/display.cpp index edcf8cdd..842f0249 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -128,7 +128,7 @@ void refreshtheDisplay() { u8x8.printf("Sats:%.2d", gps.satellites.value()); u8x8.setInverseFont(0); } else - u8x8.printf("Sats:%.d", gps.satellites.value()); + u8x8.printf("Sats:%.2d", gps.satellites.value()); #endif // update bluetooth counter + LoRa SF (line 3)