From 1d6fea927c23f2aef5ecf1a7d372deac3e059d26 Mon Sep 17 00:00:00 2001 From: nonix <1888777+nonix@users.noreply.github.com> Date: Wed, 24 Feb 2021 18:51:37 +0100 Subject: [PATCH 01/25] Adding Paul's Stofregen low level driver library for XPT2046 --- lib/lib_display/XPT2046_Touchscreen/README.md | 78 +++++++ .../XPT2046_Touchscreen.cpp | 219 ++++++++++++++++++ .../XPT2046_Touchscreen/XPT2046_Touchscreen.h | 89 +++++++ .../XPT2046_Touchscreen/doc/ILI9431Test.jpg | Bin 0 -> 69231 bytes .../docs/issue_template.md | 64 +++++ .../examples/ILI9341Test/ILI9341Test.ino | 69 ++++++ .../examples/TouchTest/TouchTest.ino | 33 +++ .../examples/TouchTestIRQ/TouchTestIRQ.ino | 37 +++ .../XPT2046_Touchscreen/keywords.txt | 8 + .../XPT2046_Touchscreen/library.json | 13 ++ .../XPT2046_Touchscreen/library.properties | 10 + 11 files changed, 620 insertions(+) create mode 100755 lib/lib_display/XPT2046_Touchscreen/README.md create mode 100755 lib/lib_display/XPT2046_Touchscreen/XPT2046_Touchscreen.cpp create mode 100755 lib/lib_display/XPT2046_Touchscreen/XPT2046_Touchscreen.h create mode 100755 lib/lib_display/XPT2046_Touchscreen/doc/ILI9431Test.jpg create mode 100755 lib/lib_display/XPT2046_Touchscreen/docs/issue_template.md create mode 100755 lib/lib_display/XPT2046_Touchscreen/examples/ILI9341Test/ILI9341Test.ino create mode 100755 lib/lib_display/XPT2046_Touchscreen/examples/TouchTest/TouchTest.ino create mode 100755 lib/lib_display/XPT2046_Touchscreen/examples/TouchTestIRQ/TouchTestIRQ.ino create mode 100755 lib/lib_display/XPT2046_Touchscreen/keywords.txt create mode 100755 lib/lib_display/XPT2046_Touchscreen/library.json create mode 100755 lib/lib_display/XPT2046_Touchscreen/library.properties diff --git a/lib/lib_display/XPT2046_Touchscreen/README.md b/lib/lib_display/XPT2046_Touchscreen/README.md new file mode 100755 index 000000000..d73fa5cb3 --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/README.md @@ -0,0 +1,78 @@ +# XPT2046 Touchscreen Arduino Library + +XPT2046_Touchscreen is a library for the XPT2046 resistive touchscreen controllers used on many low cost TFT displays. + +![ILI9431Test Example Program](doc/ILI9431Test.jpg) + +## Setup Functions + +First, create an instance of the library for your touchscreen. The digital pin +used for chip select is required. The normal MISO, MOSI and SCK pins will be +used automatically. + + #define CS_PIN 8 + XPT2046_Touchscreen ts(CS_PIN); + +The use of the Touch interrupt pin can be optionally specified. If the Teensy +pin specified is actively connected to the T_IRQ display pin then the normal +touch calls will respond, but can be called more often as each call returns +without hardware access when no interrupt was recorded. + + #define TIRQ_PIN 2 + XPT2046_Touchscreen ts(CS_PIN, TIRQ_PIN); + +In setup(), use the begin() function to initialize the touchscreen, and +optionally use setRotation(n), where n is 0 to 3, matching the rotation +setting in ILI9341_t3, Adafruit_ILI9341 or other Adafruit compatible TFT +libraries. + + ts.begin(); + ts.setRotation(1); + +## Reading Touch Info + +The touched() function tells if the display is currently being touched, +returning true or false. + + if (ts.touched()) { + // do something.... + } + +You can read the touch coordinates with readData() + + uint16_t x, y, z; + ts.readData(&x, &y, &z); + +or with getPoint(), which returns a TS_Point object: + + TS_Point p = ts.getPoint(); + Serial.print("x = "); + Serial.print(p.x); + Serial.print(", y = "); + Serial.print(p.y); + +The Z coordinate represents the amount of pressure applied to the screen. + +## Adafruit Library Compatibility + +XPT2046_Touchscreen is meant to be a compatible with sketches written for Adafruit_STMPE610, offering the same functions, parameters and numerical ranges as Adafruit's library. + +## Using The Interrupt Pin : Built in support when connected nothing else is needed. When specified as above +no SPI calls are made unless a Touch was detected. On normal connections - this means the Teensy LED +won't blink on every touch query. + +## Using The Interrupt Pin : Custom use would preclude the normal built in usage. The warning below is justified. + +The XPT2046 chip has an interrupt output, which is typically labeled T_IRQ on many low cost TFT displays. No special software support is needed in this library. The interrupt pin always outputs a digital signal related to the touch controller signals, which is LOW when the display is touched. It also is driven low while software reads the touch position. + +The interrupt can be used as a wakeup signal, if you put your microcontroller into a deep sleep mode. Normally, you would stop reading the touch data, then enable the interrupt pin with attachInterrupt(), and then configure your processor to wake when the interrupt occurs, before enter a deep sleep mode. Upon waking, you would normally disable the interrupt before reading the display, to prevent false interrupts caused by the process of reading touch positions. + +You can also use the interrupt to respond to touch events. Setup might look similar to this: + + SPI.usingInterrupt(digitalPinToInterrupt(pin)) + attachInterrupt(digitalPinToInterrupt(pin), myFunction, FALLING); + +However, inside your interrupt function, if the display is no longer being touched, any attempt to read the touch position will cause the interrupt pin to create another falling edge. This can lead to an infinite loop of falsely triggered interrupts. Special care is needed to avoid triggering more interrupts on the low signal due to reading the touch position. + +For most applications, regularly reading the touch position from the main program is much simpler. + diff --git a/lib/lib_display/XPT2046_Touchscreen/XPT2046_Touchscreen.cpp b/lib/lib_display/XPT2046_Touchscreen/XPT2046_Touchscreen.cpp new file mode 100755 index 000000000..3b9bc4077 --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/XPT2046_Touchscreen.cpp @@ -0,0 +1,219 @@ +/* Touchscreen library for XPT2046 Touch Controller Chip + * Copyright (c) 2015, Paul Stoffregen, paul@pjrc.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice, development funding notice, and this permission + * notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "XPT2046_Touchscreen.h" + +#define Z_THRESHOLD 400 +#define Z_THRESHOLD_INT 75 +#define MSEC_THRESHOLD 3 +#define SPI_SETTING SPISettings(2000000, MSBFIRST, SPI_MODE0) + +static XPT2046_Touchscreen *isrPinptr; +void isrPin(void); + +bool XPT2046_Touchscreen::begin(SPIClass &wspi) +{ + _pspi = &wspi; + _pspi->begin(); + pinMode(csPin, OUTPUT); + digitalWrite(csPin, HIGH); + if (255 != tirqPin) { + pinMode( tirqPin, INPUT ); + attachInterrupt(digitalPinToInterrupt(tirqPin), isrPin, FALLING); + isrPinptr = this; + } + return true; +} + +#if defined(_FLEXIO_SPI_H_) +#define FLEXSPI_SETTING FlexIOSPISettings(2000000, MSBFIRST, SPI_MODE0) +bool XPT2046_Touchscreen::begin(FlexIOSPI &wflexspi) +{ + _pspi = nullptr; // make sure we dont use this one... + _pflexspi = &wflexspi; + _pflexspi->begin(); + pinMode(csPin, OUTPUT); + digitalWrite(csPin, HIGH); + if (255 != tirqPin) { + pinMode( tirqPin, INPUT ); + attachInterrupt(digitalPinToInterrupt(tirqPin), isrPin, FALLING); + isrPinptr = this; + } + return true; +} +#endif + + +ISR_PREFIX +void isrPin( void ) +{ + XPT2046_Touchscreen *o = isrPinptr; + o->isrWake = true; +} + +TS_Point XPT2046_Touchscreen::getPoint() +{ + update(); + return TS_Point(xraw, yraw, zraw); +} + +bool XPT2046_Touchscreen::tirqTouched() +{ + return (isrWake); +} + +bool XPT2046_Touchscreen::touched() +{ + update(); + return (zraw >= Z_THRESHOLD); +} + +void XPT2046_Touchscreen::readData(uint16_t *x, uint16_t *y, uint8_t *z) +{ + update(); + *x = xraw; + *y = yraw; + *z = zraw; +} + +bool XPT2046_Touchscreen::bufferEmpty() +{ + return ((millis() - msraw) < MSEC_THRESHOLD); +} + +static int16_t besttwoavg( int16_t x , int16_t y , int16_t z ) { + int16_t da, db, dc; + int16_t reta = 0; + if ( x > y ) da = x - y; else da = y - x; + if ( x > z ) db = x - z; else db = z - x; + if ( z > y ) dc = z - y; else dc = y - z; + + if ( da <= db && da <= dc ) reta = (x + y) >> 1; + else if ( db <= da && db <= dc ) reta = (x + z) >> 1; + else reta = (y + z) >> 1; // else if ( dc <= da && dc <= db ) reta = (x + y) >> 1; + + return (reta); +} + +// TODO: perhaps a future version should offer an option for more oversampling, +// with the RANSAC algorithm https://en.wikipedia.org/wiki/RANSAC + +void XPT2046_Touchscreen::update() +{ + int16_t data[6]; + int z; + if (!isrWake) return; + uint32_t now = millis(); + if (now - msraw < MSEC_THRESHOLD) return; + if (_pspi) { + _pspi->beginTransaction(SPI_SETTING); + digitalWrite(csPin, LOW); + _pspi->transfer(0xB1 /* Z1 */); + int16_t z1 = _pspi->transfer16(0xC1 /* Z2 */) >> 3; + z = z1 + 4095; + int16_t z2 = _pspi->transfer16(0x91 /* X */) >> 3; + z -= z2; + if (z >= Z_THRESHOLD) { + _pspi->transfer16(0x91 /* X */); // dummy X measure, 1st is always noisy + data[0] = _pspi->transfer16(0xD1 /* Y */) >> 3; + data[1] = _pspi->transfer16(0x91 /* X */) >> 3; // make 3 x-y measurements + data[2] = _pspi->transfer16(0xD1 /* Y */) >> 3; + data[3] = _pspi->transfer16(0x91 /* X */) >> 3; + } + else data[0] = data[1] = data[2] = data[3] = 0; // Compiler warns these values may be used unset on early exit. + data[4] = _pspi->transfer16(0xD0 /* Y */) >> 3; // Last Y touch power down + data[5] = _pspi->transfer16(0) >> 3; + digitalWrite(csPin, HIGH); + _pspi->endTransaction(); + } +#if defined(_FLEXIO_SPI_H_) + else if (_pflexspi) { + _pflexspi->beginTransaction(FLEXSPI_SETTING); + digitalWrite(csPin, LOW); + _pflexspi->transfer(0xB1 /* Z1 */); + int16_t z1 = _pflexspi->transfer16(0xC1 /* Z2 */) >> 3; + z = z1 + 4095; + int16_t z2 = _pflexspi->transfer16(0x91 /* X */) >> 3; + z -= z2; + if (z >= Z_THRESHOLD) { + _pflexspi->transfer16(0x91 /* X */); // dummy X measure, 1st is always noisy + data[0] = _pflexspi->transfer16(0xD1 /* Y */) >> 3; + data[1] = _pflexspi->transfer16(0x91 /* X */) >> 3; // make 3 x-y measurements + data[2] = _pflexspi->transfer16(0xD1 /* Y */) >> 3; + data[3] = _pflexspi->transfer16(0x91 /* X */) >> 3; + } + else data[0] = data[1] = data[2] = data[3] = 0; // Compiler warns these values may be used unset on early exit. + data[4] = _pflexspi->transfer16(0xD0 /* Y */) >> 3; // Last Y touch power down + data[5] = _pflexspi->transfer16(0) >> 3; + digitalWrite(csPin, HIGH); + _pflexspi->endTransaction(); + + } +#endif + // If we do not have either _pspi or _pflexspi than bail. + else return; + + //Serial.printf("z=%d :: z1=%d, z2=%d ", z, z1, z2); + if (z < 0) z = 0; + if (z < Z_THRESHOLD) { // if ( !touched ) { + // Serial.println(); + zraw = 0; + if (z < Z_THRESHOLD_INT) { // if ( !touched ) { + if (255 != tirqPin) isrWake = false; + } + return; + } + zraw = z; + + // Average pair with least distance between each measured x then y + //Serial.printf(" z1=%d,z2=%d ", z1, z2); + //Serial.printf("p=%d, %d,%d %d,%d %d,%d", zraw, + //data[0], data[1], data[2], data[3], data[4], data[5]); + int16_t x = besttwoavg( data[0], data[2], data[4] ); + int16_t y = besttwoavg( data[1], data[3], data[5] ); + + //Serial.printf(" %d,%d", x, y); + //Serial.println(); + if (z >= Z_THRESHOLD) { + msraw = now; // good read completed, set wait + switch (rotation) { + case 0: + xraw = 4095 - y; + yraw = x; + break; + case 1: + xraw = x; + yraw = y; + break; + case 2: + xraw = y; + yraw = 4095 - x; + break; + default: // 3 + xraw = 4095 - x; + yraw = 4095 - y; + } + } +} + + + diff --git a/lib/lib_display/XPT2046_Touchscreen/XPT2046_Touchscreen.h b/lib/lib_display/XPT2046_Touchscreen/XPT2046_Touchscreen.h new file mode 100755 index 000000000..279bebe8e --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/XPT2046_Touchscreen.h @@ -0,0 +1,89 @@ +/* Touchscreen library for XPT2046 Touch Controller Chip + * Copyright (c) 2015, Paul Stoffregen, paul@pjrc.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice, development funding notice, and this permission + * notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef _XPT2046_Touchscreen_h_ +#define _XPT2046_Touchscreen_h_ + +#include "Arduino.h" +#include + +#if defined(__IMXRT1062__) +#if __has_include() + #include +#endif +#endif + +#if ARDUINO < 10600 +#error "Arduino 1.6.0 or later (SPI library) is required" +#endif + +class TS_Point { +public: + TS_Point(void) : x(0), y(0), z(0) {} + TS_Point(int16_t x, int16_t y, int16_t z) : x(x), y(y), z(z) {} + bool operator==(TS_Point p) { return ((p.x == x) && (p.y == y) && (p.z == z)); } + bool operator!=(TS_Point p) { return ((p.x != x) || (p.y != y) || (p.z != z)); } + int16_t x, y, z; +}; + +class XPT2046_Touchscreen { +public: + constexpr XPT2046_Touchscreen(uint8_t cspin, uint8_t tirq=255) + : csPin(cspin), tirqPin(tirq) { } + bool begin(SPIClass &wspi = SPI); +#if defined(_FLEXIO_SPI_H_) + bool begin(FlexIOSPI &wflexspi); +#endif + + TS_Point getPoint(); + bool tirqTouched(); + bool touched(); + void readData(uint16_t *x, uint16_t *y, uint8_t *z); + bool bufferEmpty(); + uint8_t bufferSize() { return 1; } + void setRotation(uint8_t n) { rotation = n % 4; } +// protected: + volatile bool isrWake=true; + +private: + void update(); + uint8_t csPin, tirqPin, rotation=1; + int16_t xraw=0, yraw=0, zraw=0; + uint32_t msraw=0x80000000; + SPIClass *_pspi = nullptr; +#if defined(_FLEXIO_SPI_H_) + FlexIOSPI *_pflexspi = nullptr; +#endif +}; + +#ifndef ISR_PREFIX + #if defined(ESP8266) + #define ISR_PREFIX ICACHE_RAM_ATTR + #elif defined(ESP32) + // TODO: should this also be ICACHE_RAM_ATTR ?? + #define ISR_PREFIX IRAM_ATTR + #else + #define ISR_PREFIX + #endif +#endif + +#endif diff --git a/lib/lib_display/XPT2046_Touchscreen/doc/ILI9431Test.jpg b/lib/lib_display/XPT2046_Touchscreen/doc/ILI9431Test.jpg new file mode 100755 index 0000000000000000000000000000000000000000..4b85f077186554eb5080ea9f0e1b9f8c14ec469d GIT binary patch literal 69231 zcmb4qWmr_-7w(yXp=0Rok`$0eap8DZ1jfLGU}58+N41~;2m_3X35HFI~1HuSQ~E7*E%ye*og5>>SG{`H%~AiTH_fu4{KgkJu?_Wvx09+W0Q z?*)VjU}9iFFu~|;0RRY0f`N%%41!*aRoI4{?TKe-gPd-1VK0S*{h zIOW%rv)t1oun51W*F<`w0?Nd3>vt$@;I`eESK6S#Nj9#*QgI5fXc}gB$KT6w% zTiZAsXZsZwOra+k84e=D#UqNoP^}^3@$uL12J5Lt*=|#C_UXsBC#QQ;yN7d{QtOTB zkuVeM1C=-^JQ(9aY}ht87^W^inePKQ=n}z5zvRtN1L7J04h#$hnhkWs%05c3HO%$e zKdu*G7Jn7hKA>Xl9!e(`<0M zfHjU8JOTp?AZg)%901J%lLB(ql!_^MgxbjMA%RBgD(Gok30SHOiCKrFm=$PLx7S4R za^0qnsVFt;6)7z@95bLU`U-Nex}m z;?UwTc}=Z{EGgMo7SSNpBaoGTFN|nknjL^5G0A~Ar1ldLUl`OEW*}9x-!Nm6mJa^2 zrny<6IH)WMBY58X^ef!jU7I$5Wr=J=KV0El5t4+`jJ1WBsWh4+OUH1hjlFV~WsHw2 z3j1;e5IQ=}HQ==Ee%2ZNQ4|uofI<%rI;fOZaP8 ze9Ac*=c8)~pVkEto@N82(#%*`(e*HV9z{VC-!uzp(?pSEt;hgYC zr`iA-HJWz9e_BPNYG2Q`sKJ2IOB4u(!TCCF}#EhrTfW%9I_2WV5hd;}^41Y$6INZ|<8 z1QaH{G|!q3NH16SOR9W5roZA1cVP-$WpfpYw`x5(NT3SF!_F5*3V>9HJ1}WWRAD01 zB6#e~*a#C066ZtilBJ{AXt&vfZ7Hpj zeZ#Qy4`4jyaMg*%So514)@yd0m`5&ZiK7H9&0GGwrqfa!*6NsN%j6hsI8=VUqQ{Vg zxC17yW~`4e%9#Y{S_+;@H0f#qRyYz*eo&AxmU9G56kxWXe~7}$E?Js=lu~(EEP843 z(Y~nZ5$9JeiPO>q$aF~op@1|mCZb%9&y~ZN*)W+u)s>KeA{6Okq_A`rZlWCtep4Xp zW6p_*LiGw%kg!{@#IRI$*wXSrG2t+XAU-Ot0MneL2!NtS@@Bcg%&L`x7(CJ?x^{&o zl!_6+XJ5fhG7>w>I+rkOLdq1OyZ zK;o<62ieMP{drknyc5K_aX3+;e>~pUgqj>zq)-qOke6mlnH)vK9?ddi-Q>th*f<@v z%>vk_6v1J!xxXj`P-!`MJObj{-ebwh zli<{#+7vtW7Svy!-DVNT5(JV%9Uy^WhDW~05#d?~%txd;4ICZ8J^XgGES(G~gdK&% zk7N0MAPEL(qV%Pi% z$hC}xtHY0nA1bNQe0o@^H0s)pS~jKzcaT2jG89ZJ5E`9RCISoB4ozm=q0}7UWjvq@k5m@-CH)nP32QgpOjdMa&~GS! zr!iA6tL}r{SWNjUg-u+PVr}mhNTu^fx+}oe;qyoqG>P`&pm~ok9mPWn#py2d1dcFmR4CDY2O64jprK(<;6H?RT<5JTes7_#ZTx~^wrf63r6Kb*Zw{R?l2gDB zPquYU%Z|pwIbl2@TOhHgLn`=#%uawQEg$&1P!U~y%iLHEs^K&ag@itvZvw{x9Retl z$1eUn?|6{QYU$+3aU8PG42UvzYel%*8bRP+JTtT>@%5l+(;TZ5Bw7BugfF4dF) z$IbA_(Kya5LV#Ib4p1oI#E^q2lp_WxAI&pEynX&KV|&CE*sfNh{R~8i{+LQ5AQNe{ zhKs0xrvbe!`!QK_V60b?>-%khP3JI9p>c2^dVVKO^px3pHgIyK~%NkLm`lfg`T>9eO4 zcZR?B2QNf;pe8tAGUCDYhK`1^AB+cY;uQ{)J=9ZRMf==@>`14A!vAJOBMrE)5CgyP ze_lXR$hkYRu9%lDx3en+@n$7lYLj=me2P4LC`f+tx+;EcQ7dJJj7LL! z@zAlznzY81eLp;MIe`15&8QJN<69MzV;_p99eDDxAO#Lz95dvDk9ir(?rD542L5Up z3Je9WqIsJcldRF25?<5;>GOXC{ll&b*deZ&t&{%~!WlaNrRwMsPFQgjrs8m~;U2eR zHP35CXakSnMWXw2Lc!AaN`I?9p;OYhC}|p<(~qZ08ETU~S<{&asi^VGRCZIFKvQ_mnjJ zyU>r+W5;*6&}=e%$2ewv+M<2x)4W-p|3M2Hf2W05!1$~qXq?$wp{Px~PLJazo9Vzj zR*gI^iNP1;g1?8UN>~YW5>up%824I>p0*6r?j&>Vn;?v9;rOWdmE$a;4e1h)PaW1i zjoF@y(h;R;qaUv%yoHQXB`)#s(-AK}3#q=FPi)~?6GQs%S+W`3{U{}Q?#AkG3?2NW z3Sy(6s=RcxPT)dl54!2WLUGW)9Fpt?1rr{|L9?`-!;UZka5fC*5hwwL#7rzjgD+Y( zx5J_k&PeuhEcP;rp2QvlN~I^DNd87o@ts z27ExZp~6IIO{o=IG#qGGpi;>q7sc)h*o75jH^fA3}U4cYDilYMSZ9 z%u93OM3hj<+YE6PUv?sj-&tZYVNR?7w4;=tb;YVXwyl ze9*8^5g_%ynbNT8Ulj{U3;2z}QT6jB2Y~9oN9wMrbRte`_JgX6&OaTihw*n(xs7l> zyP>FLQPZdaErmI#V-cJ|w_!#-wI$DxucdCkOqW)D&{u`atm?~;zsAkZ?-W`*^kV zEc>3njN42vU>eWTI{9_0bGA!%6Zo9mU7l(DmC6G!zk_~oJT{(4YSil$jSZKneA%~BM|YS^sKWTb8fp~r?97QH4~ zP4IW;QOo!a(|p8X6+Q})5rt@KT999^c)p7(RJ7eufYw&Hoamq$2Nz9ZJw+oxBNS96 zfW$8gjS&G&MXl@VJ!vUc9Cc_`Y?Lvwx8|_#R|xhgU#%ipJ=XP!Z~e&J*g=VdOzHgiTGTA>DR(1Z_0mh zzurd4wz2!%X4^$e1YZ!b-P0(i3x0q5D-O7GV)`}q9H%WL*y)}IEuz;+0zdcnYoZ@? zuR;Jye~Epc3p`8n{y^DJD#(`Q99`Mx+g{IcJTPpIo}ZDw_C6<^VcNg~&s;5h_9@=E z#mtFq_-mddVAH|uc?(&go%lqbl<7ft|K{=b_NVqFAJ|qF@k#$Zg5Cwh_2&U-8U5sa zLAjr_Q$rxGE?e{f?0OMB3GED22ztSWxQpsw7Rn`XIg7sR8dIk2g+BnFzPBYHz&aM?K)dpw`m_v{h)G(GDl-@uP4=5o6-Zo^8nnKm!A!h&5^(OjVCs>B)}@s zO}_O21c-zTa>pCtHDFWNe15I^-w3l1AA87V@_R{xx0L;EWn2tV_qJv)j;TnMsS4Td z&!aK!Ju=6dVF$58PbEW%gfAEdUQiTyekG4O`gjW8)EDrxi;*o?KT%o^fPC=ctO{NK zmnLIARL1)VGCU?2w^& z+33BhZFd&a6gTcOc+#n6=`mi{h)hw2p0|lc2$Au9EPqPI1CZaNoI_fP-3M1uhvevK zg@|!q514qxW<49M2za}}l1&#N3MH;WUJzCl$m|wx7F47NTixng9t9mlCI4|=0v-Rn zRNbZ^0Uv+;+q!;dbz1JBlq&u&>b~R>&clYK(Y|%md0#RVU_AZ+9Q)tFSNl-G5~d57 zOn1r;zQ&D_K|f;{1}=--rvAD0I|a>yQ3S zegNKJrq{02Y1KUdU35#joXgdq_Da;%$j>gFXj_VNePILP*Nk>l;}agAS1MJOT8r~C zXE{0WUH2Jx6ULnx%Kw^uC*JIcf78hpKZEs6UOF!%wtcW-1)=yy|4m#DYPoE4O+S~7?YV6j*fwOSx4}=Yn*&}ha(nab&AGWN(idB%=w#NLP8CNUo4%H zWbxxL+HA)60#w?sIF%$Y$_|8z=9z5}CLlQ$7N1c<9UmNlEZ8v_yaWeEh(nzUV@Ahr ztRz(sWtJnzX_LEF*hG0@ikg&OdRHbNTYJvO7P?182>vEW`nyaGBb@&F37@8-9-CfP z#pAr#umh^(1xkqSf|yXHv7W8la>pZkHp0K{pH&seiWpkAdOwCug<*|MWfQr8-^~FMu8{v!Zu`M3$#;11^<>u3*kb1_ger-T~ zXDLel9R2h%2us51hd%2TpRi=RSFi5Z@5^e6r#a)8_QXUJVq0^T@u_)vJA0J^3$GF- zbq~MzH3qDUl&zd&jTdXSyZ+(oRipO$t2_2GpstC9lGOjvNI)8ZGb)`nV74{_@UjLVM} zV8NDJH9BxU*8yJ1FZVN?C05^f7d)8}8}E|zAoqh)@AQ<~?lh(7iHbT26YIF@yS~y_ z-zcqRD)&$aW?9PoEA!~wayfDPFr%=W?dU91-q>m0jmB3CnucW>Jf=5iSKExOU}yX84P_bH?x&Hoq>r4?@m?q-1r{#H zUxb)85g`!Bo29kR#v^t5FRTa$6_pmprhjZ%Tdk?HRh8BeFzkOe_a!tE@wN_yj=~Hf zUh|EdYlAN3gTPfOrLI!9^Vy&34Bo@X3%KO^S4hk}WV#Dl# z0r>g=H2GUel=M40)4Q_9zi)Wumd7?ebs+7%_U4W6@)?GXl@WL$XH!5Vgk9; zh2|Cx(3e;FfKs{J9truvfbezsmm*?<(~s8xLsM~C>-vVpkphDF@r(=URbTlc{?#I6 zOp=D~2nmdaB$-?YapHsct%xH`bZ}PpG+Ca^7VElv9HZ(HMrPT;e*=MC;!3h>-V@b89omSt~ss4 zy&_&Ud%KXNEDC-%5cngBd;ZuxB!h^HIK0FzRLLmLWjhL?dhfEp!*AgtGNe2aVpQpDSqcy zjK`KTy}@Hn2e=yRMOX1OzFXaqe}9XR)(}T+TGu9&opy(a0dmcU$ZI@dcmPO#LktO= z6rjq?8Pd4}`-Y8~37)Ez3fdbp@INypbtXp~G{^8upYJ45VtEd^D^I2Fo5wFgdX zTyoE3XgMVsW)HOZ@z>5R_nw^aWYqp0MBTDJ7uNkScOg{OHcVc`_C8{0+Rx)Pzl@^J z1K>#U{I2_Xo#9K%&4YpcsL;(^e`PKv)Nl!2Vw?B#=&bWladXMf1IphXfX(OUI1j*C zh^_oA{?9r_>|3o1iwKdvvTVr}KE~-IfdrZINjD?qoJ|?pX3zMvx^eA}DQ1DV0zVOU zpqYYyocG(NzE&~(LJk$mpRo&&+Z}Tvj9JI+r0}^A5J(uGbb(Klbt+1%4Zjdv{|H9N z4#Yy4<*ac_fN%m)Eqy{`3tUJT^ojN+Qb1I7P;ML%0OPRHr%9+m{=%w^9k^PW@%mBE z)8HTo?e{8ZcB&G>9y>8_u+lXs?U~WZ%C8{i5Q!Xf$+zp@3Au88c0@%|-!B*ogC+Do zIa$p!dSAV3aQ*;rO}|pDake@OSz%w6=Lpg(47az_tdkbuAD|{;!UtXncdVr7C=;BC zUS&yqr;5_sn9~=@ilf!M`N#i)KvCqxb&6e>dO&_y_b4FcUL<)T0sY+&xu022MJxUW zQ&s-t$oXxlLRkkKQ0qc6T?5p( zJS&dnP*KZWE3&^~_?gqs%D*RCW)eAFmLYA0jWzR)boiJpq@f5Y$MnQeWWS2oVLA$g zaLq%SRU9nvQ{Z4nYv@Az$kcJkQ`KQ{X~!Tc&7Kr8b$M(9!855^+fh3%L1v7q#pK$s z-G%N1p@K&KqD>LzV2?kR{c^8!g-CvC-BHrq5ens&;c8L%5==x#E;Pb6cs~>?+s(Mw zWA#k(BL`#yHg8>)0~39($vP&r8js$#Zow`1W$0lSd43N7N7(@Vq1-BC_>LI;Wy1H< zkI%x+yg3_H>i#J&I;CyAHMFT1TJ?N^GZ&7fe`$X|t@Gb^ zu*qrKbKj??p%1WlA(Oze zT-nc~zQ8W>*Md)B2a_Vr;)zNok&P4sc~6LMmD@XAhP*YlTwl$laK@9w11V)uy092! zF+1DZHIZYNB@#qm`2TWL;XoZayUz^5MNXrpr<37OB))STCyz+Ko-c3BP}n{?hbu1u z-mzJ*6&k?7Bu#vSnZRMle1N++ZpOEFKvAMFBbz*2JG(1!N}+3AOF4v}IOytBVIk0} z`;TEcW)Rw%osy9icu>-Q#&lcjn34l6@+DNr&20Hvo_0)Z`>AGD>xtIaw?|K(lV?Oe ziQN%<0EUmvWaIU1gs6Ged+w|^X^H+HPNh?M^L2fcdpIZ3eKbmI#&nj6VLcuIzvsdiepg9E|XpbC}AdYe0z*^+ER@j8&gE$fId%b;gOpQxr#Sur@ znA%uUC-miZYXK4m9jb|j%f=w()BmHrC?w8F0xg`p7DKLl95!?7?}53)Syom z<|6ol{HQk{R;0Q{*{19}KwY9I8$LnTUo5bT;ht$6Tr=$HN%{#(KmN<>rM1^7d;HIR zZ{2zH6~#fB(#bgzdqgYO)~4Je&(ZzNOQG$UQRtGcb-pez$DfpNnST4 zJzS6LK|87AnL;C)aCiH3(eJU}#y@ZN+k(iwHU_HHJpGc-Y)9vM>v3e)pO@wfJ@ebC zT#br7Ttokn+g>*>`|UG*XMCLa_*VbEVmZgR>xh9XofV(CL>3*A^qu?h! z;{M7t9X&y}d!FOR@fPv6FU$M0UhCkuPp(D`XKZ1!`^a*uzE@Vbb*k5w*Sxei`%7@4 z{LbWiYl!i}P|NG;2cW1lR!m4Y#2CAmO>Zt+X5g8j(a~MZ+19b0(Pm_1@*mWJT8M0k zc{!oo3xZ(V*9mQy$5k|ElC@KzpIlrV$I3h32bhlYziHUt{fo&1;><#sP{MuwEB*YJ zwyZGZF32Wzq{W;T!GT+6gJ@yD?H|9H2qSrgk5>(iC39UVd7@<$*M3PThHu1g;`edE zR9+}g^pYocXJ}l3puN`9ByCwmwvsQ^po45%rWD>vqf98F0A~rVsPY zR^uKBjs@ht<{{}ea`0ropftaZaK< z;5(pmvZ-|=V2O3$dun?{@;l{t#Bn;Eql#U@S1}c2f;|DS*C5duQK$(kI?u?5?yfKc z;b`18{mzXv!2uf4&Y({?jnNtn13%BUrFKU+X9Kv&Rp=6Ji@NL#uVW>S6EX{HL>&)JqppVf+#+5av6`*8kwBRqq)gUN>P>E31WItHndkXw6&rv zD?32lLNA*F`=U#EyKMbx>Ls+%YKTJ1t6F%Pi)gx+k%7!>alandnWe|#Pfz}QJHBRMq#$pt ziq-!-ey6J4o_Ho;@fAFpLm1$DVQ%CQarb-s-)tA%?aVyGrRGjR`9R@|Us@81kVrTE z6i}@)nf26A{YQC>elGVKF53X^lxA~_Cxb%qbWHtaJ_g3V8lm;x#^!TB@oRs?Q#3e{zJhnq9IPEs_wq<|Bs}vsA(t5@gk!Xn8y}3{k?eZWDe3oZOXe>k^CDmHr&=`l* zSPc4#cq4>gD2VnUi4X8kfv?2!aSq;*r|YeTa*Vhrn?d$IUu-0jc#Y|7j6Q$UR&qo3 zS#IP`)Nab%hJuDsh^fo?nW){@+m-MKpx^&awRIpT+4_7Y(YLJNp6&rSL}x==0}&#O zN{m`M$=5*@D`Cr2Z(N6{S0_vRPwu1c4zC|;NR%qdQ+r%L~5C4D<-AEFzoHJ^D+O!g7FQn8P}c)|?8n`4 zf$1}G_$(xB6P~p1Ee4KwLNBZY3?G0J*OnHd{ZA^LBg@yE`E(+`lNzlWA~%*LzWMW< z6D?;f9?vZJQLCF*u>{v~t%}^om8;#V|GIsBOTBfKuQupw**@_11D()qL+!nL)JaEQ zM60#swgG!!oPgtnkK7C?gc(W57iY{2aU+=la8{Wiv->beO)?b>%!kB$*c8l(eQSZ~jlKVN2=6{%#a;=!{o}S-h?HX3Nv7 z8a@np*fh}fBy82HA8J)Pyiz}PmThm5(Cw2WAeghANeU*$n_I#PFCrZ|4Ji)>!z)QZO|88}Q zYFxprDg|ZN0f)vOzk=+gYk%9P?`RH*PkJ)AuwQx|yk~B4!X|u;v7zlC>!CUH>tten`>;&G%;m%cD z-FLWKm|vj}#b^qHe=Vz)6i~C`2Kd%|<<0z3eZkECN>aG#=;G*2VXs!X4L0~nH?B=b z8fXwcd%T7G^<6QXm-=s~Pb9F4^ZIp$G`>9M<1Mvd7YG|^1c!|*nHP}!u_`$>3=Bwc zZg=x8D!Vm%3ar+_wKX?FUTn6+{?M4o)KLAmHFjfR&dR(mSH(ISoscjw3h#9fCCB7^ zD=qB!p?8@=h7C6#HZqR%zK=dlRd<>>e^0oXCBqUa%*R-pPNLT=xPwVju5GwfV+4_b zxo6wL!eh||Hf0ivM69)j_iv{4H;Ix5Cq4D3(_FTz1pFfO4^&K}qn%zaBjlAV6~hF= z5?UAu_D~iD_^-7Tm;nH9Ot%}v*)AN)PsbhgwmViV$&+)b(bG)mL;t%B=8TOujnrJ|nqiq!WpBuC%{?Dex~nrPm?ilx=6+%?43ZBio>K z3BzAaj0U-qxS%=A7l!k%U=84(M&ffI05p}|!`ookUF<#fyv*1gWWYX>3_1-3mCiA! zr~_852P-`V5cmBVU1&2pI$SMdR?3@3lBa?k!+XGfxY1H&6;7vF9Xm1g5z%7YRQ_;J?c5Iizm+iaS|BuHTY3E^(}Y4Se5&e_uoa2Gq+s} zs|qq&vvKR+XfKakWbHYHyjk-2L!+2zOFq-wQ0w2g4k^($G-xs6VpKuord)9uYH z&%Ky0{bJc9V?!+Uq|~0mz;IArjYDC0*}6N=NMKz>P)cs(*YbNiH+r(;F>g;N4nO#{ zc2WF_x{O2q;0DHQE7fA+%R1=hi;M6)5w17JeIP<|Um{O2LMP11&IIm;HyJveMAfB_ zmnOe_yyR;%T&(#;qV}|3r@p}9>|bjnD|=$!`$=9UOm9)q6ftH=FWh=D<6}3A1!%N4 zzTKidiZ?U#Rui#42t5z&-+H}`uA<}yzV1Jx!z+KgMWf|HIxm~UC(h9S+rKMK?+-p? z?HCpB)ihsUiNKoj7?~0-{pEsNS*;xIsQ}%`Z`X%;?=b`II1SJlJN4n$ZFfh;KHOwi z8lM}7u&?tj%g@4<7R#)E5%I(bn;79A6FyN+v~5)8gyGvb-kqfOm8ZY2v<$;7fJ2kvj<6}} z?3&b$!NBdeRn5paZ-xv7k<-_-q>)eDl*7B#NckvFbBGh6A`AB1-txsZbhI-CjrD|v zu85k{o|7ua49=ma898xfLe`&9Dn366Fi!L`3=;BaCEVw?tn@wqvT?KHi}i?)jT`XR z8O3%x1S9Vi^nImxx7-ca%sZY$IgCdgOc=86F%-W<7lfA05+7HPe-bi0UN$#L@3!|p zNLOUy*wS08bXjJUgmy+{PdF|F;QUe}I%IbWsDg4W0K8l?z+(}HY492)E|Ba}24>SY z9RXNa{}Y~o3IH%4XJPF(qG<_-B)AeeGb0pz^doN$ddk0mxdyOLrPxAgxez_jEaECM z&@wX_)b!w+{xm?#x|RwXN~}UfzRxaUiHLZ-!?25Hq}Wh=7O!k*s{O(D#I-=s$+d8Z zVq^&FQ&8@E(jjH&Vq|mRO-_!F`OVuLnX>DuD7e@0Nt^FP01g583y}QPpo1mwzUlXN z&{X*ITZ!P$ZLw7R`32vK@=mqYj@LYng$!h4^hT*s-C4_}PPVVp&$9y9>}Y2(5_{j6 zIw>kDUcZ;zlsJ2un)|xyE-{DHmx%V7bNH$5)EB2wnr@vf`&6YL-vz5N?{z}gGE&kk z4J;Gr|21rznn+f)eO_b0{UTMd_bjvQM#k4vo|GHAvT(U(Q*Q3VGU(mhWvbf3678(Bz_1Z)iS?Tl05l&9;!o*t}NRgU%0Z=u8UZy=7q* zZ#CoNO4N1B=`E=^K4Qs%*2dOlC)6S?THgDo?pW7{|7q-aLz^Y3E9b|7SYS_}YIk&- z_~}(f?dHoIgSBd6H$NrHyZN$O@+57C2)jzfc%loNU|5fA?WtCz+!G1)BZUIKylxX+3Q&FQGY(pW=!DuF~vNwfKg|#oZz#-^%J5Kki@ z%f}Or|IB8vXWhQLAMqp-R4HY`DH8A*_s14iPhrGF^DM#H>-;z`L%1t6E9{`akqlnk z8xwRct{2PI^>y60qC>3N*n5XF{yzjznYO)WCsL_6dtk`qotlN20&+^8PD?zjN{V=cS!J&(TULe(%YTmgbT& z-$UdY+7=KAG^Ylv%szo^6qS2vN%=>_{^&fHOBbg>md7insLLs;v-Ru8TAJN@n@=^( zS|n+9e~Fzk;`6tD=~8&j-chJcY@GrKgdwTgg+*on#b=+Lx7LaS&kx6s~AW9tIvfQT!N+!oJE2cppRfx`l@CoDrb$;qOr+JX5 zcJ}E=l2dhsk4oSF!f}bod7u-0eVJ#8e~kYN2P31wN2qD~DTkw(F8a1yR?p=JV8$kA zTV!~7!qZjbmaD3r^#=oChk%z+u%MZHY{P8@$MUkrT*S!NU0<`k#~9|_ckf^RcmPh= zuJ5<~6;F*=juJX6+n}yKvcyf&knAej_AX-wfr!3E(?DUhz80^3 zzeZWff0?Q@4V>M|$h;C4P5UjV_!LUn+;O^iK{d^Pb2dB#l@M%J16E%at@X-t;V;nh z@-;av9+-V&e*hY0yIKuUT}pwC`|xS}G_H|s2}|a|Auc18v4r1^+-oFXeEh;KJgGAX z`IUtDJX~BH*!L-9tKHt&$N408g?ab4(VvSND^Li$;*!av`dPFULkyWgJ?;di`9zk? zxl-JIE!P#Zdd3IE(^k+AWh&vYJA-sLmXf(P|$3m5>@=)uXvZT`eUwF1sxh1FhV6 zA481jaYnelH}-^&LR`RCi%MQ4I&%vKERdB|lqbD!TlBlxx84)%Z3nBh!%+Rtq>*Kn zeW%QAM2jZd`ZK03nmR9%=JeDOZH347jIEx0>g216m*VlFr6qYGPsI|N>DXAhp^Iqj zES?bLM>dbG_H7=0m0*i>{7qXWwY0R@$mDK#?nxI^ExAUSWUL|!3m+wglGB!<*aj;r zn0S#cs{4)}O7-AhF=NR^O1rd%3VgKSW(9$URx#Ao3v0cmz9LCRFL7s-hg7M&x?)cp zQI$N556Ql!kqL^Mf3qlRB`EGf&-$`cYu|{o@)rB!kNPh8;PKmHE#03Ed7NA?VE;+kvy&TVl9ZDO)Euez~&J0aGdu`$K^ zwOFbsp;~|hpLbwH|E|a<_dbaXIqXS0jpO#Hk3!Q7L>0qlh2u@r^|!)2aGQ$oPcbGH z(u5f%%YheVny>la`)=sy%&~Wk6ndlCwIdtXVLjse;#U3J5Jw58?UoiLxHFD4TI;a`6gh#re?w zozsflKp)&~-nG5}h2#VoY{kVSI&q82r@XtO-4mYh8%S9E7DdQN7^8Wtl=7ah!QN&2 zEBDQUrt}s(*K6e@ubauDubf!O$Ph0MKRR1D^d8 za8FUUoN*Bgot8?}n@Dc#+NaOb33EXVqf}in`=vV3qD$e8e;6`aQy+04No?je6XLsu zpCzgb$>WG437jjG`7|iK?WC9l@i@E5QJZULRzv^Z6U*e| zc)VFE8v*&ow2S}NSBY1Pn^C0TH={`dY`o)KjTk>*jK+%Y`y4b5(J*A6@{DjFa1+Gm z*986;1}pa(^}kA&fi83N6cA+?wJv{fz(4f4SF6UfzwqhObZtt?U)ewThuo0MOD7nh z(UGp99TG%he@ZV8QkSC=R-whnQYtmq`!-6qwL5$9bR5gS+U}2h7O=T-QJct3(Q@3( zo#l~e$8WDbW6#bR{zX@G4O>x;jxc&5fU77TW-g~SNog`wG;d60a9i+1QO{eRi2g*Tg}-OAu^PaZIb7r)b|r< z={5Jp`ZDmqaxZnG{=PcOc!52L2|2jBa}|4R#kw}0y{vOeu|`+4voDo3&mxC0@(mu9ivLB5MWy?vPGW& z$1T7wuWeBW;p~GC!h6JU+SOrMT_4)ifzu@N9x^%}DCPzFC}&)?9K&(kel2cN$3TvX z>VL5<#1S7g{dO|0`yBDZw)&+7-njV4c+?kL&pBZDn}@)DWOd>zPoh(zRyC@T*vuD| zKYs37|6=G358+T*3taU5-E?xxaW|6en|wsrSy`Yy))&6ct4Hf^s(yiQ#Vdp>G`4gY zok-2Wa>}OLDQIiLqP2SFoBh6NceT^yHV?Blu?a$-?U;4Zxg22jLb6+I#$nM(HuMW! z{g_tlI=fis_2bozx!rXN%9GQ~OZ9Er?BZXE1)`Hgc5CknR{D#2hbkTaWz#4$;05a9 zXSCsa1N%k|iROAT236^~B05ss`1Cj)Vk{<4FdADgIu+ga!+=-3qG3p#2V zsvi|H9fKbfdv7OyE}B&_>CkP;_xxTFp@pfXC3mpSUux4->`nM>xBWF|etvlh-|8^; zX9Xo^gc{Lel#!L;GJ$3Q+z|to!&RBjWySl}_o9P!za^H1l0$-(Bu#W^xM+F$M+zFs349PR8r9v)rvKDy#0-h(Rp3@KX!U>z<(8$FvA&>tBt zp^vValcrScO{0^U%Bq7DbP_D#T2Y&mVUn+>8wxjW%$Xqd)IoYn?5+w8!*U&M>{Y6` zOBnG=iyb*fyn=^Whu>viU6n3MnKhQEDcAhst@zzdUiu+FUPBT!%xwolO9UTtEqWWm z@+}kYgTgU((3gawUOOVrC4^ZayfG7~j?6XKvD?BLX!%Al_S-Y#Jqae2=m$r!&jxc& zbwqrxc9KLt>%A|}OpO^7l190A1S)>(jTVx^;18=W@sD^J*m- z5W$y4g+xjXp9SA@e7_bSAfS$eoK)z@#42}*M2V0zP$^Xlhf=|eSP5OQj3-WLK}U!p=cZ{O67Pr#rjdq{?3si=6i#E|luL{cHSx+J|e|zD)5rG@K$#%8Q0gzN-sb zDfAYJ8Rc(#@fh&{*fr;E;yE$;*{N94S!rLdY8I6#Q%RYaZ)%yPpITcq{b@fp?!f>K zYaOSZW_*h|MbJ0F*a-yCHP{qD(FDR_VC-35mf_lYwq)@#j83gVER*kDpQljy=-=nP z{;SF8Za0&`EmhE{ds4fM>$9GXSEvU7^xvDY=bW2Vw?pOs1a6W$WHxsDSFJcVnd;{s z0Niyi8I6JGHkJ2e-P`U-ZMP!TpboG#Wux`F4`BIU6=!>z5Ume*xpE_Su;ph2Nl_XRM0d%E z0l{Y@GEN255Nt)b>`$%lwYU+B?CWW~^bY{H4s*(u2=iHIl9Iq}kY32h^X#d||3L+P z@M_zH^eJTXzB^8}Tar$toO-&Y6q8py=D(YI!ZPr9l=Fz$1c==Gp z{x2O3$kUXmjKN7t7cF0x?etaAC>554bV}fLZKJ~<{>OH48ij8VxoaOE|C;2A=H`rw zzF<)t9x4{tnxYEVNMa`L%RXsBI^oW$uolC-r2(+hN ze^uiPbIEfNNLwQvcXrs(lX|q~uMVT+{pkK!54M87pkl`E>PUobk=E@v%=V2=EbBR~ z&F|5t|Gs!HjQoCw{QTm|@!3&aM&aDnL3=`BI+);V)L(@9qx&q#aEP6}%>{_MGRu@rh_DCJ z3GG;8rKU2s+={BBCV7V@QA2n{i@=N=U1>`6cM6S$H0N7r%gY1 z1t0y`ULv5PDU_n`LOnWk{O6vzKiv5Cab)3YrbyT&L3^~c>7o9Du(GBpopdhY5?x^R z$7Y1t5ea}XsHTEx&oU0~c)z%rNKMBQ@Z6T9GAx~JimllGHvg~%LWA1`|0Za1H7 zU$yyWM_$hc6v@2mi836& z8SLGo6uiBvHXRIinIUBoBHtv>9O80qJ0<+3Hp;Fe@^qr5-uMacEmQL9c^TWQP!QsK zMq^WA`{1|K&aVzReTL zT@TuDY_#m&?-j}c0Vv}vc0$9?~wdl{Kd3&ITj9-3`t@A??P_kH>vpq>JMztDV&`!n*=F?<6N~Zd3fYqB`|V2rq9t zga7WjW;XQhXkK%U9dcXJ5UP8%9nvm`a((di?ywYh+KhE+G^afwfAbgW>9QC40c(;d zuOKn|@NKlVF_UrmH#N<83a5%0 zVtb6YJh=m`06o0cxKr~Lm5Usd(`ewre)Z~}h;&8506HN68hIo6CIFU*08KFk`=Fo! zHr$W^*QJvH)FSDCU%5r>m;yH_KnB@&xm^zfLlsUx%bFHU#=_&4KnT8_)Maf!(VkC{ zo#rElP&W;!EyL*A;S-JdEz?gW5>4RifwLUY`YGSJME$g0{bR_ z0r3fHtq3lpH&lccShN0Ath0QjN3x_YLqZN+r=ufk`jLGL7I}U~R6b(=07&tZT}VT+ zg2E5QaMA$G*$vcTe+hH!^sWX88m&p_x-!G~zK5Ho&27QHcPIULJk!#H3_MXAnn>or$F6#V z8LWyqDB_p#a(w3>(X#ZSlg5vq$g3+~Qyyuo<6=&`tQ_RLySmiPmcrus+DZAkuSde^ zU5ar1G<56_nnD=ecSC7Wj!5o}8#>Md7<6P}5;7NZ^S1If?mhu>bu@2|p|F^_g@){D zyGy(cwL+fjL_8}UG;iwoH;5d_s-K5LKw7}ukpMuydy{kKv1IgrjmGz|y@a-Q!0R|@ z_gcftRhIatM@?B=zK*f@L*{H8!4!rwkcS+@Q>OM?nhbiHZXlr($2c*^>h2Os>4|r6 z9rJazfl_<9Mswnh+O~<+Gs!dSVD*PIGQ=|eL805MOg4v?Af8Hwx+|9Dc;y##HoKM& z8g9!|X&l!Wj5;|fDPM%iHd$RLF_yFzo|*RFs>h_mX=^Y!jXoM;tj~5yB|vjp-rmmZ zZ{cy(+ONe$gB(lj>x#jm2x zA9FIMzP6mlLk(RORYO^nbEul&K=C5$k;bu#{6)>BuBWDp)m>&s1R^jG zxki&y9O>~*3sr(xYwB!pz8kNcLiYy1*;|>8oR$OSajL-MIyoc2Wn``PqHs4GE~Sw- zNaB{A#!;I*%ym)+MQ3v?r<0c`cZ%s;$t9u>V9a}ti85KOmGVk$X6fT~Q&>iXfPgZM z6HHOLm9Uq)b`VBiGqS#gl2cVN$1Ad(+UtZcN6>r~SSqkK0Cj5UmI%-gm;yxr2uzdD z+}&;zSDbsIDT3ze(m_F+Me$-XL~Q>6Xe{{aAeawq1K6#hFGg`?#ayDyta0{F!90X) zR@QRRdqZthn&Z?WkT*vakI(BE{Y%02X`&1sfBcfPH4gSl@w2-ro;DxVdH`WQ$q(CA z=W4$Y;?MJ*OJ6yg5vY(8?aJf!sxzA}MJ^7-&}Sp69P5qK83K_g0Rm3R+1-j@7a(j< z3IJgU0bLhB08Ib|E2aT;AX5+*Su6Oa0?do10}f8@KUq|}!1M~;+4$%39o?pXe zc|JyWn5ecBRB|`+PV3;jajvmgX#Q4Xr|FZ;eUlHjRoZlnKI23MLLTX=LLIN!FcT16 zV$OdUvd!|99?FusoujZJwRCJ=aDlZHton92%TUE=Wc6)BgYja7<13WgLu(mzwtvoabm3P9Ev% zcyAOkIB4+wJFc1LmP%Dhndair!%wT;6KEI5m8=l|%t}+tl?bUkN zImVBh)ciT5zNNMxT=h0s&K7wcirD8wqjo}1g75fZsz^9tu7fkAib6pK#l2L^(24ZS zyeEr&Ul@+J7^XGiFmBl#NhhZIEKGlh#y5>Vx;kj!VF9d)F<{a6rxIdi5Vk9Uw8t`ZmvW)&>Um980cKU?@K#Vye%Dybfqg5j0330&4R zzA_{Q?zW2tCgR5ymO7d^olhj0bKIPy>!2QKZin_6!wDTWl&h4xgkxV_cUqr+-27%NUB`Ra3SDZ?`h`K>N1 zTqm!VsgdL_b;CL|c&XCqXNx0~>!E7(Yb+v>&|q9<>a1#)<=U4^Ljbd33>jE$V07Ng zJB_g%H;5@_iwtF$A~$huu2)x=@mb3~O7iylo+E2GgSbMoteXe43hl6%BB;aKW|8i1 zMt+aMDG0@{r7dYN$7T<5*2hTs9i~yOwJ1zR#w-!;iO=<#QE$;N)cb-Vqp7EGm8>>t z`OP5z03>d-I-}Kn8e&J7X(T4^ZkJw;vMC8P3=%*B5F>&iM6WpaNRW|1EfFo%ROY5j zMB=EL6Lvh?ve{DNZIFgb?4IjL%D87fTuG}&z!2u$q!Hkmb6CRTuoo+IaJA@p#L}5t zJwZCuPEWXh@~ei58j4zqnwiW|Kn3!Pl1lNCQ^`#G=O=?raaQ@Y-f_TZvUR zF#Fk%4Sn}kM^A=h7>D+H=P~oX&2J*WAByYS7aFt5S#rZGnu7ZBW6uj|>Lh`$&LeSd zYg+bQehCjN3y9jq+Ni*)avzN3y(~Vhe#a5%GP6BxEhzr%7ax6x^ec~sXH`YR*j)=+VZ)*?Wyq24yOw7v8|zO_0NVCmWU|gq z+dTH%owAyoCW5xYqK2K}zc%_?zRQ=!I1(chro$xx;9NrIUpJ8}Y&o1=K8weaP<7~W zGDjp5x+z%ZJ*LI379lI>y)Ms6)8y&+BMhU&sw%y580Bjk+@ed^6MoBm#5e{GM}Sq- zK}QQ^Ydx1VyF+o}xaXEjF4XuqvPw%WCV9wGU59BG73laU4B)jGH6+y(x#p4}=bd%G zyUD#M^>wY;2Oq85>KU z<6zJ(NnMT?z)zKpn-iK?%Z1EQvrv3V`;yI<%U@At$+2%vP46s(l;z<} z7HCX=C&3jqyUq5X~CUbUtfkg;R2=acXkXTwx9kJvUvsA;M>gr_J$Ad(-}n z96zKgVLlgw)D~TVk7Lc3okqajS9Y1+Bzbz(TA8n?r+w@PLaLlvXIrY=zz(gkx0p%vMn!RG>nS)sD6q(}EI>FxE4`<*UEV+~E;WFP0%{;+tfnsZdCjwk z@=~%lF1R9CQiZO}8Y;;sYia7KKoY?0!NqRsc%u}>4=Q?kS)(TBBrLzhYSQe{y zV8m!MwZLVMI4!uo_C85YlxV11m6=$NuU2CeZfsDtnx0GKwTK;smeY;!-&(<8dtZ23II}M!n^jo?3aN#oKWl)umZ$_WF zdMYPc$f%oYm4}-5IU66(w(Hsp@s7sw0k3RdM&(Iq|}tnf){0I z>(vcdiUmaTy4QQ}?_IwJ@Ug`zm@4V!a~-aN+9;H%4J$fMFW{;yE|s%UQ%xIoCFbMe zG+abs@XM6k6r$~NHjbBtaUWC0Y2JFsHD1!C_B1q*FiTX)VRT&qXA5N#7n*zGvW=1v z5)=xeS|MR{%RoUS;HaJlU5?YSSv9zzb`ww<2$aS{M{-t*beW67rIIh|jxKS_ju@;) zMhoA$*ZQr#6NS^_SZs8aIfEya^2hEcT}w9Q^wxSTpDh7GurXTwm9MW{{Vx@_|mvn@f?M%0kJi#_;2==s)Sj- z_3gn_QSx%^wVNqjYKZ`k6aa!~0VIGLNC6U&*TDcK7hM2%-jCtSh15yy-bpR&^{`uh zB9z0!^{*Vu80-l>$RE{tnK{k+o_zVtc|h<|UYY0t8YsJD0ow3a4z3hb2mJjQNfYCnJDx8LyEB)?9FU~7>&kA#kB2p zsr9MH_tJf0yJ3njRCdA&Dv`6pc&8G>tkEwvG5RtK_2|7D48$VDsUxX~Lua<+6RS^R zxpf$nUgUFXF{HO6mgCGKE>m$xhvNwikVmzlsQN-^iN;q6YjB!fs8854|Pi8WIp6d!OBE_PWvI_Z} zLA{*fLhgU^H4A|92vzze;t-nYwLnFK`ol|#?&q6UD z)wAm^Yainnu&Req($mK*?G}5Te62s!uRj(yl2mcxhWS%DIry^~o@YtxZjS!|qQC$i z<*~we>b9Niim|cJ88$C))w=3*-FM4P2O3?SlEnuaqtM}%lSbKMbXgwI;BB}edG8+K z{EStVam5>Aox8B^;l7KTO7`TVmk+}_Ii?Q-J|Hg>Sibe2SKzOY(8sw4O%fq-pF|36*{%Xy6$YIti4a6Z#6 z+}G3<(S_2JU-y%XQw{RQpI81Fhk{m3hc;K!(>7uS-1lre*2e*srn?fZbnh9Zf!VE~ z3#sLvO}42XV$tB!tgTArU3(n&7_d#iF#_zbilo`!(Rn-E=D4u#7p0`<5}f?a;(p79JIR8$llf~(_I8345Fxs%{IM}yB zK+{c@8ivwenbW4`?4#b4w9l-gsHcnPJ|o+r$}*;zyp_eAF_aM4=8d2;*|-+B!*rg= z#DqrTMYJcX?`N0MnS?S&;&3sb<#XB`=COfwE$Y3mrn+&DO=pFdUUe?BC@j1x4 z=gQNl-zQEROLZ7er^QWabEvW~I_2ExZSc)?m5x$5*fndQc{$i0au^0-+Q-3Vix$Rh zIqpXr^0psX*p4?(KQ=8h#b8gEC1UXwKNc>?X{e!e6wnW3yn^HBWjOR3Gc1?F47%4m zUR`oW2gWOKrm>vT>SsGNkTt{!JjYeo)p4|Vr3j^hf+z=rK5vrimOakS8EX8vSE=OO zYf53ogn$wr+mqtC%}BbVs|&nwWzDRWjmq4h5Av>y1L5Xk7<8DXCt4|>X|z!_p4`X! z4;3<%BT6>ZRWLSDOF>VgH#V?7WqNN4VthQxF%5BuRsNP>U8kQ#%{R{`v0QL%>KW=* zs-&vt(a3D*NzTiA*B9vv2ce>xj=qV4CRMcFF|a-f;%rh#({SZHm00BzQCzvxH{GiD zMXMnV2@DcIm?dPyaXD0Myu;`OLWQ+JQ4$qoSf))Ck*ZEQAXG?d5J!S!v|(0Z(bY8R zmEFS9@hj(|6egZWv(;j-a|W4?>o*;($~~Rfvw6xma`dnEXy<-Ws*$oW(};8LX4tuJ zFldb?0be_8Ii-;TcRfnE<(!`-VbWm5AA{AU{m~pUGcF?#*F%4XtGSVeII{s@8>msI z`F~TN$7OlrA7%be5yYi+ZYx<-1+&cKVbChsq+l;`aH#o~THl%xb!d>kf>+a20Pc(U zpaVn_LI79sNT2~N!T?GxKpvIB(3rSx18D(~&VHVD{)!xHA%};a%RF-=Yjr+ld7bC| zP7d>ar<*lO;@U4v?*%GoeP*sG2ltN+_>``df$P+i%%G!&mN>(i3j;xOwb6O!8P~F_ z88EShf}C>Bx4FLS&(p2hHyk)Eg#XPsDICtB31jbj)aL3;mI)wxBO1xlyHm3o+AttsaHw{BrP$hvsWbrRpzx1$Q1y zM-PSUqm`Qafn96S#rhdrcta(Loq=mTYYr4+Y7p=^VPA zk>?zC{MY|T8UjF&hBx!T{ok~lZ(|^q1K0DZTdkhw6Qc! zRrhS}aT(u7MLk1eiOz~L;0u?J%PF3B%I)6bRysA?G1H|@irQ?V;iB(HN$R{CA$JT@ zmq^?DUpNwXTCYf_H9Z%!*IDk^S$VMk0CD=9QT$VgZLFCq(>qArM&W3XfUIf$c=A!3 z3eWa2Fy_yw&-oup?oumKZ)$S@w0j2;d;8 zjBFkj{{R%yp{JXc?b^%6*8VF#o53G7^f0r3{*U=kN&Tn(Rm6QaA(;NqKB}>iG&iBvN%0K;_w-~g31=U9DffZmj16Aa6 zKQxYiLb|kngmL(n``qq5Y^9BGy(1{!H3zrKj5+egeS_hs-DdK%`rwFRi-*+W!&pSDIkjQfE~A5~s$;+m zj)B4_xRyTm&10{UTA!2p2Ai{To#S9}mN!bv5LnYqeLoHQPDcDc6K+vw z4|AO8re@4@?&o`eUZk={9iD7->nu+`^nlsmsi(V$X=xT*%64hIqss%&~G*vmJF;F*T$u`**4Ge6nmKMm{I0C6N zHjPzh4`H)mm4)VCSO6FrHP-s;dx(gYuTb@(Jz%DPUg;Is95}>R&fW9x)qF zD={S88!LKly>xhtx4|M+k{$>^!}%jipaB}71#J=2bO02P0#SCTLp^5$rsQCBOfk5T z3rY5^xWDyU9zT#X45W)G1=17quX_tGGZ#6(Q_hDue^Y}4M9)R(o`AMl{67~rAEIS} z{AfD(YNlT+6X~9;+h1shP1liP?=oWbPj>E=$L(BNPT9$)UlTEgh%3D8BJG!9=;^kr ztdx)djjg$D`mD458#2uP?VUFhOe5jy-^MiFD+@N)=)9L@mXqXWdT*7UnZV5D@Z(N* zG`eRKX5YbL`a_d06-v%yoE<4Tw%Y}Y=TKwbf7IX`WhLmH;*4jVjqczVwnhtB(Uxlg zZS1)9T_cZG=y`t|I{Z&Ku^C#y?QgQDgd^thMH_5Rt~!6Dt=HjxhRcj!q3W(}ntg}Z zbkEgz#aoN3XL0>wzna5I+cDF2$c>ZeyNU(W<7O2E1w6-Ym>94^nT*!RRh$5x*TWnv7hp< zD_p+upGV-&lxqi?kz5DU3tsBR6Kh=$piGc?ZmXrIs*U^$D{c$psTaRRwjM6Mjy+#0 z7yQVog{%ggmm>A)gs9v6rd@h)@U(SF7{nbs!w71(*?7Vm|m z49B66gK?m@j)_aACU&^BMNK@x(YW%3oLkWQmQFG06BEVoVTFhzf)dczx;AR-eS_Sd z#Oa)I%P6Lfa+EthS35IEwxN0t3QvT)0&OK`CMe8u7d4U#f(p+(H^*tFpC-v$GVzRO z4a3{i{ll{*DhO-+2&5>y{zC$yZWz4lz%ojkHmGLNCAn^6pKlq9=550x<4p%03# zmzeg4Ji4GY#P-x^tb-GvECy=2Mmetr{8$zB^}DEkxcojU%5tEsWt5XBCP_ zT-d;KnjSMWc`fb%=^y0kLlf}+WY78XIsX7TceJv6NZ|7x4jIbv{IX*d0}^y?4ia33 zjQ}f}!J=f;0CMJ-dsNgVSZOV z!#TfI$*9e-B#!(^UnXx0&_|6_O#vZ{r6f3w?|W#sl&+-0Dkh!4WP~4VfG=b&X@kB| zU}R?H!~4Pau7$!eeV<8T6j0&VO*JG9vm=ZF z>7Xp+2vM@~kDwWDiWx!%M737-*#K5W)oyBFKsQwFsse*5L=aO% zv{d$Luxn^zt6*$``9_Dbr^=y1P7_jIg25=+LCk4z1;+b-@5Nwo^zMs?UdI0bc;>SA zw&(qd+XAhq;Me4NGfO2^o@ua-F}rjs)__i{(?^&{=zipMR3HfQKnGorbU+VYiEGsW z7e`b8D3kzwi7|#r8o_d0;1CC~MH3klBMo2;XlKvAg5rH3yb8KlfB*=~v^~o6H2meW zgAd{ylTl^qo#3v+=k;t#o?`bfGC*AB?gjPx5`70~_0cO!1B03E0DfZgjx@vNq{d%X z_p$7%Ih&MP-V4vtx|>H12~_zx@Z?>3CwStG^UT;|7kk_q2a5HMAHxn0g+)uvlQyn@ zvDEIlbyBFLa_VJCBSXiylbKB&3n!30=QND9fp1Iq)o@l?Y1Lyd0;nFP!V$TgF-qVh z1m<_Mw(C>r3K`7}EV*7|W5{!yMuhA>#mf)Jzp2j`_ur|(hiIFM^iJb%!q`i(Y-TnV zJ2`0ZG(W``v9>pYNt`+^wTy@ zSPuctBKB#}{fn|Z<4fpt#eLdeL)V8cMu!H*TJdm8j-h#96k(?f#-?D6tcpXOfuxaR z;(D%4E2%Vc>D^9^Q%{O_UV+?72;EUp94+2Q29FD`ON)Z=K}OhRg^rjq&Xux0_qO)E zmp-E_g(Pz7CgWRO95)f+)L335X(;ECT!Y&$7P0VKuSYi()sDCyi*B~RRa29ATV+YN zirLrk3&6muy}SM--`L%G3p`Y5K2}DbrdkwSyH1nT_$vPZg7oFQ&E4}w#_Lz<9Favt zNq^-X^M``-VgBRwIAZ?z;97i8Pm1*p(e2#|(1fX*aHB6V9D6HqzNs!lK^3v#mlp9I zA^!l(3T5(*c6xsri}2M!ur{&R&3R2jvjEaZv@eu}&xfO2heN<+t*xnJP5qEpRgJUR zh1TvDm9G3od)^HGG>ubJJ?ywostc($*3kfo5pcG&*i)*wM-qD_EHOMdG<92P{(sQp z*E+948B;vYO;(SF5BV^~jtU5NA91+cr9*`Lv5Gb?Y2zisK+X0AmT(#x$mFA^rgYP=o68ey*8BpE@?8-OQE8lG zfT6@BilNz>#}?>pujj=SWIgt4^pto<_)#^Zq)ZpFxXbNN^a80_0^SuDI@_DB*Xw+YyIX3lo@W z)fR=Y%D}N48wlcPYGcJBG_D1%zGIYIu?woKp9|us9Y5J>jxLFusvQmlCU=*C}ZXqyeYaN>2 z0eVE-WtbQk_?vN*UF(s<%zPa#TSvlFm}NajJXNvN+?=c+X1{)=VR~7VQgHlYmHA&* z{DcmpsZKY#pP@~jI6`8z33k zM6oanSwZ@QAyEf6MN@*&8MIRNT;FA<$M8><>6;s*b4+IKD@aYoR%4eEoN1naUxd}+ zbtRPyIY-TS*sOMZ;%>b-Nl98fS;4|o$=o4mKwqMF>AC=SLI4UbfC52A$N?hvKnZ2w z(DU)CIP~_WZSUuGGWl4Wq|aIww($=B&tLt8=R9*Tmme(z5XLlUHtYRYpQYy^!>9Sq zcxV$|k={i`+uZyk+CZ!J|Z`Z}CU-feB;y`7RhZQ*jdR;gDjlGdhG z$~rtJhA`|T^B{&pI2Pu-i~GAPag1t{1&2KDagd9$F!6VPvgK}BW@GVWl^$b?Vl!fu z(p1diOOLV%zERLDA~*0}q*Wx3Mk76*z0Cp5o~ucd{JtJ(JK5tD&u-in@+U zS=o5y7Imbtvhy;08CmD$PBU!dYkeM6`qRxFOf7?UiC^sxf}Ibe$pn|gDd5SADAMQU> zhChsOEl&Om)kkX@FRGym>Zy7t$?}dpl_t4O1VYTfos%7nU9_NFy6Fo9% z<%W_Ps2o~A0NC!cu&+oMh6h0ep#D`#MwnNIg%H>tqpIPDYl~%NT()7NM6?aZw?^Z?KcHLFad4Klx!z6 zLO>T#V_X5xVweoaaej#wX=DKc%~UcQ>9S%TL(m;pNx?>0D`%x`EnQH1Zeyi>SA1+# zw5yT7q1wm95aEsW2jNwWdja+*m8HYX<+{^gTu~+uiMq0AJ!pbNLGM1m@JU5&;OCZS z2g$U>G2z6M=bDkRFtW}Oo4Lcyax^nw^?3b08-_<&9FLH(=65#N>1!#;8pQsC(q;2= zN&ZImmxJo#;*$;~Q0QqlJh7=BYjZ=wa8%>Q8a6-GWBn(M$BLXeR@CFw9XTU%XMYzD zVbts#h(uC`*h=4=J?oC*{uQX26Hz^6lXBW}G31sv#3`41ieAT^Q4S9=mSR3z zvg!_IiHxX)62(YNth2hU?66G`%sSc_t7+W~@rN{pz+(J6Q-(RxM$;rW(ZFmxS8ksr zI%l7&#kp=?hMYMtbY16^G{>3-G<4k{1k(W^_CNACa%OM+<}jI=P!sQu$cG z!0kgvMJzVC_+}v$WNd5Q8MqGHWi4808ns7D{{Uze_rI$_->T;ETIn%NLQ2U%50cjd zVPR#CEDIaEGKMI@+A%sMblXxbh*!Hb8pH;>{+-nICTA2!B3KAEHqq>;Oi zFZW#0(@DRJqX_A#^3Gx$W^b0_#A?f@bZ-tK_V*6UtsapfzL#uLs{Wd zYnnKU`jcCTPf}ZMY=!pia;RM^g+{4TqESltBy5qmwaqsK9hX^#arJdM4Xqxq!TLi- z0cD<-_UZj#7Snj<_D>$JbX>Vihi^sgTI`Zcjzmiw@3TXGT0q3+iLpbUY=GQs7Ui zbxcyp%?%C6ZMFXOwph7A#T7X2jT@X_ji@W|s+w5o=WFF=R+l?KxL_1ugU1$`81d{> zO6e8SXm-3i#kg)6gGEVE433e6+_j*MPnyEx4kMt(>8awQh3s^1E+x&jmh)V3!zG(} z&al1vtmT4zS9!RO>85~R1zf!d*+x&47iE6rE@MFwufp70i&Ky_4fV8KVD&f1bQ~Qe@!X?QV`60VbD!GoJBiBTHHb-)Rzs+G?$z9nHI=I&c2k?O1kX|7`d8z0v^3zcqCnD1jfIy>HUWp>)#iv(GP+kX z?DcbU!{V}XeIg-_HrqKIQwC-ajTonENN~@VaCGo1dx5@$%QQ~)0|AVa%%FEAr`&Gy z*dlVcrZ_5-M-5Wej&m61xH3X6*oy!Q8< z*Ac^9JTY@sER(P{oL$`4*ab0`*jqB5B{`&TX21m{EH%{3n8x4<%)$r%01M3VI9*GR z%RB>>BP7lXdKGx)vL-BeRvf%~p0C3EQ-^VE5mMq(HlGrNSUl(?^6~Ip?Ik;DSQ=U` zKTLIAelNOj9KAaHj?9YQIi)m1D=bw>5WlqBwC)O|$da^a@jeg3>)K?AoS6^lGjD?9u`U+GaO=IS=fpor zgI|L5Fy_7_d3p5T#RE}t97D7tp-daDvw)TQp#U9G;(!`9KnJ-mdH{Nqi10uMk}QA# z*FC|l1i8k*4yyW$I<}TRWc0H}%iA-UK)Ow$Ws(tUGIZZYE!D77R8*26h4K-;R+D6P z*;15Zu1=8&VU`EvmN!P=FO|Z@L?#MJt3k3abO4cL02J8_vV+Tw&jWm|y^sy0t&;UR zpVUe93Qnh;fS1Ddfz-5wS|MbE>7o}YT?LFW#ihZdv~F#pZDZeZ);ZomFtoi!VVCu9 z`lT*ZzpH=MK)1)-L>B1bW8#DiHBVH)5oEw=4yX{viZtw)0#>?W7$GgwKo-#(3n~z3 zi`XUr6eZXwpUO4aU6Neof!Ruk+YyA1raziqfO}RZIh;E<-tZsfykX5PU?XGkT-ZHL z@_HLi5_VoAj9)Fm1$sSWGvU$!slC^m;y*F$cuQO3`I>+PLrq=Os5#5ZKGcQOm`ZdB zlP~sdWxn9rrbmrAjz2yZfIZ7hABc`kcl&AnMyTX%W8NAG4>uh*TRcY>uB@YNE7;g! z9U3=X3pD(<6uLO2p~3cu=9A_0z1g{_E^F{AhY@E-mce*;7!Y`)5iKU%8pEQr3 z@h_4U_5InTj4-?j97B77b!#D#H;l|7t_H^173d?!WgSx;P&3f5X-UA~5w*ZEw#+(N zZE!9ytKr$@o+x6dGPfz_1AY2gLRZ1g=dxz65yz`>{vb8gF0_vJLt_AC9R{nX!QWKe zTA9JM&3o(vja+=kTw2U+?XPpPGDf0SDQ<0?pAb_|1~*?E(N4#S3yWr`Y_Hc{OLV%^ zw|kLvN26ESDXA(VXmjCnK^Fv$il`pe)v&k{!0l{pqOQbrb<~WKg}_ru9%3>%&lyeq z>rEyVH9W26y@oJAd!ul6QRJd5Y}vmKp{0$rRMX9!gdEmCa+W7)@aMZcSn*=k2OojmYRNs-*=qQIJin{nqYgTIo41Dp+cTokpEg9sV#-w%+T6==JtS^f=j){>$4kII@1Pe*+Y41^&6f!E2=&3leJVnLW z^J+y#Q-IpV{3#*U_Du((IbExo+T>QjEiEVbxt{SVIkJBurD$sPGv zkC81u?pL&_>4Ku_m%JK#~9~ixdiuStFwIkDvggQw9J8i&6_JaYzjkG(sb)L|q|> zq~UOw$QIaHR9oPH5n)g)RGBP+VqCCAiljp$8*(_c+Z~i8g8K!eS(mAy#kgLF5)N^X zl{ZJVaTuo$S79W%?B+$$>>tf{Vaxn7JpERm_~L6C;HP!edE-DR06mjP0SSPvjnV)@ z0FBE{#)K~01H@p*aJOMqM2Uk4E-krm?{yg$7wmbyC5^mmBzudqnm7$%#f<|YkdW`4 zm-KtClCHj!g|PF7(Y?>Ab8b#c7Lo?HKglUQ^o+GxKGgByhp#X4ILW1wc}$YLz3w+I zCgi9s4_Yx%Ng~El)X2d0K`WYeEny))Q>zE_X}^Y1XkKnK>v9pLscUX&KiT2>&;J0aV`3ax zSBOx_OC%9BvH%GM*WbjXu|+#EhlJyVl$4(zk)gWTV3;%UYLe3KTr3xR&YtV{TDMBi zM?POG6Q8lw;}rCiTrD(ojD$u&v1`dEUDq*#aRj(LQpX)UQu7ETzWQ6qWfi(E&nHQV z>U8sr-!1QPWq8bx51awRW5SRjJ5LqiKLJ>k} z0QW+n2HK$jKNSgf3MJ6jWojjJmIq|1&8s3OY{39~d;b6#fE#tE{)@&W%I1q$5J);M zTphD{^o;%NC5vjV}S7I9>mN$slgPf2vp#l_WphBcZ8JPxGwSES=3 zUqof%JmZV{%lA0deJt`rJ4+l!tI<4mDQA3=JDcr$woWQ(BsYnsLH<#4J;)K<@RaVG z27F^jHRYBox2WMvHw>httE!oVNYGx^yg9^?(?GLwlKD*I2+Dk%?+|eNFxSUl58~~RHOz6c^Ikypq4A$PsN25u0b5D_zlZc-{`|HmE)<&xOX} z-Mkb>;dIB@ww4ylKwQ||Nsi&Uh5YY*RvTPusDlL?<8W>Ht}Pe)5P-~*SIvfyA5dGD zS6FDNduryw3rurz!){#p$MUt%fab_I@ln4nM!!%&zJ|+UPk7;ahSl)n1bJ>5Z^dFH zyY1O|#wCT#KNp%v+~`Z+W6zbYu=5%zgR3*k9myl5rQr%T8Cv@L)HCL`DEXK3Ag)`8 zFj|_~sp8Dw_ME>qMPb@F{C9#`CTZwEicQNuM6N1$CZwFh zQ5&Op+!g3y$+=VP@%1=-xo%xDAf^i3&;fLaK*Uy759@~$gi&Va zX45ILw^Tm4Gt0Njmj0;p8n;T=C!Urf>c&PI;3n0&TzRiHo_S}T@=Wsglc^_FmMY}( zJSH*tpRf2J5-U^NH~4oG*3eKgnmQ<+)&plaKIZ2A3dsC3uj_|Eeb4*WPZV41@v?Yk z8O_Phmi~k$;|j?jk~2dCL)t~acAskIRmLhk9hyxnH0kYkV{TmS%xo{OlI>>3@EEY@ z>Ej|;W3c45GwpTMpS>xj>Wp>xvt@8t{Xgn42M}Nna1O$w4LV=d*{9mEvFrw#DkkDM zW6PjFIjs&N-R@FbqWNb&FSKcL#buSLasL2pz`-y%@i|0}Zc0`g$sxV1)CHf9V{pX~ z!*G}6r(mnYZEW9OAX3nAdu|ddNUKcDl;qTurh_+&YwzPfws44Sn4a+^8n}-CBpFdHqcd zkhDD!p#TWkFd9kN4G0iVOmIfD34$FU?avhKj6(fka8@qR-2- zTWXLV<3@R$KNnlUzqxqLK=$6;x(AVedga60H=jt#z@Fic(gJOFh3ciJ)n#vXU2yo1 zq3L*Rnwnozo&!sxqVi9>e2Fl1NJNNR9vzvHWAtu$u;vAL_?rC8bF0HO^kzmZAZHNO z^J2_1n>1`sMVBuS);9|EIB6RTT-#?l>EOFoqESvMd!94!>{(?DvXv)XdzQzY+#dtK zRa2?&s`GzI@<;erDG`>r(6gt`@m*RsbNvzK_^Mk!{iXg66-kEAY4Ki};S!u$XL+I- zWQIWao?7A58z5eXZNbxXXs2KYoacuV-E5zTzLuquO3rU7+BL;OR@u#$BZQMP_@j$! z_^y@@#6wLCE+w(F+~2*_DBDnIVspK5>|?F@!y=4%jnOEixirT;^0HiW4Bov%RO5*~ zvqCK;RFUOB=nd)SxVbDOBgH6mY{=ZSg43?xtNnVd^v0djJoKA&n=t=CpjVet}1S2jU&VU*W!|M&#HO4fuQ}G8j zm(}y%Q4B7K4>090OLIhJ`E1*8#~s0g!1!t?3*P%(@wrd_jYfWI2Ex6Y{Au zl6u7D7_RKKuXS@DEoD^9WPqMw_O<92ABuX@X=O0VOPPCLw}Q$FrAKsM2LV+xpCNOJ zF21)@B+G>n`nRrzTj<1xau(1;M9sB~ZfCMBaHWt{hD`cQQaKuOy^jtZEJ0sZ;q@N3 zhtt#o4>mTs{$UgOQA?uxpcR=mG$3Gv0m+wGC}t&jN6@4N$P#4;3M3^=ymvs9MB}20 z3PL8zB_fd?s%-;%Aq|1oMG`Nv05YQ@1jUg-(VMCw$VnaAsQjD5hrLjYkOSM8Cy@45 zp9G>Vg{~Vd7B$0kxTFP=Fb95%z|<`vCl_d(kW-6`JkJ=xD)3r@$=Xb0`!WHcSODF6 zagvg>cr$~9oLMlMWCCaaMyLT4etZKGP~aWx0hgdZ1h}gKbu`AhmNDw@)!~gJ zDevS`;xqirBM9jAaZBZruhiy(7N@jy)SP22EgUrBjfJr=G#tZUv0C0Zm#q_ixc>m9 zV)0)#cysWTDLHTN=u=z9GFDMbOeq67jV;ZeZ?SVLNJDBnw0d8&MaJ&Q!p2EE86)yB z&H>N0>^rT-0Np?$zd6HlVRZ4&1Eii83%TW>W$m#kw@o8Vx{og`AlDN0G;EFXJTZ+f zbB%xk$k=D=Gl86G9!4;p7U zzV2aMUL(>w{5$7Q>T|?d#V(7-T_1kLciAzzWRmEtq=nK~)8uuX%x#ZtQ-i4@ax2FF z03!Tb51NySDn(4Mb67*H4)4>tr~MG!Y*XeP;!=ITKg*=R|+!ZAupI14IUo3*0`-u0%eb0w>i zNWcwoxKfo!W<7{H4c5+zu9>vLDvn@qH@GB%J6hp3Bw^HYq@B)%u4@j!s>}`o_D4W^ zSE9L`43zhl-{q|3kE_*XdP$|KV`HcuyjinL z#SEYv!&_f%0=lOPn-0Efz9KFh0T)lrS4`x~r1(_Vp_PWmCL-`e0dXz(!;atIxTKOI>J_|WnDI*rDk|s^himD@X`8KuJ`$GC7(ba#-91}qD zeW==FKe}v-xE8uPWGEn3MYKSuHAT=IoOyCr=)9xp0J_0cU#bR?EW*i$go24?x|Prr z7YHZ;5P)nD1d>caYpG??0N4hgu{37iC2wc4v0omFkkJ_AFuQBDXRtC>W@*r>LSSPd zR_N}U1YqJAMLr`;_$nh0Ylg%xFybB#tif|sL=4oo(1CKnx35E< z2Is|1dIyv}5oCrtX`&78fdWMXHrOsT77=&0h)FsA^uELn!%Y?*C5TOlRlcHQtGZuh zDm>ZA9W`nmX*>S_1{TG1RJ>3dg#4dZN9zm&E|M19edv^T2Cq%$#U$ycXFmL{dy86H z(sTsg!6VsG)VdCdYJdb=L;)SPT?QG&Qqn-$YN%tWgbRCZyS$Q=cO#cordo(sO>|~E zi)b3s_%N88DAlepw?pn)99I~s#iq|YfN1%#v{|iF7*n=#>2wbi_vDqe(SgteG}+*t zKr2JlIt+J+qQ!9dX`+ej$ly+3Ew_TuFoww29Y7$dils*;lLWBh*!wyTBH}1VyJ6VtrEpx)1@nX@GS`=uARIycGy` zLUsYwLQTEuWzf+{^HEroK8gg8=ARS+?L0E`FAY*T#^lFkBfG=j#Azpv&v9;`5~>G9gy||qI;o`mqu($3DBvllQ79W zjWchI@!-UGO}JG)8|smkhQ3m6+Yh2Um88I3Q0gZ(oz>1}T}?bKJ9j5!ZTMY{WZfig zv5{X`jsZ$99up?G2VJ)m5&X5>4-GAP(!%^;C4&!+E@c zoPT`*li_Vb>-heTnx_utwbT*3Y8)bbZ$EmK^MuvJeupgeNkY9&-h!GcdPtiaTk}SsZnYd)O%tdLu5e-0 zf2n5r7(XJoEDb&E~n6* zAZ5X8s9bf1VD^r|!|w{$INLN!;b*2HWI$SC4nGdjNw&>FKOo!N>1(rL2R5+BqmsN+!1FplT zP=d_pDqA~tvFxlM6w|UArbM+>JuGz)z9||U$F#M;^$2VlwMUL|W(M2~7cj=_#URYV zeOfy$FkZenZ-SBY*(0`YB)cXSgaQ%)?2)ntbXYb8Lycjw@?R`TBMpdA8Cz+bT75&7Y2lx1ugHWEOH(spg^rQ#HfvfgO6jo<9K&#YHM7>qQt6$b zYg!r$wb40ArjjRDsh&B*9zNgfa>m!XE~xJjD7pfJaE*`v*1AUqHKoov00yOWbX*-H zp={WNOmxg}(WUJU2c^`?TTVS*t!d)8w)#dx_%@nY-|BdiA0XQA1bghcn(8UA>bKKR zmpgj2+!5U<&fPN|9=Yd}gw-;ZwbD2-ttG_h2J3kK7m}X_mb$8O9F1;f#5F$kQk5MV zol3yt9@}Py`fjtZ%p$K9l(L*O06nL^`9f{dwr!UzaKX-9BYxfpiy+U5fa_`mWa_ne z#|+hBQJKTrGyvN$1QF&6&1JVu9$uft7EFo~jbBC%7esLy_^Ctk2c29SdwZ8eco+l! z0C*dJ=pVgQ=e6`}tk?aUB7%*Ka51)($M^^DT-9|qsaDNRn?EVVxvgvakqPef9*VF=`;a$GLDd6Pz!SP?1feg@N?id((QOtb4%#Z7eWB)n zJ(GlAn}#WT8)fF0#WN+xu?8MvpU$&b)4kUmKM|YD3+`C!AF+By z8c1_m>w}bOWy!Bt)dN)k@)w$u&mTh%nq?^|bRixWHg;X*Uh zH7s;cQTr?xbg%jDJ$=j9j1r#jbL4Vol(4L6(vSYk+y;(Hc4Bn$k{aj<1G@9yNl_Q@ z_7hNCJ(oF|dZSMrxgx(Zwb>S2Q=0X0f7tV6FIf5F`E{p^_R&y=ByTugVGcD+Z*0!8 z^qOuPtKv*TPD?J>o2kuZE?ZkM$qnryyi0|c+8YPGl?)|?rKZ;lNrJ~y6D5b_ipu7& zkmkVWhTq336x7pQ1hMRrqfx|IzBEH9MHOBZKpk#DFKb+To9?Q76*p_;W3mQ;m~4IN zaE-g<$ZY}0*BIqFh1+4FQS}l?v{HLQ<3zUwG9r<;v22G+iw)EbD?rh*>M!n3a8VNu zLI@IrYl1r_V$Jip$4jGdsFPw~2F#a1d)Y{}p|^0CYud=M(YfL1>!OX=o*OKb{F7%( zm*Tu(XN_?B&K)%M6?MXL-OMq$uRZ&OMmwFNCp8sJ;xJ4P7RJo7P(oyhgx*VB z`@q$75a5;geKTsEG%|o}=CGX1J|fCzk*QJ6^vZCx+23GzzByM6Ipd^`lr#=uB)pF$ z%lbj%Og52{Qo&j4HRYwD%r|6T4KBDm{{TNPm5;0B^szz4DpNdGNu23Ro^DCGJ=WKS zs|^nmVbxm(PTQXXY=4^eYl<`XJsXQ~w#CKOH4#w9Ihq#8FSzj@E2rV?KLe_!j#=>; z1x8Ry44G~2T#`+YnM-ORpNZ?}W1aIyQ%G9U;cMmX@m#N>T9b!yy=ENqluXAm$Gc6( z^;^Mw4H`x8&ul`#fhL%N2m^;MvWa(s^3S9~w^WEBBCG+jknWIJjGY2u$pG`99~2C* zIRVD$h+)?$lcHp@*z{+LkUl6NmG7!ssw9iGRS}{Fh!L-vt?a)<7E~JO-`Gf3ZMfy6isG`DrkVz`vJonz;akO}r#dxnm| z5av!o(yu#HJnyB#FiOe8X&YaNLrVEMZIIZy+btgo&rc+_LCe`4Dd3t=2mb(7Kg0h3 zbiV>AfBUPygZ}FuHTxbH`o>TGKit)0*c4biP`Wy^Ih2uO9$Q=9wA+=eU8_BPE@z(- z$kkbuhT;-om13qi<9mQ02^(;-YcU*8h$@>HYov9(n4)f)b+S~h*)!AQ^M27c()|s; z3}CpuHFvB?*=b^L&ug2j{m1#N9x$$^ti+k>y_y_7fqyL){{S1QlBa|pQy_Cpj0V>h)P*una%|?*7$(}$?%<;_~D_$=aZOjB+7L+ z-s`fck<=VC=&_08mQun1FS)(67S`ldHU&Nn}$4m$v;m+Pr%G`YHDV1A(;bn z7g%rIQ;6VLrG6z*QBM$bPmtw2&@b^Q+?3sfv*N|AoTm7sjcsNZh~f4%5WS#_-L4KF z5U@ekoiAiBX-StxHV zMn`g71=pXRKLwq-UUH;!^=GU-SJF9-tPFIvcV9x z=i-3NbtwyR({U@EHW7r*&BRvCWsia4^b`%1^-;+rq>P{i>a6jMsz=Mu!}8|Fo-cNv z^=4iNh{=d?4MdHPmf2x&Yj3bzFQhn}cvhL`XsVpTE}ouyvPJ%5*I`(&&I{2;{GMWk z>>o5CilG8V!v>QkD@x`tQ#YhOt`u=5SvV|f?&DXWo=Y3xb;O@19aVs5>qkOzyNrt6)d#&LW;YNv_Z!6U=m*qvZ++woyF-{T0 zMQj~BXG^2luy^S!0glYV2KdZ)K~Ocq58i{g$Ry0;Ok#_4eQYMmV=Q|x>~>nk`f1;r?3 zq?#7ST;F15Fb6dES>&uzTStQ<$pqQrmE1o*8^o`{>JD6;=jSXj{3~GyqYAI?eux3}YFD;WcrO~;b1O+Y>rlrTLep2;OPfl1Z z5R|-==57 z8suzs)tQh`!<2)&AY;s?4~jx06V|~pSz}$In@R~KaGk}sREcCq7P;E zRhZPYT4dvM*`PWrT1ng}2A5|o=?a)sV9aYk=7(LUsL^=~b42pfp$AaCZ71QL7PWol znJs=`q`J-_p#uPqB*2N?cKi=pJw-g!a^3Jm0cg-#=f~c%$i5TK@kps@TYi*i_@5Y_ zzP5VulXDx-AL@TJu`Jh#h8T+By_Ph9FdDOmTI~dnr<$^%cPC$nk_Rl1C`%q zo(m~QZ^VxaRi#>KeWx4d`Tb674dNQi8W++)*qqGg=QmP^@m4Ng=QgOY6Qf!Mhk#eH zH*D>C3^SX_E^OWK-ak=_%=|vQbHl+oi33i@qUW*N2I1JWurS@fI04Weiek3Pk%pQJ zPpg(F^wpRMTAr}dKn~``7eEF2AutQ0$p9KAngB^L2>2+!=9J$+RuD?Sq9m$h*#LUi z0g^armj=bg$D+0TO-|f&;VyzfCBWufTUF=EH#T&q@S~v=t|LMgHGCOqfR;>*2Kj;6 zaA%#tqpwV>B4(m`YmD(Yk-53w#bK{D?E8#4H^RW~fSIu{qVP*}-6l%x$j0k&Snxb8 zq9Eou#f&6qvoJU*-BA>E@$BkgZP5lp^$*Pae?DAK%H!Y3sE~34Y=1hq7 zqm#kcSm+WmsNNYgZ@0{FE7R(!=EVgpG_dEAPy;|0DLEw5W*MO88bpr-OHD8K$>XPS zAT_Tf5_|&7z+!_9;#sh~Ot?0pJg~j&xW4zYjr-kD{GTqF+fC|HV3Siewwd9KEH921 zG?U_2o;!lEN|^J(Rfoto4HFx>-37v!PBD8?Sux60BRG0SZ6t8PJtP6~L}76m?mBl- zZ=~u#Aq|3hm`3ed-B`kp;$R%jGBIisY*)Gl=(5YkWLp!F9#{tnco$B4#g&vQ@;s;qDVws z&scD1w^FiM@bp{7KFDLWc$NVJr>dZ*bvxn_x=7`DXd`8y#CYzWdU>Jb(~fmMQ~1}5 zf4b_1B|c7hRcIxT9DGc?})L+*KPDhstLx2r8j*a>sA_Y=`|UXP`HUJ&O< z=KBvV$PM!Lw}OQ4gQ`lAN2Gooj}gFRlaeLKZX1}5R~hJ@p0=M9baOn#9HfR`?wDR-^j18;3zDgpwULVha)EYXzM=A+O@g*Pk~HBW;8gFT z1V|p`Hjol1k#+V`Y4Jc7W(dxLNoMS%*OE{K^Jt`AEuv(!K)Xc}@KXeUJECNuW9hEQ zu(Cq{+iI<7(LgqGoMAgmkiZVVIIl6ssW}y##*HKw_peh+#8KmGIg>Tac?D##;lXyE zc`p>TKo6n{Kq41yz)+UZ)=gvcfVge;2k%jrlt@SS}o0iF2Jbc=Z zvTFCw<&rJII7!qKwzm;uw$+ybCt^- zad#%grJve0bIl%-q+pGc-D-N{2_OM909_g&1ELcE(HB4k(R~#NHBf)eP4oq1sGY8` zD2Xb$Hb5T1=-No-;f_=6Z1bU8%KAzsLgxsg=ILh;GcPYKtc>fA%7`$(D7h-S=-V{} zk;+b2ZdX2`FD=p0r{^L}*3;9NDa34$$j~$=Qm~#qv!e21?H^;Ks%a4@OztK&LJ2ww zpeEpG9MPIcDlQ4=wb)%$PNTSMOrk9B* zy-XJ|jxJc3Bo;j&xO?!_n31QQ?OTEAU0As`){J?2z1r_Wc zHe^%l49)Pm$3Tl>eTBQ7*Pzm3^|Y{3)KX4aED-|74c2q?B5sprW*x=xe5!FAY{@&? zKPKmHd=Ev0{wRvKir~cD*ER^H2PxF$zuTgi%2LV79NO5LXKRXaZUc(MB8P}wOl@W~ zQM*qA-8?^t@JcK^LUC5rJ%;(Y4q+Zz*)Jtspz|G3D(o4>`at3b!9zX~khoYlxSpMtmB9r?eL-bBFPc|u z#y2ru?vgGzt+UJ3yPYFrNq}Q?Rgl!b3an^~K=Mo`+7;|HRju}F;DMmMpq%had;6PZ zc~YGV6~nWV^n@KX91`Slk&@>2B70f~fpA$oH^ej*pbT#ieAZxWZ4E6g?g<*HaJZXB zX-ktw#)h6+YPVtUb7O9-AnLeEZZG0!VjQe)v5k*F1jj{tBTCVpJe#E|_bIC4jC!_A z^;oS;&b6-h-$g18EXVM+Ig~i1L=ADd41Ix9<-*&eZSJX;)p5@g)>D`&F`B2wcWHL& zG#qQhSY!qoOhxgr$1wq*>)&MdMyi8zGsmRr=lpBY%JK_{euujPo z(hwmbo_5^FU`kUNVr}vjz@pjhyonmwt0Z|NWlK%Yx}`>uW$}@@SF!07*=t;_X-F(( z*D04IQVhyjwLr@Qw9x>d2{p=jh0#JJAG%I@U`H}n)Pw>i4XjC0&D8?{>x~rKw}Oym zjZmqmj+zHb_gSqMH|UPYXPwtpyAZFEpx(sUqp(^|B8NQ)lju2mSomokK3|I2%IsB* zc?EVHCPQ7AT}Twf33;FZtKh7tr)!xM@6-^RLxBM@W|#kYj%_u zIH@hIaBN2uV<7%|@2WoKt)8N4hZoHwp6?Qpid`JobeM3;;TmKkkY7P(X^Rvw zM{E@!O*261ga8fDm{E-#}2Kb%{bMx9SiFr+PPi@QGRu z&;E;Ph+qb{8EzzNWNByvmIj8!a8HbloiX`M`)bG^lWNDr8fz#diour_xahbt!l2pH zhnb{l;HRFR7Uh+)Wfmambwr}>CvOGiMpk`(k=tcMTeklIV`BOT;aL4VLG6$U z8bij)d}#w67CRHkA@up|JeA{)$ye0p)MfLrsr$kH=US$nu%vacuN3mqrT%od9g!)ma6ZU`%5$!P|o zwbM>F!JKi*DOqfpA;#n~#OazgXcr}AR8vJ&MQABp*-y>x zja;tb%yy@Ts~=NO3=+BQEG?Otv^_^KS%^i-yH`nxy+)hpB<4w!c$n9c?)R?y|Y~g**@$GXr6w=8WQjpQ0lxw zm5s>IK+mRcCCT7eh{ACQSlD@0jCo*vw_etoSC40^Jnd9#DroO0@kuJ7lBt4cNf|kq z=NBm8IEAKyBOOjBbrWq39Yj=9Q`W-kmYvs|%N|;Xx9GTq(Jp#9^r^y9i!O$j zoGc|^dD*2*;j`w-KyQgUD6_hV3JD#2kBz`~cIvrk<%zkR2Z+9*7CItnr2_o2M}m{+ z=VWEZI|06;UA(CD_=udBhKp6+L|?**ry4W5kphGpRDxyTwaCWhEkc z^h`M-td?z5f(K-Qw&gJ71%Ps!O~^o5JLr~ePy}y&s#n;cV-EJM(MTP|(7_CEeu`n; z2FgkaDjN)C&Ts$=l2;LrnVo;PHvHVac z%rf2Gg5k+4jWeqk5=|WS9Xq3r^)-dkINT0{!8wJWZOZZ^-v`y`Z`7{V^GU<1(Vm(H z?4;*=AXsVDO^ejuVK6~a!&s1#Cd&Bi_O-&|>t91Gb=5G(meM)HF!`-$3)a)L;-Y-M zzB7(YnG*G{>XEtP7t+zP3q6jQxwmUg%RomEhE!HmHOwKC(4DtBB5iN4Y78)++0>=# zrT!0{d&yzFt-8TxAB$B~HVEn-`CfEr2IV_UNWT%lYjCJxb!--M+H7rd zJ;i9~=-m8j7~C9LE5lq`HL+U7+-$+Rn@6zlttB2EBdF=#@*=Sg)DTxyT~KP_Z9AL= zjmxd7@p8@YX`ZAKq|8A@7#_@{-qlTA1eI`dBbqpeR|e`=Ja(saXyn>Av_-VoPpg1k z%8ZX>Z=xrIJ1%7dtiqU4?Jb?fORy&i6>iu}(>qvK2ho;QebXd-0I8Jc2yTo1S- z1BMAPKa_BJO;sUz4zSkeebdW{s4>GO$sJBPPF;o18{3(Z#z%-YUU|Y;%&nz%6CDjR zvdUY{bAK4!d#J^Y#~V!2Q(Mv4aTXCp9vmlp$C?{p9Cu$nV$m2edD~-bGHcSz9G+uZ@j57Mtc9m2Ni6vSzuD1DO#ydW!>eorp3>d2fu3bs!6$ zdQ>|Uc!MeAdz#Yl;nZ5nscTsz?|WEU9@`Og_KM6WuYB|;hNkBToEId& z6y1D>TZA@4gi^iW>lpq^Im~lu-fhuwW@*KpICz?DTr*$#Gc& zTKRBnJ>CBR7h)E}7;7cOsHH6dvr`aSHX19MYlfGhjyDMt7Ltyl7hC~jbrbnTE|{N3 zRW)3Ur)eRsu{W??ZXaKQP;lF+F^nvViyy`q!?PZq*G0{*#gi-|u|88HK_we<=+$Ao zdy8SxNfk84SY#PPByMs$S!c(Haie)v?4Ef8XsG?C;NPD$g%Ti;~E%*lHfMuQ#3 zv0PIS6;;zY<&M@e+@XidqN<&hn5A1ELrUo-VZGYjmaWDy+}{T`$2^pmCgwMB4hbXY zs5LM)P0oaTRzw_USsW5Z^UZsW#E<#I%FCI<7G=IXol1Ez%WRI54&p3f@OsI?dGWM% zEhorzTHIrfC6qomX+2Uy+1GPVkqaDn^!Xc%9kgpX<%&*AvqdKs@jR|&J{wg~WIL8L zU+fD}jnQI^Z!)@;Oz%0LGl$M6#dX8V#?&sGIj81iPF2CKj}HPC&n&IVe?)G(9uvp0 zEGn94+k;P2PbRG~IIy?K*UA)K8+Kdtw%40uTo)(^V^9TdW%7-E=Ezp2q16LsGSTRZ?lN3G(Ra zEWz(eZlY_aTe8J{QNvYL)KW~yqkfUGp!lAu*m3C?Eh{hL7aGe>ne?;K$0Ssj49$le zq>{nT%6wL1F)CMWGkz;oD6`IVN=*3|gY-0O*ncI$a0=LIf-5nqvec0=fZALrDVyi=aVt^+3gR@2Ug|UsS+%LI4R# zF$MfmCqkc~6s2{MK%o7Q2Y2Yf$DLB&@`ZP#Iel9Z!>TQOr08~BlKfM3@*O7z;?Bo# zn@JFiz;bAGD=!%08az5fK=w<_aK3AMD~foVlY%#dl2&sRd^trk&M8v&OxGOkV~xXa z;IikbfzAQ2oQ=?XE6m8`=g*<`T0Aj|vZu>RNXr2p zayXL^j0*u!?LF&Sy;%IGfiBr7&7^7URh2^1IZ-~POg4BvE7Q12qKDLePN)22<}&#l z-GNzU`A6z>I8|*04i!8y)5hu9CSY=s_xO1&)+2$`V_Z1u>FHvLf+F_D$6VNJa;8bC zS{6OB#(BRDI-Z}1pmj(FCcfJ2y=xeml2S(KWhJ84V;7g=%1^ZK2X0k8_T%A`F$26efkk*f8 zf0|r0E(?o7mN-iY^-~IrsWn|Yn-F0 zlNOI`u&>DS{6dC0EOpf2+5s-2`daJOV|;DF<83D`khU@HfC($B#N4F}Z_%879HZt= zKFs8}zYK!21oh(9iK9P0+pceeX2a<*K`newmOERP!}hMN1m!(7dl~BE7;2Z)^(q`9 zpAy5~A)twoJd(*I12|qu=_?NPD%J+;kd~bnMj1-lW_hC(D9hC1qhWEeHttHy7<`Xr zdUv*A;0Qkjvvge(h*aV8nHpov-5eay;jeI2b*-7%3!NEj&Dt0&Kafj{u;z+t`L7|; zM;k0M`v3r}!tlv4I)f8mlEV3jx^2?2QIA(nX=)z{aCSp$j;pBjS%`Xv9#InOHfzLu zZW5O<8tnQ}C7ED^2<0*ocTATJG%GF{wJ^VNWM*j^6sd;dOxw+K+*B61ZBGS2tgS2(DQBBkRq!ysn)rFK>4!8yx=>vLp=7%jkX$Zi@j3S#dw zZ{(pa6;%s)fvuK5ryH{cqsG@MoYB=QN^xra$j5LuA#~VnBb!SbpVVLCjW^89d|{=6 zE9z*t(!N|hkush`8{Bmva`15;EFEuIBzZ4Z5Q~ko<i$sF42h zrH%`vFE($~S)Tn%iSt%5rpmY)c3j5lvSMn^Mu^LaqfV7vBOo*72- z;bfO&OQVB%yU&_Mb)IltFi-^Ab@Nt(qGB2n0V3!au7n6ky^sNYbV~jx7+nYeLIfg& z3@(eH0SExP5CMG=bO4cbz%GOUT|1xzi=aS@paS}*bN~XC)c_#vx?(9ME~_GhR9t{N zUq(*<0O~Wg{{Wz~Jt)U|YFB}B97c zQ*pL}$@GYQn;Z)kZWD-e<%r6{($WF3)n~*o973R&sPPxczq00%7jBLw*BcY|CY}sLu9MXY*v1yY$UmV$%pF@@o z(~jQwzi`l%?2QqrrVzW>B##wc zLCpL23h~QyOy?c4DzfbT91|B123zv1AuPWm7pdIWVH2{c(nai&tCpu2G)9f}vI=`x z#@CQ6rWx_cBhB)45N~VYns;DBnVp0WdfU-e;}kK7;hYwMpmw%TFO!-(a&jzdQ;J)$ zm>886ZY?)9vR-4nPRgnrIRY5;CmO|GKq04+R%xSO|2;mMQ+{~jgd9i8p zH0S6nd39J*Q0UUX4XK&#e2u%EsU(H5!)mcyIh>|sl0Ts)pjNBx;!J!t{mXv2a?M9NwJ1D8g@njyMQEz zEh^7NGM`?&l0DHhhxECyL#rcnqCg~TkIvK|f+#FzXlmcdxr{m5J;YdfuCj>3LrQa0 z(@0p|3}6C$)^kp%sGOE7T-y4Q@e?Dqp2*!-gjJkB8KkTR+?ksF`xUBOQ9h^ERMR%1 zmMNNak%7bB6_;5RV1xn8>@U?Ls=*xsDmep=$)`eSkc^FqT#h#BD)IM6AGGM1m7sMc zVV(RmdR`SDzis*^V^vKC1s^IlvU=h@r&{7a0phvJ&s?Z^mZp4#{mshdivARPc{0W; z&Yy3uqA-hG*IAS1^A=f{eSJk8Lru#a+oP39IC4%b@YE*;Z!4+bse&5%W`+qI8zTqk zxo_fTNZQl-AO(1mw%4h_;xm3!AuiKv`W|)1Rbp&au!DYBT>UmsZcUe|50xGR>>gX3 zn}uE4cRHf0eoF686K2~wgaK7nh3$D*R zGX!TH&;-N5Q)|KivpDXI!2%4xrI>U;24I$9bO2@uxIhZwD})SpO8TGz=)Z~pE{mW7 z{wTOW1=6|zE)%#wg6SY*xJv1WE{mXHbX@`qqUZoc&@crTK!H6IKnWd{Fexc4h6P0I zfI9yGMy>-FkauBQ?@00H!|B`xqD9F+#YXY@O1%@f2{8!3u(&I_jpiOAiR}XJU<(Yt zipr15Wbx>`DBa*f<1!qf+F5yerT5JHKM8;MaQ%OB=x8dce z=)+U^Pm40~GhXZ%HdoWsLf7jwZo9AIv{4+gvE7^s(>Cqu%=DW)wxNB#{SH&oj3zE1 zl3>u@Y-~3jMVCE?vpqW+(Wze6NB2=aE(`BuQ(YNpAd6XRovoI*AHrvuQzBP!!Th{A zti*2DOZo!qSu0dvb<*Q?Ra(lz*B*mqjZ<6d^ix$zNfEa-ue!=Op&p59Oyx(UD}Ukc z{x|z82c$;z!{7XG_E+UZowD~Vi_#Pib)=(_fd2sIU-~CMqzYJ{mKs@P7Wvu0{)l+c zY?rwM=@JvY6yL-ja&7d9Piq<(NAHBZxQ5w#k{*$%#?(`NV3WNf)4NkcHT&iXd2vH= zkT=pDDE|NlIQ{0SA4rsk{3S=lsrgY2vL*ijXw-JGq^SP@l`-iWi}p%^^?vj`abi)D zGw}W`hZGLv)66Zfc3oELI*9{8EG<2al)Ou!t;R#Rg}7ZiswZuuW~W8VaX+N*maz9w zVf3ar9xV4t1<#V_j+Jjx_B2y6vc$R-k*Z{z*67`HokBY%(3puEHKoU3xf(f)O~kQtxN&S`_gvYZ zN2}rSwLh&d>di|bEr8Zb?Mo>+Z_~vTm;|_i=F04m_qOXSe+*6anK$Ns%Y$Uz7Q;hq z=Do+hWv0h)CSmZlcwSu4;4F6u%2ue)O!nnW%fMsi(#**aJ%KqQq)y3!68+mtbZq03x8#kU-GF8HopG=n@R9bat*AoVOWi? z>XO^Z4#RNHbvrA#Gvm71y__%k@fL zrCLpeG`If%Nk-%p#AvG;F6wDViD^;wYOVPaeNu}-ObsnpSS0$TBMm^m;$-@z7KKa% z6%N`75AKw?Q}v1e092yTQxT4Peo6lTRR%eS_fNn2N-QXe5`70>$uQYS{G0v-Fap^? z)s7zIuJQUV_#gw@`X}I(-_#$105dyc55X+QvH*FHiZeU0Knpn?!dcCb0n8}o6aX0t zIfwygDf0>dIfWd;fDU20IfVcm!k0AnPQZN9dZ5UAqS%P%x-;mI!!kw41F7_PCSD)| zyGrUkBL^H7H#CuHMxk=#UwVzB@|fO>2^Cd9+uCT=cT${~^&_otbX@s=6w*H_vE!C! zprd#P=MnZT9CVbJH7jas;}cZ8UN{cGcPocFUUQS_A4{N|o{kvd+iiYI`DPs0#xGhA zB*@5y&5PM+Sbr#DRMKYWOvBtfu6_BX@h*>2IP`e_>D8MEG1#ke!HYu4nsnxYX&LY8 zqlw{0ha}L+&Zd`G`MKANC18i-}eK)7p;mtBV|I+|>9tCMMIKyyaFjQPqg zr^I-wc45@HU=!#=pQpV~PQb%|qWbq$`n@JAhgDcRF^)raxx2KBTrM_hcfm8_f6ilz zPncRKW9)%#lCmfTWF}(mCG4<|D)py<75R54Kq(7w!U$!=m{3emNU=R5)L6Wmq^|IB8aXqBjK-kA*po z1J?lY$^Pnt!w1&P2CK>0cTGydk8$)_K|UM8zOO6uuu4pBwY2N@uGM5%keH5SZkp#e zBWt->2W3EF;xIB*q$({3GL)-A!{nMBHm;L_Yq=mJt zrf=v5+1};7$Qsbri!=)Wv%V3Ie}^QO)rT#|P!~Ol)x$k|A#e|doac}a(sxFntvp<( zYtXX~<oqwdJBPZT@A`1;re?*V?N6mGHcRJrw5Hl%s)sy3Cq2tK5X`lG-{GN<-Q3VTPr61`A_N@(QB zO@T)~T%`KEns_C&d(fL&;&g&TZv|khaJTkpehAvfy%Y7o-QsxnCM%c)@;l&-j5ne~ z5sRx_Ji?dCFpiG*1h%l=h`SPtA@*E6!p1f67ZH`;1opBeBtKq=ADZui4P;sX4x;;P zg%(Yrdi)o)kJVEZklSPRQ`*V2F<(z^x_^~e+UjAi{{YG_`l$8FD_-r_{HauhXa#Kz$jsZ1@}SpH1M{P~{{Si~L*PAbfx~#u?xZSk z{f8^+r|}DbH5hi6F#Eze*m<|)egPj4_!FkXZ*#~V;DG!w->5#|kBC%*HW{_4{sA-c zY1Fn4eUWksLgnFS`?`OCOhtwrL~wnHT!N5{I9brgPxc^FVL*E@)7#vh=^;`Vs$nPl zihqDnICc)JE36kUSgkNMPe{{R4<`Fo3M7{PE+wHzM>Ka#MW z{aPN$Z=bmKq8}n)#8}cpVwcD`C9MfwEy$Xif`0fO4zqX)n#e1n4$FGUHrf;`E4 zhSWsx{tD~fiW7eXhYnzkHxw}g;Lz1p*u zM&9O9jc`i?;aW@vrSnrt_h%L%!DdzhF-kX%j#)Xm&e=&)I($l)R>MJ<2L{QQ`-0+( zPsX@{cT8^_Zri{tbrHEG@BaX!-0;ksFv@?*pZ8+K@ys@o^z45z%+oAmLGN7jn2kMD znwQ2-*@la*F{Q!hHrw4N$v3gS$<41v1ADHAhbld8IdHk|=;moH{{Sn8Wx+ToX8JSV z!U6wZy?w2-1nJC(=uJyps~4XmK;l(M2>`bL6DxO#$n44jhN_=V^E zI}K!$Ncw7dW@|^c%V{}Bb8d_1BOUsB`X2|K?bS>3BaC9RRLjXF!zZ$D{Fc5(5ZjT} zZKucO!j62i-76r*7rZyQTkd}?jcC{l0d=vE*;HCZ&DTYSS7DU6S*WSR9Z6Rt%@DpY zOQHbk>m&zjO^fjK6Vg}WIMx?ah-WMnbj---#zz3jKsLYkfOTFyj|fgZT40f~L}qbg zm~ot0DCs z8Haw2$KK4tM^Qr|s>1-OrFOYS>`ZY0XtSv04jYA=ZajJldS$75lc{>IsgF^JV;GxE zG*r%sr*{qQ=h16l6ELRxL{CyR02{T3x$afP(%j092;=*#Zrknq7}OPC{Z2V5OPtrb z&+b>DGlsCK8YJZxhYQ@DR`?{^XPe^KlqVLwzhb9weiHNfCyys6{s-X%*~V7sZ=JH< z+2fTvf^f@OwRKJMktXmi8r^+zsNekIJ?ua@9}fb;wyWy|ego0lOhTx8!h3*naK$|> zHdkVmA~x*O0o`jR;rN4#VHIv?azMY@mhsbNY~klJ`9)CokG(&UCI0~Ze`nr@Fs1_l zbMMWXv7`+w%lo%bJUcdDW}>N}ow;{Qcu_;RW}x2HwOXIlCHOf~8zMlt7cckT3(Nb|DE% zatdUffIo3x1&;S9ZUXy|E1~$KaupUw!&MTzTCxM-nPxMBF(shToqHRzV)6vS{Uth&ts{)-x;2&{mMHaCw4 z1k*uGq12VP)(}saOj6bjc?0VOUkJGwWp%QcH0%gjG_)0y!^{nhy4ud&Cqv|)$u(lv zfZ5)7f%-iuQqeYRmYs z$fP@FLvZ{`NWtV(0Cl*yKg}N|;v-I7NVlKfl_2CkPOJ?Cwy1dA{pvoaR2yRIp{|C= zsD~wdiovt0o#1X0#G)CiV|HdsQFGvy)E3O>@e1bL zIL_@lg=HkO)K#(aI#+V_0pmpu3368_aOuFqsU&&JcYu5^b?5cou}1t(nj+>(P4wK0 zu6(cKgFTG`y*@?s{h7yrNhIp52Vs?VoJ@^AAn0)xQaqB$esB3o?08%QjlsT-*2{-f zt0WIw!|&|bpVRymV$Na3stXUZ%r;oJUTmMl&vz1jTyNP3ol@vj8lvtegVuO9k?tRh zYn<1+=mXcud5i-V;rS$FamZtkTRd8a4=}!qOt)c2!Rub#RQ~`oTP#&IQ@TS7+jDYD zY^5uj_CG1OX&SDyaI#o)p^l(SGjlbaJXUpebX6~DiXh7k`jl59C5BHCt88*d%yfh? zgT(B*TF*vYM_pGjikhC8ky)dgPyl%&W~X$z%8ksKe^yq^pV1O%sb=$8OTCG}&pURtBk&a&DLqExL=NQp8k6mQ0vkbacX{^s)$u zzG6BgBO4=wb7eN}mQ${qBmlQ~6f|vP<*Jh_kIe}QnQxLj$04(OomOiqu=;E@&n1qP zqWOWjJ{BoAz#T6Ga5Omf5hf?{lj-D@ko?RqV!jWkIQ1SJ;$q90_B!hbX^tI z`YgcXVJ^hyAG+0tAES;OFR;buY4Ko7&6l8UHZAm7gb0j-7?U6uzDDS@6X5(S1!<-j zy=1vfjFHoV(e|oSZ=q5*G=7fooJE1unCqnLV-N@6P1Zs5PZjjVpZqWPRGVUa6jb^u zouc%(voHSun;9R-u5*aE4;R3&$KqzTo_;|NA%NcA^-+~cCN)g;{06vUI29FO*P9Dn z_AL8^+T>S3bR-8czNj0lQM)CVssk;y>82)*e~O`=VFOvPBX#Erf}@P99>UnDT*7}yKTxpFI37!+r>k?0=Q`bG_eYrgJ|Dx8 z5*W=sU(G8%Kkk^usmAp6qvQ9b=C5IWf?ku4^(5SSkZ%QOKK}r{NO1)SACs;h6#Udb z^z5FJU(}1SoGI@QmMX_(Hy6!s=Cel@;luPQ2mCUV%@zy8bNxT5A;q{|_2&=xWk_*$ z71h%J01Ttehq!;H>OH@@AwL*l!aUG9?r{AQ!>ZS}jvf!@E+frD&j_01@Qm{2O(I{1 zCjS7KJL0iwxay!b@KBrhq_e}+(fBrR=0<)O#niPFVfF6hO>zrztiQrM9RC1}X!jL< zWIfJVcy^{Pwvrq{gOBly_!NA*gPnbGiSLiO4@0T`Mlp`zHqx>`ih;&((tVofzABbT z`;hc{kK|;AF^Ajp*oprDQkTl~2WrTEuupt_#r90=_#gBEiqH}H)L&R5OGNYbhwtP^e_Mo;H3pWX>t_YWf~?l1ZiZEYrlpAr4wrrMfA{q#Rr zBFT8C{H&8|X%F|YA9zv|QW0?^Ul6Lv^5yIz`fbFK`-oXBuPk86Gjju_+=~;0y@{M! za(;Y7_>{g>oc@$5w=+L4=pQN0(Pr{d@})@{a#GyG%QO{uO-><-$5}mdL`-$eKxkIz z2*!gd8!0LmuzEE-tUVg0#u;NK)^EW^N$4YZYo&}Hpn-H)B-`N<=i!8YS)Tx^p1TQw zh0c+rcrxjxhq8Xg4K6)}R^lRxnT{{9H*|IyEuoGtpwWK>-vqSKWQyODX5EwwP@nGG z^+KBf^OSj}-FD zY!$Pmil!$-!X;_A=;c0hewg^Kg(dECI7vQG#LXJ#9~5&MO~d}xTbH>zU$FUu>7$rF zm_@H}r|dlA>7zLMNVF>Y16^kB5uZ)M6AB(-;ERtBf_w{*HX~3Exh#w$RwI5!rMKYb4P;nGriedKO^ci5c2e~%GVa}@_ZbnW)>YQi%%v?>l?*~xU5f;>Kg4@ z50inq+a9Jh@pfmQ!}6*9+O+y)nS?df(a7k_rDlz7wkx%wft)*szN({2xtnO?>d<cz6Y2I*iIdl9?my1WcFH?7tsdRNGO3FSEOOw zFNpCT4NWCQCPumg%de%J5CHud8m2@`+~#q97OrSzO%pkk!G*7prtN8N;I4LNkOOvg zu6$AwD}}k_1E*E65@sQn_C}YoLVbYY)+ zu*ClWolN&d@GGxc%eGv*3k0l@6msF#-B`VQS`z6K96gCx=@-^i{Eu8*TGp}78`;_+f@$=}@-87^>@$C)nZhvu zbcKiak;kwn^%25x3;u53h)_6Hc)1j#;x$U+>{4&st@veV1N$Jixl82A*9)2(f!G98 zeTv^T{6Z8`6Ms}0yn>j*rQg&~)k|z2p#0pc_=Iq>0lf|H{#9AR`WhWO18m0V1n}=cLy23o|qF*LqEWA0M%eBP-K&v-N@lfW>_(=C_NK1_*Wj*Dr^(m)T$Q4S2MD zaiAC}vHsgI{p5U)C9W%_`rq!C+2{Pl{{X6D^~8YT6JJXCt=Cicpy5-G{6=35Pu`c= z@_#V@0MxYpxRDQ&>e_$r2?y^Vy+!yrd#-9TIPOYMO_BE_Sa_zM4&#o6@+mGLN;E;h zOSCfS-&H?lpSh~>j9#%TgNL0@H#(YbT%Sp|1!a1!E&l-AA8z@IuO~9;n7wv}Zs!g| z)$xXnF$bUCDVQ;#{ypCmd3pWC_KyyKz!`DCZR^hXr)+e8jyE1DY}5yt;g9%0Nslys zd4GbgdK^Nn!SxPn-?hn8Jb9$4n&H%Vei?4Ysfc0KjnGSPC#ULFqV}D}-{(-UWjcS< zG_U^v@C_w@NnU)OrrLZ#pk7He>zxZ0Popjr#~U6uy&9^~y5hqiN^*?hrFq&n>^owk`|19Mt)z z?1%D>F|ewi>lA#Wj13K3|^k5yYd% zM$}`bp@Ep411hchcFO+%ii2N)W-s+h#*wIKaM5kP66R}@nPAO}j5Aw(pP_HdbnrVh z4F*X+TPe)tuc)e;vSTen-uL$^XOnWJ(Rj4pTi-Ww{{X!ayKF^`(hdNR>asd0oS6&0 zii~l!*Ez+As{4`_ZjmDWCORgMhdSq_*~4nO6`T7ROQ%@etj z91Ft){{Yb1Cq2LZ_gjRqr)GGINCG%>lqVM9!G&Sol7 z9PEy`zJ%$b*jH0wP|`;F3`U}Ak_R#=Ajkpma3vg))3XG}AvbXvc%=PWM}nk^Nb7wR zVkVD>j9G(QD|Nqz{{So4ID(TsFFrkV)o|riGS3^wC43d{4kq0eEj33qh8Gie1*+(l zU{_N~lQ;y0zrkMCQZg7E!8?W69BrG(r~D*7&Z7*asK+D}!bquV28EMcWbP>b)!0OCf6r&K?{dniVI+$O2MbuNmoXdc<9!}E z^myhyg}h`8oJz)DVCCN}rP*(>AxS4Ce9rI+VbTXjG7!CK{!cIwr#^c2dw;hU`2(q_{gMcBN$4xtDH?ZjB3~ zF3_aej_D_t&md>0XMan)8Ea!Rr+c~Dv4kwz9JhlbB^KJDGhdAG^XKH6N_LXg*w}`xEBIEt8=7nX0L51OCu=J_Jzmpq zRzJr;!0`rZ+A`z+0IrR#0$)=GtA_Z7rgI=Kj18K}cKcO7f_z2#*@ygZ_fp{f&gf>w zl~8^P@gqfqvunTOl!p#+LruiB7a#edEv&yg5AuoF4-~ZDol$@NL<|#(MuJL@t_pit zyzCys7(W#a<)|OL5HQXw4nK{lC%Li|SUk8rhA6na9UnfP0Uoc#<6QG!{Q#n*8}$CD zVxNnE(n)>skGVjOF>8Rdgm>ixszTkTeuVGEpxm|p0Kp2kOT|rgu)N#<03;_;6LHFXDp`u3BMJq zV{;s%#Kcy90a06uR7@qy%VTtNT1Coq#H9UA?L{LX!{n$1+}6fO8{De!=%uKjifVva zE7}X%HU&&=*j>7#Bh-NXCwV36m$=ODJF23s57Yq~y~Q6Vi~RR|QONii!@>PGBrxk= z@s9FKkl<~@u0DBE7+ufh55YPWqC*I|zd7FpV8Ji1Jp2(tZ4&tj`dX*{RL%n-)KoX> zr$p~w#7+k=I!p-iOXNv^Xl=YgHnDk;_79O~x8>Mvwu-0Vl?30K%lp!%i&Kj+=ffwA zUD7byuvkw9J>B*xUbD68}xSE(_JP>l@Z;8eydKsTMg0+!#P?XOZp7ob1iH{PGh zbO&B^YIc1JQ?KG7E-Qw_-15{!(cG$k2(Ulf;P|Ron_NUZ*ZjgfLxK(Q3Ss-oapQPA z&JB*fndIrCf=?g3L1vfpnL@IJZ!BLBve`8YA!N@mdef*xd~U z6twY1!z_*2mRe2|Wjy%%rL3nWDOg2~$j#kviEUPS8kz3M?b(mSm_0o84yG`G$q56& zc|7nsnyOeIylyRB*yP)z2(B!PURZX!dW05&3!^75QQ35Mi@QV)$|FYu*r^qsvBg4` zqYJ^SYGQ1HdH~Uw4ZsVB;PPVP!3$y*dt5T*zMHIl46XEb^U~7#$q#{+zP4b!mqAIu zl$G@H)xwfyPn8j%@>Tg)(#M27_7kpql>{{**gynOE zDI*6ofw`eV~2Kz2s(#1C~i-hgkq6WXsZ;9E~ zpC=VzWWm*LXv;yqfh$|hAz2ftNiaH3N3;)r5E{(aij}4BU^a7yV7-$D$D`s(8O1d_ zn-p#_ks%iadD*3T8ulw~D?HPVu&HV0GGsOfb;-u#6p%_jYlV%wt)udpGozrDkw1|< zZFEv=sJhAg7cZ1ghP|>xPOwOH{nODFlCob6^j-?XXQp;~(IZ;H?6y|s{{We7_=gm2 zLq-`%c^@&e+yT*Mryfctfl*Z9OfeQU;o6^TWwnv+c^3h;*HWQL#tpFToCX<)Ll33d zqixyiFl8g5A$nTfJ<(G4MaeYjf)GK+HN-biLX%z!#iMk|gV`|R6qHS~5PX!~Ey5U4 z20qTZyZPLM%VuU~wCU{GMreePUfP1R ztAim*q=-}8w6N|KytON$-(kx}L<+=Mq076FI~iq_PhyhOG2IY5gU|2gSx-=kUghI)Py4f)Z#4Tb1i>agEBX8uAVA3FH<9V@QBej!lHz-;*v<|>o zUhhUm+T{x)kIrJ2R*g3ICAZkU-s9zqX{s9#-?~i>(LPs#1S&^k7kM z9*K&{2<)v5*&#Ma4`?abQ3D|&E|`cqFn3iJ9mA8qaZ!U<*)otoD@Rb>O~sN)K8;kG z?AOY`Rux2E$e^Pc&k8mHiQER6kP;G~UW=W=(IZ>kv7y zIA+b1^%hX%M(w5h6Qn(W+AqP#yt1meJr_HZ0t@WO;H?m1=eeJ*UfWF zf?}1Od)Yiu#wtKFgLk+(_g!5)rn)YD6>cV51%sIjSS))1aesozo_refB$3PUcP9{z zb4q5;S?#u1n#W)FRu=|hA_$>$ZVi$Xs5)w&@Pcc4e^a@r!Z8Z!X)#91=f_hSE@O_P zvh(jsP#NiPTEf#AFc{07)Qf+**AwLKOX4^f>DpaqO3QhvkpMQx<8Kw!!tB

|#n_ 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 literal 0 HcmV?d00001 diff --git a/lib/lib_display/XPT2046_Touchscreen/docs/issue_template.md b/lib/lib_display/XPT2046_Touchscreen/docs/issue_template.md new file mode 100755 index 000000000..06109925c --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/docs/issue_template.md @@ -0,0 +1,64 @@ +Please use this form only to report code defects or bugs. + +For any question, even questions directly pertaining to this code, post your question on the forums related to the board you are using. + +Arduino: forum.arduino.cc +Teensy: forum.pjrc.com +ESP8266: www.esp8266.com +ESP32: www.esp32.com +Adafruit Feather/Metro/Trinket: forums.adafruit.com +Particle Photon: community.particle.io + +If you are experiencing trouble but not certain of the cause, or need help using this code, ask on the appropriate forum. This is not the place to ask for support or help, even directly related to this code. Only use this form you are certain you have discovered a defect in this code! + +Please verify the problem occurs when using the very latest version, using the newest version of Arduino and any other related software. + + +----------------------------- Remove above ----------------------------- + + + +### Description + +Describe your problem. + + + +### Steps To Reproduce Problem + +Please give detailed instructions needed for anyone to attempt to reproduce the problem. + + + +### Hardware & Software + +Board +Shields / modules used +Arduino IDE version +Teensyduino version (if using Teensy) +Version info & package name (from Tools > Boards > Board Manager) +Operating system & version +Any other software or hardware? + + +### Arduino Sketch + +```cpp +// Change the code below by your sketch (please try to give the smallest code which demonstrates the problem) +#include + +// libraries: give links/details so anyone can compile your code for the same result + +void setup() { +} + +void loop() { +} +``` + + +### Errors or Incorrect Output + +If you see any errors or incorrect output, please show it here. Please use copy & paste to give an exact copy of the message. Details matter, so please show (not merely describe) the actual message or error exactly as it appears. + + diff --git a/lib/lib_display/XPT2046_Touchscreen/examples/ILI9341Test/ILI9341Test.ino b/lib/lib_display/XPT2046_Touchscreen/examples/ILI9341Test/ILI9341Test.ino new file mode 100755 index 000000000..423c03c83 --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/examples/ILI9341Test/ILI9341Test.ino @@ -0,0 +1,69 @@ +#include +#include // from ILI9341_t3 +#include +#include + +#define CS_PIN 8 +#define TFT_DC 9 +#define TFT_CS 10 +// MOSI=11, MISO=12, SCK=13 + +XPT2046_Touchscreen ts(CS_PIN); +#define TIRQ_PIN 2 +//XPT2046_Touchscreen ts(CS_PIN); // Param 2 - NULL - No interrupts +//XPT2046_Touchscreen ts(CS_PIN, 255); // Param 2 - 255 - No interrupts +//XPT2046_Touchscreen ts(CS_PIN, TIRQ_PIN); // Param 2 - Touch IRQ Pin - interrupt enabled polling + +ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC); + +void setup() { + Serial.begin(38400); + tft.begin(); + tft.setRotation(1); + tft.fillScreen(ILI9341_BLACK); + ts.begin(); + ts.setRotation(1); + while (!Serial && (millis() <= 1000)); +} + +boolean wastouched = true; + +void loop() { + boolean istouched = ts.touched(); + if (istouched) { + TS_Point p = ts.getPoint(); + if (!wastouched) { + tft.fillScreen(ILI9341_BLACK); + tft.setTextColor(ILI9341_YELLOW); + tft.setFont(Arial_60); + tft.setCursor(60, 80); + tft.print("Touch"); + } + tft.fillRect(100, 150, 140, 60, ILI9341_BLACK); + tft.setTextColor(ILI9341_GREEN); + tft.setFont(Arial_24); + tft.setCursor(100, 150); + tft.print("X = "); + tft.print(p.x); + tft.setCursor(100, 180); + tft.print("Y = "); + tft.print(p.y); + Serial.print(", x = "); + Serial.print(p.x); + Serial.print(", y = "); + Serial.println(p.y); + } else { + if (wastouched) { + tft.fillScreen(ILI9341_BLACK); + tft.setTextColor(ILI9341_RED); + tft.setFont(Arial_48); + tft.setCursor(120, 50); + tft.print("No"); + tft.setCursor(80, 120); + tft.print("Touch"); + } + Serial.println("no touch"); + } + wastouched = istouched; + delay(100); +} diff --git a/lib/lib_display/XPT2046_Touchscreen/examples/TouchTest/TouchTest.ino b/lib/lib_display/XPT2046_Touchscreen/examples/TouchTest/TouchTest.ino new file mode 100755 index 000000000..05a3fdf0a --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/examples/TouchTest/TouchTest.ino @@ -0,0 +1,33 @@ +#include +#include + +#define CS_PIN 8 +// MOSI=11, MISO=12, SCK=13 + +//XPT2046_Touchscreen ts(CS_PIN); +#define TIRQ_PIN 2 +//XPT2046_Touchscreen ts(CS_PIN); // Param 2 - NULL - No interrupts +//XPT2046_Touchscreen ts(CS_PIN, 255); // Param 2 - 255 - No interrupts +XPT2046_Touchscreen ts(CS_PIN, TIRQ_PIN); // Param 2 - Touch IRQ Pin - interrupt enabled polling + +void setup() { + Serial.begin(38400); + ts.begin(); + ts.setRotation(1); + while (!Serial && (millis() <= 1000)); +} + +void loop() { + if (ts.touched()) { + TS_Point p = ts.getPoint(); + Serial.print("Pressure = "); + Serial.print(p.z); + Serial.print(", x = "); + Serial.print(p.x); + Serial.print(", y = "); + Serial.print(p.y); + delay(30); + Serial.println(); + } +} + diff --git a/lib/lib_display/XPT2046_Touchscreen/examples/TouchTestIRQ/TouchTestIRQ.ino b/lib/lib_display/XPT2046_Touchscreen/examples/TouchTestIRQ/TouchTestIRQ.ino new file mode 100755 index 000000000..159128a32 --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/examples/TouchTestIRQ/TouchTestIRQ.ino @@ -0,0 +1,37 @@ +#include +#include + +#define CS_PIN 8 +// MOSI=11, MISO=12, SCK=13 + +// The TIRQ interrupt signal must be used for this example. +#define TIRQ_PIN 2 +XPT2046_Touchscreen ts(CS_PIN, TIRQ_PIN); // Param 2 - Touch IRQ Pin - interrupt enabled polling + +void setup() { + Serial.begin(38400); + ts.begin(); + ts.setRotation(1); + while (!Serial && (millis() <= 1000)); +} + +void loop() { + + // tirqTouched() is much faster than touched(). For projects where other SPI chips + // or other time sensitive tasks are added to loop(), using tirqTouched() can greatly + // reduce the delay added to loop() when the screen has not been touched. + if (ts.tirqTouched()) { + if (ts.touched()) { + TS_Point p = ts.getPoint(); + Serial.print("Pressure = "); + Serial.print(p.z); + Serial.print(", x = "); + Serial.print(p.x); + Serial.print(", y = "); + Serial.print(p.y); + delay(30); + Serial.println(); + } + } +} + diff --git a/lib/lib_display/XPT2046_Touchscreen/keywords.txt b/lib/lib_display/XPT2046_Touchscreen/keywords.txt new file mode 100755 index 000000000..f96cc3e5b --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/keywords.txt @@ -0,0 +1,8 @@ +XPT2046_Touchscreen KEYWORD1 +TS_Point KEYWORD1 +getPoint KEYWORD2 +touched KEYWORD2 +readData KEYWORD2 +bufferEmpty KEYWORD2 +bufferSize KEYWORD2 +setRotation KEYWORD2 diff --git a/lib/lib_display/XPT2046_Touchscreen/library.json b/lib/lib_display/XPT2046_Touchscreen/library.json new file mode 100755 index 000000000..aefefc49e --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/library.json @@ -0,0 +1,13 @@ +{ + "name": "XPT2046_Touchscreen", + "keywords": "display, tft, lcd, graphics, spi, touchscreen", + "description": "Touchscreens using the XPT2046 controller chip. Many very low cost color TFT displays with touch screens have this chip.", + "exclude": "doc", + "repository": + { + "type": "git", + "url": "https://github.com/PaulStoffregen/XPT2046_Touchscreen.git" + }, + "frameworks": "arduino", + "platforms": "*" +} diff --git a/lib/lib_display/XPT2046_Touchscreen/library.properties b/lib/lib_display/XPT2046_Touchscreen/library.properties new file mode 100755 index 000000000..09d2c7842 --- /dev/null +++ b/lib/lib_display/XPT2046_Touchscreen/library.properties @@ -0,0 +1,10 @@ +name=XPT2046_Touchscreen +version=1.3 +author=Paul Stoffregen +maintainer=Paul Stoffregen +sentence=Touchscreens using the XPT2046 controller chip. +paragraph=Many very low cost color TFT displays with touch screens have this chip. +category=Display +url=https://github.com/PaulStoffregen/XPT2046_Touchscreen +architectures=* + From 43946f4ca76efb79c1f6e2cf32d313f14f0cc2bb Mon Sep 17 00:00:00 2001 From: nonix <1888777+nonix@users.noreply.github.com> Date: Thu, 25 Feb 2021 10:58:33 +0100 Subject: [PATCH 02/25] Debug USE_UFILESYS --- tasmota/language/en_GB.h | 1 + tasmota/tasmota_template.h | 9 +++++++++ tasmota/xdrv_13_display.ino | 39 ++++++++++++++++++++++++++++++++++--- tasmota/xdsp_04_ili9341.ino | 22 ++++++++++++++++----- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h index 16d37c32c..dbf52c1aa 100644 --- a/tasmota/language/en_GB.h +++ b/tasmota/language/en_GB.h @@ -775,6 +775,7 @@ #define D_SENSOR_RC522_CS "RC522 CS" #define D_SENSOR_NRF24_CS "NRF24 CS" #define D_SENSOR_NRF24_DC "NRF24 DC" +#define D_SENSOR_XPT2046_CS "XPT2046 CS" #define D_SENSOR_ILI9341_CS "ILI9341 CS" #define D_SENSOR_ILI9341_DC "ILI9341 DC" #define D_SENSOR_ILI9488_CS "ILI9488 CS" diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index 6221c833c..234a9d690 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -150,6 +150,10 @@ enum UserSelectablePins { GPIO_NEOPOOL_TX, GPIO_NEOPOOL_RX, // Sugar Valley RS485 interface GPIO_SDM72_TX, GPIO_SDM72_RX, // SDM72 Serial interface GPIO_TM1637CLK, GPIO_TM1637DIO, // TM1637 interface +#ifdef USE_XPT2046 + GPIO_XPT2046_CS, // XPT2046 SPI Chip Select +#endif + GPIO_SENSOR_END }; enum ProgramSelectablePins { @@ -320,6 +324,7 @@ const char kSensorNames[] PROGMEM = D_SENSOR_NEOPOOL_TX "|" D_SENSOR_NEOPOOL_RX "|" D_SENSOR_SDM72_TX "|" D_SENSOR_SDM72_RX "|" D_SENSOR_TM1637_CLK "|" D_SENSOR_TM1637_DIO "|" + D_SENSOR_XPT2046_CS "|" ; const char kSensorNamesFixed[] PROGMEM = @@ -409,6 +414,10 @@ const uint16_t kGpioNiceList[] PROGMEM = { #ifdef USE_DISPLAY_ILI9341 AGPIO(GPIO_ILI9341_CS), AGPIO(GPIO_ILI9341_DC), +#ifdef USE_XPT2046 + AGPIO(GPIO_XPT2046_CS), // XPT2046 SPI Chip Select +#endif + #endif // USE_DISPLAY_ILI9341 #ifdef USE_DISPLAY_ILI9488 AGPIO(GPIO_ILI9488_CS), diff --git a/tasmota/xdrv_13_display.ino b/tasmota/xdrv_13_display.ino index c3ecd407b..7b8ead353 100755 --- a/tasmota/xdrv_13_display.ino +++ b/tasmota/xdrv_13_display.ino @@ -2018,7 +2018,6 @@ void CmndDisplayRows(void) /*********************************************************************************************\ * optional drivers \*********************************************************************************************/ - #ifdef USE_TOUCH_BUTTONS // very limited path size, so, add .jpg void draw_picture(char *path, uint32_t xp, uint32_t yp, uint32_t xs, uint32_t ys, uint32_t ocol, bool inverted) { @@ -2041,7 +2040,6 @@ char ppath[16]; } #endif - #ifdef ESP32 #ifdef JPEG_PICTS #include "img_converters.h" @@ -2573,6 +2571,7 @@ void AddValue(uint8_t num,float fval) { } #endif // USE_GRAPH +#if defined(USE_FT5206) || defined(USE_XPT2046) #ifdef USE_FT5206 #include @@ -2609,7 +2608,38 @@ uint32_t Touch_Status(uint32_t sel) { return 0; } } +#endif +#if defined(USE_XPT2046) && defined(USE_DISPLAY_ILI9341) +#include + +XPT2046_Touchscreen *touchp; +TS_Point pLoc; +bool XPT2046_found; + +bool Touch_Init(uint16_t CS) { + touchp = new XPT2046_Touchscreen(CS); + XPT2046_found = touchp->begin(); + return XPT2046_found; +} + +uint32_t Touch_Status(uint32_t sel) { + if (XPT2046_found) { + switch (sel) { + case 0: + return touchp->touched(); + case 1: + return pLoc.x; + case 2: + return pLoc.y; + } + return 0; + } else { + return 0; + } +} + +#endif #ifdef USE_TOUCH_BUTTONS void Touch_MQTT(uint8_t index, const char *cp, uint32_t val) { @@ -2636,8 +2666,11 @@ uint8_t vbutt=0; if (touchp->touched()) { // did find a hit +#if defined(USE_FT5206) pLoc = touchp->getPoint(0); - +#elif defined(USE_XPT2046) + pLoc = touchp->getPoint(); +#endif if (renderer) { #ifdef USE_M5STACK_CORE2 diff --git a/tasmota/xdsp_04_ili9341.ino b/tasmota/xdsp_04_ili9341.ino index 133cf4366..e71de0b67 100644 --- a/tasmota/xdsp_04_ili9341.ino +++ b/tasmota/xdsp_04_ili9341.ino @@ -29,9 +29,12 @@ extern uint8_t *buffer; extern uint8_t color_type; ILI9341_2 *ili9341_2; -#ifdef USE_FT5206 +#if defined(USE_FT5206) #include uint8_t ili9342_ctouch_counter = 0; +#elif defined(USE_XPT2046) +#include +uint8_t ili9342_ctouch_counter = 0; #endif // USE_FT5206 bool tft_init_done = false; @@ -136,6 +139,10 @@ void ILI9341_InitDriver() #endif // USE_FT5206 #endif // ESP32 +#ifdef USE_XPT2046 + Touch_Init(Pin(GPIO_XPT2046_CS)); +#endif + tft_init_done = true; #ifdef USE_DISPLAY_ILI9341 AddLog(LOG_LEVEL_INFO, PSTR("DSP: ILI9341")); @@ -160,8 +167,8 @@ void ili9342_dimm(uint8_t dim) { #endif } -#ifdef ESP32 -#ifdef USE_FT5206 +//#ifdef ESP32 +#if defined(USE_FT5206) || defined(USE_XPT2046) #ifdef USE_TOUCH_BUTTONS void ili9342_RotConvert(int16_t *x, int16_t *y) { @@ -201,7 +208,7 @@ ili9342_ctouch_counter++; } #endif // USE_TOUCH_BUTTONS #endif // USE_FT5206 -#endif // ESP32 +//#endif // ESP32 #ifdef USE_DISPLAY_MODES1TO5 @@ -360,10 +367,15 @@ bool Xdsp04(uint8_t function) case DISPLAY_INIT_MODE: renderer->clearDisplay(); break; -#ifdef USE_FT5206 +#if defined(USE_FT5206) || defined(USE_XPT2046) #ifdef USE_TOUCH_BUTTONS case FUNC_DISPLAY_EVERY_50_MSECOND: +#if defined(USE_FT5206) if (FT5206_found) { +#elif defined(USE_XPT2046) + if (XPT2046_found) { +#endif + ili9342_CheckTouch(); } break; From 96f8389408f3d57fdb01d25b3496e675f58b8a0d Mon Sep 17 00:00:00 2001 From: nonix <1888777+nonix@users.noreply.github.com> Date: Sun, 28 Feb 2021 10:49:49 +0100 Subject: [PATCH 03/25] Workinig --- tasmota/tasmota.h | 5 +++++ tasmota/xdrv_13_display.ino | 9 ++++++++- tasmota/xdsp_04_ili9341.ino | 38 +++++++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/tasmota/tasmota.h b/tasmota/tasmota.h index 9ac9adeec..5f2f73c0b 100644 --- a/tasmota/tasmota.h +++ b/tasmota/tasmota.h @@ -238,6 +238,11 @@ const uint32_t LOOP_SLEEP_DELAY = 50; // Lowest number of milliseconds to #define KNX_MAX_device_param 31 #define MAX_KNXTX_CMNDS 5 +// XPT2046 resistive touch driver min/max raw values +#define XPT2046_MINX 192 +#define XPT2046_MAXX 3895 +#define XPT2046_MINY 346 +#define XPT2046_MAXY 3870 /*********************************************************************************************\ * Enumeration \*********************************************************************************************/ diff --git a/tasmota/xdrv_13_display.ino b/tasmota/xdrv_13_display.ino index 2a5b52feb..02c0e1d0f 100755 --- a/tasmota/xdrv_13_display.ino +++ b/tasmota/xdrv_13_display.ino @@ -2621,6 +2621,9 @@ bool XPT2046_found; bool Touch_Init(uint16_t CS) { touchp = new XPT2046_Touchscreen(CS); XPT2046_found = touchp->begin(); + if (XPT2046_found) { + AddLog(LOG_LEVEL_INFO, PSTR("TS: XPT2046")); + } return XPT2046_found; } @@ -2644,7 +2647,11 @@ uint32_t Touch_Status(uint32_t sel) { #ifdef USE_TOUCH_BUTTONS void Touch_MQTT(uint8_t index, const char *cp, uint32_t val) { +#if defined(USE_FT5206) ResponseTime_P(PSTR(",\"FT5206\":{\"%s%d\":\"%d\"}}"), cp, index+1, val); +#elif defined(USE_XPT2046) + ResponseTime_P(PSTR(",\"XPT2046\":{\"%s%d\":\"%d\"}}"), cp, index+1, val); +#endif MqttPublishTeleSensor(); } @@ -2696,7 +2703,7 @@ uint8_t vbutt=0; rotconvert(&pLoc.x, &pLoc.y); - //AddLog(LOG_LEVEL_INFO, PSTR("touch %d - %d"), pLoc.x, pLoc.y); + // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("touch after convert %d - %d"), pLoc.x, pLoc.y); // now must compare with defined buttons for (uint8_t count = 0; count < MAX_TOUCH_BUTTONS; count++) { if (buttons[count]) { diff --git a/tasmota/xdsp_04_ili9341.ino b/tasmota/xdsp_04_ili9341.ino index e71de0b67..e8dc5e224 100644 --- a/tasmota/xdsp_04_ili9341.ino +++ b/tasmota/xdsp_04_ili9341.ino @@ -171,7 +171,8 @@ void ili9342_dimm(uint8_t dim) { #if defined(USE_FT5206) || defined(USE_XPT2046) #ifdef USE_TOUCH_BUTTONS -void ili9342_RotConvert(int16_t *x, int16_t *y) { +#if defined(USE_FT5206) +void TS_RotConvert(int16_t *x, int16_t *y) { int16_t temp; if (renderer) { @@ -196,6 +197,38 @@ int16_t temp; } } } +#elif defined(USE_XPT2046) +void TS_RotConvert(int16_t *x, int16_t *y) { + +int16_t temp; + if (renderer) { + uint8_t rot = renderer->getRotation(); +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(" TS: before convert x:%d / y:%d screen r:%d / w:%d / h:%d"), *x, *y,rot,renderer->width(),renderer->height()); + temp = map(*x,XPT2046_MINX,XPT2046_MAXX, renderer->height(), 0); + *x = map(*y,XPT2046_MINY,XPT2046_MAXY, renderer->width(), 0); + *y = temp; + switch (rot) { + case 0: + break; + case 1: + temp = *y; + *y = renderer->width() - *x; + *x = temp; + break; + case 2: + *x = renderer->width() - *x; + *y = renderer->height() - *y; + break; + case 3: + temp = *y; + *y = *x; + *x = renderer->height() - temp; + break; + } +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(" TS: after convert x:%d / y:%d screen r:%d / w:%d / h:%d"), *x, *y,rot,renderer->width(),renderer->height()); + } +} +#endif // check digitizer hit void ili9342_CheckTouch() { @@ -203,7 +236,8 @@ ili9342_ctouch_counter++; if (2 == ili9342_ctouch_counter) { // every 100 ms should be enough ili9342_ctouch_counter = 0; - Touch_Check(ili9342_RotConvert); + + Touch_Check(TS_RotConvert); } } #endif // USE_TOUCH_BUTTONS From 5cafcfedf45ea54f37c803da86999e68b5f77382 Mon Sep 17 00:00:00 2001 From: nonix <1888777+nonix@users.noreply.github.com> Date: Sun, 28 Feb 2021 19:13:41 +0100 Subject: [PATCH 04/25] remove xpt.patch --- tasmota/language/xpt.patch | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 tasmota/language/xpt.patch diff --git a/tasmota/language/xpt.patch b/tasmota/language/xpt.patch deleted file mode 100644 index 9fc7c4f3c..000000000 --- a/tasmota/language/xpt.patch +++ /dev/null @@ -1,2 +0,0 @@ -779a780 -> #define D_SENSOR_XPT2046_CS "XPT2046 CS" From 814e144d21904fd9e24ca1c9f78148a1baafb0b5 Mon Sep 17 00:00:00 2001 From: nonix <1888777+nonix@users.noreply.github.com> Date: Mon, 1 Mar 2021 12:16:12 +0100 Subject: [PATCH 05/25] ifdef removed from tasmota_template --- tasmota/tasmota_template.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index 6d7a0ae8b..c1b1480ec 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -152,9 +152,7 @@ enum UserSelectablePins { GPIO_TM1637CLK, GPIO_TM1637DIO, // TM1637 interface GPIO_PROJECTOR_CTRL_TX, GPIO_PROJECTOR_CTRL_RX, // LCD/DLP Projector Serial Control GPIO_SSD1351_DC, -#ifdef USE_XPT2046 GPIO_XPT2046_CS, // XPT2046 SPI Chip Select -#endif GPIO_SENSOR_END }; enum ProgramSelectablePins { From 59603e5ad0dcbb8befc555719dd0a6b023c65aa5 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 1 Mar 2021 12:42:42 +0100 Subject: [PATCH 06/25] Use Arduino ESP32 1.0.5 (= espressif32 @ 3.1.0) --- platformio_tasmota32.ini | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/platformio_tasmota32.ini b/platformio_tasmota32.ini index 8bbbe5d0d..caa655c4f 100644 --- a/platformio_tasmota32.ini +++ b/platformio_tasmota32.ini @@ -94,8 +94,7 @@ build_flags = ${esp_defaults.build_flags} [core32] -platform = espressif32 @ 3.0.0 -platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/arduino-esp32/releases/download/1.0.5-rc6/esp32-1.0.5-rc6.zip - platformio/tool-mklittlefs @ ~1.203.200522 +platform = espressif32 @ 3.1.0 +platform_packages = platformio/tool-mklittlefs @ ~1.203.200522 build_unflags = ${esp32_defaults.build_unflags} build_flags = ${esp32_defaults.build_flags} From 731b6337b3d71a1c984bf401b9c4b00c75b72b2a Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 1 Mar 2021 12:47:39 +0100 Subject: [PATCH 07/25] Use for ESP32 stage Git version --- platformio_override_sample.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index 0209e7091..03ac27562 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -129,11 +129,11 @@ build_unflags = ${core32_stage.build_unflags} build_flags = ${core32_stage.build_flags} [core32_stage] -platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/arduino-esp32/releases/download/1.0.5-rc7/esp32-1.0.5-rc7.zip +platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git platformio/tool-mklittlefs @ ~1.203.200522 build_unflags = ${esp32_defaults.build_unflags} build_flags = ${esp32_defaults.build_flags} - ;-DESP32_STAGE=true + -DESP32_STAGE=true [library] shared_libdeps_dir = lib From 451dfb2573e3a61db382c3c77092c2e569a06e43 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 1 Mar 2021 12:48:38 +0100 Subject: [PATCH 08/25] Disable ESP32 stage as default core --- platformio_override_sample.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index 03ac27562..1f124264d 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -124,9 +124,9 @@ lib_extra_dirs = ${library.lib_extra_dirs} [core32] ; Activate Stage Core32 by removing ";" in next 3 lines, if you want to override the standard core32 -platform_packages = ${core32_stage.platform_packages} -build_unflags = ${core32_stage.build_unflags} -build_flags = ${core32_stage.build_flags} +;platform_packages = ${core32_stage.platform_packages} +;build_unflags = ${core32_stage.build_unflags} +;build_flags = ${core32_stage.build_flags} [core32_stage] platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git From 5a0d1c8165ba672642ca8a52cb42880cab69a65c Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 1 Mar 2021 12:49:55 +0100 Subject: [PATCH 09/25] Use ESP32 1.0.5 release --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0de14bbf8..8db216e84 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ - [ ] Only relevant files were touched - [ ] Only one feature/fix was added per PR and the code change compiles without warnings - [ ] The code change is tested and works on Tasmota core ESP8266 V.2.7.4.9 - - [ ] The code change is tested and works on Tasmota core ESP32 V.1.0.5-rc6 + - [ ] The code change is tested and works with core ESP32 V.1.0.5 - [ ] I accept the [CLA](https://github.com/arendst/Tasmota/blob/development/CONTRIBUTING.md#contributor-license-agreement-cla). _NOTE: The code change must pass CI tests. **Your PR cannot be merged unless tests pass**_ From 4fb55544bb433c9866507d9cb38b02c312ff3ceb Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Mon, 1 Mar 2021 18:22:26 +0100 Subject: [PATCH 10/25] Prep support CSE7761 --- tasmota/language/af_AF.h | 4 +++- tasmota/language/bg_BG.h | 2 ++ tasmota/language/cs_CZ.h | 2 ++ tasmota/language/de_DE.h | 2 ++ tasmota/language/el_GR.h | 2 ++ tasmota/language/en_GB.h | 2 ++ tasmota/language/es_ES.h | 2 ++ tasmota/language/fr_FR.h | 2 ++ tasmota/language/fy_NL.h | 2 ++ tasmota/language/he_HE.h | 2 ++ tasmota/language/hu_HU.h | 2 ++ tasmota/language/it_IT.h | 6 ++++-- tasmota/language/ko_KO.h | 2 ++ tasmota/language/nl_NL.h | 2 ++ tasmota/language/pl_PL.h | 2 ++ tasmota/language/pt_BR.h | 2 ++ tasmota/language/pt_PT.h | 2 ++ tasmota/language/ro_RO.h | 2 ++ tasmota/language/ru_RU.h | 2 ++ tasmota/language/sk_SK.h | 2 ++ tasmota/language/sv_SE.h | 2 ++ tasmota/language/tr_TR.h | 2 ++ tasmota/language/uk_UA.h | 2 ++ tasmota/language/vi_VN.h | 2 ++ tasmota/language/zh_CN.h | 2 ++ tasmota/language/zh_TW.h | 2 ++ tasmota/tasmota_template.h | 8 +++++++- 27 files changed, 62 insertions(+), 4 deletions(-) diff --git a/tasmota/language/af_AF.h b/tasmota/language/af_AF.h index 266486276..b445d8dcd 100644 --- a/tasmota/language/af_AF.h +++ b/tasmota/language/af_AF.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" @@ -777,7 +779,7 @@ #define D_SENSOR_RC522_CS "RC522 CS" #define D_SENSOR_NRF24_CS "NRF24 CS" #define D_SENSOR_NRF24_DC "NRF24 DC" -#define D_SENSOR_XPT2046_CS "XPT2046 CS" +#define D_SENSOR_XPT2046_CS "XPT2046 CS" #define D_SENSOR_ILI9341_CS "ILI9341 CS" #define D_SENSOR_ILI9341_DC "ILI9341 DC" #define D_SENSOR_ILI9488_CS "ILI9488 CS" diff --git a/tasmota/language/bg_BG.h b/tasmota/language/bg_BG.h index 6c30d6c9a..c9ddf4cbf 100644 --- a/tasmota/language/bg_BG.h +++ b/tasmota/language/bg_BG.h @@ -673,6 +673,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/cs_CZ.h b/tasmota/language/cs_CZ.h index ef8d3ea40..6bd44919c 100644 --- a/tasmota/language/cs_CZ.h +++ b/tasmota/language/cs_CZ.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/de_DE.h b/tasmota/language/de_DE.h index 2c945ce75..c7221243b 100644 --- a/tasmota/language/de_DE.h +++ b/tasmota/language/de_DE.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/el_GR.h b/tasmota/language/el_GR.h index 45a104063..e11ae5194 100644 --- a/tasmota/language/el_GR.h +++ b/tasmota/language/el_GR.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h index 9b20a9335..df99b6504 100644 --- a/tasmota/language/en_GB.h +++ b/tasmota/language/en_GB.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/es_ES.h b/tasmota/language/es_ES.h index 93e1de64e..b05540083 100644 --- a/tasmota/language/es_ES.h +++ b/tasmota/language/es_ES.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/fr_FR.h b/tasmota/language/fr_FR.h index e93b51968..a5c8ab250 100644 --- a/tasmota/language/fr_FR.h +++ b/tasmota/language/fr_FR.h @@ -670,6 +670,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 TX" #define D_SENSOR_MCP39F5_RX "MCP39F5 RX" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 TX" #define D_SENSOR_CSE7766_RX "CSE7766 RX" #define D_SENSOR_PN532_TX "PN532 TX" diff --git a/tasmota/language/fy_NL.h b/tasmota/language/fy_NL.h index 533c6ee5e..bb33ebc2d 100644 --- a/tasmota/language/fy_NL.h +++ b/tasmota/language/fy_NL.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/he_HE.h b/tasmota/language/he_HE.h index aa64de0e1..1d5785433 100644 --- a/tasmota/language/he_HE.h +++ b/tasmota/language/he_HE.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/hu_HU.h b/tasmota/language/hu_HU.h index 1ab0c850c..6baf1960f 100644 --- a/tasmota/language/hu_HU.h +++ b/tasmota/language/hu_HU.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/it_IT.h b/tasmota/language/it_IT.h index 7055b39a3..3adc683d8 100644 --- a/tasmota/language/it_IT.h +++ b/tasmota/language/it_IT.h @@ -666,7 +666,7 @@ #define D_SENSOR_MAX31855_CS "MX31855 - CS" #define D_SENSOR_MAX31855_CLK "MX31855 - CLK" #define D_SENSOR_MAX31855_DO "MX31855 - DO" -#define D_SENSOR_MAX31865_CS "MX31865 CS" +#define D_SENSOR_MAX31865_CS "MX31865 - CS" #define D_SENSOR_NRG_SEL "HLWBL - SEL" // Suffix "i" #define D_SENSOR_NRG_CF1 "HLWBL - CF1" #define D_SENSOR_HLW_CF "HLW8012 - CF" @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 - TX" #define D_SENSOR_MCP39F5_RX "MCP39F5 - RX" #define D_SENSOR_MCP39F5_RST "MCP39F5 - Reset" +#define D_SENSOR_CSE7761_TX "CSE7761 - Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 - Rx" #define D_SENSOR_CSE7766_TX "CSE7766 - TX" #define D_SENSOR_CSE7766_RX "CSE7766 - RX" #define D_SENSOR_PN532_TX "PN532 - TX" @@ -777,7 +779,7 @@ #define D_SENSOR_RC522_CS "RC522 - CS" #define D_SENSOR_NRF24_CS "NRF24 - CS" #define D_SENSOR_NRF24_DC "NRF24 - DC" -#define D_SENSOR_XPT2046_CS "XPT2046 CS" +#define D_SENSOR_XPT2046_CS "XPT2046 - CS" #define D_SENSOR_ILI9341_CS "ILI9341 - CS" #define D_SENSOR_ILI9341_DC "ILI9341 - DC" #define D_SENSOR_ILI9488_CS "ILI9488 - CS" diff --git a/tasmota/language/ko_KO.h b/tasmota/language/ko_KO.h index bd1ef471f..907f840e5 100644 --- a/tasmota/language/ko_KO.h +++ b/tasmota/language/ko_KO.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/nl_NL.h b/tasmota/language/nl_NL.h index 38e3046c9..96991391f 100644 --- a/tasmota/language/nl_NL.h +++ b/tasmota/language/nl_NL.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/pl_PL.h b/tasmota/language/pl_PL.h index 4ae81c0cc..818aba4cb 100644 --- a/tasmota/language/pl_PL.h +++ b/tasmota/language/pl_PL.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/pt_BR.h b/tasmota/language/pt_BR.h index c753d1a8a..c9ba30c50 100644 --- a/tasmota/language/pt_BR.h +++ b/tasmota/language/pt_BR.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/pt_PT.h b/tasmota/language/pt_PT.h index 7893bf4ad..0687aedcd 100644 --- a/tasmota/language/pt_PT.h +++ b/tasmota/language/pt_PT.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/ro_RO.h b/tasmota/language/ro_RO.h index 09152b026..d9d779948 100644 --- a/tasmota/language/ro_RO.h +++ b/tasmota/language/ro_RO.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/ru_RU.h b/tasmota/language/ru_RU.h index 6f5ccac38..ab546c0f1 100644 --- a/tasmota/language/ru_RU.h +++ b/tasmota/language/ru_RU.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/sk_SK.h b/tasmota/language/sk_SK.h index 393507016..9310a6d24 100644 --- a/tasmota/language/sk_SK.h +++ b/tasmota/language/sk_SK.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/sv_SE.h b/tasmota/language/sv_SE.h index 1896d25a2..67de1e2aa 100644 --- a/tasmota/language/sv_SE.h +++ b/tasmota/language/sv_SE.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/tr_TR.h b/tasmota/language/tr_TR.h index 8deecf270..b21b0574a 100644 --- a/tasmota/language/tr_TR.h +++ b/tasmota/language/tr_TR.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/uk_UA.h b/tasmota/language/uk_UA.h index 7310f8d0b..b1baca8dd 100644 --- a/tasmota/language/uk_UA.h +++ b/tasmota/language/uk_UA.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/vi_VN.h b/tasmota/language/vi_VN.h index 4c3607d07..40ea941d0 100644 --- a/tasmota/language/vi_VN.h +++ b/tasmota/language/vi_VN.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/zh_CN.h b/tasmota/language/zh_CN.h index 8f0e36606..73f9424ed 100644 --- a/tasmota/language/zh_CN.h +++ b/tasmota/language/zh_CN.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/language/zh_TW.h b/tasmota/language/zh_TW.h index 5dc1ce00c..8a759f0eb 100644 --- a/tasmota/language/zh_TW.h +++ b/tasmota/language/zh_TW.h @@ -674,6 +674,8 @@ #define D_SENSOR_MCP39F5_TX "MCP39F5 Tx" #define D_SENSOR_MCP39F5_RX "MCP39F5 Rx" #define D_SENSOR_MCP39F5_RST "MCP39F5 Rst" +#define D_SENSOR_CSE7761_TX "CSE7761 Tx" +#define D_SENSOR_CSE7761_RX "CSE7761 Rx" #define D_SENSOR_CSE7766_TX "CSE7766 Tx" #define D_SENSOR_CSE7766_RX "CSE7766 Rx" #define D_SENSOR_PN532_TX "PN532 Tx" diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index c1b1480ec..4465baea4 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -153,6 +153,7 @@ enum UserSelectablePins { GPIO_PROJECTOR_CTRL_TX, GPIO_PROJECTOR_CTRL_RX, // LCD/DLP Projector Serial Control GPIO_SSD1351_DC, GPIO_XPT2046_CS, // XPT2046 SPI Chip Select + GPIO_CSE7761_TX, GPIO_CSE7761_RX, // CSE7761 Serial interface (Dual R3) GPIO_SENSOR_END }; enum ProgramSelectablePins { @@ -326,6 +327,7 @@ const char kSensorNames[] PROGMEM = D_SENSOR_PROJECTOR_CTRL_TX "|" D_SENSOR_PROJECTOR_CTRL_RX "|" D_SENSOR_SSD1351_DC "|" D_SENSOR_XPT2046_CS "|" + D_SENSOR_CSE7761_TX "|" D_SENSOR_CSE7761_RX "|" ; const char kSensorNamesFixed[] PROGMEM = @@ -418,7 +420,7 @@ const uint16_t kGpioNiceList[] PROGMEM = { #ifdef USE_XPT2046 AGPIO(GPIO_XPT2046_CS), // XPT2046 SPI Chip Select #endif - + #endif // USE_DISPLAY_ILI9341 #ifdef USE_DISPLAY_ILI9488 AGPIO(GPIO_ILI9488_CS), @@ -569,6 +571,10 @@ const uint16_t kGpioNiceList[] PROGMEM = { #if defined(USE_I2C) && defined(USE_ADE7953) AGPIO(GPIO_ADE7953_IRQ), // ADE7953 IRQ #endif +#ifdef USE_CSE7761 + AGPIO(GPIO_CSE7761_TX), // CSE7761 Serial interface (Dual R3) + AGPIO(GPIO_CSE7761_RX), // CSE7761 Serial interface (Dual R3) +#endif #ifdef USE_CSE7766 AGPIO(GPIO_CSE7766_TX), // CSE7766 Serial interface (S31 and Pow R2) AGPIO(GPIO_CSE7766_RX), // CSE7766 Serial interface (S31 and Pow R2) From e7a870c3bf347abbc9852604d9e3b37df3b54142 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:30:22 +0100 Subject: [PATCH 11/25] Clarify where the changes has to be --- tasmota/user_config_override_sample.h | 1 + 1 file changed, 1 insertion(+) diff --git a/tasmota/user_config_override_sample.h b/tasmota/user_config_override_sample.h index cf5f27a25..8c8e02b7e 100644 --- a/tasmota/user_config_override_sample.h +++ b/tasmota/user_config_override_sample.h @@ -82,6 +82,7 @@ Examples : #define WIFI_DNS MY_DNS // If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) #endif +// !!! Place your changes AFTER the line with "*/" !!! */ From 56f922a7a77ff13c27d5c8a1c6b736f59b03672d Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Tue, 2 Mar 2021 09:11:08 +0100 Subject: [PATCH 12/25] dumpsize configurable, =m,=h mqtt fix, --- tasmota/xsns_53_sml.ino | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/tasmota/xsns_53_sml.ino b/tasmota/xsns_53_sml.ino index ef7c1ef88..a84ad58de 100755 --- a/tasmota/xsns_53_sml.ino +++ b/tasmota/xsns_53_sml.ino @@ -822,8 +822,12 @@ uint8_t Serial_peek() { return meter_ss[num-1]->peek(); } -uint8_t sml_logindex; -char log_data[128]; +#ifndef SML_DUMP_SIZE +#define SML_DUMP_SIZE 128 +#endif + +uint16_t sml_logindex; +char log_data[SML_DUMP_SIZE]; #define SML_EBUS_SKIP_SYNC_DUMPS @@ -884,8 +888,8 @@ void Dump2log(void) { char c = SML_SREAD&0x7f; if (c == '\n' || c == '\r') { if (sml_logindex > 2) { - AddLogData(LOG_LEVEL_INFO, log_data); log_data[sml_logindex] = 0; + AddLogData(LOG_LEVEL_INFO, log_data); log_data[0] = ':'; log_data[1] = ' '; sml_logindex = 2; @@ -903,6 +907,7 @@ void Dump2log(void) { while (SML_SAVAILABLE) { c = SML_SREAD; if (c == VBUS_SYNC) { + log_data[sml_logindex] = 0; AddLogData(LOG_LEVEL_INFO, log_data); log_data[0] = ':'; log_data[1] = ' '; @@ -922,6 +927,7 @@ void Dump2log(void) { p = SML_SPEAK; if (p != EBUS_SYNC && sml_logindex > 5) { // new packet, plot last one + log_data[sml_logindex] = 0; AddLogData(LOG_LEVEL_INFO, log_data); strcpy(&log_data[0], ": aa "); sml_logindex = 5; @@ -939,6 +945,7 @@ void Dump2log(void) { while (SML_SAVAILABLE) { c = SML_SREAD; if (c == SML_SYNC) { + log_data[sml_logindex] = 0; AddLogData(LOG_LEVEL_INFO, log_data); log_data[0] = ':'; log_data[1] = ' '; @@ -962,6 +969,7 @@ void Dump2log(void) { } } if (sml_logindex > 2) { + log_data[sml_logindex] = 0; AddLogData(LOG_LEVEL_INFO, log_data); } } @@ -1472,6 +1480,7 @@ void SML_Decode(uint8_t index) { double fac = CharToDouble((char*)mp); meter_vars[vindex] /= fac; SML_Immediate_MQTT((const char*)mp, vindex, mindex); + dvalid[vindex] = 1; // get sfac } else if (*mp=='d') { // calc deltas d ind 10 (eg every 10 secs) @@ -1489,7 +1498,16 @@ void SML_Decode(uint8_t index) { dtimes[dindex] = millis(); double vdiff = meter_vars[ind - 1] - dvalues[dindex]; dvalues[dindex] = meter_vars[ind - 1]; - meter_vars[vindex] = (double)360000.0 * vdiff / ((double)dtime / 10000.0); + double dres = (double)360000.0 * vdiff / ((double)dtime / 10000.0); +#ifdef USE_SML_MEDIAN_FILTER + if (meter_desc_p[mindex].flag & 16) { + meter_vars[vindex] = sml_median(&sml_mf[vindex], dres); + } else { + meter_vars[vindex] = dres; + } +#else + meter_vars[vindex] = dres; +#endif mp=strchr(mp,'@'); if (mp) { @@ -1499,6 +1517,7 @@ void SML_Decode(uint8_t index) { SML_Immediate_MQTT((const char*)mp, vindex, mindex); } } + dvalid[vindex] = 1; dindex++; } } else if (*mp == 'h') { @@ -1731,6 +1750,7 @@ void SML_Decode(uint8_t index) { } if (found) { // matches, get value + dvalid[vindex] = 1; mp++; #ifdef ED300L g_mindex=mindex; @@ -1838,7 +1858,6 @@ void SML_Decode(uint8_t index) { SML_Immediate_MQTT((const char*)mp, vindex, mindex); } } - dvalid[vindex] = 1; //AddLog(LOG_LEVEL_INFO, PSTR("set valid in line %d"), vindex); } nextsect: From 20579122764d779b20a3ba357c54497c4b4b579e Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 2 Mar 2021 11:44:06 +0100 Subject: [PATCH 13/25] Change TasmotaSerial library from v3.2.0 to v3.3.0 Change TasmotaSerial library from v3.2.0 to v3.3.0 --- CHANGELOG.md | 1 + RELEASENOTES.md | 1 + .../README.md | 0 .../examples/swsertest/swsertest.ino | 0 .../keywords.txt | 0 .../library.json | 2 +- .../library.properties | 2 +- .../src/TasmotaSerial.cpp | 35 ++++++++++--------- .../src/TasmotaSerial.h | 3 +- 9 files changed, 24 insertions(+), 20 deletions(-) rename lib/default/{TasmotaSerial-3.2.0 => TasmotaSerial-3.3.0}/README.md (100%) rename lib/default/{TasmotaSerial-3.2.0 => TasmotaSerial-3.3.0}/examples/swsertest/swsertest.ino (100%) rename lib/default/{TasmotaSerial-3.2.0 => TasmotaSerial-3.3.0}/keywords.txt (100%) rename lib/default/{TasmotaSerial-3.2.0 => TasmotaSerial-3.3.0}/library.json (94%) rename lib/default/{TasmotaSerial-3.2.0 => TasmotaSerial-3.3.0}/library.properties (94%) rename lib/default/{TasmotaSerial-3.2.0 => TasmotaSerial-3.3.0}/src/TasmotaSerial.cpp (94%) rename lib/default/{TasmotaSerial-3.2.0 => TasmotaSerial-3.3.0}/src/TasmotaSerial.h (97%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b7395a0..02e2ce7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. ### Changed - TuyaMcu dimmer timeout (#11121) +- TasmotaSerial library from v3.2.0 to v3.3.0 ### Fixed - Refactor acceleration function for shutter stepper and servo (#11088) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f02d78da4..ba3720df4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -87,6 +87,7 @@ The attached binaries can also be downloaded from http://ota.tasmota.com/tasmota ### Changed - TuyaMcu dimmer timeout [#11121](https://github.com/arendst/Tasmota/issues/11121) +- TasmotaSerial library from v3.2.0 to v3.3.0 ### Fixed - Refactor acceleration function for shutter stepper and servo [#11088](https://github.com/arendst/Tasmota/issues/11088) diff --git a/lib/default/TasmotaSerial-3.2.0/README.md b/lib/default/TasmotaSerial-3.3.0/README.md similarity index 100% rename from lib/default/TasmotaSerial-3.2.0/README.md rename to lib/default/TasmotaSerial-3.3.0/README.md diff --git a/lib/default/TasmotaSerial-3.2.0/examples/swsertest/swsertest.ino b/lib/default/TasmotaSerial-3.3.0/examples/swsertest/swsertest.ino similarity index 100% rename from lib/default/TasmotaSerial-3.2.0/examples/swsertest/swsertest.ino rename to lib/default/TasmotaSerial-3.3.0/examples/swsertest/swsertest.ino diff --git a/lib/default/TasmotaSerial-3.2.0/keywords.txt b/lib/default/TasmotaSerial-3.3.0/keywords.txt similarity index 100% rename from lib/default/TasmotaSerial-3.2.0/keywords.txt rename to lib/default/TasmotaSerial-3.3.0/keywords.txt diff --git a/lib/default/TasmotaSerial-3.2.0/library.json b/lib/default/TasmotaSerial-3.3.0/library.json similarity index 94% rename from lib/default/TasmotaSerial-3.2.0/library.json rename to lib/default/TasmotaSerial-3.3.0/library.json index caaa9f6f7..f6d1aaaeb 100644 --- a/lib/default/TasmotaSerial-3.2.0/library.json +++ b/lib/default/TasmotaSerial-3.3.0/library.json @@ -1,6 +1,6 @@ { "name": "TasmotaSerial", - "version": "3.2.0", + "version": "3.3.0", "keywords": [ "serial", "io", "TasmotaSerial" ], diff --git a/lib/default/TasmotaSerial-3.2.0/library.properties b/lib/default/TasmotaSerial-3.3.0/library.properties similarity index 94% rename from lib/default/TasmotaSerial-3.2.0/library.properties rename to lib/default/TasmotaSerial-3.3.0/library.properties index 30cc6138c..6b43764a6 100644 --- a/lib/default/TasmotaSerial-3.2.0/library.properties +++ b/lib/default/TasmotaSerial-3.3.0/library.properties @@ -1,5 +1,5 @@ name=TasmotaSerial -version=3.2.0 +version=3.3.0 author=Theo Arends maintainer=Theo Arends sentence=Implementation of software serial with hardware serial fallback for ESP8266 and ESP32. diff --git a/lib/default/TasmotaSerial-3.2.0/src/TasmotaSerial.cpp b/lib/default/TasmotaSerial-3.3.0/src/TasmotaSerial.cpp similarity index 94% rename from lib/default/TasmotaSerial-3.2.0/src/TasmotaSerial.cpp rename to lib/default/TasmotaSerial-3.3.0/src/TasmotaSerial.cpp index d49a5ff2a..253c3094b 100644 --- a/lib/default/TasmotaSerial-3.2.0/src/TasmotaSerial.cpp +++ b/lib/default/TasmotaSerial-3.3.0/src/TasmotaSerial.cpp @@ -109,16 +109,27 @@ bool TasmotaSerial::isValidGPIOpin(int pin) { return (pin >= -1 && pin <= 5) || (pin >= 12 && pin <= 15); } -bool TasmotaSerial::begin(long speed, int stop_bits) { - m_stop_bits = ((stop_bits -1) &1) +1; +bool TasmotaSerial::begin(uint32_t speed, uint32_t config) { + if (config > 2) { + // Legacy support where software serial fakes two stop bits if either stop bits is 2 or parity is not None + m_stop_bits = ((config &0x30) >> 5) +1; + if ((1 == m_stop_bits) && (config &0x03)) { + m_stop_bits++; + } + } else { + m_stop_bits = ((config -1) &1) +1; +#ifdef ESP8266 + config = (2 == m_stop_bits) ? (uint32_t)SERIAL_8N2 : (uint32_t)SERIAL_8N1; +#endif // ESP8266 +#ifdef ESP32 + config = (2 == m_stop_bits) ? SERIAL_8N2 : SERIAL_8N1; +#endif // ESP32 + } + if (m_hardserial) { #ifdef ESP8266 Serial.flush(); - if (2 == m_stop_bits) { - Serial.begin(speed, SERIAL_8N2); - } else { - Serial.begin(speed, SERIAL_8N1); - } + Serial.begin(speed, (SerialConfig)config); if (m_hardswap) { Serial.swap(); } @@ -131,11 +142,7 @@ bool TasmotaSerial::begin(long speed, int stop_bits) { m_uart = tasmota_serial_index; tasmota_serial_index--; TSerial = new HardwareSerial(m_uart); - if (2 == m_stop_bits) { - TSerial->begin(speed, SERIAL_8N2, m_rx_pin, m_tx_pin); - } else { - TSerial->begin(speed, SERIAL_8N1, m_rx_pin, m_tx_pin); - } + TSerial->begin(speed, config, m_rx_pin, m_tx_pin); if (serial_buffer_size > 256) { TSerial->setRxBufferSize(serial_buffer_size); } @@ -154,10 +161,6 @@ bool TasmotaSerial::begin(long speed, int stop_bits) { return m_valid; } -bool TasmotaSerial::begin(void) { - return begin(TM_SERIAL_BAUDRATE); -} - bool TasmotaSerial::hardwareSerial(void) { #ifdef ESP8266 return m_hardserial; diff --git a/lib/default/TasmotaSerial-3.2.0/src/TasmotaSerial.h b/lib/default/TasmotaSerial-3.3.0/src/TasmotaSerial.h similarity index 97% rename from lib/default/TasmotaSerial-3.2.0/src/TasmotaSerial.h rename to lib/default/TasmotaSerial-3.3.0/src/TasmotaSerial.h index 432344321..3a90626fe 100644 --- a/lib/default/TasmotaSerial-3.2.0/src/TasmotaSerial.h +++ b/lib/default/TasmotaSerial-3.3.0/src/TasmotaSerial.h @@ -40,8 +40,7 @@ class TasmotaSerial : public Stream { TasmotaSerial(int receive_pin, int transmit_pin, int hardware_fallback = 0, int nwmode = 0, int buffer_size = TM_SERIAL_BUFFER_SIZE); virtual ~TasmotaSerial(); - bool begin(long speed, int stop_bits = 1); - bool begin(void); + bool begin(uint32_t speed = TM_SERIAL_BAUDRATE, uint32_t config = SERIAL_8N1); bool hardwareSerial(void); int peek(void); From 18ed1d900783b1bab70d54e0a6e477a081940be8 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 2 Mar 2021 12:40:14 +0100 Subject: [PATCH 14/25] Fix comment error --- tasmota/user_config_override_sample.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/user_config_override_sample.h b/tasmota/user_config_override_sample.h index 8c8e02b7e..ae434bb9c 100644 --- a/tasmota/user_config_override_sample.h +++ b/tasmota/user_config_override_sample.h @@ -82,7 +82,7 @@ Examples : #define WIFI_DNS MY_DNS // If not using DHCP set DNS IP address (might be equal to WIFI_GATEWAY) #endif -// !!! Place your changes AFTER the line with "*/" !!! +// !!! Remember that your changes GOES AT THE BOTTOM OF THIS FILE right before the last #endif !!! */ From 2cef4cdcf48356fda5c43822700619016bda0cb9 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Tue, 2 Mar 2021 15:04:59 +0100 Subject: [PATCH 15/25] Zigbee fix crash when bad frame is received --- tasmota/xdrv_23_zigbee_9_serial.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/xdrv_23_zigbee_9_serial.ino b/tasmota/xdrv_23_zigbee_9_serial.ino index c8fce4df8..9b70496eb 100644 --- a/tasmota/xdrv_23_zigbee_9_serial.ino +++ b/tasmota/xdrv_23_zigbee_9_serial.ino @@ -265,7 +265,7 @@ void ZigbeeInputLoop(void) { } } else { // the buffer timed-out, print error and discard - AddLog_P(LOG_LEVEL_INFO, PSTR(D_JSON_ZIGBEE_EZSP_RECEIVED ": time-out, discarding %s, %_B"), zigbee_buffer); + AddLog_P(LOG_LEVEL_INFO, PSTR(D_JSON_ZIGBEE_EZSP_RECEIVED ": time-out, discarding %_B"), zigbee_buffer); } zigbee_buffer->setLen(0); // empty buffer escape = false; From 8f3f1dcf31222cc513ccf604f9379a97a08f6058 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 2 Mar 2021 18:09:54 +0100 Subject: [PATCH 16/25] Initial CSE7761 support Initial CSE7761 support (#10793) --- tasmota/xnrg_19_cse7761.ino | 362 ++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 tasmota/xnrg_19_cse7761.ino diff --git a/tasmota/xnrg_19_cse7761.ino b/tasmota/xnrg_19_cse7761.ino new file mode 100644 index 000000000..3f214b691 --- /dev/null +++ b/tasmota/xnrg_19_cse7761.ino @@ -0,0 +1,362 @@ +/* + xnrg_19_cse7761.ino - CSE7761 energy sensor support for Tasmota + + Copyright (C) 2021 Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_ENERGY_SENSOR +#ifdef USE_CSE7761 +/*********************************************************************************************\ + * CSE7761 - Energy (Sonoff Dual R3 Pow) + * + * Based on datasheet from ChipSea +\*********************************************************************************************/ + +#define XNRG_19 19 + +#define CSE7761_K1 2 // Current channel sampling resistance in milli Ohm +#define CSE7761_K2 2 // Voltage divider resistance in 1k/1M + +#define CSE7761_2POWER22 4194304 +#define CSE7761_2POWER23 8388608 +#define CSE7761_2POWER31 2147483648 + +enum CSE7761 { RmsIAC, RmsIBC, RmsUC, PowerPAC, PowerPBC, PowerSC, EnergyAc, EnergyBC }; + +#include + +TasmotaSerial *Cse7761Serial = nullptr; + +struct { + uint32_t voltage_rms = 0; + uint32_t current_rms[2] = { 0 }; + uint32_t active_power[2] = { 0 }; + uint16_t coefficient[8] = { 0 }; + uint8_t init = 0; + bool found = false; +} CSE7761Data; + +void Cse7761Write(uint32_t reg, uint32_t data) { + uint8_t buffer[5]; + + buffer[0] = 0xA5; + buffer[1] = reg; + uint32_t len = 2; + if (data) { + if (data < 0xFF) { + buffer[2] = data & 0xFF; + len = 3; + } else { + buffer[2] = (data >> 8) & 0xFF; + buffer[3] = data & 0xFF; + len = 4; + } + uint8_t crc = 0; + for (uint32_t i = 0; i < len; i++) { + crc += buffer[i]; + } + buffer[len] = ~crc; + len++; + } + Cse7761Serial->write(buffer, len); + + AddLog(LOG_LEVEL_DEBUG, PSTR("C61: Send %d, Data %*_H"), len, len, buffer); +} + +uint32_t Cse7761Read(uint32_t reg, uint32_t request) { + delay(3); + Cse7761Write(reg, 0); + + uint8_t buffer[5]; + uint32_t rcvd = 0; + uint32_t timeout = millis() + 3; + while (!TimeReached(timeout) && (rcvd <= request) && (rcvd <= sizeof(buffer))) { + int value = Cse7761Serial->read(); + if (value > -1) { + buffer[rcvd++] = value; + } + } + + if (!rcvd) { + AddLog(LOG_LEVEL_DEBUG, PSTR("C61: Rcvd %d"), rcvd); + return 0; + } + + AddLog(LOG_LEVEL_DEBUG, PSTR("C61: Rcvd %d, Data %*_H"), rcvd, rcvd, buffer); + + int result = 0; + uint8_t crc = 0xA5 + reg; + for (uint32_t i = 0; i < rcvd -1; i++) { + result = (result << 8) | buffer[i]; + crc += buffer[i]; + } + crc = ~crc; + if (crc != buffer[rcvd]) { + result = 0; + } + + return result; +} + +bool Cse7761ChipInit(void) { + uint16_t calc_chksum = 0xFFFF; + for (uint32_t i = 0; i < 8; i++) { + CSE7761Data.coefficient[i] = Cse7761Read(0x70 + i, 2); + calc_chksum += CSE7761Data.coefficient[i]; + } + uint16_t dummy = Cse7761Read(0x6E, 2); + uint16_t coeff_chksum = Cse7761Read(0x6F, 2); + if (calc_chksum != coeff_chksum) { + AddLog(LOG_LEVEL_DEBUG, PSTR("C61: Coefficients invalid")); +// return false; + } + + delay(3); + Cse7761Write(0xEA, 0xE5); // Enable write operation + delay(5); + uint8_t sys_status = Cse7761Read(0x43, 1); + if (sys_status & 0x10) { // Write enable to protected registers (WREN) + delay(3); +/* + System Control Register (SYSCON) Addr:0x00 Default value: 0x0A04 + Bit name Function description + 15-11 NC -, the default is 1 + 10 ADC2ON + =1, means ADC current channel B is on (Sonoff Dual R3 Pow) + =0, means ADC current channel B is closed + 9 NC -, the default is 1. + 8-6 PGAIB[2:0] Current channel B analog gain selection highest bit + =1XX, PGA of current channel B=16 + =011, PGA of current channel B=8 + =010, PGA of current channel B=4 + =001, PGA of current channel B=2 + =000, PGA of current channel B=1 (Sonoff Dual R3 Pow) + 5-3 PGAU[2:0] Highest bit of voltage channel analog gain selection + =1XX, PGA of current channel U=16 + =011, PGA of current channel U=8 + =010, PGA of current channel U=4 + =001, PGA of current channel U=2 (Sonoff Dual R3 Pow) + =000, PGA of current channel U=1 + 2-0 PGAIA[2:0] Current channel A analog gain selection highest bit + =1XX, PGA of current channel A=16 + =011, PGA of current channel A=8 + =010, PGA of current channel A=4 + =001, PGA of current channel A=2 + =000, PGA of current channel A=1 (Sonoff Dual R3 Pow) +*/ + Cse7761Write(0x80, 0xFF04); // Set SYSCON +/* + Energy Measure Control Register (EMUCON) Addr:0x01 Default value: 0x0000 + Bit name Function description + 15-14 Tsensor_Step[1:0] Measurement steps of temperature sensor: + =2'b00 The first step of temperature sensor measurement, the Offset of OP1 and OP2 is +/+. (Sonoff Dual R3 Pow) + =2'b01 The second step of temperature sensor measurement, the Offset of OP1 and OP2 is +/-. + =2'b10 The third step of temperature sensor measurement, the Offset of OP1 and OP2 is -/+. + =2'b11 The fourth step of temperature sensor measurement, the Offset of OP1 and OP2 is -/-. + After measuring these four results and averaging, the AD value of the current measured temperature can be obtained. + 13 tensor_en Temperature measurement module control + =0 when the temperature measurement module is closed; (Sonoff Dual R3 Pow) + =1 when the temperature measurement module is turned on; + 12 comp_off Comparator module close signal: + =0 when the comparator module is in working state + =1 when the comparator module is off (Sonoff Dual R3 Pow) + 11-10 Pmode[1:0] Selection of active energy calculation method: + Pmode =00, both positive and negative active energy participate in the accumulation, + the accumulation method is algebraic sum mode, the reverse REVQ symbol indicates to active power; (Sonoff Dual R3 Pow) + Pmode = 01, only accumulate positive active energy; + Pmode = 10, both positive and negative active energy participate in the accumulation, + and the accumulation method is absolute value method. No reverse active power indication; + Pmode =11, reserved, the mode is the same as Pmode =00 + 9 NC - + 8 ZXD1 The initial value of ZX output is 0, and different waveforms are output according to the configuration of ZXD1 and ZXD0: + =0, it means that the ZX output changes only at the selected zero-crossing point (Sonoff Dual R3 Pow) + =1, indicating that the ZX output changes at both the positive and negative zero crossings + 7 ZXD0 + =0, indicates that the positive zero-crossing point is selected as the zero-crossing detection signal (Sonoff Dual R3 Pow) + =1, indicating that the negative zero-crossing point is selected as the zero-crossing detection signal + 6 HPFIBOFF + =0, enable current channel B digital high-pass filter (Sonoff Dual R3 Pow) + =1, turn off the digital high-pass filter of current channel B + 5 HPFIAOFF + =0, enable current channel A digital high-pass filter (Sonoff Dual R3 Pow) + =1, turn off the digital high-pass filter of current channel A + 4 HPFUOFF + =0, enable U channel digital high pass filter (Sonoff Dual R3 Pow) + =1, turn off the U channel digital high-pass filter + 3-2 NC - + 1 PBRUN + =1, enable PFB pulse output and active energy register accumulation; (Sonoff Dual R3 Pow) + =0 (default), turn off PFB pulse output and active energy register accumulation. + 0 PARUN + =1, enable PFA pulse output and active energy register accumulation; (Sonoff Dual R3 Pow) + =0 (default), turn off PFA pulse output and active energy register accumulation. +*/ + Cse7761Write(0x81, 0x1003); // Set EMUCON +/* + Energy Measure Control Register (EMUCON2) Addr: 0x13 Default value: 0x0001 + Bit name Function description + 15-13 NC - + 12 SDOCmos + =1, SDO pin CMOS open-drain output (Sonoff Dual R3 Pow) + =0, SDO pin CMOS output + 11 EPB_CB Energy_PB clear signal control, the default is 0, and it needs to be configured to 1 in UART mode. + Clear after reading is not supported in UART mode + =1, Energy_PB will not be cleared after reading; (Sonoff Dual R3 Pow) + =0, Energy_PB is cleared after reading; + 10 EPA_CB Energy_PA clear signal control, the default is 0, it needs to be configured to 1 in UART mode, + Clear after reading is not supported in UART mode + =1, Energy_PA will not be cleared after reading; (Sonoff Dual R3 Pow) + =0, Energy_PA is cleared after reading; + 9-8 DUPSEL[1:0] Average register update frequency control + =00, Update frequency 3.4Hz + =01, Update frequency 6.8Hz + =10, Update frequency 13.65Hz + =11, Update frequency 27.3Hz (Sonoff Dual R3 Pow) + 7 CHS_IB Current channel B measurement selection signal + =1, measure the current of channel B (Sonoff Dual R3 Pow) + =0, measure the internal temperature of the chip + 6 PfactorEN Power factor function enable + =1, turn on the power factor output function (Sonoff Dual R3 Pow) + =0, turn off the power factor output function + 5 WaveEN Waveform data, instantaneous data output enable signal + =1, turn on the waveform data output function + =0, turn off the waveform data output function (Sonoff Dual R3 Pow) + 4 SAGEN Voltage drop detection enable signal, WaveEN=1 must be configured first + =1, turn on the voltage drop detection function + =0, turn off the voltage drop detection function (Sonoff Dual R3 Pow) + 3 OverEN Overvoltage, overcurrent, and overload detection enable signal, WaveEN=1 must be configured first + =1, turn on the overvoltage, overcurrent, and overload detection functions + =0, turn off the overvoltage, overcurrent, and overload detection functions (Sonoff Dual R3 Pow) + 2 ZxEN Zero-crossing detection, phase angle, voltage frequency measurement enable signal + =1, turn on the zero-crossing detection, phase angle, and voltage frequency measurement functions + =0, disable zero-crossing detection, phase angle, voltage frequency measurement functions (Sonoff Dual R3 Pow) + 1 PeakEN Peak detect enable signal + =1, turn on the peak detection function + =0, turn off the peak detection function (Sonoff Dual R3 Pow) + 0 NC Default is 1 +*/ + Cse7761Write(0x93, 0x0FC1); // Set EMUCON2 + } else { + AddLog(LOG_LEVEL_DEBUG, PSTR("C61: Write enable failed")); +// return false; + } + delay(80); + Cse7761Write(0xEA, 0xDC); // Close write operation + return true; +} + +void Cse7761GetData(void) { + CSE7761Data.voltage_rms = Cse7761Read(0x26, 3); + CSE7761Data.current_rms[0] = Cse7761Read(0x24, 3); + CSE7761Data.active_power[0] = Cse7761Read(0x2C, 4); + CSE7761Data.current_rms[1] = Cse7761Read(0x25, 3); + CSE7761Data.active_power[1] = Cse7761Read(0x2D, 4); + + if (Energy.power_on) { // Powered on + Energy.voltage[0] = ((float)CSE7761Data.voltage_rms * ((double)CSE7761Data.coefficient[RmsUC] / (CSE7761_K2 * 2 * CSE7761_2POWER22))) / 1000; // V + + for (uint32_t channel = 0; channel < 2; channel++) { + Energy.data_valid[channel] = 0; + Energy.active_power[channel] = (float)CSE7761Data.active_power[channel] * ((double)CSE7761Data.coefficient[PowerPAC + channel] / (CSE7761_K1 * CSE7761_K2 * 2 * CSE7761_2POWER31)); // W + if (0 == Energy.active_power[channel]) { + Energy.current[channel] = 0; + } else { + Energy.current[channel] = (float)CSE7761Data.current_rms[channel] * ((double)CSE7761Data.coefficient[RmsIAC + channel] / (CSE7761_K1 * 2 * CSE7761_2POWER23)); // mA + } + } + + uint32_t active_power_sum = (Energy.active_power[0] + Energy.active_power[1]) * 1000; + if (active_power_sum) { + Energy.kWhtoday_delta += active_power_sum / 36; + EnergyUpdateToday(); + } + } else { // Powered off + Energy.data_valid[0] = ENERGY_WATCHDOG; + Energy.data_valid[1] = ENERGY_WATCHDOG; + } +} + +/********************************************************************************************/ + +void Cse7761EverySecond(void) { + if (CSE7761Data.init) { + if (2 == CSE7761Data.init) { + Cse7761Write(0xEA, 0x96); // Reset chip + } + else if (1 == CSE7761Data.init) { + uint16_t syscon = Cse7761Read(0x00, 2); // Default 0x0A04 + if (0x0A04 == syscon) { + CSE7761Data.found = Cse7761ChipInit(); + } + if (CSE7761Data.found) { + AddLog(LOG_LEVEL_INFO, PSTR("C61: CSE7761 found")); + } + } + CSE7761Data.init--; + } + else { + if (CSE7761Data.found) { + Cse7761GetData(); + } + } +} + +void Cse7761SnsInit(void) { + // Software serial init needs to be done here as earlier (serial) interrupts may lead to Exceptions + Cse7761Serial = new TasmotaSerial(Pin(GPIO_CSE7766_RX), Pin(GPIO_CSE7766_TX), 1); + if (Cse7761Serial->begin(38400, SERIAL_8E1)) { + if (Cse7761Serial->hardwareSerial()) { +// SetSerial(38400, TS_SERIAL_8E1); + ClaimSerial(); + } + } else { + TasmotaGlobal.energy_driver = ENERGY_NONE; + } +} + +void Cse7761DrvInit(void) { + if (PinUsed(GPIO_CSE7761_RX) && PinUsed(GPIO_CSE7761_TX)) { + CSE7761Data.found = false; + CSE7761Data.init = 3; // Init setup steps + Energy.phase_count = 2; // Handle two channels as two phases + Energy.voltage_common = true; // Use common voltage + TasmotaGlobal.energy_driver = XNRG_19; + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xnrg19(uint8_t function) { + bool result = false; + + switch (function) { + case FUNC_EVERY_SECOND: + Cse7761EverySecond(); + break; + case FUNC_INIT: + Cse7761SnsInit(); + break; + case FUNC_PRE_INIT: + Cse7761DrvInit(); + break; + } + return result; +} + +#endif // USE_CSE7761 +#endif // USE_ENERGY_SENSOR From 67c1831a100353e123a58995c4b3113bda59a6f1 Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Tue, 2 Mar 2021 19:34:18 +0100 Subject: [PATCH 17/25] ili9341/2 software configurable --- .../src/renderer.cpp | 3 ++ .../Display_Renderer-gemu-1.0/src/renderer.h | 1 + .../ILI9341-gemu-1.0/ILI9341_2.cpp | 22 ++++++++ lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.h | 1 + tasmota/settings.h | 16 +++++- tasmota/tasmota_template.h | 4 +- tasmota/xdrv_13_display.ino | 34 +++++++++--- tasmota/xdsp_04_ili9341.ino | 53 ++++++++----------- 8 files changed, 91 insertions(+), 43 deletions(-) diff --git a/lib/lib_display/Display_Renderer-gemu-1.0/src/renderer.cpp b/lib/lib_display/Display_Renderer-gemu-1.0/src/renderer.cpp index 641645ef1..e63f3d673 100644 --- a/lib/lib_display/Display_Renderer-gemu-1.0/src/renderer.cpp +++ b/lib/lib_display/Display_Renderer-gemu-1.0/src/renderer.cpp @@ -549,6 +549,9 @@ void Renderer::setDrawMode(uint8_t mode) { void Renderer::invertDisplay(boolean i) { } +void Renderer::reverseDisplay(boolean i) { +} + void Renderer::setScrollMargins(uint16_t top, uint16_t bottom) { } diff --git a/lib/lib_display/Display_Renderer-gemu-1.0/src/renderer.h b/lib/lib_display/Display_Renderer-gemu-1.0/src/renderer.h index 507267d37..86aa98025 100644 --- a/lib/lib_display/Display_Renderer-gemu-1.0/src/renderer.h +++ b/lib/lib_display/Display_Renderer-gemu-1.0/src/renderer.h @@ -38,6 +38,7 @@ public: virtual void pushColors(uint16_t *data, uint16_t len, boolean first); virtual void setAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); virtual void invertDisplay(boolean i); + virtual void reverseDisplay(boolean i); virtual void setScrollMargins(uint16_t top, uint16_t bottom); virtual void scrollTo(uint16_t y); void setDrawMode(uint8_t mode); diff --git a/lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.cpp b/lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.cpp index eb6e3a277..a006d0e52 100644 --- a/lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.cpp +++ b/lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.cpp @@ -568,6 +568,28 @@ void ILI9341_2::invertDisplay(boolean i) { SPI_END_TRANSACTION(); } +void ILI9341_2::reverseDisplay(boolean i) { + SPI_BEGIN_TRANSACTION(); + ILI9341_2_CS_LOW + if (i) { + writecmd(ILI9341_2_FRMCTR1); + spiwrite(0x00); + spiwrite(0x13); + writecmd(ILI9341_2_MADCTL); + spiwrite(0x01); + spiwrite(0x08); + } else { + writecmd(ILI9341_2_FRMCTR1); + spiwrite(0x00); + spiwrite(0x18); + writecmd(ILI9341_2_MADCTL); + spiwrite(0x01); + spiwrite(0x48); + } + ILI9341_2_CS_HIGH + SPI_END_TRANSACTION(); +} + void ili9342_dimm(uint8_t dim); // dimmer 0-100 diff --git a/lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.h b/lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.h index 8c1874329..88843016c 100644 --- a/lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.h +++ b/lib/lib_display/ILI9341-gemu-1.0/ILI9341_2.h @@ -138,6 +138,7 @@ class ILI9341_2 : public Renderer { void dim(uint8_t dim); void pushColors(uint16_t *data, uint16_t len, boolean first); void invertDisplay(boolean i); + void reverseDisplay(boolean i); void spiwrite(uint8_t c); void spiwrite16(uint16_t c); void spiwrite32(uint32_t c); diff --git a/tasmota/settings.h b/tasmota/settings.h index cb70f0a27..c4c1d0467 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -327,6 +327,18 @@ typedef struct { uint8_t dpid = 0; } TuyaFnidDpidMap; +typedef union { + uint8_t data; + struct { + uint8_t ilimode : 3; + uint8_t Invert : 1; + uint8_t spare2 : 1; + uint8_t spare3 : 1; + uint8_t spare4 : 1; + uint8_t spare5 : 1; + }; +} DisplayOptions; + const uint32_t settings_text_size = 699; // Settings.text_pool[size] = Settings.display_model (2D2) - Settings.text_pool (017) const uint8_t MAX_TUYA_FUNCTIONS = 16; @@ -355,7 +367,6 @@ struct { uint8_t text_pool_290[66]; // 290 // End of single char array of 698 chars max **************** - uint8_t display_model; // 2D2 uint8_t display_mode; // 2D3 uint8_t display_refresh; // 2D4 @@ -375,8 +386,9 @@ struct { uint8_t param[PARAM8_SIZE]; // 2FC SetOption32 .. SetOption49 int16_t toffset[2]; // 30E uint8_t display_font; // 312 + DisplayOptions display_options; // 313 - uint8_t free_313[44]; // 313 + uint8_t free_314[43]; // 314 uint8_t tuyamcu_topic; // 33F Manage tuyaSend topic. ex_energy_power_delta on 6.6.0.20, replaced on 8.5.0.1 uint16_t domoticz_update_timer; // 340 diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index 4465baea4..c61329260 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -2630,7 +2630,7 @@ const mytmplt kModules[] PROGMEM = { AGPIO(GPIO_USER), // 2 IO GPIO2, SPKR_DATA AGPIO(GPIO_USER), // 3 IO RXD0 GPIO3, U0RXD AGPIO(GPIO_SDCARD_CS), // 4 IO GPIO4, SPI_CS_CARD - 0, // 5 IO GPIO5, SPI_CS_LCD + AGPIO(GPIO_ILI9341_CS), // 5 IO GPIO5, SPI_CS_LCD // 6 IO GPIO6, Flash CLK // 7 IO GPIO7, Flash D0 // 8 IO GPIO8, Flash D1 @@ -2640,7 +2640,7 @@ const mytmplt kModules[] PROGMEM = { 0, // 12 (I)O GPIO12, SPKR_CLK AGPIO(GPIO_USER), // 13 IO GPIO13, ADC2_CH4, TOUCH4, RTC_GPIO14, MTCK, HSPID, HS2_DATA3, SD_DATA3, EMAC_RX_ER AGPIO(GPIO_USER), // 14 IO GPIO14, ADC2_CH6, TOUCH6, RTC_GPIO16, MTMS, HSPICLK, HS2_CLK, SD_CLK, EMAC_TXD2 - 0, // 15 (I)O GPIO15, SPI_DC_LCD + AGPIO(GPIO_ILI9341_DC), // 15 (I)O GPIO15, SPI_DC_LCD 0, // 16 IO GPIO16, PSRAM_CS 0, // 17 IO GPIO17, PSRAM_CLK AGPIO(GPIO_SPI_CLK), // 18 IO GPIO18, SPI_CLK diff --git a/tasmota/xdrv_13_display.ino b/tasmota/xdrv_13_display.ino index c056f3570..1d319750b 100755 --- a/tasmota/xdrv_13_display.ino +++ b/tasmota/xdrv_13_display.ino @@ -82,7 +82,8 @@ const uint8_t DISPLAY_LOG_ROWS = 32; // Number of lines in display log #define D_CMND_DISP_SETLED "SetLED" #define D_CMND_DISP_BUTTONS "Buttons" #define D_CMND_DISP_SCROLLTEXT "ScrollText" - +#define D_CMND_DISP_ILIMODE "ILIMode" +#define D_CMND_DISP_ILIINVERT "Invert" enum XdspFunctions { FUNC_DISPLAY_INIT_DRIVER, FUNC_DISPLAY_INIT, FUNC_DISPLAY_EVERY_50_MSECOND, FUNC_DISPLAY_EVERY_SECOND, @@ -114,7 +115,7 @@ const char kDisplayCommands[] PROGMEM = D_PRFX_DISPLAY "|" // Prefix "|" D_CMND_DISP_CLEAR "|" D_CMND_DISP_NUMBER "|" D_CMND_DISP_FLOAT "|" D_CMND_DISP_NUMBERNC "|" D_CMND_DISP_FLOATNC "|" D_CMND_DISP_BRIGHTNESS "|" D_CMND_DISP_RAW "|" D_CMND_DISP_LEVEL "|" D_CMND_DISP_SEVENSEG_TEXT "|" D_CMND_DISP_SEVENSEG_TEXTNC "|" D_CMND_DISP_SCROLLDELAY "|" D_CMND_DISP_CLOCK "|" D_CMND_DISP_TEXTNC "|" D_CMND_DISP_SETLEDS "|" D_CMND_DISP_SETLED "|" - D_CMND_DISP_BUTTONS "|" D_CMND_DISP_SCROLLTEXT + D_CMND_DISP_BUTTONS "|" D_CMND_DISP_SCROLLTEXT "|" D_CMND_DISP_ILIMODE "|" D_CMND_DISP_ILIINVERT ; void (* const DisplayCommand[])(void) PROGMEM = { @@ -127,7 +128,7 @@ void (* const DisplayCommand[])(void) PROGMEM = { , &CmndDisplayClear, &CmndDisplayNumber, &CmndDisplayFloat, &CmndDisplayNumberNC, &CmndDisplayFloatNC, &CmndDisplayBrightness, &CmndDisplayRaw, &CmndDisplayLevel, &CmndDisplaySevensegText, &CmndDisplaySevensegTextNC, &CmndDisplayScrollDelay, &CmndDisplayClock, &CmndDisplayTextNC, &CmndDisplaySetLEDs, &CmndDisplaySetLED, - &CmndDisplayButtons, &CmndDisplayScrollText + &CmndDisplayButtons, &CmndDisplayScrollText, &CmndDisplayILIMOde , &CmndDisplayILIInvert }; char *dsp_str; @@ -1927,10 +1928,29 @@ void CmndDisplayFont(void) ResponseCmndNumber(Settings.display_font); } + +void CmndDisplayILIMOde(void) +{ + if ((XdrvMailbox.payload >= 1) && (XdrvMailbox.payload < 16)) { + Settings.display_options.ilimode = XdrvMailbox.payload; + TasmotaGlobal.restart_flag = 2; + } + ResponseCmndNumber(Settings.display_options.ilimode); +} + +void CmndDisplayILIInvert(void) +{ + if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { + Settings.display_options.Invert = XdrvMailbox.payload; + if (renderer) renderer->invertDisplay(Settings.display_options.Invert); + } + ResponseCmndNumber(Settings.display_options.Invert); +} + void CmndDisplayRotate(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload < 4)) { - if (Settings.display_rotate != XdrvMailbox.payload) { + if ((Settings.display_rotate) != XdrvMailbox.payload) { /* // Needs font info regarding height and width if ((Settings.display_rotate &1) != (XdrvMailbox.payload &1)) { @@ -2041,6 +2061,7 @@ char ppath[16]; } #endif + #ifdef ESP32 #ifdef JPEG_PICTS #include "img_converters.h" @@ -2672,7 +2693,7 @@ uint8_t rbutt=0; uint8_t vbutt=0; - if (touchp->touched()) { + if (touchp->touched()) { // did find a hit #if defined(USE_FT5206) pLoc = touchp->getPoint(0); @@ -2681,8 +2702,6 @@ uint8_t vbutt=0; #endif if (renderer) { - rotconvert(&pLoc.x, &pLoc.y); - #ifdef USE_M5STACK_CORE2 // handle 3 built in touch buttons uint16_t xcenter = 80; @@ -2703,6 +2722,7 @@ uint8_t vbutt=0; } #endif + rotconvert(&pLoc.x, &pLoc.y); // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("touch after convert %d - %d"), pLoc.x, pLoc.y); // now must compare with defined buttons diff --git a/tasmota/xdsp_04_ili9341.ino b/tasmota/xdsp_04_ili9341.ino index b5c36d1e4..a8f2ce402 100644 --- a/tasmota/xdsp_04_ili9341.ino +++ b/tasmota/xdsp_04_ili9341.ino @@ -19,7 +19,7 @@ #ifdef USE_SPI #ifdef USE_DISPLAY -#if (defined(USE_DISPLAY_ILI9341) || defined(USE_DISPLAY_ILI9342)) +#ifdef USE_DISPLAY_ILI9341 #define XDSP_04 4 @@ -37,21 +37,22 @@ uint8_t ili9342_ctouch_counter = 0; uint8_t ili9342_ctouch_counter = 0; #endif // USE_FT5206 + bool tft_init_done = false; +#define ILI9341_ID 1 +#define ILI9342_ID 2 + +//Settings.display_options.ilimode = ILI9341_ID; /*********************************************************************************************/ void ILI9341_InitDriver() { -#if (defined(USE_M5STACK_CORE2) || defined(USE_M5STACK_CORE_BASIC)) - if (TasmotaGlobal.spi_enabled) { -#else // There are displays without CS if (PinUsed(GPIO_ILI9341_CS) || PinUsed(GPIO_ILI9341_DC) && (TasmotaGlobal.spi_enabled || TasmotaGlobal.soft_spi_enabled)) { -#endif Settings.display_model = XDSP_04; @@ -65,35 +66,25 @@ void ILI9341_InitDriver() // disable screen buffer buffer = NULL; -#ifdef USE_DISPLAY_ILI9341 - uint8_t dtype = 1; -#else - uint8_t dtype = 3; // sign ili9342 with variable spi pins -#endif // USE_DISPLAY_ILI9341 + if (!Settings.display_options.ilimode) { + Settings.display_options.ilimode = ILI9341_ID; + } // default colors fg_color = ILI9341_WHITE; bg_color = ILI9341_BLACK; -#ifdef USE_M5STACK_CORE2 - // fixed pins on m5stack core2 - ili9341_2 = new ILI9341_2(5, -2, 15, -2); -#elif defined(USE_M5STACK_CORE_BASIC) - // int8_t cs, int8_t res, int8_t dc, int8_t bp) - ili9341_2 = new ILI9341_2(14, 33, 27, 32); -#else // check for special case with 2 SPI busses (ESP32 bitcoin) if (TasmotaGlobal.soft_spi_enabled) { // Init renderer, may use hardware spi, however we use SSPI defintion because SD card uses SPI definition (2 spi busses) if (PinUsed(GPIO_SSPI_MOSI) && PinUsed(GPIO_SSPI_MISO) && PinUsed(GPIO_SSPI_SCLK)) { - ili9341_2 = new ILI9341_2(Pin(GPIO_ILI9341_CS), Pin(GPIO_SSPI_MOSI), Pin(GPIO_SSPI_MISO), Pin(GPIO_SSPI_SCLK), Pin(GPIO_OLED_RESET), Pin(GPIO_ILI9341_DC), Pin(GPIO_BACKLIGHT), 2, dtype); + ili9341_2 = new ILI9341_2(Pin(GPIO_ILI9341_CS), Pin(GPIO_SSPI_MOSI), Pin(GPIO_SSPI_MISO), Pin(GPIO_SSPI_SCLK), Pin(GPIO_OLED_RESET), Pin(GPIO_ILI9341_DC), Pin(GPIO_BACKLIGHT), 2, Settings.display_options.ilimode & 3); } } else if (TasmotaGlobal.spi_enabled) { if (PinUsed(GPIO_ILI9341_DC)) { - ili9341_2 = new ILI9341_2(Pin(GPIO_ILI9341_CS), Pin(GPIO_SPI_MOSI), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_CLK), Pin(GPIO_OLED_RESET), Pin(GPIO_ILI9341_DC), Pin(GPIO_BACKLIGHT), 1, dtype); + ili9341_2 = new ILI9341_2(Pin(GPIO_ILI9341_CS), Pin(GPIO_SPI_MOSI), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_CLK), Pin(GPIO_OLED_RESET), Pin(GPIO_ILI9341_DC), Pin(GPIO_BACKLIGHT), 1, Settings.display_options.ilimode & 3); } } -#endif // USE_M5STACK_CORE2 if (ili9341_2 == nullptr) { AddLog(LOG_LEVEL_INFO, PSTR("DSP: ILI934x invalid GPIOs")); @@ -102,19 +93,20 @@ void ILI9341_InitDriver() ili9341_2->init(Settings.display_width, Settings.display_height); renderer = ili9341_2; + renderer->DisplayInit(DISPLAY_INIT_MODE, Settings.display_size, Settings.display_rotate, Settings.display_font); renderer->dim(Settings.display_dimmer); + if (Settings.display_options.ilimode & 4) { + renderer->reverseDisplay(1); + } + #ifdef SHOW_SPLASH // Welcome text renderer->setTextFont(2); renderer->setTextSize(1); renderer->setTextColor(ILI9341_WHITE, ILI9341_BLACK); -#ifdef USE_DISPLAY_ILI9341 - renderer->DrawStringAt(50, (Settings.display_height/2)-12, "ILI9341 TFT!", ILI9341_WHITE, 0); -#else - renderer->DrawStringAt(50, (Settings.display_height/2)-12, "ILI9342 TFT!", ILI9341_WHITE, 0); -#endif + renderer->DrawStringAt(50, (Settings.display_height/2)-12, (Settings.display_options.ilimode & 3)==ILI9341_ID?"ILI9341 TFT!":"ILI9342 TFT!", ILI9341_WHITE, 0); delay(1000); #endif // SHOW_SPLASH @@ -140,18 +132,15 @@ void ILI9341_InitDriver() #endif // ESP32 #ifdef USE_XPT2046 - Touch_Init(Pin(GPIO_XPT2046_CS)); + Touch_Init(Pin(GPIO_XPT2046_CS)); #endif tft_init_done = true; -#ifdef USE_DISPLAY_ILI9341 AddLog(LOG_LEVEL_INFO, PSTR("DSP: ILI9341")); -#else - AddLog(LOG_LEVEL_INFO, PSTR("DSP: ILI9342")); -#endif } } + void core2_disp_pwr(uint8_t on); void core2_disp_dim(uint8_t dim); @@ -235,7 +224,6 @@ ili9342_ctouch_counter++; if (2 == ili9342_ctouch_counter) { // every 100 ms should be enough ili9342_ctouch_counter = 0; - Touch_Check(TS_RotConvert); } } @@ -243,6 +231,7 @@ ili9342_ctouch_counter++; #endif // USE_FT5206 + #ifdef USE_DISPLAY_MODES1TO5 #define TFT_TOP 16 @@ -274,7 +263,7 @@ bool Ili9341Header(void) { void Ili9341InitMode(void) { // renderer->setRotation(Settings.display_rotate); // 0 #ifdef USE_DISPLAY_ILI9341 - renderer->invertDisplay(0); +// renderer->invertDisplay(0); #endif renderer->fillScreen(ILI9341_BLACK); renderer->setTextWrap(false); // Allow text to run off edges From 4ea1b4d7c985d4b352634976db2da9c8dcfa8a70 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Tue, 2 Mar 2021 21:16:30 +0100 Subject: [PATCH 18/25] Zigbee support for lumi.sensor_wleak --- tasmota/xdrv_23_zigbee_5_converters.ino | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index 182ebfe69..46bd5ea93 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -1739,6 +1739,10 @@ void ZCLFrame::syntheticAqaraSensor(Z_attribute_list &attr_list, class Z_attribu if (0x64 == attrid) { attr_list.addAttributePMEM(PSTR("SmokeDensity")).copyVal(attr); } + } else if (modelId.startsWith(F("lumi.sensor_wleak"))) { // gas leak + if (0x64 == attrid) { + attr_list.addAttributePMEM(PSTR("Water")).copyVal(attr); + } } else if (modelId.startsWith(F("lumi.sensor_natgas"))) { // gas leak if (0x64 == attrid) { attr_list.addAttributePMEM(PSTR("GasDensity")).copyVal(attr); From 4bfd22f9460a0cf02cb352f97a680366eef7d6a4 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Tue, 2 Mar 2021 21:18:08 +0100 Subject: [PATCH 19/25] Zigbee allow numbers as string --- .../jsmn-shadinger-1.0/src/JsonParser.cpp | 37 ++++++++++++++----- .../jsmn-shadinger-1.0/src/JsonParser.h | 3 ++ tasmota/xdrv_23_zigbee_1z_libs.ino | 14 +++++++ tasmota/xdrv_23_zigbee_A_impl.ino | 4 +- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/lib/default/jsmn-shadinger-1.0/src/JsonParser.cpp b/lib/default/jsmn-shadinger-1.0/src/JsonParser.cpp index 73703d6ed..41b79c868 100644 --- a/lib/default/jsmn-shadinger-1.0/src/JsonParser.cpp +++ b/lib/default/jsmn-shadinger-1.0/src/JsonParser.cpp @@ -26,18 +26,29 @@ const char * k_current_json_buffer = ""; +// returns nibble value or -1 if not an hex digit +static int32_t asc2byte(char chr) { + if (chr >= '0' && chr <= '9') { return chr - '0'; } + else if (chr >= 'A' && chr <= 'F') { return chr + 10 - 'A'; } + else if (chr >= 'a' && chr <= 'f') { return chr + 10 - 'a'; } + return -1; +} + /*********************************************************************************************\ * Lightweight String to Float, because atof() or strtof() takes 10KB * * To remove code, exponents are not parsed * (commented out below, just in case we need them after all) + * + * Moved to double to be able to parse 32 bits int as well without loss in accuracy \*********************************************************************************************/ // Inspired from https://searchcode.com/codesearch/view/22115068/ -float json_strtof(const char* s) { +double JsonParserToken::json_strtof(const char* s) { const char* p = s; - float value = 0.; + double value = 0.; int32_t sign = +1; - float factor; + double factor; + uint32_t base = 10; // support hex mode if start with Ox or OX // unsigned int expo; while (isspace(*p)){ // skip any leading white-spaces @@ -45,22 +56,30 @@ float json_strtof(const char* s) { } switch (*p) { - case '-': sign = -1; + case '-': sign = -1; // no break on purpose case '+': p++; default : break; } - while ((unsigned int)(*p - '0') < 10u) { - value = value*10 + (*p++ - '0'); + if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { // detect hex mode + base = 16; + p += 2; + } + + int32_t v; // temp nibble value + while ((v = asc2byte(*p)) >= 0) { + value = value * base + v; + p++; } if (*p == '.' ) { factor = 1.0f; p++; - while ((unsigned int)(*p - '0') < 10u) { - factor *= 0.1f; - value += (*p++ - '0') * factor; + while ((v = asc2byte(*p)) >= 0) { + factor /= base; + value += v * factor; + p++; } } diff --git a/lib/default/jsmn-shadinger-1.0/src/JsonParser.h b/lib/default/jsmn-shadinger-1.0/src/JsonParser.h index a31f28c91..03a0aba65 100644 --- a/lib/default/jsmn-shadinger-1.0/src/JsonParser.h +++ b/lib/default/jsmn-shadinger-1.0/src/JsonParser.h @@ -114,6 +114,9 @@ public: JsonParserObject getObject(void) const; JsonParserArray getArray(void) const; + // general parser from string to int/hex/float + static double json_strtof(const char* s); + public: // the following should be 'protected' but then it can't be accessed by iterators const jsmntok_t * t; diff --git a/tasmota/xdrv_23_zigbee_1z_libs.ino b/tasmota/xdrv_23_zigbee_1z_libs.ino index c2510a01e..98a9f3a7d 100644 --- a/tasmota/xdrv_23_zigbee_1z_libs.ino +++ b/tasmota/xdrv_23_zigbee_1z_libs.ino @@ -191,6 +191,9 @@ public: int32_t getInt(void) const; uint32_t getUInt(void) const; bool getBool(void) const; + // optimistically try to get any value as literal or in string - double to not lose precision for 32 bits + double getOptimisticDouble(void) const; + // const SBuffer * getRaw(void) const; // always return a point to a string, if not defined then empty string. @@ -437,6 +440,17 @@ JsonGeneratorArray & Z_attribute::newJsonArray(void) { } // get num values +double Z_attribute::getOptimisticDouble(void) const { + switch (type) { + case Za_type::Za_bool: + case Za_type::Za_uint: return (double) val.uval32; + case Za_type::Za_int: return (double) val.ival32; + case Za_type::Za_float: return (double) val.fval; + case Za_type::Za_str: return JsonParserToken::json_strtof(val.sval); + default: return 0.0; + } +} + float Z_attribute::getFloat(void) const { switch (type) { case Za_type::Za_bool: diff --git a/tasmota/xdrv_23_zigbee_A_impl.ino b/tasmota/xdrv_23_zigbee_A_impl.ino index fc8bfdc3c..00309085b 100644 --- a/tasmota/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/xdrv_23_zigbee_A_impl.ino @@ -235,7 +235,7 @@ void ZbApplyMultiplier(double &val_d, int8_t multiplier) { // Write Tuya-Moes attribute // bool ZbTuyaWrite(SBuffer & buf, const Z_attribute & attr) { - double val_d = attr.getFloat(); + double val_d = attr.getOptimisticDouble(); const char * val_str = attr.getStr(); if (attr.key_is_str) { return false; } // couldn't find attr if so skip @@ -294,7 +294,7 @@ bool ZbTuyaWrite(SBuffer & buf, const Z_attribute & attr) { // Send Attribute Write, apply mutlipliers before // bool ZbAppendWriteBuf(SBuffer & buf, const Z_attribute & attr, bool prepend_status_ok) { - double val_d = attr.getFloat(); + double val_d = attr.getOptimisticDouble(); const char * val_str = attr.getStr(); if (attr.key_is_str) { return false; } // couldn't find attr if so skip From 38e83113bb9f26e989f431f43d6ef7eefd24baa3 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Tue, 2 Mar 2021 21:43:16 +0100 Subject: [PATCH 20/25] Add crash protection --- lib/default/Ext-printf/src/ext_printf.cpp | 89 +++++++++++++---------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/lib/default/Ext-printf/src/ext_printf.cpp b/lib/default/Ext-printf/src/ext_printf.cpp index 157b2b30a..575689a02 100644 --- a/lib/default/Ext-printf/src/ext_printf.cpp +++ b/lib/default/Ext-printf/src/ext_printf.cpp @@ -212,6 +212,9 @@ char * copyStr(const char * str) { return cpy; } +const char ext_invalid_mem[] PROGMEM = "<--INVALID-->"; +const uint32_t min_valid_ptr = 0x3FF00000; // addresses below this line are invalid + int32_t ext_vsnprintf_P(char * buf, size_t buf_len, const char * fmt_P, va_list va) { va_list va_cpy; va_copy(va_cpy, va); @@ -222,7 +225,7 @@ int32_t ext_vsnprintf_P(char * buf, size_t buf_len, const char * fmt_P, va_list char * fmt = fmt_cpy; const uint32_t ALLOC_SIZE = 12; - static char * allocs[ALLOC_SIZE] = {}; // initialized to zeroes + static const char * allocs[ALLOC_SIZE] = {}; // initialized to zeroes uint32_t alloc_idx = 0; static char hex[20]; // buffer used for 64 bits, favor RAM instead of stack to remove pressure @@ -264,12 +267,13 @@ int32_t ext_vsnprintf_P(char * buf, size_t buf_len, const char * fmt_P, va_list fmt++; uint32_t cur_val = va_arg(va, uint32_t); // current value const char ** cur_val_ptr = va_cur_ptr4(va, const char*); // pointer to value on stack - char * new_val_str = (char*) ""; + const char * new_val_str = ""; switch (*fmt) { case 'H': // Hex, decimals indicates the length, default 2 { if (decimals < 0) { decimals = 0; } - if (decimals > 0) { + if (cur_val < min_valid_ptr) { new_val_str = ext_invalid_mem; } + else if (decimals > 0) { char * hex_char = (char*) malloc(decimals*2 + 2); ToHex_P((const uint8_t *)cur_val, decimals, hex_char, decimals*2 + 2); new_val_str = hex_char; @@ -280,13 +284,16 @@ int32_t ext_vsnprintf_P(char * buf, size_t buf_len, const char * fmt_P, va_list break; case 'B': // Pointer to SBuffer { - const SBuffer & buf = *(const SBuffer*)cur_val; - size_t buf_len = (&buf != nullptr) ? buf.len() : 0; - if (buf_len) { - char * hex_char = (char*) malloc(buf_len*2 + 2); - ToHex_P(buf.getBuffer(), buf_len, hex_char, buf_len*2 + 2); - new_val_str = hex_char; - allocs[alloc_idx++] = new_val_str; + if (cur_val < min_valid_ptr) { new_val_str = ext_invalid_mem; } + else { + const SBuffer & buf = *(const SBuffer*)cur_val; + size_t buf_len = (&buf != nullptr) ? buf.len() : 0; + if (buf_len) { + char * hex_char = (char*) malloc(buf_len*2 + 2); + ToHex_P(buf.getBuffer(), buf_len, hex_char, buf_len*2 + 2); + new_val_str = hex_char; + allocs[alloc_idx++] = new_val_str; + } } } break; @@ -315,40 +322,46 @@ int32_t ext_vsnprintf_P(char * buf, size_t buf_len, const char * fmt_P, va_list // Note: float MUST be passed by address, because C alsays promoted float to double when in vararg case 'f': // input is `float`, printed to float with 2 decimals { - bool truncate = false; - if (decimals < 0) { - decimals = -decimals; - truncate = true; - } - float number = *(float*)cur_val; - if (isnan(number) || isinf(number)) { - new_val_str = (char*) "null"; - } else { - dtostrf(*(float*)cur_val, (decimals + 2), decimals, hex); - - if (truncate) { - uint32_t last = strlen(hex) - 1; - // remove trailing zeros - while (hex[last] == '0') { - hex[last--] = 0; // remove last char - } - // remove trailing dot - if (hex[last] == '.') { - hex[last] = 0; - } + if (cur_val < min_valid_ptr) { new_val_str = ext_invalid_mem; } + else { + bool truncate = false; + if (decimals < 0) { + decimals = -decimals; + truncate = true; + } + float number = *(float*)cur_val; + if (isnan(number) || isinf(number)) { + new_val_str = "null"; + } else { + dtostrf(*(float*)cur_val, (decimals + 2), decimals, hex); + + if (truncate) { + uint32_t last = strlen(hex) - 1; + // remove trailing zeros + while (hex[last] == '0') { + hex[last--] = 0; // remove last char + } + // remove trailing dot + if (hex[last] == '.') { + hex[last] = 0; + } + } + new_val_str = copyStr(hex); + allocs[alloc_idx++] = new_val_str; } - new_val_str = copyStr(hex); - allocs[alloc_idx++] = new_val_str; } } break; // '%_X' outputs a 64 bits unsigned int to uppercase HEX with 16 digits case 'X': // input is `uint64_t*`, printed as 16 hex digits (no prefix 0x) { - if ((decimals < 0) || (decimals > 16)) { decimals = 16; } - U64toHex(*(uint64_t*)cur_val, hex, decimals); - new_val_str = copyStr(hex); - allocs[alloc_idx++] = new_val_str; + if (cur_val < min_valid_ptr) { new_val_str = ext_invalid_mem; } + else { + if ((decimals < 0) || (decimals > 16)) { decimals = 16; } + U64toHex(*(uint64_t*)cur_val, hex, decimals); + new_val_str = copyStr(hex); + allocs[alloc_idx++] = new_val_str; + } } break; // Trying to do String allocation alternatives, but not as interesting as I thought in the beginning @@ -382,7 +395,7 @@ int32_t ext_vsnprintf_P(char * buf, size_t buf_len, const char * fmt_P, va_list // disallocated all temporary strings for (uint32_t i = 0; i < alloc_idx; i++) { - free(allocs[i]); // it is ok to call free() on nullptr so we don't test for nullptr first + free((void*)allocs[i]); // it is ok to call free() on nullptr so we don't test for nullptr first allocs[i] = nullptr; } free(fmt_cpy); // free the local copy of the format string From d0168863a2b50c334a43a28f1dd68b7f7a4b0592 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Tue, 2 Mar 2021 22:38:41 +0100 Subject: [PATCH 21/25] Renamed CC2530 to CCxxxx --- tasmota/i18n.h | 2 +- tasmota/xdrv_23_zigbee_8_parsers.ino | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 9b7a59d33..2a4000d98 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -550,7 +550,7 @@ #define D_CMND_ZIGBEE_PERMITJOIN "PermitJoin" #define D_CMND_ZIGBEE_STATUS "Status" #define D_CMND_ZIGBEE_RESET "Reset" - #define D_JSON_ZIGBEE_CC2530 "CC2530" + #define D_JSON_ZIGBEE_CC2530 "CCxxxx" #define D_JSON_ZIGBEE_EZSP "EZSP" #define D_CMND_ZIGBEEZNPRECEIVE "ZNPReceive" // only for debug #define D_CMND_ZIGBEE_EZSP_RECEIVE "EZSPReceive" // only for debug diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index 39db75771..083e6c966 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -418,7 +418,7 @@ int32_t ZNP_Reboot(int32_t res, SBuffer &buf) { } Response_P(PSTR("{\"" D_JSON_ZIGBEE_STATE "\":{" - "\"Status\":%d,\"Message\":\"CC2530 booted\",\"RestartReason\":\"%s\"" + "\"Status\":%d,\"Message\":\"CCxxxx booted\",\"RestartReason\":\"%s\"" ",\"MajorRel\":%d,\"MinorRel\":%d}}"), ZIGBEE_STATUS_BOOT, reason_str, major_rel, minor_rel); From d8c59e1b12f4005db3a0786ea55b3b4a7a58bae5 Mon Sep 17 00:00:00 2001 From: Barbudor Date: Tue, 2 Mar 2021 22:47:40 +0100 Subject: [PATCH 22/25] add some setoptions override --- tasmota/my_user_config.h | 7 +++++++ tasmota/settings.ino | 10 +++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 33ced12ed..96808f6a0 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -275,8 +275,11 @@ #define KEY_SWAP_DOUBLE_PRESS false // [SetOption11] Swap button single and double press functionality #define KEY_ONLY_SINGLE_PRESS false // [SetOption13] Enable only single press to speed up button press recognition +#define MQTT_BUTTONS false // [SetOption73] Detach buttons from relays and send multi-press and hold MQTT messages instead + #define SWITCH_DEBOUNCE_TIME 50 // [SwitchDebounce] Number of mSeconds switch press debounce time #define SWITCH_MODE TOGGLE // [SwitchMode] TOGGLE, FOLLOW, FOLLOW_INV, PUSHBUTTON, PUSHBUTTON_INV, PUSHBUTTONHOLD, PUSHBUTTONHOLD_INV, PUSHBUTTON_TOGGLE, TOGGLEMULTI, FOLLOWMULTI, FOLLOWMULTI_INV (the wall switch state) +#define MQTT_SWITCHES false // [SetOption114] Detach switches from relays and send MQTT messages instead #define TEMP_CONVERSION false // [SetOption8] Return temperature in (false = Celsius or true = Fahrenheit) #define PRESSURE_CONVERSION false // [SetOption24] Return pressure in (false = hPa or true = mmHg) @@ -330,6 +333,7 @@ #define SHUTTER_SUPPORT false // [SetOption80] Enable shutter support #define PCF8574_INVERT_PORTS false // [SetOption81] Invert all ports on PCF8574 devices #define ZIGBEE_FRIENDLY_NAMES false // [SetOption83] Enable Zigbee FriendlyNames instead of ShortAddresses when possible +#define ZIGBEE_DISTINCT_TOPICS false // [SetOption89] Enable unique device topic based on Zigbee device ShortAddr #define ZIGBEE_RMV_ZBRECEIVED false // [SetOption100] Remove ZbReceived form JSON message #define ZIGBEE_INDEX_EP false // [SetOption101] Add the source endpoint as suffix to attributes, ex `Power3` instead of `Power` if sent from endpoint 3 @@ -515,6 +519,9 @@ // -- I2C sensors --------------------------------- #define USE_I2C // I2C using library wire (+10k code, 0k2 mem, 124 iram) +#define I2CDRIVERS_0_31 0xFFFFFFFF // Enable I2CDriver0 to I2CDriver31 +#define I2CDRIVERS_32_63 0xFFFFFFFF // Enable I2CDriver32 to I2CDriver63 +#define I2CDRIVERS_64_95 0xFFFFFFFF // Enable I2CDriver64 to I2CDriver95 #ifdef USE_I2C // #define USE_SHT // [I2cDriver8] Enable SHT1X sensor (+1k4 code) diff --git a/tasmota/settings.ino b/tasmota/settings.ino index fe5316e9f..db0b620fc 100644 --- a/tasmota/settings.ino +++ b/tasmota/settings.ino @@ -726,6 +726,7 @@ void SettingsDefaultSet2(void) { flag3.no_power_feedback |= APP_NO_RELAY_SCAN; flag3.fast_power_cycle_disable |= APP_DISABLE_POWERCYCLE; flag3.bootcount_update |= DEEPSLEEP_BOOTCOUNT; + flag3.mqtt_buttons |= MQTT_BUTTONS; Settings.save_data = SAVE_DATA; Settings.param[P_BACKLOG_DELAY] = MIN_BACKLOG_DELAY; Settings.param[P_BOOT_LOOP_OFFSET] = BOOT_LOOP_OFFSET; // SetOption36 @@ -833,6 +834,7 @@ void SettingsDefaultSet2(void) { flag.mqtt_sensor_retain |= MQTT_SENSOR_RETAIN; flag5.mqtt_info_retain |= MQTT_INFO_RETAIN; flag5.mqtt_state_retain |= MQTT_STATE_RETAIN; + flag5.mqtt_switches |= MQTT_SWITCHES; // flag.mqtt_serial |= 0; flag.device_index_enable |= MQTT_POWER_FORMAT; flag3.time_append_timezone |= MQTT_APPEND_TIMEZONE; @@ -1058,6 +1060,7 @@ void SettingsDefaultSet2(void) { flag3.shutter_mode |= SHUTTER_SUPPORT; flag3.pcf8574_ports_inverted |= PCF8574_INVERT_PORTS; flag4.zigbee_use_names |= ZIGBEE_FRIENDLY_NAMES; + flag4.zigbee_distinct_topics |= ZIGBEE_DISTINCT_TOPICS; flag4.remove_zbreceived |= ZIGBEE_RMV_ZBRECEIVED; flag4.zb_index_ep |= ZIGBEE_INDEX_EP; flag4.mqtt_tls |= MQTT_TLS_ENABLED; @@ -1072,6 +1075,7 @@ void SettingsDefaultSet2(void) { Settings.flag2 = flag2; Settings.flag3 = flag3; Settings.flag4 = flag4; + Settings.flag5 = flag5; } /********************************************************************************************/ @@ -1102,9 +1106,9 @@ void SettingsDefaultWebColor(void) { } void SettingsEnableAllI2cDrivers(void) { - Settings.i2c_drivers[0] = 0xFFFFFFFF; - Settings.i2c_drivers[1] = 0xFFFFFFFF; - Settings.i2c_drivers[2] = 0xFFFFFFFF; + Settings.i2c_drivers[0] = I2CDRIVERS_0_31; + Settings.i2c_drivers[1] = I2CDRIVERS_32_63; + Settings.i2c_drivers[2] = I2CDRIVERS_64_95; } /********************************************************************************************/ From 8aa8ba1474e027fec1e93f73a58bce1bab49b288 Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Wed, 3 Mar 2021 06:04:45 +0100 Subject: [PATCH 23/25] remove code test --- tasmota/xdsp_04_ili9341.ino | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tasmota/xdsp_04_ili9341.ino b/tasmota/xdsp_04_ili9341.ino index a8f2ce402..599964ab9 100644 --- a/tasmota/xdsp_04_ili9341.ino +++ b/tasmota/xdsp_04_ili9341.ino @@ -97,10 +97,6 @@ void ILI9341_InitDriver() renderer->DisplayInit(DISPLAY_INIT_MODE, Settings.display_size, Settings.display_rotate, Settings.display_font); renderer->dim(Settings.display_dimmer); - if (Settings.display_options.ilimode & 4) { - renderer->reverseDisplay(1); - } - #ifdef SHOW_SPLASH // Welcome text renderer->setTextFont(2); From d5ef4afceba2a74f87076a1243357b5c256f06af Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Wed, 3 Mar 2021 08:34:38 +0100 Subject: [PATCH 24/25] Berry improvements --- lib/libesp32/Berry-0.1.10/src/be_gc.c | 6 ++ lib/libesp32/Berry-0.1.10/src/be_vm.c | 10 ++++ lib/libesp32/Berry-0.1.10/src/be_vm.h | 3 + lib/libesp32/Berry-0.1.10/src/berry.h | 11 ++++ .../Berry-0.1.10/src/port/be_tasmotalib.c | 2 + .../Berry-0.1.10/src/port/be_wirelib.c | 3 + .../Berry-0.1.10/src/port/berry_conf.h | 6 ++ tasmota/xdrv_52_3_berry_native.ino | 29 ++++++++++ tasmota/xdrv_52_7_berry_embedded.ino | 25 ++------- tasmota/xdrv_52_9_berry.ino | 55 +++++++++++++++---- 10 files changed, 121 insertions(+), 29 deletions(-) diff --git a/lib/libesp32/Berry-0.1.10/src/be_gc.c b/lib/libesp32/Berry-0.1.10/src/be_gc.c index 37abd1f94..8993cbaf7 100644 --- a/lib/libesp32/Berry-0.1.10/src/be_gc.c +++ b/lib/libesp32/Berry-0.1.10/src/be_gc.c @@ -504,6 +504,9 @@ void be_gc_collect(bvm *vm) if (vm->gc.status & GC_HALT) { return; /* the GC cannot run for some reason */ } +#if BE_USE_OBSERVABILITY_HOOK + if (vm->obshook != NULL) (*vm->obshook)(vm, BE_OBS_GC_START, vm->gc.usage); +#endif /* step 1: set root-set reference objects to unscanned */ premark_internal(vm); /* object internal the VM */ premark_global(vm); /* global objects */ @@ -520,4 +523,7 @@ void be_gc_collect(bvm *vm) reset_fixedlist(vm); /* step 5: calculate the next GC threshold */ vm->gc.threshold = next_threshold(vm->gc); +#if BE_USE_OBSERVABILITY_HOOK + if (vm->obshook != NULL) (*vm->obshook)(vm, BE_OBS_GC_END, vm->gc.usage); +#endif } diff --git a/lib/libesp32/Berry-0.1.10/src/be_vm.c b/lib/libesp32/Berry-0.1.10/src/be_vm.c index 06b80cb7f..fd45cabfb 100644 --- a/lib/libesp32/Berry-0.1.10/src/be_vm.c +++ b/lib/libesp32/Berry-0.1.10/src/be_vm.c @@ -398,6 +398,9 @@ BERRY_API bvm* be_vm_new(void) be_globalvar_init(vm); be_gc_setpause(vm, 1); be_loadlibs(vm); +#if BE_USE_OBSERVABILITY_HOOK + vm->obshook = NULL; +#endif return vm; } @@ -1051,3 +1054,10 @@ void be_dofunc(bvm *vm, bvalue *v, int argc) default: call_error(vm, v); } } + +BERRY_API void be_set_obs_hook(bvm *vm, beobshook hook) +{ +#if BE_USE_OBSERVABILITY_HOOK + vm->obshook = hook; +#endif +} \ No newline at end of file diff --git a/lib/libesp32/Berry-0.1.10/src/be_vm.h b/lib/libesp32/Berry-0.1.10/src/be_vm.h index 9c806fd2d..562587100 100644 --- a/lib/libesp32/Berry-0.1.10/src/be_vm.h +++ b/lib/libesp32/Berry-0.1.10/src/be_vm.h @@ -86,6 +86,9 @@ struct bvm { bmap *ntvclass; /* native class table */ blist *registry; /* registry list */ struct bgc gc; +#if BE_USE_OBSERVABILITY_HOOK + beobshook obshook; +#endif #if BE_USE_DEBUG_HOOK bvalue hook; bbyte hookmask; diff --git a/lib/libesp32/Berry-0.1.10/src/berry.h b/lib/libesp32/Berry-0.1.10/src/berry.h index f3eb7cde6..40ceecab4 100644 --- a/lib/libesp32/Berry-0.1.10/src/berry.h +++ b/lib/libesp32/Berry-0.1.10/src/berry.h @@ -273,6 +273,14 @@ typedef void(*bntvhook)(bvm *vm, bhookinfo *info); #define be_assert(expr) ((void)0) #endif +/* Observability hook */ + +typedef void(*beobshook)(bvm *vm, int event, ...); +enum beobshookevents { + BE_OBS_GC_START, // start of GC, arg = allocated size + BE_OBS_GC_END, // end of GC, arg = allocated size +}; + /* FFI functions */ #define be_writestring(s) be_writebuffer((s), strlen(s)) #define be_writenewline() be_writebuffer("\n", 1) @@ -406,6 +414,9 @@ BERRY_API void be_regclass(bvm *vm, const char *name, const bnfuncinfo *lib); BERRY_API bvm* be_vm_new(void); BERRY_API void be_vm_delete(bvm *vm); +/* Observability hook */ +BERRY_API void be_set_obs_hook(bvm *vm, beobshook hook); + /* code load APIs */ BERRY_API int be_loadbuffer(bvm *vm, const char *name, const char *buffer, size_t length); diff --git a/lib/libesp32/Berry-0.1.10/src/port/be_tasmotalib.c b/lib/libesp32/Berry-0.1.10/src/port/be_tasmotalib.c index 098cb3844..bb02f19ca 100644 --- a/lib/libesp32/Berry-0.1.10/src/port/be_tasmotalib.c +++ b/lib/libesp32/Berry-0.1.10/src/port/be_tasmotalib.c @@ -18,6 +18,7 @@ extern int l_respCmndStr(bvm *vm); extern int l_respCmndDone(bvm *vm); extern int l_respCmndError(bvm *vm); extern int l_respCmndFailed(bvm *vm); +extern int l_resolveCmnd(bvm *vm); // #if !BE_USE_PRECOMPILED_OBJECT #if 1 // TODO we will do pre-compiled later @@ -36,6 +37,7 @@ be_native_module_attr_table(tasmota_ntv) { be_native_module_function("respcmnd_done", l_respCmndDone), be_native_module_function("respcmnd_error", l_respCmndError), be_native_module_function("respcmnd_failed", l_respCmndFailed), + be_native_module_function("resolvecmnd", l_resolveCmnd), be_native_module_str("_operators", "=<>!|"), }; diff --git a/lib/libesp32/Berry-0.1.10/src/port/be_wirelib.c b/lib/libesp32/Berry-0.1.10/src/port/be_wirelib.c index b52865710..0d3cd51f4 100644 --- a/lib/libesp32/Berry-0.1.10/src/port/be_wirelib.c +++ b/lib/libesp32/Berry-0.1.10/src/port/be_wirelib.c @@ -14,6 +14,8 @@ extern int b_wire_available(bvm *vm); extern int b_wire_write(bvm *vm); extern int b_wire_read(bvm *vm); +extern int b_wire_validread(bvm *vm); + // #if !BE_USE_PRECOMPILED_OBJECT #if 1 // TODO we will do pre-compiled later be_native_module_attr_table(wire) { @@ -23,6 +25,7 @@ be_native_module_attr_table(wire) { be_native_module_function("available", b_wire_available), be_native_module_function("write", b_wire_write), be_native_module_function("read", b_wire_read), + be_native_module_function("validread", b_wire_validread), }; be_define_native_module(wire, NULL); diff --git a/lib/libesp32/Berry-0.1.10/src/port/berry_conf.h b/lib/libesp32/Berry-0.1.10/src/port/berry_conf.h index 94f0c27b3..e3720b4d1 100644 --- a/lib/libesp32/Berry-0.1.10/src/port/berry_conf.h +++ b/lib/libesp32/Berry-0.1.10/src/port/berry_conf.h @@ -64,6 +64,12 @@ **/ #define BE_DEBUG_VAR_INFO 0 +/* Macro: BE_USE_OBSERVABILITY_HOOK + * Use the obshook function to report low-level actions. + * Default: 0 + **/ +#define BE_USE_OBSERVABILITY_HOOK 1 + /* Macro: BE_STACK_TOTAL_MAX * Set the maximum total stack size. * Default: 20000 diff --git a/tasmota/xdrv_52_3_berry_native.ino b/tasmota/xdrv_52_3_berry_native.ino index 22cf0b9f1..8897603a4 100644 --- a/tasmota/xdrv_52_3_berry_native.ino +++ b/tasmota/xdrv_52_3_berry_native.ino @@ -177,6 +177,17 @@ extern "C" { ResponseCmndFailed(); be_return_nil(vm); } + + // update XdrvMailbox.command with actual command + int32_t l_resolveCmnd(bvm *vm); + int32_t l_resolveCmnd(bvm *vm) { + int32_t top = be_top(vm); // Get the number of arguments + if (top == 1 && be_isstring(vm, 1)) { + const char *msg = be_tostring(vm, 1); + strlcpy(XdrvMailbox.command, msg, CMDSZ); + } + be_return_nil(vm); // Return nil when something goes wrong + } } /*********************************************************************************************\ @@ -277,6 +288,24 @@ extern "C" { be_return_nil(vm); // Return nil when something goes wrong } + // Berry: `validread(address:int, reg:int, size:int) -> int or nil` + int32_t b_wire_validread(struct bvm *vm); + int32_t b_wire_validread(struct bvm *vm) { + int32_t top = be_top(vm); // Get the number of arguments + if (top == 3 && be_isint(vm, 1) && be_isint(vm, 2) && be_isint(vm, 3)) { + uint8_t addr = be_toint(vm, 1); + uint8_t reg = be_toint(vm, 2); + uint8_t size = be_toint(vm, 3); + bool ok = I2cValidRead(addr, reg, size); + if (ok) { + be_pushint(vm, i2c_buffer); + } else { + be_pushnil(vm); + } + be_return(vm); // Return + } + be_return_nil(vm); // Return nil when something goes wrong + } } /*********************************************************************************************\ diff --git a/tasmota/xdrv_52_7_berry_embedded.ino b/tasmota/xdrv_52_7_berry_embedded.ino index 4904210d1..43b33652c 100644 --- a/tasmota/xdrv_52_7_berry_embedded.ino +++ b/tasmota/xdrv_52_7_berry_embedded.ino @@ -44,7 +44,7 @@ const char berry_prog[] = // Map all native functions to methods // Again, this will be eventually pre-compiled "var getfreeheap, publish, cmd, getoption, millis, timereached, yield " - "var respcmnd, respcmndstr, respcmnd_done, respcmnd_error, respcmnd_failed " + "var respcmnd, respcmndstr, respcmnd_done, respcmnd_error, respcmnd_failed, resolvecmnd " "def init_ntv() " "import tasmota_ntv " "self.getfreeheap = tasmota_ntv.getfreeheap " @@ -61,6 +61,7 @@ const char berry_prog[] = "self.respcmnd_done = tasmota_ntv.respcmnd_done " "self.respcmnd_error = tasmota_ntv.respcmnd_error " "self.respcmnd_failed = tasmota_ntv.respcmnd_failed " + "self.resolvecmnd = tasmota_ntv.resolvecmnd " "end " "def init() " @@ -207,8 +208,11 @@ const char berry_prog[] = "var payload_json = json.load(payload) " "var cmd_found = self.findkeyi(self._cmd, cmd) " "if cmd_found != nil " - "return self._cmd[cmd_found](cmd_found, idx, payload, payload_json) " + "self.resolvecmnd(cmd_found) " // set the command name in XdrvMailbox.command + "self._cmd[cmd_found](cmd_found, idx, payload, payload_json) " + "return true " "end " + "return false " "end " // Force gc and return allocated memory @@ -259,23 +263,6 @@ const char berry_prog[] = // "try compile('/autoexec.be','file')() except .. log('BRY: no /autoexec.bat file') end " // Wire - "wire.validread = def(addr, reg, size) " - "var ret = nil " - "for i:0..2 " - "wire.begintransmission(addr) " - "wire.write(reg) " - "if wire.endtransmission(false) == 0 " - "wire.requestfrom(addr, size) " - "if wire.available() == size " - "for j:0..size-1 " - "ret = ((ret != nil ? ret : 0) << 8) + wire.read() " - "end " - "return ret " - "end " - "end " - "end " - "wire.endtransmission() " - "end " ; #endif // USE_BERRY diff --git a/tasmota/xdrv_52_9_berry.ino b/tasmota/xdrv_52_9_berry.ino index 346572d9b..666b36818 100644 --- a/tasmota/xdrv_52_9_berry.ino +++ b/tasmota/xdrv_52_9_berry.ino @@ -102,7 +102,7 @@ bool callBerryRule(void) { } bool callBerryCommand(void) { - const char * command = nullptr; + bool serviced = false; checkBeTop(); be_getglobal(berry.vm, "_exec_cmd"); @@ -111,16 +111,21 @@ bool callBerryCommand(void) { be_pushint(berry.vm, XdrvMailbox.index); be_pushstring(berry.vm, XdrvMailbox.data); int ret = be_pcall(berry.vm, 3); - command = be_tostring(berry.vm, 3); - strlcpy(XdrvMailbox.topic, command, CMDSZ); - // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_BERRY "Command (%s) serviced=%d"), XdrvMailbox.command, serviced); + // AddLog(LOG_LEVEL_INFO, "callBerryCommand: top=%d", be_top(berry.vm)); + // AddLog(LOG_LEVEL_INFO, "callBerryCommand: type(1)=%s", be_typename(berry.vm, 1)); + // AddLog(LOG_LEVEL_INFO, "callBerryCommand: type(2)=%s", be_typename(berry.vm, 2)); + // AddLog(LOG_LEVEL_INFO, "callBerryCommand: type(3)=%s", be_typename(berry.vm, 3)); + // AddLog(LOG_LEVEL_INFO, "callBerryCommand: type(4)=%s", be_typename(berry.vm, 4)); + // AddLog(LOG_LEVEL_INFO, "callBerryCommand: type(5)=%s", be_typename(berry.vm, 5)); + serviced = be_tobool(berry.vm, 1); // return value is in slot 1 + // AddLog(LOG_LEVEL_INFO, "callBerryCommand: serviced=%d", serviced); be_pop(berry.vm, 4); // remove function object } else { be_pop(berry.vm, 1); // remove nil object } checkBeTop(); - return command != nullptr; // TODO event not handled + return serviced; // TODO event not handled } size_t callBerryGC(void) { @@ -173,6 +178,35 @@ void callBerryFunctionVoid(const char * fname) { checkBeTop(); } +/*********************************************************************************************\ + * VM Observability +\*********************************************************************************************/ +void BerryObservability(bvm *vm, int32_t event...); +void BerryObservability(bvm *vm, int32_t event...) { + va_list param; + va_start(param, event); + static int32_t vm_usage = 0; + static uint32_t gc_time = 0; + + switch (event) { + case BE_OBS_GC_START: + { + gc_time = millis(); + vm_usage = va_arg(param, int32_t); + } + break; + case BE_OBS_GC_END: + { + int32_t vm_usage2 = va_arg(param, int32_t); + uint32_t gc_elapsed = millis() - gc_time; + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_BERRY "GC from %i to %i bytes (in %d ms)"), vm_usage, vm_usage2, gc_elapsed); + } + break; + default: break; + } + va_end(param); +} + /*********************************************************************************************\ * VM Init \*********************************************************************************************/ @@ -191,6 +225,7 @@ void BrReset(void) { bool berry_init_ok = false; do { berry.vm = be_vm_new(); /* create a virtual machine instance */ + be_set_obs_hook(berry.vm, &BerryObservability); // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_BERRY "Berry VM created, RAM used=%u"), be_gc_memcount(berry.vm)); // Register functions @@ -264,12 +299,12 @@ void CmndBrRun(void) { } while (0); if (0 == ret_code) { + // AddLog(LOG_LEVEL_INFO, "run: top=%d", be_top(berry.vm)); + // AddLog(LOG_LEVEL_INFO, "run: type(1)=%s", be_typename(berry.vm, 1)); + // AddLog(LOG_LEVEL_INFO, "run: type(2)=%s", be_typename(berry.vm, 2)); + // code taken from REPL, look first at top, and if nil, look at return value - if (be_isnil(berry.vm, 0)) { - ret_val = be_tostring(berry.vm, -1); - } else { - ret_val = be_tostring(berry.vm, 0); - } + ret_val = be_tostring(berry.vm, 1); Response_P("{\"" D_PRFX_BR "\":\"%s\"}", ret_val); // can't use XdrvMailbox.command as it may have been overwritten by subcommand be_pop(berry.vm, 1); } else { From 2addbca761f32b3706ed5bdeca4e266e56374f5c Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Wed, 3 Mar 2021 12:44:09 +0100 Subject: [PATCH 25/25] Fix initial CSE7761 support --- tasmota/my_user_config.h | 2 ++ tasmota/xnrg_19_cse7761.ino | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 96808f6a0..a89fd65eb 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -890,6 +890,8 @@ //#define USE_IBEACON_ESP32 //#define USE_WEBCAM // Add support for webcam +#define USE_CSE7761 // Add support for CSE7761 Energy monitor as used in Sonoff Dual R3 + #endif // ESP32 /*********************************************************************************************\ diff --git a/tasmota/xnrg_19_cse7761.ino b/tasmota/xnrg_19_cse7761.ino index 3f214b691..e83dc09b5 100644 --- a/tasmota/xnrg_19_cse7761.ino +++ b/tasmota/xnrg_19_cse7761.ino @@ -71,21 +71,22 @@ void Cse7761Write(uint32_t reg, uint32_t data) { buffer[len] = ~crc; len++; } + Cse7761Serial->write(buffer, len); AddLog(LOG_LEVEL_DEBUG, PSTR("C61: Send %d, Data %*_H"), len, len, buffer); } uint32_t Cse7761Read(uint32_t reg, uint32_t request) { - delay(3); + Cse7761Serial->flush(); Cse7761Write(reg, 0); - uint8_t buffer[5]; + uint8_t buffer[8]; uint32_t rcvd = 0; uint32_t timeout = millis() + 3; - while (!TimeReached(timeout) && (rcvd <= request) && (rcvd <= sizeof(buffer))) { + while (!TimeReached(timeout)) { int value = Cse7761Serial->read(); - if (value > -1) { + if ((value > -1) && (rcvd < sizeof(buffer) -1)) { buffer[rcvd++] = value; } } @@ -316,7 +317,7 @@ void Cse7761EverySecond(void) { void Cse7761SnsInit(void) { // Software serial init needs to be done here as earlier (serial) interrupts may lead to Exceptions - Cse7761Serial = new TasmotaSerial(Pin(GPIO_CSE7766_RX), Pin(GPIO_CSE7766_TX), 1); + Cse7761Serial = new TasmotaSerial(Pin(GPIO_CSE7761_RX), Pin(GPIO_CSE7761_TX), 1); if (Cse7761Serial->begin(38400, SERIAL_8E1)) { if (Cse7761Serial->hardwareSerial()) { // SetSerial(38400, TS_SERIAL_8E1);