From 1cfdd9e8b4ddbc0289b2e50d6de02d66a00c2b27 Mon Sep 17 00:00:00 2001 From: svollebregt <42436341+svollebregt@users.noreply.github.com> Date: Sun, 10 Oct 2021 20:17:13 +0200 Subject: [PATCH 01/17] Adding Itho library to main reposity Moving P145 from the playgorund to the main repo as P118. --- .gitignore | 1 + lib/Itho/CC1101.cpp | 276 ++++++++ lib/Itho/CC1101.h | 207 ++++++ lib/Itho/CC1101Packet.h | 23 + lib/Itho/IthoCC1101.cpp | 849 +++++++++++++++++++++++ lib/Itho/IthoCC1101.h | 116 ++++ lib/Itho/IthoPacket.h | 49 ++ platformio.ini | 4 +- src/_P118_Itho.ino | 548 +++++++++++++++ src/src/CustomBuild/define_plugin_sets.h | 49 +- 10 files changed, 2096 insertions(+), 26 deletions(-) create mode 100644 lib/Itho/CC1101.cpp create mode 100644 lib/Itho/CC1101.h create mode 100644 lib/Itho/CC1101Packet.h create mode 100644 lib/Itho/IthoCC1101.cpp create mode 100644 lib/Itho/IthoCC1101.h create mode 100644 lib/Itho/IthoPacket.h create mode 100644 src/_P118_Itho.ino diff --git a/.gitignore b/.gitignore index 981c8ae04..6eefe271d 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ build_output/ **/__tmpfile.cpp src/_Custom.h +platformio.ini diff --git a/lib/Itho/CC1101.cpp b/lib/Itho/CC1101.cpp new file mode 100644 index 000000000..903a6cab7 --- /dev/null +++ b/lib/Itho/CC1101.cpp @@ -0,0 +1,276 @@ +/* + * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 + */ + +#include "CC1101.h" + +// default constructor +CC1101::CC1101() +{ + SPI.begin(); +#ifdef ESP8266 + pinMode(SS, OUTPUT); +#endif +} //CC1101 + +// default destructor +CC1101::~CC1101() +{ +} //~CC1101 + +/***********************/ +// SPI helper functions select() and deselect() +inline void CC1101::select(void) { + digitalWrite(SS, LOW); +} + +inline void CC1101::deselect(void) { + digitalWrite(SS, HIGH); +} + +void CC1101::spi_waitMiso() +{ + while(digitalRead(MISO) == HIGH) yield(); +} + +void CC1101::init() +{ + reset(); +} + +void CC1101::reset() +{ + deselect(); + delayMicroseconds(5); + select(); + delayMicroseconds(10); + deselect(); + delayMicroseconds(45); + select(); + + spi_waitMiso(); + SPI.transfer(CC1101_SRES); + delay(10); + spi_waitMiso(); + deselect(); +} + +uint8_t CC1101::writeCommand(uint8_t command) +{ + uint8_t result; + + select(); + spi_waitMiso(); + result = SPI.transfer(command); + deselect(); + + return result; +} + +void CC1101::writeRegister(uint8_t address, uint8_t data) +{ + select(); + spi_waitMiso(); + SPI.transfer(address); + SPI.transfer(data); + deselect(); +} + +uint8_t CC1101::readRegister(uint8_t address) +{ + uint8_t val; + + select(); + spi_waitMiso(); + SPI.transfer(address); + val = SPI.transfer(0); + deselect(); + + return val; +} + +uint8_t CC1101::readRegisterMedian3(uint8_t address) +{ + uint8_t val, val1, val2, val3; + + select(); + spi_waitMiso(); + SPI.transfer(address); + val1 = SPI.transfer(0); + SPI.transfer(address); + val2 = SPI.transfer(0); + SPI.transfer(address); + val3 = SPI.transfer(0); + deselect(); + // reverse sort (largest in val1) because this is te expected order for TX_BUFFER + if (val3 > val2) {val = val3; val3 = val2; val2 = val; } //Swap(val3,val2) + if (val2 > val1) {val = val2; val2 = val1, val1 = val; } //Swap(val2,val1) + if (val3 > val2) {val = val3; val3 = val2, val2 = val; } //Swap(val3,val2) + + return val2; +} + +/* Known SPI/26MHz synchronization bug (see CC1101 errata) +This issue affects the following registers: SPI status byte (fields STATE and FIFO_BYTES_AVAILABLE), +FREQEST or RSSI while the receiver is active, MARCSTATE at any time other than an IDLE radio state, +RXBYTES when receiving or TXBYTES when transmitting, and WORTIME1/WORTIME0 at any time.*/ +//uint8_t CC1101::readRegisterWithSyncProblem(uint8_t address, uint8_t registerType) +uint8_t /* ICACHE_RAM_ATTR */ CC1101::readRegisterWithSyncProblem(uint8_t address, uint8_t registerType) +{ + uint8_t value1, value2; + + value1 = readRegister(address | registerType); + + //if two consecutive reads gives us the same result then we know we are ok + do + { + value2 = value1; + value1 = readRegister(address | registerType); + } + while (value1 != value2); + + return value1; +} + +//registerType = CC1101_CONFIG_REGISTER or CC1101_STATUS_REGISTER +uint8_t CC1101::readRegister(uint8_t address, uint8_t registerType) +{ + switch (address) + { + case CC1101_FREQEST: + case CC1101_MARCSTATE: + case CC1101_RXBYTES: + case CC1101_TXBYTES: + case CC1101_WORTIME1: + case CC1101_WORTIME0: + return readRegisterWithSyncProblem(address, registerType); + + default: + return readRegister(address | registerType); + } +} + +void CC1101::writeBurstRegister(uint8_t address, uint8_t* data, uint8_t length) +{ + uint8_t i; + + select(); + spi_waitMiso(); + SPI.transfer(address | CC1101_WRITE_BURST); + for (i = 0; i < length; i++) { + SPI.transfer(data[i]); + } + deselect(); +} + +void CC1101::readBurstRegister(uint8_t* buffer, uint8_t address, uint8_t length) +{ + uint8_t i; + + select(); + spi_waitMiso(); + SPI.transfer(address | CC1101_READ_BURST); + + for (i = 0; i < length; i++) { + buffer[i] = SPI.transfer(0x00); + } + + deselect(); +} + +//wait for fixed length in rx fifo +uint8_t CC1101::receiveData(CC1101Packet* packet, uint8_t length) +{ + uint8_t rxBytes = readRegisterWithSyncProblem(CC1101_RXBYTES, CC1101_STATUS_REGISTER); + rxBytes = rxBytes & CC1101_BITS_RX_BYTES_IN_FIFO; + + //check for rx fifo overflow + if ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) & CC1101_BITS_MARCSTATE) == CC1101_MARCSTATE_RXFIFO_OVERFLOW) + { + writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SFRX); //flush RX buffer + writeCommand(CC1101_SRX); //switch to RX state + } + else if (rxBytes == length) + { + readBurstRegister(packet->data, CC1101_RXFIFO, rxBytes); + + //continue RX + writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SFRX); //flush RX buffer + writeCommand(CC1101_SRX); //switch to RX state + + packet->length = rxBytes; + } + else + { + //empty fifo + packet->length = 0; + writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SFRX); //flush RX buffer + writeCommand(CC1101_SRX); //switch to RX state + } + + return packet->length; +} + +//This function is able to send packets bigger then the FIFO size. +void CC1101::sendData(CC1101Packet *packet) +{ + uint8_t index = 0; + uint8_t txStatus, MarcState; + uint8_t length; + + writeCommand(CC1101_SIDLE); //idle + + txStatus = readRegisterWithSyncProblem(CC1101_TXBYTES, CC1101_STATUS_REGISTER); + + //clear TX fifo if needed + if (txStatus & CC1101_BITS_TX_FIFO_UNDERFLOW) + { + writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SFTX); //flush TX buffer + } + + writeCommand(CC1101_SIDLE); //idle + + //determine how many bytes to send + length = (packet->length <= CC1101_DATA_LEN ? packet->length : CC1101_DATA_LEN); + + writeBurstRegister(CC1101_TXFIFO, packet->data, length); + + writeCommand(CC1101_SIDLE); + //start sending packet + writeCommand(CC1101_STX); + + //continue sending when packet is bigger than 64 bytes + if (packet->length > CC1101_DATA_LEN) + { + index += length; + + //loop until all bytes are transmitted + while (index < packet->length) + { + //check if there is free space in the fifo + while ((txStatus = (readRegisterMedian3(CC1101_TXBYTES | CC1101_STATUS_REGISTER) & CC1101_BITS_RX_BYTES_IN_FIFO)) > (CC1101_DATA_LEN - 2)); + + //calculate how many bytes we can send + length = (CC1101_DATA_LEN - txStatus); + length = ((packet->length - index) < length ? (packet->length - index) : length); + + //send some more bytes + for (int i=0; idata[index+i]); + + index += length; + } + } + + //wait until transmission is finished (TXOFF_MODE is expected to be set to 0/IDLE or TXFIFO_UNDERFLOW) + do + { + MarcState = (readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) & CC1101_BITS_MARCSTATE); +// if (MarcState == CC1101_MARCSTATE_TXFIFO_UNDERFLOW) Serial.print(F("TXFIFO_UNDERFLOW occured in sendData() \n")); + } + while((MarcState != CC1101_MARCSTATE_IDLE) && (MarcState != CC1101_MARCSTATE_TXFIFO_UNDERFLOW)); +} diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h new file mode 100644 index 000000000..b30e0162a --- /dev/null +++ b/lib/Itho/CC1101.h @@ -0,0 +1,207 @@ +/* + * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 + */ + +#ifndef __CC1101_H__ +#define __CC1101_H__ + +#include +#include "CC1101Packet.h" +#include +// On Arduino, SPI pins are predefined + +/* Type of transfers */ +#define CC1101_WRITE_BURST 0x40 +#define CC1101_READ_SINGLE 0x80 +#define CC1101_READ_BURST 0xC0 + +/* Type of register */ +#define CC1101_CONFIG_REGISTER CC1101_READ_SINGLE +#define CC1101_STATUS_REGISTER CC1101_READ_BURST + +/* PATABLE & FIFO's */ +#define CC1101_PATABLE 0x3E // PATABLE address +#define CC1101_TXFIFO 0x3F // TX FIFO address +#define CC1101_RXFIFO 0x3F // RX FIFO address +#define CC1101_PA_LowPower 0x60 +#define CC1101_PA_LongDistance 0xC0 + +/* Command strobes */ +#define CC1101_SRES 0x30 // Reset CC1101 chip +#define CC1101_SFSTXON 0x31 // Enable and calibrate frequency synthesizer (if MCSM0.FS_AUTOCAL=1). If in RX (with CCA): Go to a wait state where only the synthesizer is running (for quick RX / TX turnaround). +#define CC1101_SXOFF 0x32 // Turn off crystal oscillator +#define CC1101_SCAL 0x33 // Calibrate frequency synthesizer and turn it off. SCAL can be strobed from IDLE mode without setting manual calibration mode (MCSM0.FS_AUTOCAL=0) +#define CC1101_SRX 0x34 // Enable RX. Perform calibration first if coming from IDLE and MCSM0.FS_AUTOCAL=1 +#define CC1101_STX 0x35 // In IDLE state: Enable TX. Perform calibration first if MCSM0.FS_AUTOCAL=1. If in RX state and CCA is enabled: Only go to TX if channel is clear +#define CC1101_SIDLE 0x36 // Exit RX / TX, turn off frequency synthesizer and exit Wake-On-Radio mode if applicable +#define CC1101_SWOR 0x38 // Start automatic RX polling sequence (Wake-on-Radio) as described in Section 19.5 if WORCTRL.RC_PD=0 +#define CC1101_SPWD 0x39 // Enter power down mode when CSn goes high +#define CC1101_SFRX 0x3A // Flush the RX FIFO buffer. Only issue SFRX in IDLE or RXFIFO_OVERFLOW states +#define CC1101_SFTX 0x3B // Flush the TX FIFO buffer. Only issue SFTX in IDLE or TXFIFO_UNDERFLOW states +#define CC1101_SWORRST 0x3C // Reset real time clock to Event1 value +#define CC1101_SNOP 0x3D // No operation. May be used to get access to the chip status byte + +/* CC1101 configuration registers */ +#define CC1101_IOCFG2 0x00 // GDO2 Output Pin Configuration +#define CC1101_IOCFG1 0x01 // GDO1 Output Pin Configuration +#define CC1101_IOCFG0 0x02 // GDO0 Output Pin Configuration +#define CC1101_FIFOTHR 0x03 // RX FIFO and TX FIFO Thresholds +#define CC1101_SYNC1 0x04 // Sync Word, High Byte +#define CC1101_SYNC0 0x05 // Sync Word, Low Byte +#define CC1101_PKTLEN 0x06 // Packet Length +#define CC1101_PKTCTRL1 0x07 // Packet Automation Control +#define CC1101_PKTCTRL0 0x08 // Packet Automation Control +#define CC1101_ADDR 0x09 // Device Address +#define CC1101_CHANNR 0x0A // Channel Number +#define CC1101_FSCTRL1 0x0B // Frequency Synthesizer Control +#define CC1101_FSCTRL0 0x0C // Frequency Synthesizer Control +#define CC1101_FREQ2 0x0D // Frequency Control Word, High Byte +#define CC1101_FREQ1 0x0E // Frequency Control Word, Middle Byte +#define CC1101_FREQ0 0x0F // Frequency Control Word, Low Byte +#define CC1101_MDMCFG4 0x10 // Modem Configuration +#define CC1101_MDMCFG3 0x11 // Modem Configuration +#define CC1101_MDMCFG2 0x12 // Modem Configuration +#define CC1101_MDMCFG1 0x13 // Modem Configuration +#define CC1101_MDMCFG0 0x14 // Modem Configuration +#define CC1101_DEVIATN 0x15 // Modem Deviation Setting +#define CC1101_MCSM2 0x16 // Main Radio Control State Machine Configuration +#define CC1101_MCSM1 0x17 // Main Radio Control State Machine Configuration +#define CC1101_MCSM0 0x18 // Main Radio Control State Machine Configuration +#define CC1101_FOCCFG 0x19 // Frequency Offset Compensation Configuration +#define CC1101_BSCFG 0x1A // Bit Synchronization Configuration +#define CC1101_AGCCTRL2 0x1B // AGC Control +#define CC1101_AGCCTRL1 0x1C // AGC Control +#define CC1101_AGCCTRL0 0x1D // AGC Control +#define CC1101_WOREVT1 0x1E // High Byte Event0 Timeout +#define CC1101_WOREVT0 0x1F // Low Byte Event0 Timeout +#define CC1101_WORCTRL 0x20 // Wake On Radio Control +#define CC1101_FREND1 0x21 // Front End RX Configuration +#define CC1101_FREND0 0x22 // Front End TX Configuration +#define CC1101_FSCAL3 0x23 // Frequency Synthesizer Calibration +#define CC1101_FSCAL2 0x24 // Frequency Synthesizer Calibration +#define CC1101_FSCAL1 0x25 // Frequency Synthesizer Calibration +#define CC1101_FSCAL0 0x26 // Frequency Synthesizer Calibration +#define CC1101_RCCTRL1 0x27 // RC Oscillator Configuration +#define CC1101_RCCTRL0 0x28 // RC Oscillator Configuration +#define CC1101_FSTEST 0x29 // Frequency Synthesizer Calibration Control +#define CC1101_PTEST 0x2A // Production Test +#define CC1101_AGCTEST 0x2B // AGC Test +#define CC1101_TEST2 0x2C // Various Test Settings +#define CC1101_TEST1 0x2D // Various Test Settings +#define CC1101_TEST0 0x2E // Various Test Settings + +/* Status registers */ +#define CC1101_PARTNUM 0x30 // Chip ID +#define CC1101_VERSION 0x31 // Chip ID +#define CC1101_FREQEST 0x32 // Frequency Offset Estimate from Demodulator +#define CC1101_LQI 0x33 // Demodulator Estimate for Link Quality +#define CC1101_RSSI 0x34 // Received Signal Strength Indication +#define CC1101_MARCSTATE 0x35 // Main Radio Control State Machine State +#define CC1101_WORTIME1 0x36 // High Byte of WOR Time +#define CC1101_WORTIME0 0x37 // Low Byte of WOR Time +#define CC1101_PKTSTATUS 0x38 // Current GDOx Status and Packet Status +#define CC1101_VCO_VC_DAC 0x39 // Current Setting from PLL Calibration Module +#define CC1101_TXBYTES 0x3A // Underflow and Number of Bytes +#define CC1101_RXBYTES 0x3B // Overflow and Number of Bytes +#define CC1101_RCCTRL1_STATUS 0x3C // Last RC Oscillator Calibration Result +#define CC1101_RCCTRL0_STATUS 0x3D // Last RC Oscillator Calibration Result + +/* Bit fields in the chip status byte */ +#define CC1101_STATUS_CHIP_RDYn_BM 0x80 +#define CC1101_STATUS_STATE_BM 0x70 +#define CC1101_STATUS_FIFO_BYTES_AVAILABLE_BM 0x0F + +/* Masks to retrieve status bit */ +#define CC1101_BITS_TX_FIFO_UNDERFLOW 0x80 +#define CC1101_BITS_RX_BYTES_IN_FIFO 0x7F +#define CC1101_BITS_MARCSTATE 0x1F + + +/* Marc states */ +enum CC1101MarcStates +{ + CC1101_MARCSTATE_SLEEP = 0x00, + CC1101_MARCSTATE_IDLE = 0x01, + CC1101_MARCSTATE_XOFF = 0x02, + CC1101_MARCSTATE_VCOON_MC = 0x03, + CC1101_MARCSTATE_REGON_MC = 0x04, + CC1101_MARCSTATE_MANCAL = 0x05, + CC1101_MARCSTATE_VCOON = 0x06, + CC1101_MARCSTATE_REGON = 0x07, + CC1101_MARCSTATE_STARTCAL = 0x08, + CC1101_MARCSTATE_BWBOOST = 0x09, + CC1101_MARCSTATE_FS_LOCK = 0x0A, + CC1101_MARCSTATE_IFADCON = 0x0B, + CC1101_MARCSTATE_ENDCAL = 0x0C, + CC1101_MARCSTATE_RX = 0x0D, + CC1101_MARCSTATE_RX_END = 0x0E, + CC1101_MARCSTATE_RX_RST = 0x0F, + CC1101_MARCSTATE_TXRX_SWITCH = 0x10, + CC1101_MARCSTATE_RXFIFO_OVERFLOW = 0x11, + CC1101_MARCSTATE_FSTXON = 0x12, + CC1101_MARCSTATE_TX = 0x13, + CC1101_MARCSTATE_TX_END = 0x14, + CC1101_MARCSTATE_RXTX_SWITCH = 0x15, + CC1101_MARCSTATE_TXFIFO_UNDERFLOW = 0x16 +}; + + +/* Chip states */ +enum CC1101ChipStates +{ + CC1101_STATE_MASK = 0x70, + CC1101_STATE_IDLE = 0x00, + CC1101_STATE_RX = 0x10, + CC1101_STATE_TX = 0x20, + CC1101_STATE_FSTXON = 0x30, + CC1101_STATE_CALIBRATE = 0x40, + CC1101_STATE_SETTLING = 0x50, + CC1101_STATE_RX_OVERFLOW = 0x60, + CC1101_STATE_TX_UNDERFLOW = 0x70 +}; + + + +class CC1101 +{ + protected: + + //functions + public: + CC1101(); + ~CC1101(); + + //spi + void spi_waitMiso(); + + //cc1101 + void init(); + + uint8_t writeCommand(uint8_t command); + void writeRegister(uint8_t address, uint8_t data); + + uint8_t readRegister(uint8_t address, uint8_t registerType); + + void writeBurstRegister(uint8_t address, uint8_t* data, uint8_t length); + void readBurstRegister(uint8_t* buffer, uint8_t address, uint8_t length); + + void sendData(CC1101Packet *packet); + uint8_t receiveData(CC1101Packet* packet, uint8_t length); + + private: + CC1101( const CC1101 &c ); + CC1101& operator=( const CC1101 &c ); + // SPI helper functions + void select(void); + void deselect(void); + + protected: + uint8_t readRegister(uint8_t address); + uint8_t readRegisterMedian3(uint8_t address); + uint8_t readRegisterWithSyncProblem(uint8_t address, uint8_t registerType); + + void reset(); + +}; //CC1101 + +#endif //__CC1101_H__ diff --git a/lib/Itho/CC1101Packet.h b/lib/Itho/CC1101Packet.h new file mode 100644 index 000000000..f8c4f01b4 --- /dev/null +++ b/lib/Itho/CC1101Packet.h @@ -0,0 +1,23 @@ +/* + * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 + */ + +#ifndef CC1101PACKET_H_ +#define CC1101PACKET_H_ + +#include +#include + +#define CC1101_BUFFER_LEN 64 +#define CC1101_DATA_LEN CC1101_BUFFER_LEN - 3 + + +class CC1101Packet +{ + public: + uint8_t length; + uint8_t data[128]; +}; + + +#endif /* CC1101PACKET_H_ */ diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp new file mode 100644 index 000000000..97d158879 --- /dev/null +++ b/lib/Itho/IthoCC1101.cpp @@ -0,0 +1,849 @@ +/* + Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra +*/ +#define DEBUG 0 + +#define BYTE_TO_BINARY_PATTERN "%c,%c,%c,%c,%c,%c,%c,%c," +#define BYTE_TO_BINARY(byte) \ + (byte & 0x80 ? '1' : '0'), \ + (byte & 0x40 ? '1' : '0'), \ + (byte & 0x20 ? '1' : '0'), \ + (byte & 0x10 ? '1' : '0'), \ + (byte & 0x08 ? '1' : '0'), \ + (byte & 0x04 ? '1' : '0'), \ + (byte & 0x02 ? '1' : '0'), \ + (byte & 0x01 ? '1' : '0') + +#include "IthoCC1101.h" +#include +#include +#include + +//#define CRC_FILTER + +////original sync byte pattern +//#define STARTBYTE 6 //relevant data starts 6 bytes after the sync pattern bytes 170/171 +//#define SYNC1 170 +//#define SYNC0 171 +//#define MDMCFG2 0x02 //16bit sync word / 16bit specific + +////alternative sync byte pattern (filter much more non-itho messages out. Maybe too strict? Testing needed. +//#define STARTBYTE 0 //relevant data starts 0 bytes after the sync pattern bytes 179/42/171/42 +//#define SYNC1 187 //byte11 = 179, byte13 = 171 with SYNC1 = 163, 179 and 171 differ only by 1 bit +//#define SYNC0 42 +//#define MDMCFG2 0x03 //32bit sync word / 30bit specific + +//alternative sync byte pattern +#define STARTBYTE 2 //relevant data starts 2 bytes after the sync pattern bytes 179/42 +#define SYNC1 179 +#define SYNC0 42 +#define MDMCFG2 0x02 //16bit sync word / 16bit specific + +// default constructor +IthoCC1101::IthoCC1101(uint8_t counter, uint8_t sendTries) : CC1101() +{ + this->outIthoPacket.counter = counter; + this->sendTries = sendTries; + + this->outIthoPacket.deviceId[0] = 33; + this->outIthoPacket.deviceId[1] = 66; + this->outIthoPacket.deviceId[2] = 99; + + this->outIthoPacket.deviceType = 22; + +} //IthoCC1101 + +// default destructor +IthoCC1101::~IthoCC1101() +{ +} //~IthoCC1101 + +void IthoCC1101::initSendMessage(uint8_t len) +{ + //finishTransfer(); + writeCommand(CC1101_SIDLE); + delayMicroseconds(1); + writeRegister(CC1101_IOCFG0 , 0x2E); + delayMicroseconds(1); + writeRegister(CC1101_IOCFG1 , 0x2E); + delayMicroseconds(1); + writeCommand(CC1101_SIDLE); + writeCommand(CC1101_SPWD); + delayMicroseconds(2); + + /* + Configuration reverse engineered from remote print. The commands below are used by IthoDaalderop. + Base frequency 868.299866MHz + Channel 0 + Channel spacing 199.951172kHz + Carrier frequency 868.299866MHz + Xtal frequency 26.000000MHz + Data rate 38.3835kBaud + Manchester disabled + Modulation 2-FSK + Deviation 50.781250kHz + TX power ? + PA ramping enabled + Whitening disabled + */ + writeCommand(CC1101_SRES); + delayMicroseconds(1); + writeRegister(CC1101_IOCFG0 , 0x2E); //High impedance (3-state) + writeRegister(CC1101_FREQ2 , 0x21); //00100001 878MHz-927.8MHz + writeRegister(CC1101_FREQ1 , 0x65); //01100101 + writeRegister(CC1101_FREQ0 , 0x6A); //01101010 + writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 + writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 + writeRegister(CC1101_MDMCFG2 , 0x00); //00000000 2-FSK, no manchester encoding/decoding, no preamble/sync + writeRegister(CC1101_MDMCFG1 , 0x22); //00100010 + writeRegister(CC1101_MDMCFG0 , 0xF8); //11111000 + writeRegister(CC1101_CHANNR , 0x00); //00000000 + writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 + writeRegister(CC1101_FREND0 , 0x17); //00010111 use index 7 in PA table + writeRegister(CC1101_MCSM0 , 0x18); //00011000 PO timeout Approx. 146microseconds - 171microseconds, Auto calibrate When going from IDLE to RX or TX (or FSTXON) + writeRegister(CC1101_FSCAL3 , 0xA9); //10101001 + writeRegister(CC1101_FSCAL2 , 0x2A); //00101010 + writeRegister(CC1101_FSCAL1 , 0x00); //00000000 + writeRegister(CC1101_FSCAL0 , 0x11); //00010001 + writeRegister(CC1101_FSTEST , 0x59); //01011001 For test only. Do not write to this register. + writeRegister(CC1101_TEST2 , 0x81); //10000001 For test only. Do not write to this register. + writeRegister(CC1101_TEST1 , 0x35); //00110101 For test only. Do not write to this register. + writeRegister(CC1101_TEST0 , 0x0B); //00001011 For test only. Do not write to this register. + writeRegister(CC1101_PKTCTRL0 , 0x12); //00010010 Enable infinite length packets, CRC disabled, Turn data whitening off, Serial Synchronous mode + writeRegister(CC1101_ADDR , 0x00); //00000000 + writeRegister(CC1101_PKTLEN , 0xFF); //11111111 //Not used, no hardware packet handling + + //0x6F,0x26,0x2E,0x8C,0x87,0xCD,0xC7,0xC0 + writeBurstRegister(CC1101_PATABLE | CC1101_WRITE_BURST, (uint8_t*)ithoPaTableSend, 8); + + //difference, message1 sends a STX here + writeCommand(CC1101_SIDLE); + writeCommand(CC1101_SIDLE); + + writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 + writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 + writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 + writeRegister(CC1101_IOCFG0 , 0x2D); //GDO0_Z_EN_N. When this output is 0, GDO0 is configured as input (for serial TX data). + writeRegister(CC1101_IOCFG1 , 0x0B); //Serial Clock. Synchronous to the data in synchronous serial mode. + + writeCommand(CC1101_STX); + writeCommand(CC1101_SIDLE); + + writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 + writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 + writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 + //writeRegister(CC1101_IOCFG0 ,0x2D); //GDO0_Z_EN_N. When this output is 0, GDO0 is configured as input (for serial TX data). + //writeRegister(CC1101_IOCFG1 ,0x0B); //Serial Clock. Synchronous to the data in synchronous serial mode. + + //Itho is using serial mode for transmit. We want to use the TX FIFO with fixed packet length for simplicity. + writeRegister(CC1101_IOCFG0 , 0x2E); + writeRegister(CC1101_IOCFG1 , 0x2E); + writeRegister(CC1101_PKTCTRL0 , 0x00); + writeRegister(CC1101_PKTCTRL1 , 0x00); + + writeRegister(CC1101_PKTLEN , len); + +} + +void IthoCC1101::finishTransfer() +{ + writeCommand(CC1101_SIDLE); + delayMicroseconds(1); + + writeRegister(CC1101_IOCFG0 , 0x2E); + writeRegister(CC1101_IOCFG1 , 0x2E); + + writeCommand(CC1101_SIDLE); + writeCommand(CC1101_SPWD); +} + +void IthoCC1101::initReceive() +{ + /* + Configuration reverse engineered from RFT print. + + Base frequency 868.299866MHz + Channel 0 + Channel spacing 199.951172kHz + Carrier frequency 868.299866MHz + Xtal frequency 26.000000MHz + Data rate 38.3835kBaud + RX filter BW 325.000000kHz + Manchester disabled + Modulation 2-FSK + Deviation 50.781250kHz + TX power 0x6F,0x26,0x2E,0x7F,0x8A,0x84,0xCA,0xC4 + PA ramping enabled + Whitening disabled + */ + writeCommand(CC1101_SRES); + + writeRegister(CC1101_TEST0 , 0x09); + writeRegister(CC1101_FSCAL2 , 0x00); + + //0x6F,0x26,0x2E,0x7F,0x8A,0x84,0xCA,0xC4 + writeBurstRegister(CC1101_PATABLE | CC1101_WRITE_BURST, (uint8_t*)ithoPaTableReceive, 8); + + writeCommand(CC1101_SCAL); + + //wait for calibration to finish + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) yield(); + + writeRegister(CC1101_FSCAL2 , 0x00); + writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate + writeRegister(CC1101_FREQ2 , 0x21); + writeRegister(CC1101_FREQ1 , 0x65); + writeRegister(CC1101_FREQ0 , 0x6A); + writeRegister(CC1101_IOCFG0 , 0x2E); //High impedance (3-state) + writeRegister(CC1101_IOCFG2 , 0x06); //0x06 Assert when sync word has been sent / received, and de-asserts at the end of the packet. + writeRegister(CC1101_FSCTRL1 , 0x06); + writeRegister(CC1101_FSCTRL0 , 0x00); + writeRegister(CC1101_MDMCFG4 , 0x5A); + writeRegister(CC1101_MDMCFG3 , 0x83); + writeRegister(CC1101_MDMCFG2 , 0x00); //Enable digital DC blocking filter before demodulator, 2-FSK, Disable Manchester encoding/decoding, No preamble/sync + writeRegister(CC1101_MDMCFG1 , 0x22); //Disable FEC + writeRegister(CC1101_MDMCFG0 , 0xF8); + writeRegister(CC1101_CHANNR , 0x00); + writeRegister(CC1101_DEVIATN , 0x50); + writeRegister(CC1101_FREND1 , 0x56); + writeRegister(CC1101_FREND0 , 0x17); + writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate + writeRegister(CC1101_FOCCFG , 0x16); + writeRegister(CC1101_BSCFG , 0x6C); + writeRegister(CC1101_AGCCTRL2 , 0x43); + writeRegister(CC1101_AGCCTRL1 , 0x40); + writeRegister(CC1101_AGCCTRL0 , 0x91); + writeRegister(CC1101_FSCAL3 , 0xE9); + writeRegister(CC1101_FSCAL2 , 0x2A); + writeRegister(CC1101_FSCAL1 , 0x00); + writeRegister(CC1101_FSCAL0 , 0x11); + writeRegister(CC1101_FSTEST , 0x59); + writeRegister(CC1101_TEST2 , 0x81); + writeRegister(CC1101_TEST1 , 0x35); + writeRegister(CC1101_TEST0 , 0x0B); + writeRegister(CC1101_PKTCTRL1 , 0x04); //No address check, Append two bytes with status RSSI/LQI/CRC OK, + writeRegister(CC1101_PKTCTRL0 , 0x32); //Infinite packet length mode, CRC disabled for TX and RX, No data whitening, Asynchronous serial mode, Data in on GDO0 and data out on either of the GDOx pins + writeRegister(CC1101_ADDR , 0x00); + writeRegister(CC1101_PKTLEN , 0xFF); + writeRegister(CC1101_TEST0 , 0x09); + + writeCommand(CC1101_SCAL); + + //wait for calibration to finish + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) yield(); + + writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate + + writeCommand(CC1101_SIDLE); + writeCommand(CC1101_SIDLE); + + writeRegister(CC1101_MDMCFG2 , 0x00); //Enable digital DC blocking filter before demodulator, 2-FSK, Disable Manchester encoding/decoding, No preamble/sync + writeRegister(CC1101_IOCFG0 , 0x0D); //Serial Data Output. Used for asynchronous serial mode. + + writeCommand(CC1101_SRX); + + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_RX) yield(); + + initReceiveMessage(); +} + +void IthoCC1101::initReceiveMessage() +{ + uint8_t marcState; + + writeCommand(CC1101_SIDLE); //idle + + //set datarate + writeRegister(CC1101_MDMCFG4 , 0x5A); // set kBaud + writeRegister(CC1101_MDMCFG3 , 0x83); // set kBaud + writeRegister(CC1101_DEVIATN , 0x50); + + //set fifo mode with fixed packet length and sync bytes + writeRegister(CC1101_PKTLEN , 63); //63 bytes message (sync at beginning of message is removed by CC1101) + + //set fifo mode with fixed packet length and sync bytes + writeRegister(CC1101_PKTCTRL0 , 0x00); + writeRegister(CC1101_SYNC1 , SYNC1); + writeRegister(CC1101_SYNC0 , SYNC0); + writeRegister(CC1101_MDMCFG2 , MDMCFG2); + writeRegister(CC1101_PKTCTRL1 , 0x00); + + writeCommand(CC1101_SRX); //switch to RX state + + // Check that the RX state has been entered + while (((marcState = readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) & CC1101_BITS_MARCSTATE) != CC1101_MARCSTATE_RX) + { + if (marcState == CC1101_MARCSTATE_RXFIFO_OVERFLOW) // RX_OVERFLOW + writeCommand(CC1101_SFRX); //flush RX buffer + } +} + +bool IthoCC1101::checkForNewPacket() { + if (receiveData(&inMessage, 63) && parseMessageCommand()) { + initReceiveMessage(); + return true; + } + return false; +} + +bool IthoCC1101::parseMessageCommand() { + + messageDecode(&inMessage, &inIthoPacket); + + //deviceType of message type? + inIthoPacket.deviceType = inIthoPacket.dataDecoded[0]; + + //deviceID + inIthoPacket.deviceId[0] = inIthoPacket.dataDecoded[1]; + inIthoPacket.deviceId[1] = inIthoPacket.dataDecoded[2]; + inIthoPacket.deviceId[2] = inIthoPacket.dataDecoded[3]; + + //counter1 + inIthoPacket.counter = inIthoPacket.dataDecoded[4]; + + bool isHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageHighCommandBytes); + bool isRVHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVHighCommandBytes); + bool isMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageMediumCommandBytes); + bool isRVMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVMediumCommandBytes); + bool isLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageLowCommandBytes); + bool isRVLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVLowCommandBytes); + bool isRVAutoCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVAutoCommandBytes); + bool isStandByCommand = checkIthoCommand(&inIthoPacket, ithoMessageStandByCommandBytes); + bool isTimer1Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer1CommandBytes); + bool isTimer2Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer2CommandBytes); + bool isTimer3Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer3CommandBytes); + bool isJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageJoinCommandBytes); + bool isJoin2Command = checkIthoCommand(&inIthoPacket, ithoMessageJoin2CommandBytes); + bool isRVJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVJoinCommandBytes); + bool isLeaveCommand = checkIthoCommand(&inIthoPacket, ithoMessageLeaveCommandBytes); + + //determine command + inIthoPacket.command = IthoUnknown; + if (isHighCommand) inIthoPacket.command = IthoHigh; + if (isRVHighCommand) inIthoPacket.command = IthoHigh; + if (isMediumCommand) inIthoPacket.command = IthoMedium; + if (isRVMediumCommand) inIthoPacket.command = IthoMedium; + if (isLowCommand) inIthoPacket.command = IthoLow; + if (isRVLowCommand) inIthoPacket.command = IthoLow; + if (isRVAutoCommand) inIthoPacket.command = IthoStandby; + if (isStandByCommand) inIthoPacket.command = IthoStandby; + if (isTimer1Command) inIthoPacket.command = IthoTimer1; + if (isTimer2Command) inIthoPacket.command = IthoTimer2; + if (isTimer3Command) inIthoPacket.command = IthoTimer3; + if (isJoinCommand) inIthoPacket.command = IthoJoin; + if (isJoin2Command) inIthoPacket.command = IthoJoin; + if (isRVJoinCommand) inIthoPacket.command = IthoJoin; + if (isLeaveCommand) inIthoPacket.command = IthoLeave; + +#if defined (CRC_FILTER) + uint8_t mLen = 0; + if (isPowerCommand || isHighCommand || isMediumCommand || isLowCommand || isStandByCommand || isTimer1Command || isTimer2Command || isTimer3Command) { + mLen = 11; + } + else if (isJoinCommand || isJoin2Command) { + mLen = 20; + } + else if (isLeaveCommand) { + mLen = 14; + } + else { + return true; + } + if (getCounter2(&inIthoPacket, mLen) != inIthoPacket.dataDecoded[mLen]) { + inIthoPacket.command = IthoUnknown; + return false; + } +#endif + + return true; +} + +bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[]) { + uint8_t offset = 0; + if (itho->deviceType == 28 || itho->deviceType == 24) offset = 2; + for (int i = 4; i < 6; i++) + { + //if (i == 2 || i == 3) continue; //skip byte3 and byte4, rft-rv and co2-auto remote device seem to sometimes have a different number there + if ( (itho->dataDecoded[i + 5 + offset] != commandBytes[i]) && (itho->dataDecodedChk[i + 5 + offset] != commandBytes[i]) ) { + return false; + } + } + return true; +} + +void IthoCC1101::sendCommand(IthoCommand command) +{ + CC1101Packet outMessage; + uint8_t maxTries = sendTries; + uint8_t delaytime = 40; + + //update itho packet data + outIthoPacket.command = command; + outIthoPacket.counter += 1; + + //get message2 bytes + switch (command) + { + case IthoJoin: + createMessageJoin(&outIthoPacket, &outMessage); + break; + + case IthoLeave: + createMessageLeave(&outIthoPacket, &outMessage); + //the leave command needs to be transmitted for 1 second according the manual + maxTries = 30; + delaytime = 4; + break; + + default: + createMessageCommand(&outIthoPacket, &outMessage); + break; + } + + //send messages + for (int i = 0; i < maxTries; i++) + { + + //message2 + initSendMessage(outMessage.length); + sendData(&outMessage); + + finishTransfer(); + delay(delaytime); + } + //initReceive(); SV - I call this from the ESPEasy plugin to prevent crashes +} + +void IthoCC1101::createMessageStart(IthoPacket *itho, CC1101Packet *packet) +{ + + //fixed, set start structure in data buffer manually + for (uint8_t i = 0; i < 7; i++) { + packet->data[i] = 170; + } + packet->data[7] = 171; + packet->data[8] = 254; + packet->data[9] = 0; + packet->data[10] = 179; + packet->data[11] = 42; + packet->data[12] = 171; + packet->data[13] = 42; + + //[start of command specific data] + +} + +void IthoCC1101::createMessageCommand(IthoPacket *itho, CC1101Packet *packet) +{ + + //set start message structure + createMessageStart(itho, packet); + + //set deviceType? (or messageType?), not sure what this is + itho->dataDecoded[0] = itho->deviceType; + + //set deviceID + itho->dataDecoded[1] = itho->deviceId[0]; + itho->dataDecoded[2] = itho->deviceId[1]; + itho->dataDecoded[3] = itho->deviceId[2]; + + //set counter1 + itho->dataDecoded[4] = itho->counter; + + //set command bytes on dataDecoded[5 - 10] + uint8_t *commandBytes = getMessageCommandBytes(itho->command); + for (uint8_t i = 0; i < 6; i++) { + itho->dataDecoded[i + 5] = commandBytes[i]; + } + + //set counter2 + itho->dataDecoded[11] = getCounter2(itho, 11); + + itho->length = 12; + + packet->length = messageEncode(itho, packet); + packet->length += 1; + + //set end byte + packet->data[packet->length] = 172; + packet->length += 1; + + //set end 'noise' + for (uint8_t i = packet->length; i < packet->length + 7; i++) { + packet->data[i] = 170; + } + packet->length += 7; + +} + +void IthoCC1101::createMessageJoin(IthoPacket *itho, CC1101Packet *packet) +{ + + //set start message structure + createMessageStart(itho, packet); + + //set deviceType? (or messageType?) + itho->dataDecoded[0] = itho->deviceType; + + //set deviceID + itho->dataDecoded[1] = itho->deviceId[0]; + itho->dataDecoded[2] = itho->deviceId[1]; + itho->dataDecoded[3] = itho->deviceId[2]; + + //set counter1 + itho->dataDecoded[4] = itho->counter; + + //set command bytes on dataDecoded[5 - ?] + uint8_t *commandBytes = getMessageCommandBytes(itho->command); + for (uint8_t i = 0; i < 6; i++) { + itho->dataDecoded[i + 5] = commandBytes[i]; + } + + //set deviceID + itho->dataDecoded[11] = itho->deviceId[0]; + itho->dataDecoded[12] = itho->deviceId[1]; + itho->dataDecoded[13] = itho->deviceId[2]; + + itho->dataDecoded[14] = 1; + itho->dataDecoded[15] = 16; + itho->dataDecoded[16] = 224; + + //set deviceID + itho->dataDecoded[17] = itho->deviceId[0]; + itho->dataDecoded[18] = itho->deviceId[1]; + itho->dataDecoded[19] = itho->deviceId[2]; + + //set counter2 + itho->dataDecoded[20] = getCounter2(itho, 20); + + itho->length = 21; + + packet->length = messageEncode(itho, packet); + packet->length += 1; + + //set end byte + packet->data[packet->length] = 202; + packet->length += 1; + + //set end 'noise' + for (uint8_t i = packet->length; i < packet->length + 7; i++) { + packet->data[i] = 170; + } + packet->length += 7; + +} + +void IthoCC1101::createMessageLeave(IthoPacket *itho, CC1101Packet *packet) +{ + + //set start message structure + createMessageStart(itho, packet); + + //set deviceType? (or messageType?) + itho->dataDecoded[0] = itho->deviceType; + + //set deviceID + itho->dataDecoded[1] = itho->deviceId[0]; + itho->dataDecoded[2] = itho->deviceId[1]; + itho->dataDecoded[3] = itho->deviceId[2]; + + //set counter1 + itho->dataDecoded[4] = itho->counter; + + //set command bytes on dataDecoded[5 - 10] + uint8_t *commandBytes = getMessageCommandBytes(itho->command); + for (uint8_t i = 0; i < 6; i++) { + itho->dataDecoded[i + 5] = commandBytes[i]; + } + + //set deviceID + itho->dataDecoded[11] = itho->deviceId[0]; + itho->dataDecoded[12] = itho->deviceId[1]; + itho->dataDecoded[13] = itho->deviceId[2]; + + //set counter2 + itho->dataDecoded[14] = getCounter2(itho, 14); + + itho->length = 15; + + packet->length = messageEncode(itho, packet); + packet->length += 1; + + //set end byte + packet->data[packet->length] = 202; + packet->length += 1; + + //set end 'noise' + for (uint8_t i = packet->length; i < packet->length + 7; i++) { + packet->data[i] = 170; + } + packet->length += 7; + +} + +uint8_t* IthoCC1101::getMessageCommandBytes(IthoCommand command) +{ + switch (command) + { + case IthoStandby: + return (uint8_t*)&ithoMessageStandByCommandBytes[0]; + case IthoHigh: + return (uint8_t*)&ithoMessageHighCommandBytes[0]; + case IthoFull: + return (uint8_t*)&ithoMessageFullCommandBytes[0]; + case IthoMedium: + return (uint8_t*)&ithoMessageMediumCommandBytes[0]; + case IthoLow: + return (uint8_t*)&ithoMessageLowCommandBytes[0]; + case IthoTimer1: + return (uint8_t*)&ithoMessageTimer1CommandBytes[0]; + case IthoTimer2: + return (uint8_t*)&ithoMessageTimer2CommandBytes[0]; + case IthoTimer3: + return (uint8_t*)&ithoMessageTimer3CommandBytes[0]; + case IthoJoin: + return (uint8_t*)&ithoMessageJoinCommandBytes[0]; + case IthoLeave: + return (uint8_t*)&ithoMessageLeaveCommandBytes[0]; + default: + return (uint8_t*)&ithoMessageLowCommandBytes[0]; + } +} + +/* + Counter2 is the decimal sum of all bytes in decoded form from + deviceType up to the last byte before counter2 subtracted + from zero. +*/ +uint8_t IthoCC1101::getCounter2(IthoPacket *itho, uint8_t len) { + + uint8_t val = 0; + + for (uint8_t i = 0; i < len; i++) { + val += itho->dataDecoded[i]; + } + + return 0 - val; +} + +uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { + + uint8_t lenOutbuf = 0; + + if ((itho->length * 20) % 8 == 0) { //inData len fits niecly in out buffer length + lenOutbuf = itho->length * 2.5; + } + else { //is this an issue? inData last byte does not fill out buffer length, add 1 out byte extra, padding is done after encode + lenOutbuf = (uint8_t)(itho->length * 2.5) + 0.5; + } + + uint8_t out_bytecounter = 14; //index of Outbuf, start at offset 14, first part of the message is set manually + uint8_t out_bitcounter = 0; //bit position of current outbuf byte + uint8_t out_patterncounter = 0; //bit counter to add 1 0 bit pattern after every 8 bits + uint8_t bitSelect = 4; //bit position of the inData byte (4 - 7, 0 - 3) + uint8_t out_shift = 7; //bit shift inData bit in position of outbuf byte + + //we need to zero the out buffer first cause we are using bitshifts + for (int i = out_bytecounter; i < sizeof(packet->data) / sizeof(packet->data[0]); i++) { + packet->data[i] = 0; + } + + //Serial.println(); + for (uint8_t dataByte = 0; dataByte < itho->length; dataByte++) { + for (uint8_t dataBit = 0; dataBit < 8; dataBit++) { //process a full dataByte at a time resulting in 20 output bits (2.5 bytes) with the pattern 7x6x5x4x 10 3x2x1x0x 10 7x6x5x4x 10 3x2x1x0x 10 etc + if (out_bitcounter == 8) { //check if new byte is needed + out_bytecounter++; + out_bitcounter = 0; + } + + if (out_patterncounter == 8) { //check if we have to start with a 1 0 pattern + out_patterncounter = 0; + packet->data[out_bytecounter] = packet->data[out_bytecounter] | 1 << out_shift; + out_shift--; + out_bitcounter++; + packet->data[out_bytecounter] = packet->data[out_bytecounter] | 0 << out_shift; + if (out_shift == 0) out_shift = 8; + out_shift--; + out_bitcounter++; + } + + if (out_bitcounter == 8) { //check if new byte is needed + out_bytecounter++; + out_bitcounter = 0; + } + + //set the even bit + uint8_t bit = (itho->dataDecoded[dataByte] & (1 << bitSelect)) >> bitSelect; //select bit and shift to bit pos 0 + bitSelect++; + if (bitSelect == 8) bitSelect = 0; + + packet->data[out_bytecounter] = packet->data[out_bytecounter] | bit << out_shift; //shift bit in corect pos of current outbuf byte + out_shift--; + out_bitcounter++; + out_patterncounter++; + + //set the odd bit (inverse of even bit) + bit = ~bit & 0b00000001; + packet->data[out_bytecounter] = packet->data[out_bytecounter] | bit << out_shift; + if (out_shift == 0) out_shift = 8; + out_shift--; + out_bitcounter++; + out_patterncounter++; + + } + + } + if (out_bitcounter < 8) { //add closing 1 0 pattern to fill last packet->data byte and ensure DC balance in the message + for (uint8_t i = out_bitcounter; i < 8; i += 2) { + packet->data[out_bytecounter] = packet->data[out_bytecounter] | 1 << out_shift; + out_shift--; + packet->data[out_bytecounter] = packet->data[out_bytecounter] | 0 << out_shift; + if (out_shift == 0) out_shift = 8; + out_shift--; + } + } + + return out_bytecounter; +} + + +void IthoCC1101::messageDecode(CC1101Packet *packet, IthoPacket *itho) { + + itho->length = 0; + int lenInbuf = packet->length; + + lenInbuf -= STARTBYTE; //correct for sync byte pos + + while (lenInbuf >= 5) { + lenInbuf -= 5; + itho->length += 2; + } + if (lenInbuf >= 3) { + itho->length++; + } + + for (int i = 0; i < sizeof(itho->dataDecoded) / sizeof(itho->dataDecoded[0]); i++) { + itho->dataDecoded[i] = 0; + } + for (int i = 0; i < sizeof(itho->dataDecodedChk) / sizeof(itho->dataDecodedChk[0]); i++) { + itho->dataDecodedChk[i] = 0; + } + + uint8_t out_i = 0; //byte index + uint8_t out_j = 4; //bit index + uint8_t out_i_chk = 0; //byte index + uint8_t out_j_chk = 4; //bit index + uint8_t in_bitcounter = 0; //process per 10 input bits + + for (int i = STARTBYTE; i < packet->length; i++) { + + for (int j = 7; j > -1; j--) { + if (in_bitcounter == 0 || in_bitcounter == 2 || in_bitcounter == 4 || in_bitcounter == 6) { //select input bits for output + uint8_t x = packet->data[i]; //select input byte + x = x >> j; //select input bit + x = x & 0b00000001; + x = x << out_j; //set value for output bit + itho->dataDecoded[out_i] = itho->dataDecoded[out_i] | x; + out_j += 1; //next output bit + if (out_j > 7) out_j = 0; + if (out_j == 4) out_i += 1; + } + if (in_bitcounter == 1 || in_bitcounter == 3 || in_bitcounter == 5 || in_bitcounter == 7) { //select input bits for check output + uint8_t x = packet->data[i]; //select input byte + x = x >> j; //select input bit + x = x & 0b00000001; + x = x << out_j_chk; //set value for output bit + itho->dataDecodedChk[out_i_chk] = itho->dataDecodedChk[out_i_chk] | x; + out_j_chk += 1; //next output bit + if (out_j_chk > 7) out_j_chk = 0; + if (out_j_chk == 4) { + itho->dataDecodedChk[out_i_chk] = ~itho->dataDecodedChk[out_i_chk]; //inverse bits + out_i_chk += 1; + } + } + in_bitcounter += 1; //continue cyling in groups of 10 bits + if (in_bitcounter > 9) in_bitcounter = 0; + } + } +} + +uint8_t IthoCC1101::ReadRSSI() +{ + uint8_t rssi = 0; + uint8_t value = 0; + + rssi = (readRegister(CC1101_RSSI, CC1101_STATUS_REGISTER)); + + if (rssi >= 128) + { + value = 255 - rssi; + value /= 2; + value += 74; + } + else + { + value = rssi / 2; + value += 74; + } + return (value); +} + +bool IthoCC1101::checkID(const uint8_t *id) +{ + for (uint8_t i = 0; i < 3; i++) + if (id[i] != inIthoPacket.deviceId[i]) + return false; + return true; +} + +String IthoCC1101::getLastIDstr(bool ashex) { + String str; + for (uint8_t i = 0; i < 3; i++) { + if (ashex) str += String(inIthoPacket.deviceId[i], HEX); + else str += String(inIthoPacket.deviceId[i]); + if (i < 2) str += ","; + } + return str; +} + +int * IthoCC1101::getLastID() { + static int id[3]; + for (uint8_t i = 0; i < 3; i++) { + id[i] = inIthoPacket.deviceId[i]; + } + return id; +} + +String IthoCC1101::getLastMessagestr(bool ashex) { + String str = "Length=" + String(inMessage.length) + "."; + for (uint8_t i = 0; i < inMessage.length; i++) { + if (ashex) str += String(inMessage.data[i], HEX); + else str += String(inMessage.data[i]); + if (i < inMessage.length - 1) str += ":"; + } + return str; +} + +String IthoCC1101::LastMessageDecoded() { + + String str; + if (inIthoPacket.length > 11) { + str += "Device type?: " + String(inIthoPacket.deviceType); + str += " - CMD: "; + for (int i = 4; i < inIthoPacket.length; i++) { + str += String(inIthoPacket.dataDecoded[i]); + if (i < inIthoPacket.length - 1) str += ","; + } + + } + else { + for (uint8_t i = 0; i < inIthoPacket.length; i++) { + str += String(inIthoPacket.dataDecoded[i]); + if (i < inIthoPacket.length - 1) str += ","; + } + + } + str += "\n"; + return str; + +} diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h new file mode 100644 index 000000000..f4fd7b0da --- /dev/null +++ b/lib/Itho/IthoCC1101.h @@ -0,0 +1,116 @@ +/* + * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra + */ + +#ifndef __ITHOCC1101_H__ +#define __ITHOCC1101_H__ + +#include +#include "CC1101.h" +#include "IthoPacket.h" + + +//pa table settings +const uint8_t ithoPaTableSend[8] = {0x6F, 0x26, 0x2E, 0x8C, 0x87, 0xCD, 0xC7, 0xC0}; +const uint8_t ithoPaTableReceive[8] = {0x6F, 0x26, 0x2E, 0x7F, 0x8A, 0x84, 0xCA, 0xC4}; + +//message command bytes +const uint8_t ithoMessageRVHighCommandBytes[] = {49,224,4,0,0,200}; +const uint8_t ithoMessageHighCommandBytes[] = {34,241,3,0,4,4}; +const uint8_t ithoMessageFullCommandBytes[] = {34,241,3,0,4,4}; +const uint8_t ithoMessageMediumCommandBytes[] = {34,241,3,0,3,4}; +const uint8_t ithoMessageRVMediumCommandBytes[] = {34,241,3,0,3,7}; +const uint8_t ithoMessageLowCommandBytes[] = {34,241,3,0,2,4}; +const uint8_t ithoMessageRVLowCommandBytes[] = {49,224,4,0,0,1}; +const uint8_t ithoMessageRVAutoCommandBytes[] = {34,241,3,0,5,7}; +const uint8_t ithoMessageStandByCommandBytes[] = {0,0,0,0,0,0}; //unkown, tbd +const uint8_t ithoMessageTimer1CommandBytes[] = {34,243,3,0,0,10}; //10 minutes full speed +const uint8_t ithoMessageTimer2CommandBytes[] = {34,243,3,0,0,20}; //20 minutes full speed +const uint8_t ithoMessageTimer3CommandBytes[] = {34,243,3,0,0,30}; //30 minutes full speed +const uint8_t ithoMessageJoinCommandBytes[] = {31,201,12,0,34,241}; +const uint8_t ithoMessageJoin2CommandBytes[] = {31,201,12,99,34,248}; //join command of RFT AUTO Co2 remote +const uint8_t ithoMessageRVJoinCommandBytes[] = {31,201,24,0,49,224}; //join command of RFT-RV +const uint8_t ithoMessageLeaveCommandBytes[] = {31,201,6,0,31,201}; +//itho rft-rv +//unknown, high +//148,216,43,49,224,4,0,0,200,0,3,127,244,78,11,155,154,225,11,96,138 +//148,216,43,49,224,4,0,0,200,0,3,127,51,80,47,233,94,6,189,114,73 + +//low +//148,216,43,49,224,4,0,0,1,0,202,127,242,212,160,123,15,64,7,129,33 +//148,216,43,34,241,3,0,4,4,194,127,255,189,90,107,88,72,115,49,192,105 + +//join +//151,149,65,31,201,24,0,49,224,151,149,65,0,18,160,151,149,65,1,16,224 + + +class IthoCC1101 : protected CC1101 +{ + private: + //receive + CC1101Packet inMessage; //temp storage message2 + IthoPacket inIthoPacket; //stores last received message data + + //send + IthoPacket outIthoPacket; //stores state of "remote" + + //settings + uint8_t sendTries; //number of times a command is send at one button press + + //functions + public: + IthoCC1101(uint8_t counter = 0, uint8_t sendTries = 3); //set initial counter value + ~IthoCC1101(); + + //init + void init() { CC1101::init(); initReceive(); } //init,reset CC1101 + void initReceive(); + uint8_t getLastCounter() { return outIthoPacket.counter; } //counter is increased before sending a command + void setSendTries(uint8_t sendTries) { this->sendTries = sendTries; } + void setDeviceID(uint8_t byte0, uint8_t byte1, uint8_t byte2) { this->outIthoPacket.deviceId[0] = byte0; this->outIthoPacket.deviceId[1] = byte1; this->outIthoPacket.deviceId[2] = byte2;} + + //receive + bool checkForNewPacket(); //check RX fifo for new data + IthoPacket getLastPacket() { return inIthoPacket; } //retrieve last received/parsed packet from remote + IthoCommand getLastCommand() { return inIthoPacket.command; } //retrieve last received/parsed command from remote + uint8_t getLastInCounter() { return inIthoPacket.counter; } //retrieve last received/parsed command from remote + uint8_t ReadRSSI(); + bool checkID(const uint8_t *id); + int * getLastID(); + String getLastIDstr(bool ashex=true); + String getLastMessagestr(bool ashex=true); + String LastMessageDecoded(); + + //send + void sendCommand(IthoCommand command); + protected: + private: + IthoCC1101( const IthoCC1101 &c); + IthoCC1101& operator=( const IthoCC1101 &c); + + //init CC1101 for receiving + void initReceiveMessage(); + + //init CC1101 for sending + void initSendMessage(uint8_t len); + void finishTransfer(); + + //parse received message + bool parseMessageCommand(); + bool checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[]); + + //send + void createMessageStart(IthoPacket *itho, CC1101Packet *packet); + void createMessageCommand(IthoPacket *itho, CC1101Packet *packet); + void createMessageJoin(IthoPacket *itho, CC1101Packet *packet); + void createMessageLeave(IthoPacket *itho, CC1101Packet *packet); + uint8_t* getMessageCommandBytes(IthoCommand command); + uint8_t getCounter2(IthoPacket *itho, uint8_t len); + + uint8_t messageEncode(IthoPacket *itho, CC1101Packet *packet); + void messageDecode(CC1101Packet *packet, IthoPacket *itho); + + +}; //IthoCC1101 + +#endif //__ITHOCC1101_H__ \ No newline at end of file diff --git a/lib/Itho/IthoPacket.h b/lib/Itho/IthoPacket.h new file mode 100644 index 000000000..3a5c3e807 --- /dev/null +++ b/lib/Itho/IthoPacket.h @@ -0,0 +1,49 @@ +/* + * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra + */ + +#ifndef ITHOPACKET_H_ +#define ITHOPACKET_H_ + +enum IthoCommand +{ + IthoUnknown = 0, + + IthoJoin = 1, + IthoLeave = 2, + + IthoStandby = 3, + IthoLow = 4, + IthoMedium = 5, + IthoHigh = 6, + IthoFull = 7, + + IthoTimer1 = 8, + IthoTimer2 = 9, + IthoTimer3 = 10, + + //duco c system remote + DucoStandby = 11, + DucoLow = 12, + DucoMedium = 13, + DucoHigh = 14 +}; + + +class IthoPacket +{ + public: + IthoCommand command; + + uint8_t dataDecoded[32]; + uint8_t dataDecodedChk[32]; + uint8_t length; + + uint8_t deviceType; + uint8_t deviceId[3]; + + uint8_t counter; //0-255, counter is increased on every remote button press +}; + + +#endif /* ITHOPACKET_H_ */ \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index c8afabf10..eaa24f15c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,8 +20,8 @@ extra_configs = platformio_esp32_envs.ini platformio_special_envs.ini -default_envs = custom_ESP8266_4M1M, custom_ESP32_4M316k -; default_envs = custom_ESP8266_4M1M +;default_envs = custom_ESP8266_4M1M, custom_ESP32_4M316k +default_envs = test_D_ESP8266_4M1M ;default_envs = normal_ESP8266_4M1M ;default_envs = test_beta_ESP8266_4M1M diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino new file mode 100644 index 000000000..c3b19119f --- /dev/null +++ b/src/_P118_Itho.ino @@ -0,0 +1,548 @@ +//####################################################################################################### +//############################## Plugin 118: Itho ventilation unit 868Mhz remote ######################## +//####################################################################################################### + +// author :jodur, 13-1-2018 +// changed :jeroen, 2-11-2019 +// changed :svollebregt, 30-1-2020 - changes to improve stability: volatile decleration of state, +// disable logging within interrupts unles enabled, removed some unused code, +// reduce noInterrupts() blockage of code fragments to prevent crashes +// svollebregt, 16-2-2020 - ISR now sets flag which is checked by 50 per seconds plugin call as +// receive ISR with Ticker was the cause of instability. Inspired by: https://github.com/arnemauer/Ducobox-ESPEasy-Plugin +// svollebregt, 11-04-2020 - Minor changes to make code compatible with latest mega 20200410, removed SYNC1 option for now; +// better to change this value in the Itho-lib code and compile it yourself +// svollebreggt, 13-2-2021 - Now uses rewirtten library made by arjenhiemstra: https://github.com/arjenhiemstra/IthoEcoFanRFT + +// Recommended to disable RF receive logging to minimize code execution within interrupts + +// List of commands: +// 1111 to join ESP8266 with Itho ventilation unit +// 9999 to leaveESP8266 with Itho ventilation unit +// 0 to set Itho ventilation unit to standby +// 1 - set Itho ventilation unit to low speed +// 2 - set Itho ventilation unit to medium speed +// 3 - set Itho ventilation unit to high speed +// 4 - set Itho ventilation unit to full speed +// 13 - set itho to high speed with hardware timer (10 min) +// 23 - set itho to high speed with hardware timer (20 min) +// 33 - set itho to high speed with hardware timer (30 min) + +//List of States: + +// 1 - Itho ventilation unit to lowest speed +// 2 - Itho ventilation unit to medium speed +// 3 - Itho ventilation unit to high speed +// 4 - Itho ventilation unit to full speed +// 13 -Itho to high speed with hardware timer (10 min) +// 23 -Itho to high speed with hardware timer (20 min) +// 33 -Itho to high speed with hardware timer (30 min) + +// Usage for http (not case sensitive): +// http://ip/control?cmd=STATE,1111 +// http://ip/control?cmd=STATE,1 +// http://ip/control?cmd=STATE,2 +// http://ip/control?cmd=STATE,3 + +// usage for example mosquito MQTT +// mosquitto_pub -t /Fan/cmd -m 'state 1111' +// mosquitto_pub -t /Fan/cmd -m 'state 1' +// mosquitto_pub -t /Fan/cmd -m 'state 2' +// mosquitto_pub -t /Fan/cmd -m 'state 3' + + +// This code needs the library made by 'supersjimmie': https://github.com/supersjimmie/IthoEcoFanRFT/tree/master/Master/Itho +// A CC1101 868Mhz transmitter is needed +// See https://gathering.tweakers.net/forum/list_messages/1690945 for more information +// code/idea was inspired by first release of code from 'Thinkpad' + +#include +#include "IthoCC1101.h" +#include "IthoPacket.h" +#include "_Plugin_Helper.h" +#ifdef USES_P118 + +//This extra settings struct is needed because the default settingsstruct doesn't support strings +struct PLUGIN__ExtraSettingsStruct +{ char ID1[9]; + char ID2[9]; + char ID3[9]; +} PLUGIN_118_ExtraSettings; + +IthoCC1101 PLUGIN_118_rf; + +// extra for interrupt handling +bool PLUGIN_118_ITHOhasPacket = false; +int PLUGIN_118_State=1; // after startup it is assumed that the fan is running low +int PLUGIN_118_OldState=1; +int PLUGIN_118_Timer=0; +int PLUGIN_118_LastIDindex = 0; +int PLUGIN_118_OldLastIDindex = 0; +int8_t Plugin_118_IRQ_pin=-1; +bool PLUGIN_118_InitRunned = false; +bool PLUGIN_118_Log = false; + +// volatile for interrupt function +volatile bool PLUGIN_118_Int = false; +//volatile unsigned long PLUGIN_118_Int_time = 0; + +#define PLUGIN_118 +#define PLUGIN_ID_118 118 +#define PLUGIN_NAME_118 "Itho ventilation remote" +#define PLUGIN_VALUENAME1_118 "State" +#define PLUGIN_VALUENAME2_118 "Timer" +#define PLUGIN_VALUENAME3_118 "LastIDindex" + +// Timer values for hardware timer in Fan +#define PLUGIN_118_Time1 10*60 +#define PLUGIN_118_Time2 20*60 +#define PLUGIN_118_Time3 30*60 + +boolean Plugin_118(byte function, struct EventStruct *event, String &string) +{ + boolean success = false; + + switch (function) + { + + case PLUGIN_DEVICE_ADD: + { + Device[++deviceCount].Number = PLUGIN_ID_118; + Device[deviceCount].Type = DEVICE_TYPE_SINGLE; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; + Device[deviceCount].Ports = 0; + Device[deviceCount].PullUpOption = false; + Device[deviceCount].InverseLogicOption = false; + Device[deviceCount].FormulaOption = false; + Device[deviceCount].ValueCount = 3; + Device[deviceCount].SendDataOption = true; + Device[deviceCount].TimerOption = true; + Device[deviceCount].GlobalSyncOption = true; + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_118); + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_118)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_118)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_118)); + break; + } + + case PLUGIN_GET_DEVICEGPIONAMES: + { + event->String1 = formatGpioName_input(F("Interrupt pin (CC1101 GDO2)")); + break; + } + + case PLUGIN_SET_DEFAULTS: + { + PCONFIG(0) = 1; + PCONFIG(1) = 10; + PCONFIG(2) = 87; + PCONFIG(3) = 81; + success = true; + break; + } + + case PLUGIN_INIT: + { + //If configured interrupt pin differs from configured, release old pin first + if ((Settings.TaskDevicePin1[event->TaskIndex]!=Plugin_118_IRQ_pin) && (Plugin_118_IRQ_pin!=-1)) + { + addLog(LOG_LEVEL_DEBUG, F("IO-PIN changed, deatachinterrupt old pin")); + detachInterrupt(Plugin_118_IRQ_pin); + } + LoadCustomTaskSettings(event->TaskIndex, (byte*)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + addLog(LOG_LEVEL_INFO, F("Extra Settings PLUGIN_118 loaded")); + PLUGIN_118_rf.setDeviceID(PCONFIG(1), PCONFIG(2), PCONFIG(3)); //DeviceID used to send commands, can also be changed on the fly for multi itho control, 10,87,81 corresponds with old library + PLUGIN_118_rf.init(); + Plugin_118_IRQ_pin = Settings.TaskDevicePin1[event->TaskIndex]; + pinMode(Plugin_118_IRQ_pin, INPUT); + attachInterrupt(Plugin_118_IRQ_pin, PLUGIN_118_ITHOinterrupt, FALLING); + addLog(LOG_LEVEL_INFO, F("CC1101 868Mhz transmitter initialized")); + PLUGIN_118_rf.initReceive(); + PLUGIN_118_InitRunned=true; + success = true; + break; + } + + case PLUGIN_EXIT: + { + addLog(LOG_LEVEL_INFO, F("EXIT PLUGIN_118")); + //remove interupt when plugin is removed + detachInterrupt(Plugin_118_IRQ_pin); + success = true; + break; + } + + case PLUGIN_ONCE_A_SECOND: + { + //decrement timer when timermode is running + if (PLUGIN_118_State>=10) PLUGIN_118_Timer--; + + //if timer has elapsed set Fan state to low + if ((PLUGIN_118_State>=10) && (PLUGIN_118_Timer<=0)) + { + PLUGIN_118_State=1; + PLUGIN_118_Timer=0; + } + + //Publish new data when vars are changed or init has runned or timer is running (update every 2 sec) + if ((PLUGIN_118_OldState!=PLUGIN_118_State) || ((PLUGIN_118_Timer>0) && (PLUGIN_118_Timer % 2==0)) || (PLUGIN_118_OldLastIDindex!=PLUGIN_118_LastIDindex)|| PLUGIN_118_InitRunned) + { + addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_ONCE_A_SECOND")); + PLUGIN_118_Publishdata(event); + sendData(event); + //reset flag set by init + PLUGIN_118_InitRunned=false; + } + //Remeber current state for next cycle + PLUGIN_118_OldState=PLUGIN_118_State; + PLUGIN_118_OldLastIDindex =PLUGIN_118_LastIDindex; + success = true; + break; + } + + case PLUGIN_FIFTY_PER_SECOND: + { + if (PLUGIN_118_Int) + { + PLUGIN_118_Int = false; // reset flag + PLUGIN_118_ITHOcheck(); + /* + unsigned long time_elapsed = millis() - PLUGIN_118_Int_time; //Disabled as it doesn't appear to be required + if (time_elapsed >= 10) + { + PLUGIN_118_ITHOcheck(); + } + else + { + delay(10-time_elapsed); + PLUGIN_118_ITHOcheck(); + }*/ + } + success = true; + break; + } + + + case PLUGIN_READ: { + // This ensures that even when Values are not changing, data is send at the configured interval for aquisition + addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_READ")); + PLUGIN_118_Publishdata(event); + //sendData(event); //SV - Added to send status every xx secnds as set within plugin + success = true; + break; + } + + case PLUGIN_WRITE: { + String tmpString = string; + String cmd = parseString(tmpString, 1); + String param1 = parseString(tmpString, 2); + if (cmd.equalsIgnoreCase(F("STATE"))) + { + + if (param1.equalsIgnoreCase(F("1111"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoJoin); + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'join' to Itho unit")); + printWebString += F("Sent command for 'join' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("9999"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoLeave); + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'leave' to Itho unit")); + printWebString += F("Sent command for 'leave' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("0"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoStandby); + PLUGIN_118_State=0; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'standby' to Itho unit")); + printWebString += F("Sent command for 'standby' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("1"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoLow); + PLUGIN_118_State=1; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'low speed' to Itho unit")); + printWebString += F("Sent command for 'low speed' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("2"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoMedium); + PLUGIN_118_State=2; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'medium speed' to Itho unit")); + printWebString += F("Sent command for 'medium speed' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("3"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoHigh); + PLUGIN_118_State=3; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'high speed' to Itho unit")); + printWebString += F("Sent command for 'high speed' to Itho unit"); + + success = true; + } + if (param1.equalsIgnoreCase(F("4"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoFull); + PLUGIN_118_State=4; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'full speed' to Itho unit")); + printWebString += F("Sent command for 'full speed' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("13"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer1); + PLUGIN_118_State=13; + PLUGIN_118_Timer=PLUGIN_118_Time1; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 1' to Itho unit")); + printWebString += F("Sent command for 'timer 1' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("23"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer2); + PLUGIN_118_State=23; + PLUGIN_118_Timer=PLUGIN_118_Time2; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 2' to Itho unit")); + printWebString += F("Sent command for 'timer 2' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("33"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer3); + PLUGIN_118_State=33; + PLUGIN_118_Timer=PLUGIN_118_Time3; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 3' to Itho unit")); + printWebString += F("Sent command for 'timer 3' to Itho unit"); + success = true; + } + } + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + addFormSubHeader(F("Remote RF Controls")); + addFormTextBox(F("Unit ID remote 1"), F("PLUGIN_118_ID1"), PLUGIN_118_ExtraSettings.ID1, 8); + addFormTextBox(F("Unit ID remote 2"), F("PLUGIN_118_ID2"), PLUGIN_118_ExtraSettings.ID2, 8); + addFormTextBox(F("Unit ID remote 3"), F("PLUGIN_118_ID3"), PLUGIN_118_ExtraSettings.ID3, 8); + addFormCheckBox(F("Enable RF receive log"), F("p118_log"), PCONFIG(0)); + addFormNumericBox(F("Device ID byte 1"), F("p118_deviceid1"), PCONFIG(1), 0, 255); + addFormNumericBox(F("Device ID byte 2"), F("p118_deviceid2"), PCONFIG(2), 0, 255); + addFormNumericBox(F("Device ID byte 3"), F("p118_deviceid3"), PCONFIG(3), 0, 255); + addFormNote(F("Device ID of your ESP, should not be the same as your neighbours ;-). Defaults to 10,87,81 which corresponds to the old Itho code")); + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + strcpy(PLUGIN_118_ExtraSettings.ID1, web_server.arg(F("PLUGIN_118_ID1")).c_str()); + strcpy(PLUGIN_118_ExtraSettings.ID2, web_server.arg(F("PLUGIN_118_ID2")).c_str()); + strcpy(PLUGIN_118_ExtraSettings.ID3, web_server.arg(F("PLUGIN_118_ID3")).c_str()); + SaveCustomTaskSettings(event->TaskIndex, (byte*)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + + PCONFIG(0) = isFormItemChecked(F("p118_log")); + PLUGIN_118_Log = PCONFIG(0); + PCONFIG(1) = getFormItemInt(F("p118_deviceid1"), 10); + PCONFIG(2) = getFormItemInt(F("p118_deviceid2"), 87); + PCONFIG(3) = getFormItemInt(F("p118_deviceid3"), 81); + success = true; + break; + } + } +return success; +} + +ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt() +{ + PLUGIN_118_Int = true; //flag + //PLUGIN_118_Int_time = millis(); //used to register time since interrupt, to make sure we don't read within 10 ms as the RX buffer needs some time to get ready. Update: Disabled as it appear not necessary +} + +void PLUGIN_118_ITHOcheck() +{ + noInterrupts(); + if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, "RF signal received\n");} + if(PLUGIN_118_rf.checkForNewPacket()) + { + IthoCommand cmd = PLUGIN_118_rf.getLastCommand(); + String Id = PLUGIN_118_rf.getLastIDstr(); + + //Move check here to prevent function calling within ISR + byte index = 0; + if (Id == PLUGIN_118_ExtraSettings.ID1){ + index = 1; + } + else if (Id == PLUGIN_118_ExtraSettings.ID2){ + index = 2; + } + else if (Id == PLUGIN_118_ExtraSettings.ID3){ + index = 3; + } + + //int index = PLUGIN_118_RFRemoteIndex(Id); + // IF id is know index should be >0 + if (index>0) + { + if(PLUGIN_118_Log){ + String log = F("Command received from remote-ID: "); + log += Id; + addLog(LOG_LEVEL_DEBUG, log); + } + String log2 = ""; + if(PLUGIN_118_Log){log2 += F("Command received=");} + switch (cmd) + { + case IthoUnknown: + if(PLUGIN_118_Log){log2 += F("unknown\n");} + break; + case IthoStandby: + case DucoStandby: + if(PLUGIN_118_Log){log2 += F("standby\n");} + PLUGIN_118_State = 0; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoLow: + case DucoLow: + if(PLUGIN_118_Log){log2 += F("low\n");} + PLUGIN_118_State = 1; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoMedium: + case DucoMedium: + if(PLUGIN_118_Log){log2 += F("medium\n");} + PLUGIN_118_State = 2; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoHigh: + case DucoHigh: + if(PLUGIN_118_Log){log2 += F("high\n");} + PLUGIN_118_State = 3; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoFull: + if(PLUGIN_118_Log){log2 += F("full\n");} + PLUGIN_118_State = 4; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoTimer1: + if(PLUGIN_118_Log){log2 += +F("timer1\n");} + PLUGIN_118_State = 13; + PLUGIN_118_Timer = PLUGIN_118_Time1; + PLUGIN_118_LastIDindex = index; + break; + case IthoTimer2: + if(PLUGIN_118_Log){log2 += F("timer2\n");} + PLUGIN_118_State = 23; + PLUGIN_118_Timer = PLUGIN_118_Time2; + PLUGIN_118_LastIDindex = index; + break; + case IthoTimer3: + if(PLUGIN_118_Log){log2 += F("timer3\n");} + PLUGIN_118_State = 33; + PLUGIN_118_Timer = PLUGIN_118_Time3; + PLUGIN_118_LastIDindex = index; + break; + case IthoJoin: + if(PLUGIN_118_Log){log2 += F("join\n");} + break; + case IthoLeave: + if(PLUGIN_118_Log){log2 += F("leave\n");} + break; + } + if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, log2);} + } + else { + if(PLUGIN_118_Log){ + String log = F("Device-ID:"); + log += Id; + log += F(" IGNORED"); + addLog(LOG_LEVEL_DEBUG, log); + } + } + } + interrupts(); +} + +void PLUGIN_118_Publishdata(struct EventStruct *event) +{ + UserVar[event->BaseVarIndex]=PLUGIN_118_State; + UserVar[event->BaseVarIndex+1]=PLUGIN_118_Timer; + UserVar[event->BaseVarIndex+2]=PLUGIN_118_LastIDindex; + + String log = F("State: "); + log += UserVar[event->BaseVarIndex]; + addLog(LOG_LEVEL_DEBUG, log); + log = F("Timer: "); + log += UserVar[event->BaseVarIndex+1]; + addLog(LOG_LEVEL_DEBUG, log); + log = F("LastIDindex: "); + log += UserVar[event->BaseVarIndex+2]; + addLog(LOG_LEVEL_DEBUG, log); +} + + +#endif // USES_P118 diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index bb0835465..df729d989 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1133,6 +1133,7 @@ To create/register a plugin, you have to : #define USES_P094 // CUL Reader #define USES_P114 // VEML6075 UVA/UVB sensor #define USES_P115 // Fuel Gauge MAX1704x + #define USES_P118 // Itho ventilation control #endif @@ -1144,14 +1145,14 @@ To create/register a plugin, you have to : #ifndef USES_P027 #define USES_P027 // INA219 #endif - #ifndef USES_P076 + #ifndef USES_P076 #define USES_P076 // HWL8012 in POW r1 #endif - #ifndef USES_P077 + #ifndef USES_P077 // Needs CSE7766 Energy sensor, via Serial RXD 4800 baud 8E1 (GPIO1), TXD (GPIO3) #define USES_P077 // CSE7766 in POW R2 #endif - #ifndef USES_P078 + #ifndef USES_P078 #define USES_P078 // Eastron Modbus Energy meters #endif #ifndef USES_P085 @@ -1163,7 +1164,7 @@ To create/register a plugin, you have to : #ifndef USES_P102 #define USES_P102 // PZEM-004Tv30 #endif - #ifndef USES_P108 + #ifndef USES_P108 #define USES_P108 // DDS238-x ZN MODBUS energy meter (was P224 in the Playground) #endif #ifndef USES_P115 @@ -1179,31 +1180,31 @@ To create/register a plugin, you have to : #ifndef USES_P023 #define USES_P023 // OLED #endif - #ifndef USES_P036 + #ifndef USES_P036 #define USES_P036 // FrameOLED #endif - #ifndef USES_P038 + #ifndef USES_P038 #define USES_P038 // NeoPixel #endif - #ifndef USES_P041 + #ifndef USES_P041 #define USES_P041 // NeoClock #endif - #ifndef USES_P042 + #ifndef USES_P042 #define USES_P042 // Candle #endif - #ifndef USES_P057 + #ifndef USES_P057 #define USES_P057 // HT16K33_LED #endif - #ifndef USES_P070 + #ifndef USES_P070 #define USES_P070 // NeoPixel_Clock #endif - #ifndef USES_P075 + #ifndef USES_P075 #define USES_P075 // Nextion #endif - #ifndef USES_P095 + #ifndef USES_P095 #define USES_P095 // TFT ILI9341 #endif - #ifndef USES_P096 + #ifndef USES_P096 #define USES_P096 // eInk (Needs lib_deps = Adafruit GFX Library, LOLIN_EPD ) #endif #ifndef USES_P099 @@ -1280,7 +1281,7 @@ To create/register a plugin, you have to : #define USES_P142 // RGB-Strip #define USES_P143 // AnyonePresent #define USES_P144 // RC-Switch-TX - #define USES_P145 // Itho + #define USES_P145 // Itho - P118 in the main repo. #define USES_P149 // MHZ19 #define USES_P150 // SDM120C #define USES_P151 // CISA @@ -1401,34 +1402,34 @@ To create/register a plugin, you have to : #define USES_P115 // Fuel gauge MAX1704x #endif #ifndef USES_P116 - #define USES_P116 // + #define USES_P116 // #endif #ifndef USES_P117 - #define USES_P117 // + #define USES_P117 // #endif #ifndef USES_P118 - #define USES_P118 // + #define USES_P118 // Itho ventilation coontrol #endif #ifndef USES_P119 - #define USES_P119 // + #define USES_P119 // #endif #ifndef USES_P120 - #define USES_P120 // + #define USES_P120 // #endif #ifndef USES_P121 - #define USES_P121 // + #define USES_P121 // #endif #ifndef USES_P122 - #define USES_P122 // + #define USES_P122 // #endif #ifndef USES_P123 - #define USES_P123 // + #define USES_P123 // #endif #ifndef USES_P124 - #define USES_P124 // + #define USES_P124 // #endif #ifndef USES_P125 - #define USES_P125 // + #define USES_P125 // #endif // Controllers From ffac89221d7fc76f17e964beb5695f767bc8bf42 Mon Sep 17 00:00:00 2001 From: svollebregt <42436341+svollebregt@users.noreply.github.com> Date: Sun, 10 Oct 2021 20:20:48 +0200 Subject: [PATCH 02/17] Revert "Adding Itho library to main reposity" This reverts commit 1cfdd9e8b4ddbc0289b2e50d6de02d66a00c2b27. --- .gitignore | 1 - lib/Itho/CC1101.cpp | 276 -------- lib/Itho/CC1101.h | 207 ------ lib/Itho/CC1101Packet.h | 23 - lib/Itho/IthoCC1101.cpp | 849 ----------------------- lib/Itho/IthoCC1101.h | 116 ---- lib/Itho/IthoPacket.h | 49 -- platformio.ini | 4 +- src/_P118_Itho.ino | 548 --------------- src/src/CustomBuild/define_plugin_sets.h | 49 +- 10 files changed, 26 insertions(+), 2096 deletions(-) delete mode 100644 lib/Itho/CC1101.cpp delete mode 100644 lib/Itho/CC1101.h delete mode 100644 lib/Itho/CC1101Packet.h delete mode 100644 lib/Itho/IthoCC1101.cpp delete mode 100644 lib/Itho/IthoCC1101.h delete mode 100644 lib/Itho/IthoPacket.h delete mode 100644 src/_P118_Itho.ino diff --git a/.gitignore b/.gitignore index 6eefe271d..981c8ae04 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,3 @@ build_output/ **/__tmpfile.cpp src/_Custom.h -platformio.ini diff --git a/lib/Itho/CC1101.cpp b/lib/Itho/CC1101.cpp deleted file mode 100644 index 903a6cab7..000000000 --- a/lib/Itho/CC1101.cpp +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 - */ - -#include "CC1101.h" - -// default constructor -CC1101::CC1101() -{ - SPI.begin(); -#ifdef ESP8266 - pinMode(SS, OUTPUT); -#endif -} //CC1101 - -// default destructor -CC1101::~CC1101() -{ -} //~CC1101 - -/***********************/ -// SPI helper functions select() and deselect() -inline void CC1101::select(void) { - digitalWrite(SS, LOW); -} - -inline void CC1101::deselect(void) { - digitalWrite(SS, HIGH); -} - -void CC1101::spi_waitMiso() -{ - while(digitalRead(MISO) == HIGH) yield(); -} - -void CC1101::init() -{ - reset(); -} - -void CC1101::reset() -{ - deselect(); - delayMicroseconds(5); - select(); - delayMicroseconds(10); - deselect(); - delayMicroseconds(45); - select(); - - spi_waitMiso(); - SPI.transfer(CC1101_SRES); - delay(10); - spi_waitMiso(); - deselect(); -} - -uint8_t CC1101::writeCommand(uint8_t command) -{ - uint8_t result; - - select(); - spi_waitMiso(); - result = SPI.transfer(command); - deselect(); - - return result; -} - -void CC1101::writeRegister(uint8_t address, uint8_t data) -{ - select(); - spi_waitMiso(); - SPI.transfer(address); - SPI.transfer(data); - deselect(); -} - -uint8_t CC1101::readRegister(uint8_t address) -{ - uint8_t val; - - select(); - spi_waitMiso(); - SPI.transfer(address); - val = SPI.transfer(0); - deselect(); - - return val; -} - -uint8_t CC1101::readRegisterMedian3(uint8_t address) -{ - uint8_t val, val1, val2, val3; - - select(); - spi_waitMiso(); - SPI.transfer(address); - val1 = SPI.transfer(0); - SPI.transfer(address); - val2 = SPI.transfer(0); - SPI.transfer(address); - val3 = SPI.transfer(0); - deselect(); - // reverse sort (largest in val1) because this is te expected order for TX_BUFFER - if (val3 > val2) {val = val3; val3 = val2; val2 = val; } //Swap(val3,val2) - if (val2 > val1) {val = val2; val2 = val1, val1 = val; } //Swap(val2,val1) - if (val3 > val2) {val = val3; val3 = val2, val2 = val; } //Swap(val3,val2) - - return val2; -} - -/* Known SPI/26MHz synchronization bug (see CC1101 errata) -This issue affects the following registers: SPI status byte (fields STATE and FIFO_BYTES_AVAILABLE), -FREQEST or RSSI while the receiver is active, MARCSTATE at any time other than an IDLE radio state, -RXBYTES when receiving or TXBYTES when transmitting, and WORTIME1/WORTIME0 at any time.*/ -//uint8_t CC1101::readRegisterWithSyncProblem(uint8_t address, uint8_t registerType) -uint8_t /* ICACHE_RAM_ATTR */ CC1101::readRegisterWithSyncProblem(uint8_t address, uint8_t registerType) -{ - uint8_t value1, value2; - - value1 = readRegister(address | registerType); - - //if two consecutive reads gives us the same result then we know we are ok - do - { - value2 = value1; - value1 = readRegister(address | registerType); - } - while (value1 != value2); - - return value1; -} - -//registerType = CC1101_CONFIG_REGISTER or CC1101_STATUS_REGISTER -uint8_t CC1101::readRegister(uint8_t address, uint8_t registerType) -{ - switch (address) - { - case CC1101_FREQEST: - case CC1101_MARCSTATE: - case CC1101_RXBYTES: - case CC1101_TXBYTES: - case CC1101_WORTIME1: - case CC1101_WORTIME0: - return readRegisterWithSyncProblem(address, registerType); - - default: - return readRegister(address | registerType); - } -} - -void CC1101::writeBurstRegister(uint8_t address, uint8_t* data, uint8_t length) -{ - uint8_t i; - - select(); - spi_waitMiso(); - SPI.transfer(address | CC1101_WRITE_BURST); - for (i = 0; i < length; i++) { - SPI.transfer(data[i]); - } - deselect(); -} - -void CC1101::readBurstRegister(uint8_t* buffer, uint8_t address, uint8_t length) -{ - uint8_t i; - - select(); - spi_waitMiso(); - SPI.transfer(address | CC1101_READ_BURST); - - for (i = 0; i < length; i++) { - buffer[i] = SPI.transfer(0x00); - } - - deselect(); -} - -//wait for fixed length in rx fifo -uint8_t CC1101::receiveData(CC1101Packet* packet, uint8_t length) -{ - uint8_t rxBytes = readRegisterWithSyncProblem(CC1101_RXBYTES, CC1101_STATUS_REGISTER); - rxBytes = rxBytes & CC1101_BITS_RX_BYTES_IN_FIFO; - - //check for rx fifo overflow - if ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) & CC1101_BITS_MARCSTATE) == CC1101_MARCSTATE_RXFIFO_OVERFLOW) - { - writeCommand(CC1101_SIDLE); //idle - writeCommand(CC1101_SFRX); //flush RX buffer - writeCommand(CC1101_SRX); //switch to RX state - } - else if (rxBytes == length) - { - readBurstRegister(packet->data, CC1101_RXFIFO, rxBytes); - - //continue RX - writeCommand(CC1101_SIDLE); //idle - writeCommand(CC1101_SFRX); //flush RX buffer - writeCommand(CC1101_SRX); //switch to RX state - - packet->length = rxBytes; - } - else - { - //empty fifo - packet->length = 0; - writeCommand(CC1101_SIDLE); //idle - writeCommand(CC1101_SFRX); //flush RX buffer - writeCommand(CC1101_SRX); //switch to RX state - } - - return packet->length; -} - -//This function is able to send packets bigger then the FIFO size. -void CC1101::sendData(CC1101Packet *packet) -{ - uint8_t index = 0; - uint8_t txStatus, MarcState; - uint8_t length; - - writeCommand(CC1101_SIDLE); //idle - - txStatus = readRegisterWithSyncProblem(CC1101_TXBYTES, CC1101_STATUS_REGISTER); - - //clear TX fifo if needed - if (txStatus & CC1101_BITS_TX_FIFO_UNDERFLOW) - { - writeCommand(CC1101_SIDLE); //idle - writeCommand(CC1101_SFTX); //flush TX buffer - } - - writeCommand(CC1101_SIDLE); //idle - - //determine how many bytes to send - length = (packet->length <= CC1101_DATA_LEN ? packet->length : CC1101_DATA_LEN); - - writeBurstRegister(CC1101_TXFIFO, packet->data, length); - - writeCommand(CC1101_SIDLE); - //start sending packet - writeCommand(CC1101_STX); - - //continue sending when packet is bigger than 64 bytes - if (packet->length > CC1101_DATA_LEN) - { - index += length; - - //loop until all bytes are transmitted - while (index < packet->length) - { - //check if there is free space in the fifo - while ((txStatus = (readRegisterMedian3(CC1101_TXBYTES | CC1101_STATUS_REGISTER) & CC1101_BITS_RX_BYTES_IN_FIFO)) > (CC1101_DATA_LEN - 2)); - - //calculate how many bytes we can send - length = (CC1101_DATA_LEN - txStatus); - length = ((packet->length - index) < length ? (packet->length - index) : length); - - //send some more bytes - for (int i=0; idata[index+i]); - - index += length; - } - } - - //wait until transmission is finished (TXOFF_MODE is expected to be set to 0/IDLE or TXFIFO_UNDERFLOW) - do - { - MarcState = (readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) & CC1101_BITS_MARCSTATE); -// if (MarcState == CC1101_MARCSTATE_TXFIFO_UNDERFLOW) Serial.print(F("TXFIFO_UNDERFLOW occured in sendData() \n")); - } - while((MarcState != CC1101_MARCSTATE_IDLE) && (MarcState != CC1101_MARCSTATE_TXFIFO_UNDERFLOW)); -} diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h deleted file mode 100644 index b30e0162a..000000000 --- a/lib/Itho/CC1101.h +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 - */ - -#ifndef __CC1101_H__ -#define __CC1101_H__ - -#include -#include "CC1101Packet.h" -#include -// On Arduino, SPI pins are predefined - -/* Type of transfers */ -#define CC1101_WRITE_BURST 0x40 -#define CC1101_READ_SINGLE 0x80 -#define CC1101_READ_BURST 0xC0 - -/* Type of register */ -#define CC1101_CONFIG_REGISTER CC1101_READ_SINGLE -#define CC1101_STATUS_REGISTER CC1101_READ_BURST - -/* PATABLE & FIFO's */ -#define CC1101_PATABLE 0x3E // PATABLE address -#define CC1101_TXFIFO 0x3F // TX FIFO address -#define CC1101_RXFIFO 0x3F // RX FIFO address -#define CC1101_PA_LowPower 0x60 -#define CC1101_PA_LongDistance 0xC0 - -/* Command strobes */ -#define CC1101_SRES 0x30 // Reset CC1101 chip -#define CC1101_SFSTXON 0x31 // Enable and calibrate frequency synthesizer (if MCSM0.FS_AUTOCAL=1). If in RX (with CCA): Go to a wait state where only the synthesizer is running (for quick RX / TX turnaround). -#define CC1101_SXOFF 0x32 // Turn off crystal oscillator -#define CC1101_SCAL 0x33 // Calibrate frequency synthesizer and turn it off. SCAL can be strobed from IDLE mode without setting manual calibration mode (MCSM0.FS_AUTOCAL=0) -#define CC1101_SRX 0x34 // Enable RX. Perform calibration first if coming from IDLE and MCSM0.FS_AUTOCAL=1 -#define CC1101_STX 0x35 // In IDLE state: Enable TX. Perform calibration first if MCSM0.FS_AUTOCAL=1. If in RX state and CCA is enabled: Only go to TX if channel is clear -#define CC1101_SIDLE 0x36 // Exit RX / TX, turn off frequency synthesizer and exit Wake-On-Radio mode if applicable -#define CC1101_SWOR 0x38 // Start automatic RX polling sequence (Wake-on-Radio) as described in Section 19.5 if WORCTRL.RC_PD=0 -#define CC1101_SPWD 0x39 // Enter power down mode when CSn goes high -#define CC1101_SFRX 0x3A // Flush the RX FIFO buffer. Only issue SFRX in IDLE or RXFIFO_OVERFLOW states -#define CC1101_SFTX 0x3B // Flush the TX FIFO buffer. Only issue SFTX in IDLE or TXFIFO_UNDERFLOW states -#define CC1101_SWORRST 0x3C // Reset real time clock to Event1 value -#define CC1101_SNOP 0x3D // No operation. May be used to get access to the chip status byte - -/* CC1101 configuration registers */ -#define CC1101_IOCFG2 0x00 // GDO2 Output Pin Configuration -#define CC1101_IOCFG1 0x01 // GDO1 Output Pin Configuration -#define CC1101_IOCFG0 0x02 // GDO0 Output Pin Configuration -#define CC1101_FIFOTHR 0x03 // RX FIFO and TX FIFO Thresholds -#define CC1101_SYNC1 0x04 // Sync Word, High Byte -#define CC1101_SYNC0 0x05 // Sync Word, Low Byte -#define CC1101_PKTLEN 0x06 // Packet Length -#define CC1101_PKTCTRL1 0x07 // Packet Automation Control -#define CC1101_PKTCTRL0 0x08 // Packet Automation Control -#define CC1101_ADDR 0x09 // Device Address -#define CC1101_CHANNR 0x0A // Channel Number -#define CC1101_FSCTRL1 0x0B // Frequency Synthesizer Control -#define CC1101_FSCTRL0 0x0C // Frequency Synthesizer Control -#define CC1101_FREQ2 0x0D // Frequency Control Word, High Byte -#define CC1101_FREQ1 0x0E // Frequency Control Word, Middle Byte -#define CC1101_FREQ0 0x0F // Frequency Control Word, Low Byte -#define CC1101_MDMCFG4 0x10 // Modem Configuration -#define CC1101_MDMCFG3 0x11 // Modem Configuration -#define CC1101_MDMCFG2 0x12 // Modem Configuration -#define CC1101_MDMCFG1 0x13 // Modem Configuration -#define CC1101_MDMCFG0 0x14 // Modem Configuration -#define CC1101_DEVIATN 0x15 // Modem Deviation Setting -#define CC1101_MCSM2 0x16 // Main Radio Control State Machine Configuration -#define CC1101_MCSM1 0x17 // Main Radio Control State Machine Configuration -#define CC1101_MCSM0 0x18 // Main Radio Control State Machine Configuration -#define CC1101_FOCCFG 0x19 // Frequency Offset Compensation Configuration -#define CC1101_BSCFG 0x1A // Bit Synchronization Configuration -#define CC1101_AGCCTRL2 0x1B // AGC Control -#define CC1101_AGCCTRL1 0x1C // AGC Control -#define CC1101_AGCCTRL0 0x1D // AGC Control -#define CC1101_WOREVT1 0x1E // High Byte Event0 Timeout -#define CC1101_WOREVT0 0x1F // Low Byte Event0 Timeout -#define CC1101_WORCTRL 0x20 // Wake On Radio Control -#define CC1101_FREND1 0x21 // Front End RX Configuration -#define CC1101_FREND0 0x22 // Front End TX Configuration -#define CC1101_FSCAL3 0x23 // Frequency Synthesizer Calibration -#define CC1101_FSCAL2 0x24 // Frequency Synthesizer Calibration -#define CC1101_FSCAL1 0x25 // Frequency Synthesizer Calibration -#define CC1101_FSCAL0 0x26 // Frequency Synthesizer Calibration -#define CC1101_RCCTRL1 0x27 // RC Oscillator Configuration -#define CC1101_RCCTRL0 0x28 // RC Oscillator Configuration -#define CC1101_FSTEST 0x29 // Frequency Synthesizer Calibration Control -#define CC1101_PTEST 0x2A // Production Test -#define CC1101_AGCTEST 0x2B // AGC Test -#define CC1101_TEST2 0x2C // Various Test Settings -#define CC1101_TEST1 0x2D // Various Test Settings -#define CC1101_TEST0 0x2E // Various Test Settings - -/* Status registers */ -#define CC1101_PARTNUM 0x30 // Chip ID -#define CC1101_VERSION 0x31 // Chip ID -#define CC1101_FREQEST 0x32 // Frequency Offset Estimate from Demodulator -#define CC1101_LQI 0x33 // Demodulator Estimate for Link Quality -#define CC1101_RSSI 0x34 // Received Signal Strength Indication -#define CC1101_MARCSTATE 0x35 // Main Radio Control State Machine State -#define CC1101_WORTIME1 0x36 // High Byte of WOR Time -#define CC1101_WORTIME0 0x37 // Low Byte of WOR Time -#define CC1101_PKTSTATUS 0x38 // Current GDOx Status and Packet Status -#define CC1101_VCO_VC_DAC 0x39 // Current Setting from PLL Calibration Module -#define CC1101_TXBYTES 0x3A // Underflow and Number of Bytes -#define CC1101_RXBYTES 0x3B // Overflow and Number of Bytes -#define CC1101_RCCTRL1_STATUS 0x3C // Last RC Oscillator Calibration Result -#define CC1101_RCCTRL0_STATUS 0x3D // Last RC Oscillator Calibration Result - -/* Bit fields in the chip status byte */ -#define CC1101_STATUS_CHIP_RDYn_BM 0x80 -#define CC1101_STATUS_STATE_BM 0x70 -#define CC1101_STATUS_FIFO_BYTES_AVAILABLE_BM 0x0F - -/* Masks to retrieve status bit */ -#define CC1101_BITS_TX_FIFO_UNDERFLOW 0x80 -#define CC1101_BITS_RX_BYTES_IN_FIFO 0x7F -#define CC1101_BITS_MARCSTATE 0x1F - - -/* Marc states */ -enum CC1101MarcStates -{ - CC1101_MARCSTATE_SLEEP = 0x00, - CC1101_MARCSTATE_IDLE = 0x01, - CC1101_MARCSTATE_XOFF = 0x02, - CC1101_MARCSTATE_VCOON_MC = 0x03, - CC1101_MARCSTATE_REGON_MC = 0x04, - CC1101_MARCSTATE_MANCAL = 0x05, - CC1101_MARCSTATE_VCOON = 0x06, - CC1101_MARCSTATE_REGON = 0x07, - CC1101_MARCSTATE_STARTCAL = 0x08, - CC1101_MARCSTATE_BWBOOST = 0x09, - CC1101_MARCSTATE_FS_LOCK = 0x0A, - CC1101_MARCSTATE_IFADCON = 0x0B, - CC1101_MARCSTATE_ENDCAL = 0x0C, - CC1101_MARCSTATE_RX = 0x0D, - CC1101_MARCSTATE_RX_END = 0x0E, - CC1101_MARCSTATE_RX_RST = 0x0F, - CC1101_MARCSTATE_TXRX_SWITCH = 0x10, - CC1101_MARCSTATE_RXFIFO_OVERFLOW = 0x11, - CC1101_MARCSTATE_FSTXON = 0x12, - CC1101_MARCSTATE_TX = 0x13, - CC1101_MARCSTATE_TX_END = 0x14, - CC1101_MARCSTATE_RXTX_SWITCH = 0x15, - CC1101_MARCSTATE_TXFIFO_UNDERFLOW = 0x16 -}; - - -/* Chip states */ -enum CC1101ChipStates -{ - CC1101_STATE_MASK = 0x70, - CC1101_STATE_IDLE = 0x00, - CC1101_STATE_RX = 0x10, - CC1101_STATE_TX = 0x20, - CC1101_STATE_FSTXON = 0x30, - CC1101_STATE_CALIBRATE = 0x40, - CC1101_STATE_SETTLING = 0x50, - CC1101_STATE_RX_OVERFLOW = 0x60, - CC1101_STATE_TX_UNDERFLOW = 0x70 -}; - - - -class CC1101 -{ - protected: - - //functions - public: - CC1101(); - ~CC1101(); - - //spi - void spi_waitMiso(); - - //cc1101 - void init(); - - uint8_t writeCommand(uint8_t command); - void writeRegister(uint8_t address, uint8_t data); - - uint8_t readRegister(uint8_t address, uint8_t registerType); - - void writeBurstRegister(uint8_t address, uint8_t* data, uint8_t length); - void readBurstRegister(uint8_t* buffer, uint8_t address, uint8_t length); - - void sendData(CC1101Packet *packet); - uint8_t receiveData(CC1101Packet* packet, uint8_t length); - - private: - CC1101( const CC1101 &c ); - CC1101& operator=( const CC1101 &c ); - // SPI helper functions - void select(void); - void deselect(void); - - protected: - uint8_t readRegister(uint8_t address); - uint8_t readRegisterMedian3(uint8_t address); - uint8_t readRegisterWithSyncProblem(uint8_t address, uint8_t registerType); - - void reset(); - -}; //CC1101 - -#endif //__CC1101_H__ diff --git a/lib/Itho/CC1101Packet.h b/lib/Itho/CC1101Packet.h deleted file mode 100644 index f8c4f01b4..000000000 --- a/lib/Itho/CC1101Packet.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 - */ - -#ifndef CC1101PACKET_H_ -#define CC1101PACKET_H_ - -#include -#include - -#define CC1101_BUFFER_LEN 64 -#define CC1101_DATA_LEN CC1101_BUFFER_LEN - 3 - - -class CC1101Packet -{ - public: - uint8_t length; - uint8_t data[128]; -}; - - -#endif /* CC1101PACKET_H_ */ diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp deleted file mode 100644 index 97d158879..000000000 --- a/lib/Itho/IthoCC1101.cpp +++ /dev/null @@ -1,849 +0,0 @@ -/* - Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra -*/ -#define DEBUG 0 - -#define BYTE_TO_BINARY_PATTERN "%c,%c,%c,%c,%c,%c,%c,%c," -#define BYTE_TO_BINARY(byte) \ - (byte & 0x80 ? '1' : '0'), \ - (byte & 0x40 ? '1' : '0'), \ - (byte & 0x20 ? '1' : '0'), \ - (byte & 0x10 ? '1' : '0'), \ - (byte & 0x08 ? '1' : '0'), \ - (byte & 0x04 ? '1' : '0'), \ - (byte & 0x02 ? '1' : '0'), \ - (byte & 0x01 ? '1' : '0') - -#include "IthoCC1101.h" -#include -#include -#include - -//#define CRC_FILTER - -////original sync byte pattern -//#define STARTBYTE 6 //relevant data starts 6 bytes after the sync pattern bytes 170/171 -//#define SYNC1 170 -//#define SYNC0 171 -//#define MDMCFG2 0x02 //16bit sync word / 16bit specific - -////alternative sync byte pattern (filter much more non-itho messages out. Maybe too strict? Testing needed. -//#define STARTBYTE 0 //relevant data starts 0 bytes after the sync pattern bytes 179/42/171/42 -//#define SYNC1 187 //byte11 = 179, byte13 = 171 with SYNC1 = 163, 179 and 171 differ only by 1 bit -//#define SYNC0 42 -//#define MDMCFG2 0x03 //32bit sync word / 30bit specific - -//alternative sync byte pattern -#define STARTBYTE 2 //relevant data starts 2 bytes after the sync pattern bytes 179/42 -#define SYNC1 179 -#define SYNC0 42 -#define MDMCFG2 0x02 //16bit sync word / 16bit specific - -// default constructor -IthoCC1101::IthoCC1101(uint8_t counter, uint8_t sendTries) : CC1101() -{ - this->outIthoPacket.counter = counter; - this->sendTries = sendTries; - - this->outIthoPacket.deviceId[0] = 33; - this->outIthoPacket.deviceId[1] = 66; - this->outIthoPacket.deviceId[2] = 99; - - this->outIthoPacket.deviceType = 22; - -} //IthoCC1101 - -// default destructor -IthoCC1101::~IthoCC1101() -{ -} //~IthoCC1101 - -void IthoCC1101::initSendMessage(uint8_t len) -{ - //finishTransfer(); - writeCommand(CC1101_SIDLE); - delayMicroseconds(1); - writeRegister(CC1101_IOCFG0 , 0x2E); - delayMicroseconds(1); - writeRegister(CC1101_IOCFG1 , 0x2E); - delayMicroseconds(1); - writeCommand(CC1101_SIDLE); - writeCommand(CC1101_SPWD); - delayMicroseconds(2); - - /* - Configuration reverse engineered from remote print. The commands below are used by IthoDaalderop. - Base frequency 868.299866MHz - Channel 0 - Channel spacing 199.951172kHz - Carrier frequency 868.299866MHz - Xtal frequency 26.000000MHz - Data rate 38.3835kBaud - Manchester disabled - Modulation 2-FSK - Deviation 50.781250kHz - TX power ? - PA ramping enabled - Whitening disabled - */ - writeCommand(CC1101_SRES); - delayMicroseconds(1); - writeRegister(CC1101_IOCFG0 , 0x2E); //High impedance (3-state) - writeRegister(CC1101_FREQ2 , 0x21); //00100001 878MHz-927.8MHz - writeRegister(CC1101_FREQ1 , 0x65); //01100101 - writeRegister(CC1101_FREQ0 , 0x6A); //01101010 - writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 - writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 - writeRegister(CC1101_MDMCFG2 , 0x00); //00000000 2-FSK, no manchester encoding/decoding, no preamble/sync - writeRegister(CC1101_MDMCFG1 , 0x22); //00100010 - writeRegister(CC1101_MDMCFG0 , 0xF8); //11111000 - writeRegister(CC1101_CHANNR , 0x00); //00000000 - writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 - writeRegister(CC1101_FREND0 , 0x17); //00010111 use index 7 in PA table - writeRegister(CC1101_MCSM0 , 0x18); //00011000 PO timeout Approx. 146microseconds - 171microseconds, Auto calibrate When going from IDLE to RX or TX (or FSTXON) - writeRegister(CC1101_FSCAL3 , 0xA9); //10101001 - writeRegister(CC1101_FSCAL2 , 0x2A); //00101010 - writeRegister(CC1101_FSCAL1 , 0x00); //00000000 - writeRegister(CC1101_FSCAL0 , 0x11); //00010001 - writeRegister(CC1101_FSTEST , 0x59); //01011001 For test only. Do not write to this register. - writeRegister(CC1101_TEST2 , 0x81); //10000001 For test only. Do not write to this register. - writeRegister(CC1101_TEST1 , 0x35); //00110101 For test only. Do not write to this register. - writeRegister(CC1101_TEST0 , 0x0B); //00001011 For test only. Do not write to this register. - writeRegister(CC1101_PKTCTRL0 , 0x12); //00010010 Enable infinite length packets, CRC disabled, Turn data whitening off, Serial Synchronous mode - writeRegister(CC1101_ADDR , 0x00); //00000000 - writeRegister(CC1101_PKTLEN , 0xFF); //11111111 //Not used, no hardware packet handling - - //0x6F,0x26,0x2E,0x8C,0x87,0xCD,0xC7,0xC0 - writeBurstRegister(CC1101_PATABLE | CC1101_WRITE_BURST, (uint8_t*)ithoPaTableSend, 8); - - //difference, message1 sends a STX here - writeCommand(CC1101_SIDLE); - writeCommand(CC1101_SIDLE); - - writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 - writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 - writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 - writeRegister(CC1101_IOCFG0 , 0x2D); //GDO0_Z_EN_N. When this output is 0, GDO0 is configured as input (for serial TX data). - writeRegister(CC1101_IOCFG1 , 0x0B); //Serial Clock. Synchronous to the data in synchronous serial mode. - - writeCommand(CC1101_STX); - writeCommand(CC1101_SIDLE); - - writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 - writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 - writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 - //writeRegister(CC1101_IOCFG0 ,0x2D); //GDO0_Z_EN_N. When this output is 0, GDO0 is configured as input (for serial TX data). - //writeRegister(CC1101_IOCFG1 ,0x0B); //Serial Clock. Synchronous to the data in synchronous serial mode. - - //Itho is using serial mode for transmit. We want to use the TX FIFO with fixed packet length for simplicity. - writeRegister(CC1101_IOCFG0 , 0x2E); - writeRegister(CC1101_IOCFG1 , 0x2E); - writeRegister(CC1101_PKTCTRL0 , 0x00); - writeRegister(CC1101_PKTCTRL1 , 0x00); - - writeRegister(CC1101_PKTLEN , len); - -} - -void IthoCC1101::finishTransfer() -{ - writeCommand(CC1101_SIDLE); - delayMicroseconds(1); - - writeRegister(CC1101_IOCFG0 , 0x2E); - writeRegister(CC1101_IOCFG1 , 0x2E); - - writeCommand(CC1101_SIDLE); - writeCommand(CC1101_SPWD); -} - -void IthoCC1101::initReceive() -{ - /* - Configuration reverse engineered from RFT print. - - Base frequency 868.299866MHz - Channel 0 - Channel spacing 199.951172kHz - Carrier frequency 868.299866MHz - Xtal frequency 26.000000MHz - Data rate 38.3835kBaud - RX filter BW 325.000000kHz - Manchester disabled - Modulation 2-FSK - Deviation 50.781250kHz - TX power 0x6F,0x26,0x2E,0x7F,0x8A,0x84,0xCA,0xC4 - PA ramping enabled - Whitening disabled - */ - writeCommand(CC1101_SRES); - - writeRegister(CC1101_TEST0 , 0x09); - writeRegister(CC1101_FSCAL2 , 0x00); - - //0x6F,0x26,0x2E,0x7F,0x8A,0x84,0xCA,0xC4 - writeBurstRegister(CC1101_PATABLE | CC1101_WRITE_BURST, (uint8_t*)ithoPaTableReceive, 8); - - writeCommand(CC1101_SCAL); - - //wait for calibration to finish - while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) yield(); - - writeRegister(CC1101_FSCAL2 , 0x00); - writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate - writeRegister(CC1101_FREQ2 , 0x21); - writeRegister(CC1101_FREQ1 , 0x65); - writeRegister(CC1101_FREQ0 , 0x6A); - writeRegister(CC1101_IOCFG0 , 0x2E); //High impedance (3-state) - writeRegister(CC1101_IOCFG2 , 0x06); //0x06 Assert when sync word has been sent / received, and de-asserts at the end of the packet. - writeRegister(CC1101_FSCTRL1 , 0x06); - writeRegister(CC1101_FSCTRL0 , 0x00); - writeRegister(CC1101_MDMCFG4 , 0x5A); - writeRegister(CC1101_MDMCFG3 , 0x83); - writeRegister(CC1101_MDMCFG2 , 0x00); //Enable digital DC blocking filter before demodulator, 2-FSK, Disable Manchester encoding/decoding, No preamble/sync - writeRegister(CC1101_MDMCFG1 , 0x22); //Disable FEC - writeRegister(CC1101_MDMCFG0 , 0xF8); - writeRegister(CC1101_CHANNR , 0x00); - writeRegister(CC1101_DEVIATN , 0x50); - writeRegister(CC1101_FREND1 , 0x56); - writeRegister(CC1101_FREND0 , 0x17); - writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate - writeRegister(CC1101_FOCCFG , 0x16); - writeRegister(CC1101_BSCFG , 0x6C); - writeRegister(CC1101_AGCCTRL2 , 0x43); - writeRegister(CC1101_AGCCTRL1 , 0x40); - writeRegister(CC1101_AGCCTRL0 , 0x91); - writeRegister(CC1101_FSCAL3 , 0xE9); - writeRegister(CC1101_FSCAL2 , 0x2A); - writeRegister(CC1101_FSCAL1 , 0x00); - writeRegister(CC1101_FSCAL0 , 0x11); - writeRegister(CC1101_FSTEST , 0x59); - writeRegister(CC1101_TEST2 , 0x81); - writeRegister(CC1101_TEST1 , 0x35); - writeRegister(CC1101_TEST0 , 0x0B); - writeRegister(CC1101_PKTCTRL1 , 0x04); //No address check, Append two bytes with status RSSI/LQI/CRC OK, - writeRegister(CC1101_PKTCTRL0 , 0x32); //Infinite packet length mode, CRC disabled for TX and RX, No data whitening, Asynchronous serial mode, Data in on GDO0 and data out on either of the GDOx pins - writeRegister(CC1101_ADDR , 0x00); - writeRegister(CC1101_PKTLEN , 0xFF); - writeRegister(CC1101_TEST0 , 0x09); - - writeCommand(CC1101_SCAL); - - //wait for calibration to finish - while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) yield(); - - writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate - - writeCommand(CC1101_SIDLE); - writeCommand(CC1101_SIDLE); - - writeRegister(CC1101_MDMCFG2 , 0x00); //Enable digital DC blocking filter before demodulator, 2-FSK, Disable Manchester encoding/decoding, No preamble/sync - writeRegister(CC1101_IOCFG0 , 0x0D); //Serial Data Output. Used for asynchronous serial mode. - - writeCommand(CC1101_SRX); - - while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_RX) yield(); - - initReceiveMessage(); -} - -void IthoCC1101::initReceiveMessage() -{ - uint8_t marcState; - - writeCommand(CC1101_SIDLE); //idle - - //set datarate - writeRegister(CC1101_MDMCFG4 , 0x5A); // set kBaud - writeRegister(CC1101_MDMCFG3 , 0x83); // set kBaud - writeRegister(CC1101_DEVIATN , 0x50); - - //set fifo mode with fixed packet length and sync bytes - writeRegister(CC1101_PKTLEN , 63); //63 bytes message (sync at beginning of message is removed by CC1101) - - //set fifo mode with fixed packet length and sync bytes - writeRegister(CC1101_PKTCTRL0 , 0x00); - writeRegister(CC1101_SYNC1 , SYNC1); - writeRegister(CC1101_SYNC0 , SYNC0); - writeRegister(CC1101_MDMCFG2 , MDMCFG2); - writeRegister(CC1101_PKTCTRL1 , 0x00); - - writeCommand(CC1101_SRX); //switch to RX state - - // Check that the RX state has been entered - while (((marcState = readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) & CC1101_BITS_MARCSTATE) != CC1101_MARCSTATE_RX) - { - if (marcState == CC1101_MARCSTATE_RXFIFO_OVERFLOW) // RX_OVERFLOW - writeCommand(CC1101_SFRX); //flush RX buffer - } -} - -bool IthoCC1101::checkForNewPacket() { - if (receiveData(&inMessage, 63) && parseMessageCommand()) { - initReceiveMessage(); - return true; - } - return false; -} - -bool IthoCC1101::parseMessageCommand() { - - messageDecode(&inMessage, &inIthoPacket); - - //deviceType of message type? - inIthoPacket.deviceType = inIthoPacket.dataDecoded[0]; - - //deviceID - inIthoPacket.deviceId[0] = inIthoPacket.dataDecoded[1]; - inIthoPacket.deviceId[1] = inIthoPacket.dataDecoded[2]; - inIthoPacket.deviceId[2] = inIthoPacket.dataDecoded[3]; - - //counter1 - inIthoPacket.counter = inIthoPacket.dataDecoded[4]; - - bool isHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageHighCommandBytes); - bool isRVHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVHighCommandBytes); - bool isMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageMediumCommandBytes); - bool isRVMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVMediumCommandBytes); - bool isLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageLowCommandBytes); - bool isRVLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVLowCommandBytes); - bool isRVAutoCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVAutoCommandBytes); - bool isStandByCommand = checkIthoCommand(&inIthoPacket, ithoMessageStandByCommandBytes); - bool isTimer1Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer1CommandBytes); - bool isTimer2Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer2CommandBytes); - bool isTimer3Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer3CommandBytes); - bool isJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageJoinCommandBytes); - bool isJoin2Command = checkIthoCommand(&inIthoPacket, ithoMessageJoin2CommandBytes); - bool isRVJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVJoinCommandBytes); - bool isLeaveCommand = checkIthoCommand(&inIthoPacket, ithoMessageLeaveCommandBytes); - - //determine command - inIthoPacket.command = IthoUnknown; - if (isHighCommand) inIthoPacket.command = IthoHigh; - if (isRVHighCommand) inIthoPacket.command = IthoHigh; - if (isMediumCommand) inIthoPacket.command = IthoMedium; - if (isRVMediumCommand) inIthoPacket.command = IthoMedium; - if (isLowCommand) inIthoPacket.command = IthoLow; - if (isRVLowCommand) inIthoPacket.command = IthoLow; - if (isRVAutoCommand) inIthoPacket.command = IthoStandby; - if (isStandByCommand) inIthoPacket.command = IthoStandby; - if (isTimer1Command) inIthoPacket.command = IthoTimer1; - if (isTimer2Command) inIthoPacket.command = IthoTimer2; - if (isTimer3Command) inIthoPacket.command = IthoTimer3; - if (isJoinCommand) inIthoPacket.command = IthoJoin; - if (isJoin2Command) inIthoPacket.command = IthoJoin; - if (isRVJoinCommand) inIthoPacket.command = IthoJoin; - if (isLeaveCommand) inIthoPacket.command = IthoLeave; - -#if defined (CRC_FILTER) - uint8_t mLen = 0; - if (isPowerCommand || isHighCommand || isMediumCommand || isLowCommand || isStandByCommand || isTimer1Command || isTimer2Command || isTimer3Command) { - mLen = 11; - } - else if (isJoinCommand || isJoin2Command) { - mLen = 20; - } - else if (isLeaveCommand) { - mLen = 14; - } - else { - return true; - } - if (getCounter2(&inIthoPacket, mLen) != inIthoPacket.dataDecoded[mLen]) { - inIthoPacket.command = IthoUnknown; - return false; - } -#endif - - return true; -} - -bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[]) { - uint8_t offset = 0; - if (itho->deviceType == 28 || itho->deviceType == 24) offset = 2; - for (int i = 4; i < 6; i++) - { - //if (i == 2 || i == 3) continue; //skip byte3 and byte4, rft-rv and co2-auto remote device seem to sometimes have a different number there - if ( (itho->dataDecoded[i + 5 + offset] != commandBytes[i]) && (itho->dataDecodedChk[i + 5 + offset] != commandBytes[i]) ) { - return false; - } - } - return true; -} - -void IthoCC1101::sendCommand(IthoCommand command) -{ - CC1101Packet outMessage; - uint8_t maxTries = sendTries; - uint8_t delaytime = 40; - - //update itho packet data - outIthoPacket.command = command; - outIthoPacket.counter += 1; - - //get message2 bytes - switch (command) - { - case IthoJoin: - createMessageJoin(&outIthoPacket, &outMessage); - break; - - case IthoLeave: - createMessageLeave(&outIthoPacket, &outMessage); - //the leave command needs to be transmitted for 1 second according the manual - maxTries = 30; - delaytime = 4; - break; - - default: - createMessageCommand(&outIthoPacket, &outMessage); - break; - } - - //send messages - for (int i = 0; i < maxTries; i++) - { - - //message2 - initSendMessage(outMessage.length); - sendData(&outMessage); - - finishTransfer(); - delay(delaytime); - } - //initReceive(); SV - I call this from the ESPEasy plugin to prevent crashes -} - -void IthoCC1101::createMessageStart(IthoPacket *itho, CC1101Packet *packet) -{ - - //fixed, set start structure in data buffer manually - for (uint8_t i = 0; i < 7; i++) { - packet->data[i] = 170; - } - packet->data[7] = 171; - packet->data[8] = 254; - packet->data[9] = 0; - packet->data[10] = 179; - packet->data[11] = 42; - packet->data[12] = 171; - packet->data[13] = 42; - - //[start of command specific data] - -} - -void IthoCC1101::createMessageCommand(IthoPacket *itho, CC1101Packet *packet) -{ - - //set start message structure - createMessageStart(itho, packet); - - //set deviceType? (or messageType?), not sure what this is - itho->dataDecoded[0] = itho->deviceType; - - //set deviceID - itho->dataDecoded[1] = itho->deviceId[0]; - itho->dataDecoded[2] = itho->deviceId[1]; - itho->dataDecoded[3] = itho->deviceId[2]; - - //set counter1 - itho->dataDecoded[4] = itho->counter; - - //set command bytes on dataDecoded[5 - 10] - uint8_t *commandBytes = getMessageCommandBytes(itho->command); - for (uint8_t i = 0; i < 6; i++) { - itho->dataDecoded[i + 5] = commandBytes[i]; - } - - //set counter2 - itho->dataDecoded[11] = getCounter2(itho, 11); - - itho->length = 12; - - packet->length = messageEncode(itho, packet); - packet->length += 1; - - //set end byte - packet->data[packet->length] = 172; - packet->length += 1; - - //set end 'noise' - for (uint8_t i = packet->length; i < packet->length + 7; i++) { - packet->data[i] = 170; - } - packet->length += 7; - -} - -void IthoCC1101::createMessageJoin(IthoPacket *itho, CC1101Packet *packet) -{ - - //set start message structure - createMessageStart(itho, packet); - - //set deviceType? (or messageType?) - itho->dataDecoded[0] = itho->deviceType; - - //set deviceID - itho->dataDecoded[1] = itho->deviceId[0]; - itho->dataDecoded[2] = itho->deviceId[1]; - itho->dataDecoded[3] = itho->deviceId[2]; - - //set counter1 - itho->dataDecoded[4] = itho->counter; - - //set command bytes on dataDecoded[5 - ?] - uint8_t *commandBytes = getMessageCommandBytes(itho->command); - for (uint8_t i = 0; i < 6; i++) { - itho->dataDecoded[i + 5] = commandBytes[i]; - } - - //set deviceID - itho->dataDecoded[11] = itho->deviceId[0]; - itho->dataDecoded[12] = itho->deviceId[1]; - itho->dataDecoded[13] = itho->deviceId[2]; - - itho->dataDecoded[14] = 1; - itho->dataDecoded[15] = 16; - itho->dataDecoded[16] = 224; - - //set deviceID - itho->dataDecoded[17] = itho->deviceId[0]; - itho->dataDecoded[18] = itho->deviceId[1]; - itho->dataDecoded[19] = itho->deviceId[2]; - - //set counter2 - itho->dataDecoded[20] = getCounter2(itho, 20); - - itho->length = 21; - - packet->length = messageEncode(itho, packet); - packet->length += 1; - - //set end byte - packet->data[packet->length] = 202; - packet->length += 1; - - //set end 'noise' - for (uint8_t i = packet->length; i < packet->length + 7; i++) { - packet->data[i] = 170; - } - packet->length += 7; - -} - -void IthoCC1101::createMessageLeave(IthoPacket *itho, CC1101Packet *packet) -{ - - //set start message structure - createMessageStart(itho, packet); - - //set deviceType? (or messageType?) - itho->dataDecoded[0] = itho->deviceType; - - //set deviceID - itho->dataDecoded[1] = itho->deviceId[0]; - itho->dataDecoded[2] = itho->deviceId[1]; - itho->dataDecoded[3] = itho->deviceId[2]; - - //set counter1 - itho->dataDecoded[4] = itho->counter; - - //set command bytes on dataDecoded[5 - 10] - uint8_t *commandBytes = getMessageCommandBytes(itho->command); - for (uint8_t i = 0; i < 6; i++) { - itho->dataDecoded[i + 5] = commandBytes[i]; - } - - //set deviceID - itho->dataDecoded[11] = itho->deviceId[0]; - itho->dataDecoded[12] = itho->deviceId[1]; - itho->dataDecoded[13] = itho->deviceId[2]; - - //set counter2 - itho->dataDecoded[14] = getCounter2(itho, 14); - - itho->length = 15; - - packet->length = messageEncode(itho, packet); - packet->length += 1; - - //set end byte - packet->data[packet->length] = 202; - packet->length += 1; - - //set end 'noise' - for (uint8_t i = packet->length; i < packet->length + 7; i++) { - packet->data[i] = 170; - } - packet->length += 7; - -} - -uint8_t* IthoCC1101::getMessageCommandBytes(IthoCommand command) -{ - switch (command) - { - case IthoStandby: - return (uint8_t*)&ithoMessageStandByCommandBytes[0]; - case IthoHigh: - return (uint8_t*)&ithoMessageHighCommandBytes[0]; - case IthoFull: - return (uint8_t*)&ithoMessageFullCommandBytes[0]; - case IthoMedium: - return (uint8_t*)&ithoMessageMediumCommandBytes[0]; - case IthoLow: - return (uint8_t*)&ithoMessageLowCommandBytes[0]; - case IthoTimer1: - return (uint8_t*)&ithoMessageTimer1CommandBytes[0]; - case IthoTimer2: - return (uint8_t*)&ithoMessageTimer2CommandBytes[0]; - case IthoTimer3: - return (uint8_t*)&ithoMessageTimer3CommandBytes[0]; - case IthoJoin: - return (uint8_t*)&ithoMessageJoinCommandBytes[0]; - case IthoLeave: - return (uint8_t*)&ithoMessageLeaveCommandBytes[0]; - default: - return (uint8_t*)&ithoMessageLowCommandBytes[0]; - } -} - -/* - Counter2 is the decimal sum of all bytes in decoded form from - deviceType up to the last byte before counter2 subtracted - from zero. -*/ -uint8_t IthoCC1101::getCounter2(IthoPacket *itho, uint8_t len) { - - uint8_t val = 0; - - for (uint8_t i = 0; i < len; i++) { - val += itho->dataDecoded[i]; - } - - return 0 - val; -} - -uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { - - uint8_t lenOutbuf = 0; - - if ((itho->length * 20) % 8 == 0) { //inData len fits niecly in out buffer length - lenOutbuf = itho->length * 2.5; - } - else { //is this an issue? inData last byte does not fill out buffer length, add 1 out byte extra, padding is done after encode - lenOutbuf = (uint8_t)(itho->length * 2.5) + 0.5; - } - - uint8_t out_bytecounter = 14; //index of Outbuf, start at offset 14, first part of the message is set manually - uint8_t out_bitcounter = 0; //bit position of current outbuf byte - uint8_t out_patterncounter = 0; //bit counter to add 1 0 bit pattern after every 8 bits - uint8_t bitSelect = 4; //bit position of the inData byte (4 - 7, 0 - 3) - uint8_t out_shift = 7; //bit shift inData bit in position of outbuf byte - - //we need to zero the out buffer first cause we are using bitshifts - for (int i = out_bytecounter; i < sizeof(packet->data) / sizeof(packet->data[0]); i++) { - packet->data[i] = 0; - } - - //Serial.println(); - for (uint8_t dataByte = 0; dataByte < itho->length; dataByte++) { - for (uint8_t dataBit = 0; dataBit < 8; dataBit++) { //process a full dataByte at a time resulting in 20 output bits (2.5 bytes) with the pattern 7x6x5x4x 10 3x2x1x0x 10 7x6x5x4x 10 3x2x1x0x 10 etc - if (out_bitcounter == 8) { //check if new byte is needed - out_bytecounter++; - out_bitcounter = 0; - } - - if (out_patterncounter == 8) { //check if we have to start with a 1 0 pattern - out_patterncounter = 0; - packet->data[out_bytecounter] = packet->data[out_bytecounter] | 1 << out_shift; - out_shift--; - out_bitcounter++; - packet->data[out_bytecounter] = packet->data[out_bytecounter] | 0 << out_shift; - if (out_shift == 0) out_shift = 8; - out_shift--; - out_bitcounter++; - } - - if (out_bitcounter == 8) { //check if new byte is needed - out_bytecounter++; - out_bitcounter = 0; - } - - //set the even bit - uint8_t bit = (itho->dataDecoded[dataByte] & (1 << bitSelect)) >> bitSelect; //select bit and shift to bit pos 0 - bitSelect++; - if (bitSelect == 8) bitSelect = 0; - - packet->data[out_bytecounter] = packet->data[out_bytecounter] | bit << out_shift; //shift bit in corect pos of current outbuf byte - out_shift--; - out_bitcounter++; - out_patterncounter++; - - //set the odd bit (inverse of even bit) - bit = ~bit & 0b00000001; - packet->data[out_bytecounter] = packet->data[out_bytecounter] | bit << out_shift; - if (out_shift == 0) out_shift = 8; - out_shift--; - out_bitcounter++; - out_patterncounter++; - - } - - } - if (out_bitcounter < 8) { //add closing 1 0 pattern to fill last packet->data byte and ensure DC balance in the message - for (uint8_t i = out_bitcounter; i < 8; i += 2) { - packet->data[out_bytecounter] = packet->data[out_bytecounter] | 1 << out_shift; - out_shift--; - packet->data[out_bytecounter] = packet->data[out_bytecounter] | 0 << out_shift; - if (out_shift == 0) out_shift = 8; - out_shift--; - } - } - - return out_bytecounter; -} - - -void IthoCC1101::messageDecode(CC1101Packet *packet, IthoPacket *itho) { - - itho->length = 0; - int lenInbuf = packet->length; - - lenInbuf -= STARTBYTE; //correct for sync byte pos - - while (lenInbuf >= 5) { - lenInbuf -= 5; - itho->length += 2; - } - if (lenInbuf >= 3) { - itho->length++; - } - - for (int i = 0; i < sizeof(itho->dataDecoded) / sizeof(itho->dataDecoded[0]); i++) { - itho->dataDecoded[i] = 0; - } - for (int i = 0; i < sizeof(itho->dataDecodedChk) / sizeof(itho->dataDecodedChk[0]); i++) { - itho->dataDecodedChk[i] = 0; - } - - uint8_t out_i = 0; //byte index - uint8_t out_j = 4; //bit index - uint8_t out_i_chk = 0; //byte index - uint8_t out_j_chk = 4; //bit index - uint8_t in_bitcounter = 0; //process per 10 input bits - - for (int i = STARTBYTE; i < packet->length; i++) { - - for (int j = 7; j > -1; j--) { - if (in_bitcounter == 0 || in_bitcounter == 2 || in_bitcounter == 4 || in_bitcounter == 6) { //select input bits for output - uint8_t x = packet->data[i]; //select input byte - x = x >> j; //select input bit - x = x & 0b00000001; - x = x << out_j; //set value for output bit - itho->dataDecoded[out_i] = itho->dataDecoded[out_i] | x; - out_j += 1; //next output bit - if (out_j > 7) out_j = 0; - if (out_j == 4) out_i += 1; - } - if (in_bitcounter == 1 || in_bitcounter == 3 || in_bitcounter == 5 || in_bitcounter == 7) { //select input bits for check output - uint8_t x = packet->data[i]; //select input byte - x = x >> j; //select input bit - x = x & 0b00000001; - x = x << out_j_chk; //set value for output bit - itho->dataDecodedChk[out_i_chk] = itho->dataDecodedChk[out_i_chk] | x; - out_j_chk += 1; //next output bit - if (out_j_chk > 7) out_j_chk = 0; - if (out_j_chk == 4) { - itho->dataDecodedChk[out_i_chk] = ~itho->dataDecodedChk[out_i_chk]; //inverse bits - out_i_chk += 1; - } - } - in_bitcounter += 1; //continue cyling in groups of 10 bits - if (in_bitcounter > 9) in_bitcounter = 0; - } - } -} - -uint8_t IthoCC1101::ReadRSSI() -{ - uint8_t rssi = 0; - uint8_t value = 0; - - rssi = (readRegister(CC1101_RSSI, CC1101_STATUS_REGISTER)); - - if (rssi >= 128) - { - value = 255 - rssi; - value /= 2; - value += 74; - } - else - { - value = rssi / 2; - value += 74; - } - return (value); -} - -bool IthoCC1101::checkID(const uint8_t *id) -{ - for (uint8_t i = 0; i < 3; i++) - if (id[i] != inIthoPacket.deviceId[i]) - return false; - return true; -} - -String IthoCC1101::getLastIDstr(bool ashex) { - String str; - for (uint8_t i = 0; i < 3; i++) { - if (ashex) str += String(inIthoPacket.deviceId[i], HEX); - else str += String(inIthoPacket.deviceId[i]); - if (i < 2) str += ","; - } - return str; -} - -int * IthoCC1101::getLastID() { - static int id[3]; - for (uint8_t i = 0; i < 3; i++) { - id[i] = inIthoPacket.deviceId[i]; - } - return id; -} - -String IthoCC1101::getLastMessagestr(bool ashex) { - String str = "Length=" + String(inMessage.length) + "."; - for (uint8_t i = 0; i < inMessage.length; i++) { - if (ashex) str += String(inMessage.data[i], HEX); - else str += String(inMessage.data[i]); - if (i < inMessage.length - 1) str += ":"; - } - return str; -} - -String IthoCC1101::LastMessageDecoded() { - - String str; - if (inIthoPacket.length > 11) { - str += "Device type?: " + String(inIthoPacket.deviceType); - str += " - CMD: "; - for (int i = 4; i < inIthoPacket.length; i++) { - str += String(inIthoPacket.dataDecoded[i]); - if (i < inIthoPacket.length - 1) str += ","; - } - - } - else { - for (uint8_t i = 0; i < inIthoPacket.length; i++) { - str += String(inIthoPacket.dataDecoded[i]); - if (i < inIthoPacket.length - 1) str += ","; - } - - } - str += "\n"; - return str; - -} diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h deleted file mode 100644 index f4fd7b0da..000000000 --- a/lib/Itho/IthoCC1101.h +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra - */ - -#ifndef __ITHOCC1101_H__ -#define __ITHOCC1101_H__ - -#include -#include "CC1101.h" -#include "IthoPacket.h" - - -//pa table settings -const uint8_t ithoPaTableSend[8] = {0x6F, 0x26, 0x2E, 0x8C, 0x87, 0xCD, 0xC7, 0xC0}; -const uint8_t ithoPaTableReceive[8] = {0x6F, 0x26, 0x2E, 0x7F, 0x8A, 0x84, 0xCA, 0xC4}; - -//message command bytes -const uint8_t ithoMessageRVHighCommandBytes[] = {49,224,4,0,0,200}; -const uint8_t ithoMessageHighCommandBytes[] = {34,241,3,0,4,4}; -const uint8_t ithoMessageFullCommandBytes[] = {34,241,3,0,4,4}; -const uint8_t ithoMessageMediumCommandBytes[] = {34,241,3,0,3,4}; -const uint8_t ithoMessageRVMediumCommandBytes[] = {34,241,3,0,3,7}; -const uint8_t ithoMessageLowCommandBytes[] = {34,241,3,0,2,4}; -const uint8_t ithoMessageRVLowCommandBytes[] = {49,224,4,0,0,1}; -const uint8_t ithoMessageRVAutoCommandBytes[] = {34,241,3,0,5,7}; -const uint8_t ithoMessageStandByCommandBytes[] = {0,0,0,0,0,0}; //unkown, tbd -const uint8_t ithoMessageTimer1CommandBytes[] = {34,243,3,0,0,10}; //10 minutes full speed -const uint8_t ithoMessageTimer2CommandBytes[] = {34,243,3,0,0,20}; //20 minutes full speed -const uint8_t ithoMessageTimer3CommandBytes[] = {34,243,3,0,0,30}; //30 minutes full speed -const uint8_t ithoMessageJoinCommandBytes[] = {31,201,12,0,34,241}; -const uint8_t ithoMessageJoin2CommandBytes[] = {31,201,12,99,34,248}; //join command of RFT AUTO Co2 remote -const uint8_t ithoMessageRVJoinCommandBytes[] = {31,201,24,0,49,224}; //join command of RFT-RV -const uint8_t ithoMessageLeaveCommandBytes[] = {31,201,6,0,31,201}; -//itho rft-rv -//unknown, high -//148,216,43,49,224,4,0,0,200,0,3,127,244,78,11,155,154,225,11,96,138 -//148,216,43,49,224,4,0,0,200,0,3,127,51,80,47,233,94,6,189,114,73 - -//low -//148,216,43,49,224,4,0,0,1,0,202,127,242,212,160,123,15,64,7,129,33 -//148,216,43,34,241,3,0,4,4,194,127,255,189,90,107,88,72,115,49,192,105 - -//join -//151,149,65,31,201,24,0,49,224,151,149,65,0,18,160,151,149,65,1,16,224 - - -class IthoCC1101 : protected CC1101 -{ - private: - //receive - CC1101Packet inMessage; //temp storage message2 - IthoPacket inIthoPacket; //stores last received message data - - //send - IthoPacket outIthoPacket; //stores state of "remote" - - //settings - uint8_t sendTries; //number of times a command is send at one button press - - //functions - public: - IthoCC1101(uint8_t counter = 0, uint8_t sendTries = 3); //set initial counter value - ~IthoCC1101(); - - //init - void init() { CC1101::init(); initReceive(); } //init,reset CC1101 - void initReceive(); - uint8_t getLastCounter() { return outIthoPacket.counter; } //counter is increased before sending a command - void setSendTries(uint8_t sendTries) { this->sendTries = sendTries; } - void setDeviceID(uint8_t byte0, uint8_t byte1, uint8_t byte2) { this->outIthoPacket.deviceId[0] = byte0; this->outIthoPacket.deviceId[1] = byte1; this->outIthoPacket.deviceId[2] = byte2;} - - //receive - bool checkForNewPacket(); //check RX fifo for new data - IthoPacket getLastPacket() { return inIthoPacket; } //retrieve last received/parsed packet from remote - IthoCommand getLastCommand() { return inIthoPacket.command; } //retrieve last received/parsed command from remote - uint8_t getLastInCounter() { return inIthoPacket.counter; } //retrieve last received/parsed command from remote - uint8_t ReadRSSI(); - bool checkID(const uint8_t *id); - int * getLastID(); - String getLastIDstr(bool ashex=true); - String getLastMessagestr(bool ashex=true); - String LastMessageDecoded(); - - //send - void sendCommand(IthoCommand command); - protected: - private: - IthoCC1101( const IthoCC1101 &c); - IthoCC1101& operator=( const IthoCC1101 &c); - - //init CC1101 for receiving - void initReceiveMessage(); - - //init CC1101 for sending - void initSendMessage(uint8_t len); - void finishTransfer(); - - //parse received message - bool parseMessageCommand(); - bool checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[]); - - //send - void createMessageStart(IthoPacket *itho, CC1101Packet *packet); - void createMessageCommand(IthoPacket *itho, CC1101Packet *packet); - void createMessageJoin(IthoPacket *itho, CC1101Packet *packet); - void createMessageLeave(IthoPacket *itho, CC1101Packet *packet); - uint8_t* getMessageCommandBytes(IthoCommand command); - uint8_t getCounter2(IthoPacket *itho, uint8_t len); - - uint8_t messageEncode(IthoPacket *itho, CC1101Packet *packet); - void messageDecode(CC1101Packet *packet, IthoPacket *itho); - - -}; //IthoCC1101 - -#endif //__ITHOCC1101_H__ \ No newline at end of file diff --git a/lib/Itho/IthoPacket.h b/lib/Itho/IthoPacket.h deleted file mode 100644 index 3a5c3e807..000000000 --- a/lib/Itho/IthoPacket.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra - */ - -#ifndef ITHOPACKET_H_ -#define ITHOPACKET_H_ - -enum IthoCommand -{ - IthoUnknown = 0, - - IthoJoin = 1, - IthoLeave = 2, - - IthoStandby = 3, - IthoLow = 4, - IthoMedium = 5, - IthoHigh = 6, - IthoFull = 7, - - IthoTimer1 = 8, - IthoTimer2 = 9, - IthoTimer3 = 10, - - //duco c system remote - DucoStandby = 11, - DucoLow = 12, - DucoMedium = 13, - DucoHigh = 14 -}; - - -class IthoPacket -{ - public: - IthoCommand command; - - uint8_t dataDecoded[32]; - uint8_t dataDecodedChk[32]; - uint8_t length; - - uint8_t deviceType; - uint8_t deviceId[3]; - - uint8_t counter; //0-255, counter is increased on every remote button press -}; - - -#endif /* ITHOPACKET_H_ */ \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index eaa24f15c..c8afabf10 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,8 +20,8 @@ extra_configs = platformio_esp32_envs.ini platformio_special_envs.ini -;default_envs = custom_ESP8266_4M1M, custom_ESP32_4M316k -default_envs = test_D_ESP8266_4M1M +default_envs = custom_ESP8266_4M1M, custom_ESP32_4M316k +; default_envs = custom_ESP8266_4M1M ;default_envs = normal_ESP8266_4M1M ;default_envs = test_beta_ESP8266_4M1M diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino deleted file mode 100644 index c3b19119f..000000000 --- a/src/_P118_Itho.ino +++ /dev/null @@ -1,548 +0,0 @@ -//####################################################################################################### -//############################## Plugin 118: Itho ventilation unit 868Mhz remote ######################## -//####################################################################################################### - -// author :jodur, 13-1-2018 -// changed :jeroen, 2-11-2019 -// changed :svollebregt, 30-1-2020 - changes to improve stability: volatile decleration of state, -// disable logging within interrupts unles enabled, removed some unused code, -// reduce noInterrupts() blockage of code fragments to prevent crashes -// svollebregt, 16-2-2020 - ISR now sets flag which is checked by 50 per seconds plugin call as -// receive ISR with Ticker was the cause of instability. Inspired by: https://github.com/arnemauer/Ducobox-ESPEasy-Plugin -// svollebregt, 11-04-2020 - Minor changes to make code compatible with latest mega 20200410, removed SYNC1 option for now; -// better to change this value in the Itho-lib code and compile it yourself -// svollebreggt, 13-2-2021 - Now uses rewirtten library made by arjenhiemstra: https://github.com/arjenhiemstra/IthoEcoFanRFT - -// Recommended to disable RF receive logging to minimize code execution within interrupts - -// List of commands: -// 1111 to join ESP8266 with Itho ventilation unit -// 9999 to leaveESP8266 with Itho ventilation unit -// 0 to set Itho ventilation unit to standby -// 1 - set Itho ventilation unit to low speed -// 2 - set Itho ventilation unit to medium speed -// 3 - set Itho ventilation unit to high speed -// 4 - set Itho ventilation unit to full speed -// 13 - set itho to high speed with hardware timer (10 min) -// 23 - set itho to high speed with hardware timer (20 min) -// 33 - set itho to high speed with hardware timer (30 min) - -//List of States: - -// 1 - Itho ventilation unit to lowest speed -// 2 - Itho ventilation unit to medium speed -// 3 - Itho ventilation unit to high speed -// 4 - Itho ventilation unit to full speed -// 13 -Itho to high speed with hardware timer (10 min) -// 23 -Itho to high speed with hardware timer (20 min) -// 33 -Itho to high speed with hardware timer (30 min) - -// Usage for http (not case sensitive): -// http://ip/control?cmd=STATE,1111 -// http://ip/control?cmd=STATE,1 -// http://ip/control?cmd=STATE,2 -// http://ip/control?cmd=STATE,3 - -// usage for example mosquito MQTT -// mosquitto_pub -t /Fan/cmd -m 'state 1111' -// mosquitto_pub -t /Fan/cmd -m 'state 1' -// mosquitto_pub -t /Fan/cmd -m 'state 2' -// mosquitto_pub -t /Fan/cmd -m 'state 3' - - -// This code needs the library made by 'supersjimmie': https://github.com/supersjimmie/IthoEcoFanRFT/tree/master/Master/Itho -// A CC1101 868Mhz transmitter is needed -// See https://gathering.tweakers.net/forum/list_messages/1690945 for more information -// code/idea was inspired by first release of code from 'Thinkpad' - -#include -#include "IthoCC1101.h" -#include "IthoPacket.h" -#include "_Plugin_Helper.h" -#ifdef USES_P118 - -//This extra settings struct is needed because the default settingsstruct doesn't support strings -struct PLUGIN__ExtraSettingsStruct -{ char ID1[9]; - char ID2[9]; - char ID3[9]; -} PLUGIN_118_ExtraSettings; - -IthoCC1101 PLUGIN_118_rf; - -// extra for interrupt handling -bool PLUGIN_118_ITHOhasPacket = false; -int PLUGIN_118_State=1; // after startup it is assumed that the fan is running low -int PLUGIN_118_OldState=1; -int PLUGIN_118_Timer=0; -int PLUGIN_118_LastIDindex = 0; -int PLUGIN_118_OldLastIDindex = 0; -int8_t Plugin_118_IRQ_pin=-1; -bool PLUGIN_118_InitRunned = false; -bool PLUGIN_118_Log = false; - -// volatile for interrupt function -volatile bool PLUGIN_118_Int = false; -//volatile unsigned long PLUGIN_118_Int_time = 0; - -#define PLUGIN_118 -#define PLUGIN_ID_118 118 -#define PLUGIN_NAME_118 "Itho ventilation remote" -#define PLUGIN_VALUENAME1_118 "State" -#define PLUGIN_VALUENAME2_118 "Timer" -#define PLUGIN_VALUENAME3_118 "LastIDindex" - -// Timer values for hardware timer in Fan -#define PLUGIN_118_Time1 10*60 -#define PLUGIN_118_Time2 20*60 -#define PLUGIN_118_Time3 30*60 - -boolean Plugin_118(byte function, struct EventStruct *event, String &string) -{ - boolean success = false; - - switch (function) - { - - case PLUGIN_DEVICE_ADD: - { - Device[++deviceCount].Number = PLUGIN_ID_118; - Device[deviceCount].Type = DEVICE_TYPE_SINGLE; - Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; - Device[deviceCount].Ports = 0; - Device[deviceCount].PullUpOption = false; - Device[deviceCount].InverseLogicOption = false; - Device[deviceCount].FormulaOption = false; - Device[deviceCount].ValueCount = 3; - Device[deviceCount].SendDataOption = true; - Device[deviceCount].TimerOption = true; - Device[deviceCount].GlobalSyncOption = true; - break; - } - - case PLUGIN_GET_DEVICENAME: - { - string = F(PLUGIN_NAME_118); - break; - } - - case PLUGIN_GET_DEVICEVALUENAMES: - { - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_118)); - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_118)); - strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_118)); - break; - } - - case PLUGIN_GET_DEVICEGPIONAMES: - { - event->String1 = formatGpioName_input(F("Interrupt pin (CC1101 GDO2)")); - break; - } - - case PLUGIN_SET_DEFAULTS: - { - PCONFIG(0) = 1; - PCONFIG(1) = 10; - PCONFIG(2) = 87; - PCONFIG(3) = 81; - success = true; - break; - } - - case PLUGIN_INIT: - { - //If configured interrupt pin differs from configured, release old pin first - if ((Settings.TaskDevicePin1[event->TaskIndex]!=Plugin_118_IRQ_pin) && (Plugin_118_IRQ_pin!=-1)) - { - addLog(LOG_LEVEL_DEBUG, F("IO-PIN changed, deatachinterrupt old pin")); - detachInterrupt(Plugin_118_IRQ_pin); - } - LoadCustomTaskSettings(event->TaskIndex, (byte*)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); - addLog(LOG_LEVEL_INFO, F("Extra Settings PLUGIN_118 loaded")); - PLUGIN_118_rf.setDeviceID(PCONFIG(1), PCONFIG(2), PCONFIG(3)); //DeviceID used to send commands, can also be changed on the fly for multi itho control, 10,87,81 corresponds with old library - PLUGIN_118_rf.init(); - Plugin_118_IRQ_pin = Settings.TaskDevicePin1[event->TaskIndex]; - pinMode(Plugin_118_IRQ_pin, INPUT); - attachInterrupt(Plugin_118_IRQ_pin, PLUGIN_118_ITHOinterrupt, FALLING); - addLog(LOG_LEVEL_INFO, F("CC1101 868Mhz transmitter initialized")); - PLUGIN_118_rf.initReceive(); - PLUGIN_118_InitRunned=true; - success = true; - break; - } - - case PLUGIN_EXIT: - { - addLog(LOG_LEVEL_INFO, F("EXIT PLUGIN_118")); - //remove interupt when plugin is removed - detachInterrupt(Plugin_118_IRQ_pin); - success = true; - break; - } - - case PLUGIN_ONCE_A_SECOND: - { - //decrement timer when timermode is running - if (PLUGIN_118_State>=10) PLUGIN_118_Timer--; - - //if timer has elapsed set Fan state to low - if ((PLUGIN_118_State>=10) && (PLUGIN_118_Timer<=0)) - { - PLUGIN_118_State=1; - PLUGIN_118_Timer=0; - } - - //Publish new data when vars are changed or init has runned or timer is running (update every 2 sec) - if ((PLUGIN_118_OldState!=PLUGIN_118_State) || ((PLUGIN_118_Timer>0) && (PLUGIN_118_Timer % 2==0)) || (PLUGIN_118_OldLastIDindex!=PLUGIN_118_LastIDindex)|| PLUGIN_118_InitRunned) - { - addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_ONCE_A_SECOND")); - PLUGIN_118_Publishdata(event); - sendData(event); - //reset flag set by init - PLUGIN_118_InitRunned=false; - } - //Remeber current state for next cycle - PLUGIN_118_OldState=PLUGIN_118_State; - PLUGIN_118_OldLastIDindex =PLUGIN_118_LastIDindex; - success = true; - break; - } - - case PLUGIN_FIFTY_PER_SECOND: - { - if (PLUGIN_118_Int) - { - PLUGIN_118_Int = false; // reset flag - PLUGIN_118_ITHOcheck(); - /* - unsigned long time_elapsed = millis() - PLUGIN_118_Int_time; //Disabled as it doesn't appear to be required - if (time_elapsed >= 10) - { - PLUGIN_118_ITHOcheck(); - } - else - { - delay(10-time_elapsed); - PLUGIN_118_ITHOcheck(); - }*/ - } - success = true; - break; - } - - - case PLUGIN_READ: { - // This ensures that even when Values are not changing, data is send at the configured interval for aquisition - addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_READ")); - PLUGIN_118_Publishdata(event); - //sendData(event); //SV - Added to send status every xx secnds as set within plugin - success = true; - break; - } - - case PLUGIN_WRITE: { - String tmpString = string; - String cmd = parseString(tmpString, 1); - String param1 = parseString(tmpString, 2); - if (cmd.equalsIgnoreCase(F("STATE"))) - { - - if (param1.equalsIgnoreCase(F("1111"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoJoin); - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'join' to Itho unit")); - printWebString += F("Sent command for 'join' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("9999"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoLeave); - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'leave' to Itho unit")); - printWebString += F("Sent command for 'leave' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("0"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoStandby); - PLUGIN_118_State=0; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'standby' to Itho unit")); - printWebString += F("Sent command for 'standby' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("1"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoLow); - PLUGIN_118_State=1; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'low speed' to Itho unit")); - printWebString += F("Sent command for 'low speed' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("2"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoMedium); - PLUGIN_118_State=2; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'medium speed' to Itho unit")); - printWebString += F("Sent command for 'medium speed' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("3"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoHigh); - PLUGIN_118_State=3; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'high speed' to Itho unit")); - printWebString += F("Sent command for 'high speed' to Itho unit"); - - success = true; - } - if (param1.equalsIgnoreCase(F("4"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoFull); - PLUGIN_118_State=4; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'full speed' to Itho unit")); - printWebString += F("Sent command for 'full speed' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("13"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoTimer1); - PLUGIN_118_State=13; - PLUGIN_118_Timer=PLUGIN_118_Time1; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 1' to Itho unit")); - printWebString += F("Sent command for 'timer 1' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("23"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoTimer2); - PLUGIN_118_State=23; - PLUGIN_118_Timer=PLUGIN_118_Time2; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 2' to Itho unit")); - printWebString += F("Sent command for 'timer 2' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("33"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoTimer3); - PLUGIN_118_State=33; - PLUGIN_118_Timer=PLUGIN_118_Time3; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 3' to Itho unit")); - printWebString += F("Sent command for 'timer 3' to Itho unit"); - success = true; - } - } - break; - } - - case PLUGIN_WEBFORM_LOAD: - { - addFormSubHeader(F("Remote RF Controls")); - addFormTextBox(F("Unit ID remote 1"), F("PLUGIN_118_ID1"), PLUGIN_118_ExtraSettings.ID1, 8); - addFormTextBox(F("Unit ID remote 2"), F("PLUGIN_118_ID2"), PLUGIN_118_ExtraSettings.ID2, 8); - addFormTextBox(F("Unit ID remote 3"), F("PLUGIN_118_ID3"), PLUGIN_118_ExtraSettings.ID3, 8); - addFormCheckBox(F("Enable RF receive log"), F("p118_log"), PCONFIG(0)); - addFormNumericBox(F("Device ID byte 1"), F("p118_deviceid1"), PCONFIG(1), 0, 255); - addFormNumericBox(F("Device ID byte 2"), F("p118_deviceid2"), PCONFIG(2), 0, 255); - addFormNumericBox(F("Device ID byte 3"), F("p118_deviceid3"), PCONFIG(3), 0, 255); - addFormNote(F("Device ID of your ESP, should not be the same as your neighbours ;-). Defaults to 10,87,81 which corresponds to the old Itho code")); - success = true; - break; - } - - case PLUGIN_WEBFORM_SAVE: - { - strcpy(PLUGIN_118_ExtraSettings.ID1, web_server.arg(F("PLUGIN_118_ID1")).c_str()); - strcpy(PLUGIN_118_ExtraSettings.ID2, web_server.arg(F("PLUGIN_118_ID2")).c_str()); - strcpy(PLUGIN_118_ExtraSettings.ID3, web_server.arg(F("PLUGIN_118_ID3")).c_str()); - SaveCustomTaskSettings(event->TaskIndex, (byte*)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); - - PCONFIG(0) = isFormItemChecked(F("p118_log")); - PLUGIN_118_Log = PCONFIG(0); - PCONFIG(1) = getFormItemInt(F("p118_deviceid1"), 10); - PCONFIG(2) = getFormItemInt(F("p118_deviceid2"), 87); - PCONFIG(3) = getFormItemInt(F("p118_deviceid3"), 81); - success = true; - break; - } - } -return success; -} - -ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt() -{ - PLUGIN_118_Int = true; //flag - //PLUGIN_118_Int_time = millis(); //used to register time since interrupt, to make sure we don't read within 10 ms as the RX buffer needs some time to get ready. Update: Disabled as it appear not necessary -} - -void PLUGIN_118_ITHOcheck() -{ - noInterrupts(); - if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, "RF signal received\n");} - if(PLUGIN_118_rf.checkForNewPacket()) - { - IthoCommand cmd = PLUGIN_118_rf.getLastCommand(); - String Id = PLUGIN_118_rf.getLastIDstr(); - - //Move check here to prevent function calling within ISR - byte index = 0; - if (Id == PLUGIN_118_ExtraSettings.ID1){ - index = 1; - } - else if (Id == PLUGIN_118_ExtraSettings.ID2){ - index = 2; - } - else if (Id == PLUGIN_118_ExtraSettings.ID3){ - index = 3; - } - - //int index = PLUGIN_118_RFRemoteIndex(Id); - // IF id is know index should be >0 - if (index>0) - { - if(PLUGIN_118_Log){ - String log = F("Command received from remote-ID: "); - log += Id; - addLog(LOG_LEVEL_DEBUG, log); - } - String log2 = ""; - if(PLUGIN_118_Log){log2 += F("Command received=");} - switch (cmd) - { - case IthoUnknown: - if(PLUGIN_118_Log){log2 += F("unknown\n");} - break; - case IthoStandby: - case DucoStandby: - if(PLUGIN_118_Log){log2 += F("standby\n");} - PLUGIN_118_State = 0; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; - break; - case IthoLow: - case DucoLow: - if(PLUGIN_118_Log){log2 += F("low\n");} - PLUGIN_118_State = 1; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; - break; - case IthoMedium: - case DucoMedium: - if(PLUGIN_118_Log){log2 += F("medium\n");} - PLUGIN_118_State = 2; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; - break; - case IthoHigh: - case DucoHigh: - if(PLUGIN_118_Log){log2 += F("high\n");} - PLUGIN_118_State = 3; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; - break; - case IthoFull: - if(PLUGIN_118_Log){log2 += F("full\n");} - PLUGIN_118_State = 4; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; - break; - case IthoTimer1: - if(PLUGIN_118_Log){log2 += +F("timer1\n");} - PLUGIN_118_State = 13; - PLUGIN_118_Timer = PLUGIN_118_Time1; - PLUGIN_118_LastIDindex = index; - break; - case IthoTimer2: - if(PLUGIN_118_Log){log2 += F("timer2\n");} - PLUGIN_118_State = 23; - PLUGIN_118_Timer = PLUGIN_118_Time2; - PLUGIN_118_LastIDindex = index; - break; - case IthoTimer3: - if(PLUGIN_118_Log){log2 += F("timer3\n");} - PLUGIN_118_State = 33; - PLUGIN_118_Timer = PLUGIN_118_Time3; - PLUGIN_118_LastIDindex = index; - break; - case IthoJoin: - if(PLUGIN_118_Log){log2 += F("join\n");} - break; - case IthoLeave: - if(PLUGIN_118_Log){log2 += F("leave\n");} - break; - } - if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, log2);} - } - else { - if(PLUGIN_118_Log){ - String log = F("Device-ID:"); - log += Id; - log += F(" IGNORED"); - addLog(LOG_LEVEL_DEBUG, log); - } - } - } - interrupts(); -} - -void PLUGIN_118_Publishdata(struct EventStruct *event) -{ - UserVar[event->BaseVarIndex]=PLUGIN_118_State; - UserVar[event->BaseVarIndex+1]=PLUGIN_118_Timer; - UserVar[event->BaseVarIndex+2]=PLUGIN_118_LastIDindex; - - String log = F("State: "); - log += UserVar[event->BaseVarIndex]; - addLog(LOG_LEVEL_DEBUG, log); - log = F("Timer: "); - log += UserVar[event->BaseVarIndex+1]; - addLog(LOG_LEVEL_DEBUG, log); - log = F("LastIDindex: "); - log += UserVar[event->BaseVarIndex+2]; - addLog(LOG_LEVEL_DEBUG, log); -} - - -#endif // USES_P118 diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index df729d989..bb0835465 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1133,7 +1133,6 @@ To create/register a plugin, you have to : #define USES_P094 // CUL Reader #define USES_P114 // VEML6075 UVA/UVB sensor #define USES_P115 // Fuel Gauge MAX1704x - #define USES_P118 // Itho ventilation control #endif @@ -1145,14 +1144,14 @@ To create/register a plugin, you have to : #ifndef USES_P027 #define USES_P027 // INA219 #endif - #ifndef USES_P076 + #ifndef USES_P076 #define USES_P076 // HWL8012 in POW r1 #endif - #ifndef USES_P077 + #ifndef USES_P077 // Needs CSE7766 Energy sensor, via Serial RXD 4800 baud 8E1 (GPIO1), TXD (GPIO3) #define USES_P077 // CSE7766 in POW R2 #endif - #ifndef USES_P078 + #ifndef USES_P078 #define USES_P078 // Eastron Modbus Energy meters #endif #ifndef USES_P085 @@ -1164,7 +1163,7 @@ To create/register a plugin, you have to : #ifndef USES_P102 #define USES_P102 // PZEM-004Tv30 #endif - #ifndef USES_P108 + #ifndef USES_P108 #define USES_P108 // DDS238-x ZN MODBUS energy meter (was P224 in the Playground) #endif #ifndef USES_P115 @@ -1180,31 +1179,31 @@ To create/register a plugin, you have to : #ifndef USES_P023 #define USES_P023 // OLED #endif - #ifndef USES_P036 + #ifndef USES_P036 #define USES_P036 // FrameOLED #endif - #ifndef USES_P038 + #ifndef USES_P038 #define USES_P038 // NeoPixel #endif - #ifndef USES_P041 + #ifndef USES_P041 #define USES_P041 // NeoClock #endif - #ifndef USES_P042 + #ifndef USES_P042 #define USES_P042 // Candle #endif - #ifndef USES_P057 + #ifndef USES_P057 #define USES_P057 // HT16K33_LED #endif - #ifndef USES_P070 + #ifndef USES_P070 #define USES_P070 // NeoPixel_Clock #endif - #ifndef USES_P075 + #ifndef USES_P075 #define USES_P075 // Nextion #endif - #ifndef USES_P095 + #ifndef USES_P095 #define USES_P095 // TFT ILI9341 #endif - #ifndef USES_P096 + #ifndef USES_P096 #define USES_P096 // eInk (Needs lib_deps = Adafruit GFX Library, LOLIN_EPD ) #endif #ifndef USES_P099 @@ -1281,7 +1280,7 @@ To create/register a plugin, you have to : #define USES_P142 // RGB-Strip #define USES_P143 // AnyonePresent #define USES_P144 // RC-Switch-TX - #define USES_P145 // Itho - P118 in the main repo. + #define USES_P145 // Itho #define USES_P149 // MHZ19 #define USES_P150 // SDM120C #define USES_P151 // CISA @@ -1402,34 +1401,34 @@ To create/register a plugin, you have to : #define USES_P115 // Fuel gauge MAX1704x #endif #ifndef USES_P116 - #define USES_P116 // + #define USES_P116 // #endif #ifndef USES_P117 - #define USES_P117 // + #define USES_P117 // #endif #ifndef USES_P118 - #define USES_P118 // Itho ventilation coontrol + #define USES_P118 // #endif #ifndef USES_P119 - #define USES_P119 // + #define USES_P119 // #endif #ifndef USES_P120 - #define USES_P120 // + #define USES_P120 // #endif #ifndef USES_P121 - #define USES_P121 // + #define USES_P121 // #endif #ifndef USES_P122 - #define USES_P122 // + #define USES_P122 // #endif #ifndef USES_P123 - #define USES_P123 // + #define USES_P123 // #endif #ifndef USES_P124 - #define USES_P124 // + #define USES_P124 // #endif #ifndef USES_P125 - #define USES_P125 // + #define USES_P125 // #endif // Controllers From b9673d42550e61dabb81515714248e3137e05986 Mon Sep 17 00:00:00 2001 From: svollebregt <42436341+svollebregt@users.noreply.github.com> Date: Sun, 10 Oct 2021 20:24:15 +0200 Subject: [PATCH 03/17] [P118] Adding Itho plugin to main reposity"" Adding P145 from the plugin playground as P118 to the main repo. Tested compiling of test_D and it seems to fit. --- lib/Itho/CC1101.cpp | 276 ++++++++ lib/Itho/CC1101.h | 207 ++++++ lib/Itho/CC1101Packet.h | 23 + lib/Itho/IthoCC1101.cpp | 849 +++++++++++++++++++++++ lib/Itho/IthoCC1101.h | 116 ++++ lib/Itho/IthoPacket.h | 49 ++ src/_P118_Itho.ino | 548 +++++++++++++++ src/src/CustomBuild/define_plugin_sets.h | 49 +- 8 files changed, 2093 insertions(+), 24 deletions(-) create mode 100644 lib/Itho/CC1101.cpp create mode 100644 lib/Itho/CC1101.h create mode 100644 lib/Itho/CC1101Packet.h create mode 100644 lib/Itho/IthoCC1101.cpp create mode 100644 lib/Itho/IthoCC1101.h create mode 100644 lib/Itho/IthoPacket.h create mode 100644 src/_P118_Itho.ino diff --git a/lib/Itho/CC1101.cpp b/lib/Itho/CC1101.cpp new file mode 100644 index 000000000..903a6cab7 --- /dev/null +++ b/lib/Itho/CC1101.cpp @@ -0,0 +1,276 @@ +/* + * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 + */ + +#include "CC1101.h" + +// default constructor +CC1101::CC1101() +{ + SPI.begin(); +#ifdef ESP8266 + pinMode(SS, OUTPUT); +#endif +} //CC1101 + +// default destructor +CC1101::~CC1101() +{ +} //~CC1101 + +/***********************/ +// SPI helper functions select() and deselect() +inline void CC1101::select(void) { + digitalWrite(SS, LOW); +} + +inline void CC1101::deselect(void) { + digitalWrite(SS, HIGH); +} + +void CC1101::spi_waitMiso() +{ + while(digitalRead(MISO) == HIGH) yield(); +} + +void CC1101::init() +{ + reset(); +} + +void CC1101::reset() +{ + deselect(); + delayMicroseconds(5); + select(); + delayMicroseconds(10); + deselect(); + delayMicroseconds(45); + select(); + + spi_waitMiso(); + SPI.transfer(CC1101_SRES); + delay(10); + spi_waitMiso(); + deselect(); +} + +uint8_t CC1101::writeCommand(uint8_t command) +{ + uint8_t result; + + select(); + spi_waitMiso(); + result = SPI.transfer(command); + deselect(); + + return result; +} + +void CC1101::writeRegister(uint8_t address, uint8_t data) +{ + select(); + spi_waitMiso(); + SPI.transfer(address); + SPI.transfer(data); + deselect(); +} + +uint8_t CC1101::readRegister(uint8_t address) +{ + uint8_t val; + + select(); + spi_waitMiso(); + SPI.transfer(address); + val = SPI.transfer(0); + deselect(); + + return val; +} + +uint8_t CC1101::readRegisterMedian3(uint8_t address) +{ + uint8_t val, val1, val2, val3; + + select(); + spi_waitMiso(); + SPI.transfer(address); + val1 = SPI.transfer(0); + SPI.transfer(address); + val2 = SPI.transfer(0); + SPI.transfer(address); + val3 = SPI.transfer(0); + deselect(); + // reverse sort (largest in val1) because this is te expected order for TX_BUFFER + if (val3 > val2) {val = val3; val3 = val2; val2 = val; } //Swap(val3,val2) + if (val2 > val1) {val = val2; val2 = val1, val1 = val; } //Swap(val2,val1) + if (val3 > val2) {val = val3; val3 = val2, val2 = val; } //Swap(val3,val2) + + return val2; +} + +/* Known SPI/26MHz synchronization bug (see CC1101 errata) +This issue affects the following registers: SPI status byte (fields STATE and FIFO_BYTES_AVAILABLE), +FREQEST or RSSI while the receiver is active, MARCSTATE at any time other than an IDLE radio state, +RXBYTES when receiving or TXBYTES when transmitting, and WORTIME1/WORTIME0 at any time.*/ +//uint8_t CC1101::readRegisterWithSyncProblem(uint8_t address, uint8_t registerType) +uint8_t /* ICACHE_RAM_ATTR */ CC1101::readRegisterWithSyncProblem(uint8_t address, uint8_t registerType) +{ + uint8_t value1, value2; + + value1 = readRegister(address | registerType); + + //if two consecutive reads gives us the same result then we know we are ok + do + { + value2 = value1; + value1 = readRegister(address | registerType); + } + while (value1 != value2); + + return value1; +} + +//registerType = CC1101_CONFIG_REGISTER or CC1101_STATUS_REGISTER +uint8_t CC1101::readRegister(uint8_t address, uint8_t registerType) +{ + switch (address) + { + case CC1101_FREQEST: + case CC1101_MARCSTATE: + case CC1101_RXBYTES: + case CC1101_TXBYTES: + case CC1101_WORTIME1: + case CC1101_WORTIME0: + return readRegisterWithSyncProblem(address, registerType); + + default: + return readRegister(address | registerType); + } +} + +void CC1101::writeBurstRegister(uint8_t address, uint8_t* data, uint8_t length) +{ + uint8_t i; + + select(); + spi_waitMiso(); + SPI.transfer(address | CC1101_WRITE_BURST); + for (i = 0; i < length; i++) { + SPI.transfer(data[i]); + } + deselect(); +} + +void CC1101::readBurstRegister(uint8_t* buffer, uint8_t address, uint8_t length) +{ + uint8_t i; + + select(); + spi_waitMiso(); + SPI.transfer(address | CC1101_READ_BURST); + + for (i = 0; i < length; i++) { + buffer[i] = SPI.transfer(0x00); + } + + deselect(); +} + +//wait for fixed length in rx fifo +uint8_t CC1101::receiveData(CC1101Packet* packet, uint8_t length) +{ + uint8_t rxBytes = readRegisterWithSyncProblem(CC1101_RXBYTES, CC1101_STATUS_REGISTER); + rxBytes = rxBytes & CC1101_BITS_RX_BYTES_IN_FIFO; + + //check for rx fifo overflow + if ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) & CC1101_BITS_MARCSTATE) == CC1101_MARCSTATE_RXFIFO_OVERFLOW) + { + writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SFRX); //flush RX buffer + writeCommand(CC1101_SRX); //switch to RX state + } + else if (rxBytes == length) + { + readBurstRegister(packet->data, CC1101_RXFIFO, rxBytes); + + //continue RX + writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SFRX); //flush RX buffer + writeCommand(CC1101_SRX); //switch to RX state + + packet->length = rxBytes; + } + else + { + //empty fifo + packet->length = 0; + writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SFRX); //flush RX buffer + writeCommand(CC1101_SRX); //switch to RX state + } + + return packet->length; +} + +//This function is able to send packets bigger then the FIFO size. +void CC1101::sendData(CC1101Packet *packet) +{ + uint8_t index = 0; + uint8_t txStatus, MarcState; + uint8_t length; + + writeCommand(CC1101_SIDLE); //idle + + txStatus = readRegisterWithSyncProblem(CC1101_TXBYTES, CC1101_STATUS_REGISTER); + + //clear TX fifo if needed + if (txStatus & CC1101_BITS_TX_FIFO_UNDERFLOW) + { + writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SFTX); //flush TX buffer + } + + writeCommand(CC1101_SIDLE); //idle + + //determine how many bytes to send + length = (packet->length <= CC1101_DATA_LEN ? packet->length : CC1101_DATA_LEN); + + writeBurstRegister(CC1101_TXFIFO, packet->data, length); + + writeCommand(CC1101_SIDLE); + //start sending packet + writeCommand(CC1101_STX); + + //continue sending when packet is bigger than 64 bytes + if (packet->length > CC1101_DATA_LEN) + { + index += length; + + //loop until all bytes are transmitted + while (index < packet->length) + { + //check if there is free space in the fifo + while ((txStatus = (readRegisterMedian3(CC1101_TXBYTES | CC1101_STATUS_REGISTER) & CC1101_BITS_RX_BYTES_IN_FIFO)) > (CC1101_DATA_LEN - 2)); + + //calculate how many bytes we can send + length = (CC1101_DATA_LEN - txStatus); + length = ((packet->length - index) < length ? (packet->length - index) : length); + + //send some more bytes + for (int i=0; idata[index+i]); + + index += length; + } + } + + //wait until transmission is finished (TXOFF_MODE is expected to be set to 0/IDLE or TXFIFO_UNDERFLOW) + do + { + MarcState = (readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER) & CC1101_BITS_MARCSTATE); +// if (MarcState == CC1101_MARCSTATE_TXFIFO_UNDERFLOW) Serial.print(F("TXFIFO_UNDERFLOW occured in sendData() \n")); + } + while((MarcState != CC1101_MARCSTATE_IDLE) && (MarcState != CC1101_MARCSTATE_TXFIFO_UNDERFLOW)); +} diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h new file mode 100644 index 000000000..b30e0162a --- /dev/null +++ b/lib/Itho/CC1101.h @@ -0,0 +1,207 @@ +/* + * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 + */ + +#ifndef __CC1101_H__ +#define __CC1101_H__ + +#include +#include "CC1101Packet.h" +#include +// On Arduino, SPI pins are predefined + +/* Type of transfers */ +#define CC1101_WRITE_BURST 0x40 +#define CC1101_READ_SINGLE 0x80 +#define CC1101_READ_BURST 0xC0 + +/* Type of register */ +#define CC1101_CONFIG_REGISTER CC1101_READ_SINGLE +#define CC1101_STATUS_REGISTER CC1101_READ_BURST + +/* PATABLE & FIFO's */ +#define CC1101_PATABLE 0x3E // PATABLE address +#define CC1101_TXFIFO 0x3F // TX FIFO address +#define CC1101_RXFIFO 0x3F // RX FIFO address +#define CC1101_PA_LowPower 0x60 +#define CC1101_PA_LongDistance 0xC0 + +/* Command strobes */ +#define CC1101_SRES 0x30 // Reset CC1101 chip +#define CC1101_SFSTXON 0x31 // Enable and calibrate frequency synthesizer (if MCSM0.FS_AUTOCAL=1). If in RX (with CCA): Go to a wait state where only the synthesizer is running (for quick RX / TX turnaround). +#define CC1101_SXOFF 0x32 // Turn off crystal oscillator +#define CC1101_SCAL 0x33 // Calibrate frequency synthesizer and turn it off. SCAL can be strobed from IDLE mode without setting manual calibration mode (MCSM0.FS_AUTOCAL=0) +#define CC1101_SRX 0x34 // Enable RX. Perform calibration first if coming from IDLE and MCSM0.FS_AUTOCAL=1 +#define CC1101_STX 0x35 // In IDLE state: Enable TX. Perform calibration first if MCSM0.FS_AUTOCAL=1. If in RX state and CCA is enabled: Only go to TX if channel is clear +#define CC1101_SIDLE 0x36 // Exit RX / TX, turn off frequency synthesizer and exit Wake-On-Radio mode if applicable +#define CC1101_SWOR 0x38 // Start automatic RX polling sequence (Wake-on-Radio) as described in Section 19.5 if WORCTRL.RC_PD=0 +#define CC1101_SPWD 0x39 // Enter power down mode when CSn goes high +#define CC1101_SFRX 0x3A // Flush the RX FIFO buffer. Only issue SFRX in IDLE or RXFIFO_OVERFLOW states +#define CC1101_SFTX 0x3B // Flush the TX FIFO buffer. Only issue SFTX in IDLE or TXFIFO_UNDERFLOW states +#define CC1101_SWORRST 0x3C // Reset real time clock to Event1 value +#define CC1101_SNOP 0x3D // No operation. May be used to get access to the chip status byte + +/* CC1101 configuration registers */ +#define CC1101_IOCFG2 0x00 // GDO2 Output Pin Configuration +#define CC1101_IOCFG1 0x01 // GDO1 Output Pin Configuration +#define CC1101_IOCFG0 0x02 // GDO0 Output Pin Configuration +#define CC1101_FIFOTHR 0x03 // RX FIFO and TX FIFO Thresholds +#define CC1101_SYNC1 0x04 // Sync Word, High Byte +#define CC1101_SYNC0 0x05 // Sync Word, Low Byte +#define CC1101_PKTLEN 0x06 // Packet Length +#define CC1101_PKTCTRL1 0x07 // Packet Automation Control +#define CC1101_PKTCTRL0 0x08 // Packet Automation Control +#define CC1101_ADDR 0x09 // Device Address +#define CC1101_CHANNR 0x0A // Channel Number +#define CC1101_FSCTRL1 0x0B // Frequency Synthesizer Control +#define CC1101_FSCTRL0 0x0C // Frequency Synthesizer Control +#define CC1101_FREQ2 0x0D // Frequency Control Word, High Byte +#define CC1101_FREQ1 0x0E // Frequency Control Word, Middle Byte +#define CC1101_FREQ0 0x0F // Frequency Control Word, Low Byte +#define CC1101_MDMCFG4 0x10 // Modem Configuration +#define CC1101_MDMCFG3 0x11 // Modem Configuration +#define CC1101_MDMCFG2 0x12 // Modem Configuration +#define CC1101_MDMCFG1 0x13 // Modem Configuration +#define CC1101_MDMCFG0 0x14 // Modem Configuration +#define CC1101_DEVIATN 0x15 // Modem Deviation Setting +#define CC1101_MCSM2 0x16 // Main Radio Control State Machine Configuration +#define CC1101_MCSM1 0x17 // Main Radio Control State Machine Configuration +#define CC1101_MCSM0 0x18 // Main Radio Control State Machine Configuration +#define CC1101_FOCCFG 0x19 // Frequency Offset Compensation Configuration +#define CC1101_BSCFG 0x1A // Bit Synchronization Configuration +#define CC1101_AGCCTRL2 0x1B // AGC Control +#define CC1101_AGCCTRL1 0x1C // AGC Control +#define CC1101_AGCCTRL0 0x1D // AGC Control +#define CC1101_WOREVT1 0x1E // High Byte Event0 Timeout +#define CC1101_WOREVT0 0x1F // Low Byte Event0 Timeout +#define CC1101_WORCTRL 0x20 // Wake On Radio Control +#define CC1101_FREND1 0x21 // Front End RX Configuration +#define CC1101_FREND0 0x22 // Front End TX Configuration +#define CC1101_FSCAL3 0x23 // Frequency Synthesizer Calibration +#define CC1101_FSCAL2 0x24 // Frequency Synthesizer Calibration +#define CC1101_FSCAL1 0x25 // Frequency Synthesizer Calibration +#define CC1101_FSCAL0 0x26 // Frequency Synthesizer Calibration +#define CC1101_RCCTRL1 0x27 // RC Oscillator Configuration +#define CC1101_RCCTRL0 0x28 // RC Oscillator Configuration +#define CC1101_FSTEST 0x29 // Frequency Synthesizer Calibration Control +#define CC1101_PTEST 0x2A // Production Test +#define CC1101_AGCTEST 0x2B // AGC Test +#define CC1101_TEST2 0x2C // Various Test Settings +#define CC1101_TEST1 0x2D // Various Test Settings +#define CC1101_TEST0 0x2E // Various Test Settings + +/* Status registers */ +#define CC1101_PARTNUM 0x30 // Chip ID +#define CC1101_VERSION 0x31 // Chip ID +#define CC1101_FREQEST 0x32 // Frequency Offset Estimate from Demodulator +#define CC1101_LQI 0x33 // Demodulator Estimate for Link Quality +#define CC1101_RSSI 0x34 // Received Signal Strength Indication +#define CC1101_MARCSTATE 0x35 // Main Radio Control State Machine State +#define CC1101_WORTIME1 0x36 // High Byte of WOR Time +#define CC1101_WORTIME0 0x37 // Low Byte of WOR Time +#define CC1101_PKTSTATUS 0x38 // Current GDOx Status and Packet Status +#define CC1101_VCO_VC_DAC 0x39 // Current Setting from PLL Calibration Module +#define CC1101_TXBYTES 0x3A // Underflow and Number of Bytes +#define CC1101_RXBYTES 0x3B // Overflow and Number of Bytes +#define CC1101_RCCTRL1_STATUS 0x3C // Last RC Oscillator Calibration Result +#define CC1101_RCCTRL0_STATUS 0x3D // Last RC Oscillator Calibration Result + +/* Bit fields in the chip status byte */ +#define CC1101_STATUS_CHIP_RDYn_BM 0x80 +#define CC1101_STATUS_STATE_BM 0x70 +#define CC1101_STATUS_FIFO_BYTES_AVAILABLE_BM 0x0F + +/* Masks to retrieve status bit */ +#define CC1101_BITS_TX_FIFO_UNDERFLOW 0x80 +#define CC1101_BITS_RX_BYTES_IN_FIFO 0x7F +#define CC1101_BITS_MARCSTATE 0x1F + + +/* Marc states */ +enum CC1101MarcStates +{ + CC1101_MARCSTATE_SLEEP = 0x00, + CC1101_MARCSTATE_IDLE = 0x01, + CC1101_MARCSTATE_XOFF = 0x02, + CC1101_MARCSTATE_VCOON_MC = 0x03, + CC1101_MARCSTATE_REGON_MC = 0x04, + CC1101_MARCSTATE_MANCAL = 0x05, + CC1101_MARCSTATE_VCOON = 0x06, + CC1101_MARCSTATE_REGON = 0x07, + CC1101_MARCSTATE_STARTCAL = 0x08, + CC1101_MARCSTATE_BWBOOST = 0x09, + CC1101_MARCSTATE_FS_LOCK = 0x0A, + CC1101_MARCSTATE_IFADCON = 0x0B, + CC1101_MARCSTATE_ENDCAL = 0x0C, + CC1101_MARCSTATE_RX = 0x0D, + CC1101_MARCSTATE_RX_END = 0x0E, + CC1101_MARCSTATE_RX_RST = 0x0F, + CC1101_MARCSTATE_TXRX_SWITCH = 0x10, + CC1101_MARCSTATE_RXFIFO_OVERFLOW = 0x11, + CC1101_MARCSTATE_FSTXON = 0x12, + CC1101_MARCSTATE_TX = 0x13, + CC1101_MARCSTATE_TX_END = 0x14, + CC1101_MARCSTATE_RXTX_SWITCH = 0x15, + CC1101_MARCSTATE_TXFIFO_UNDERFLOW = 0x16 +}; + + +/* Chip states */ +enum CC1101ChipStates +{ + CC1101_STATE_MASK = 0x70, + CC1101_STATE_IDLE = 0x00, + CC1101_STATE_RX = 0x10, + CC1101_STATE_TX = 0x20, + CC1101_STATE_FSTXON = 0x30, + CC1101_STATE_CALIBRATE = 0x40, + CC1101_STATE_SETTLING = 0x50, + CC1101_STATE_RX_OVERFLOW = 0x60, + CC1101_STATE_TX_UNDERFLOW = 0x70 +}; + + + +class CC1101 +{ + protected: + + //functions + public: + CC1101(); + ~CC1101(); + + //spi + void spi_waitMiso(); + + //cc1101 + void init(); + + uint8_t writeCommand(uint8_t command); + void writeRegister(uint8_t address, uint8_t data); + + uint8_t readRegister(uint8_t address, uint8_t registerType); + + void writeBurstRegister(uint8_t address, uint8_t* data, uint8_t length); + void readBurstRegister(uint8_t* buffer, uint8_t address, uint8_t length); + + void sendData(CC1101Packet *packet); + uint8_t receiveData(CC1101Packet* packet, uint8_t length); + + private: + CC1101( const CC1101 &c ); + CC1101& operator=( const CC1101 &c ); + // SPI helper functions + void select(void); + void deselect(void); + + protected: + uint8_t readRegister(uint8_t address); + uint8_t readRegisterMedian3(uint8_t address); + uint8_t readRegisterWithSyncProblem(uint8_t address, uint8_t registerType); + + void reset(); + +}; //CC1101 + +#endif //__CC1101_H__ diff --git a/lib/Itho/CC1101Packet.h b/lib/Itho/CC1101Packet.h new file mode 100644 index 000000000..f8c4f01b4 --- /dev/null +++ b/lib/Itho/CC1101Packet.h @@ -0,0 +1,23 @@ +/* + * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 + */ + +#ifndef CC1101PACKET_H_ +#define CC1101PACKET_H_ + +#include +#include + +#define CC1101_BUFFER_LEN 64 +#define CC1101_DATA_LEN CC1101_BUFFER_LEN - 3 + + +class CC1101Packet +{ + public: + uint8_t length; + uint8_t data[128]; +}; + + +#endif /* CC1101PACKET_H_ */ diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp new file mode 100644 index 000000000..97d158879 --- /dev/null +++ b/lib/Itho/IthoCC1101.cpp @@ -0,0 +1,849 @@ +/* + Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra +*/ +#define DEBUG 0 + +#define BYTE_TO_BINARY_PATTERN "%c,%c,%c,%c,%c,%c,%c,%c," +#define BYTE_TO_BINARY(byte) \ + (byte & 0x80 ? '1' : '0'), \ + (byte & 0x40 ? '1' : '0'), \ + (byte & 0x20 ? '1' : '0'), \ + (byte & 0x10 ? '1' : '0'), \ + (byte & 0x08 ? '1' : '0'), \ + (byte & 0x04 ? '1' : '0'), \ + (byte & 0x02 ? '1' : '0'), \ + (byte & 0x01 ? '1' : '0') + +#include "IthoCC1101.h" +#include +#include +#include + +//#define CRC_FILTER + +////original sync byte pattern +//#define STARTBYTE 6 //relevant data starts 6 bytes after the sync pattern bytes 170/171 +//#define SYNC1 170 +//#define SYNC0 171 +//#define MDMCFG2 0x02 //16bit sync word / 16bit specific + +////alternative sync byte pattern (filter much more non-itho messages out. Maybe too strict? Testing needed. +//#define STARTBYTE 0 //relevant data starts 0 bytes after the sync pattern bytes 179/42/171/42 +//#define SYNC1 187 //byte11 = 179, byte13 = 171 with SYNC1 = 163, 179 and 171 differ only by 1 bit +//#define SYNC0 42 +//#define MDMCFG2 0x03 //32bit sync word / 30bit specific + +//alternative sync byte pattern +#define STARTBYTE 2 //relevant data starts 2 bytes after the sync pattern bytes 179/42 +#define SYNC1 179 +#define SYNC0 42 +#define MDMCFG2 0x02 //16bit sync word / 16bit specific + +// default constructor +IthoCC1101::IthoCC1101(uint8_t counter, uint8_t sendTries) : CC1101() +{ + this->outIthoPacket.counter = counter; + this->sendTries = sendTries; + + this->outIthoPacket.deviceId[0] = 33; + this->outIthoPacket.deviceId[1] = 66; + this->outIthoPacket.deviceId[2] = 99; + + this->outIthoPacket.deviceType = 22; + +} //IthoCC1101 + +// default destructor +IthoCC1101::~IthoCC1101() +{ +} //~IthoCC1101 + +void IthoCC1101::initSendMessage(uint8_t len) +{ + //finishTransfer(); + writeCommand(CC1101_SIDLE); + delayMicroseconds(1); + writeRegister(CC1101_IOCFG0 , 0x2E); + delayMicroseconds(1); + writeRegister(CC1101_IOCFG1 , 0x2E); + delayMicroseconds(1); + writeCommand(CC1101_SIDLE); + writeCommand(CC1101_SPWD); + delayMicroseconds(2); + + /* + Configuration reverse engineered from remote print. The commands below are used by IthoDaalderop. + Base frequency 868.299866MHz + Channel 0 + Channel spacing 199.951172kHz + Carrier frequency 868.299866MHz + Xtal frequency 26.000000MHz + Data rate 38.3835kBaud + Manchester disabled + Modulation 2-FSK + Deviation 50.781250kHz + TX power ? + PA ramping enabled + Whitening disabled + */ + writeCommand(CC1101_SRES); + delayMicroseconds(1); + writeRegister(CC1101_IOCFG0 , 0x2E); //High impedance (3-state) + writeRegister(CC1101_FREQ2 , 0x21); //00100001 878MHz-927.8MHz + writeRegister(CC1101_FREQ1 , 0x65); //01100101 + writeRegister(CC1101_FREQ0 , 0x6A); //01101010 + writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 + writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 + writeRegister(CC1101_MDMCFG2 , 0x00); //00000000 2-FSK, no manchester encoding/decoding, no preamble/sync + writeRegister(CC1101_MDMCFG1 , 0x22); //00100010 + writeRegister(CC1101_MDMCFG0 , 0xF8); //11111000 + writeRegister(CC1101_CHANNR , 0x00); //00000000 + writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 + writeRegister(CC1101_FREND0 , 0x17); //00010111 use index 7 in PA table + writeRegister(CC1101_MCSM0 , 0x18); //00011000 PO timeout Approx. 146microseconds - 171microseconds, Auto calibrate When going from IDLE to RX or TX (or FSTXON) + writeRegister(CC1101_FSCAL3 , 0xA9); //10101001 + writeRegister(CC1101_FSCAL2 , 0x2A); //00101010 + writeRegister(CC1101_FSCAL1 , 0x00); //00000000 + writeRegister(CC1101_FSCAL0 , 0x11); //00010001 + writeRegister(CC1101_FSTEST , 0x59); //01011001 For test only. Do not write to this register. + writeRegister(CC1101_TEST2 , 0x81); //10000001 For test only. Do not write to this register. + writeRegister(CC1101_TEST1 , 0x35); //00110101 For test only. Do not write to this register. + writeRegister(CC1101_TEST0 , 0x0B); //00001011 For test only. Do not write to this register. + writeRegister(CC1101_PKTCTRL0 , 0x12); //00010010 Enable infinite length packets, CRC disabled, Turn data whitening off, Serial Synchronous mode + writeRegister(CC1101_ADDR , 0x00); //00000000 + writeRegister(CC1101_PKTLEN , 0xFF); //11111111 //Not used, no hardware packet handling + + //0x6F,0x26,0x2E,0x8C,0x87,0xCD,0xC7,0xC0 + writeBurstRegister(CC1101_PATABLE | CC1101_WRITE_BURST, (uint8_t*)ithoPaTableSend, 8); + + //difference, message1 sends a STX here + writeCommand(CC1101_SIDLE); + writeCommand(CC1101_SIDLE); + + writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 + writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 + writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 + writeRegister(CC1101_IOCFG0 , 0x2D); //GDO0_Z_EN_N. When this output is 0, GDO0 is configured as input (for serial TX data). + writeRegister(CC1101_IOCFG1 , 0x0B); //Serial Clock. Synchronous to the data in synchronous serial mode. + + writeCommand(CC1101_STX); + writeCommand(CC1101_SIDLE); + + writeRegister(CC1101_MDMCFG4 , 0x5A); //difference compared to message1 + writeRegister(CC1101_MDMCFG3 , 0x83); //difference compared to message1 + writeRegister(CC1101_DEVIATN , 0x50); //difference compared to message1 + //writeRegister(CC1101_IOCFG0 ,0x2D); //GDO0_Z_EN_N. When this output is 0, GDO0 is configured as input (for serial TX data). + //writeRegister(CC1101_IOCFG1 ,0x0B); //Serial Clock. Synchronous to the data in synchronous serial mode. + + //Itho is using serial mode for transmit. We want to use the TX FIFO with fixed packet length for simplicity. + writeRegister(CC1101_IOCFG0 , 0x2E); + writeRegister(CC1101_IOCFG1 , 0x2E); + writeRegister(CC1101_PKTCTRL0 , 0x00); + writeRegister(CC1101_PKTCTRL1 , 0x00); + + writeRegister(CC1101_PKTLEN , len); + +} + +void IthoCC1101::finishTransfer() +{ + writeCommand(CC1101_SIDLE); + delayMicroseconds(1); + + writeRegister(CC1101_IOCFG0 , 0x2E); + writeRegister(CC1101_IOCFG1 , 0x2E); + + writeCommand(CC1101_SIDLE); + writeCommand(CC1101_SPWD); +} + +void IthoCC1101::initReceive() +{ + /* + Configuration reverse engineered from RFT print. + + Base frequency 868.299866MHz + Channel 0 + Channel spacing 199.951172kHz + Carrier frequency 868.299866MHz + Xtal frequency 26.000000MHz + Data rate 38.3835kBaud + RX filter BW 325.000000kHz + Manchester disabled + Modulation 2-FSK + Deviation 50.781250kHz + TX power 0x6F,0x26,0x2E,0x7F,0x8A,0x84,0xCA,0xC4 + PA ramping enabled + Whitening disabled + */ + writeCommand(CC1101_SRES); + + writeRegister(CC1101_TEST0 , 0x09); + writeRegister(CC1101_FSCAL2 , 0x00); + + //0x6F,0x26,0x2E,0x7F,0x8A,0x84,0xCA,0xC4 + writeBurstRegister(CC1101_PATABLE | CC1101_WRITE_BURST, (uint8_t*)ithoPaTableReceive, 8); + + writeCommand(CC1101_SCAL); + + //wait for calibration to finish + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) yield(); + + writeRegister(CC1101_FSCAL2 , 0x00); + writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate + writeRegister(CC1101_FREQ2 , 0x21); + writeRegister(CC1101_FREQ1 , 0x65); + writeRegister(CC1101_FREQ0 , 0x6A); + writeRegister(CC1101_IOCFG0 , 0x2E); //High impedance (3-state) + writeRegister(CC1101_IOCFG2 , 0x06); //0x06 Assert when sync word has been sent / received, and de-asserts at the end of the packet. + writeRegister(CC1101_FSCTRL1 , 0x06); + writeRegister(CC1101_FSCTRL0 , 0x00); + writeRegister(CC1101_MDMCFG4 , 0x5A); + writeRegister(CC1101_MDMCFG3 , 0x83); + writeRegister(CC1101_MDMCFG2 , 0x00); //Enable digital DC blocking filter before demodulator, 2-FSK, Disable Manchester encoding/decoding, No preamble/sync + writeRegister(CC1101_MDMCFG1 , 0x22); //Disable FEC + writeRegister(CC1101_MDMCFG0 , 0xF8); + writeRegister(CC1101_CHANNR , 0x00); + writeRegister(CC1101_DEVIATN , 0x50); + writeRegister(CC1101_FREND1 , 0x56); + writeRegister(CC1101_FREND0 , 0x17); + writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate + writeRegister(CC1101_FOCCFG , 0x16); + writeRegister(CC1101_BSCFG , 0x6C); + writeRegister(CC1101_AGCCTRL2 , 0x43); + writeRegister(CC1101_AGCCTRL1 , 0x40); + writeRegister(CC1101_AGCCTRL0 , 0x91); + writeRegister(CC1101_FSCAL3 , 0xE9); + writeRegister(CC1101_FSCAL2 , 0x2A); + writeRegister(CC1101_FSCAL1 , 0x00); + writeRegister(CC1101_FSCAL0 , 0x11); + writeRegister(CC1101_FSTEST , 0x59); + writeRegister(CC1101_TEST2 , 0x81); + writeRegister(CC1101_TEST1 , 0x35); + writeRegister(CC1101_TEST0 , 0x0B); + writeRegister(CC1101_PKTCTRL1 , 0x04); //No address check, Append two bytes with status RSSI/LQI/CRC OK, + writeRegister(CC1101_PKTCTRL0 , 0x32); //Infinite packet length mode, CRC disabled for TX and RX, No data whitening, Asynchronous serial mode, Data in on GDO0 and data out on either of the GDOx pins + writeRegister(CC1101_ADDR , 0x00); + writeRegister(CC1101_PKTLEN , 0xFF); + writeRegister(CC1101_TEST0 , 0x09); + + writeCommand(CC1101_SCAL); + + //wait for calibration to finish + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) yield(); + + writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate + + writeCommand(CC1101_SIDLE); + writeCommand(CC1101_SIDLE); + + writeRegister(CC1101_MDMCFG2 , 0x00); //Enable digital DC blocking filter before demodulator, 2-FSK, Disable Manchester encoding/decoding, No preamble/sync + writeRegister(CC1101_IOCFG0 , 0x0D); //Serial Data Output. Used for asynchronous serial mode. + + writeCommand(CC1101_SRX); + + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_RX) yield(); + + initReceiveMessage(); +} + +void IthoCC1101::initReceiveMessage() +{ + uint8_t marcState; + + writeCommand(CC1101_SIDLE); //idle + + //set datarate + writeRegister(CC1101_MDMCFG4 , 0x5A); // set kBaud + writeRegister(CC1101_MDMCFG3 , 0x83); // set kBaud + writeRegister(CC1101_DEVIATN , 0x50); + + //set fifo mode with fixed packet length and sync bytes + writeRegister(CC1101_PKTLEN , 63); //63 bytes message (sync at beginning of message is removed by CC1101) + + //set fifo mode with fixed packet length and sync bytes + writeRegister(CC1101_PKTCTRL0 , 0x00); + writeRegister(CC1101_SYNC1 , SYNC1); + writeRegister(CC1101_SYNC0 , SYNC0); + writeRegister(CC1101_MDMCFG2 , MDMCFG2); + writeRegister(CC1101_PKTCTRL1 , 0x00); + + writeCommand(CC1101_SRX); //switch to RX state + + // Check that the RX state has been entered + while (((marcState = readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) & CC1101_BITS_MARCSTATE) != CC1101_MARCSTATE_RX) + { + if (marcState == CC1101_MARCSTATE_RXFIFO_OVERFLOW) // RX_OVERFLOW + writeCommand(CC1101_SFRX); //flush RX buffer + } +} + +bool IthoCC1101::checkForNewPacket() { + if (receiveData(&inMessage, 63) && parseMessageCommand()) { + initReceiveMessage(); + return true; + } + return false; +} + +bool IthoCC1101::parseMessageCommand() { + + messageDecode(&inMessage, &inIthoPacket); + + //deviceType of message type? + inIthoPacket.deviceType = inIthoPacket.dataDecoded[0]; + + //deviceID + inIthoPacket.deviceId[0] = inIthoPacket.dataDecoded[1]; + inIthoPacket.deviceId[1] = inIthoPacket.dataDecoded[2]; + inIthoPacket.deviceId[2] = inIthoPacket.dataDecoded[3]; + + //counter1 + inIthoPacket.counter = inIthoPacket.dataDecoded[4]; + + bool isHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageHighCommandBytes); + bool isRVHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVHighCommandBytes); + bool isMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageMediumCommandBytes); + bool isRVMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVMediumCommandBytes); + bool isLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageLowCommandBytes); + bool isRVLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVLowCommandBytes); + bool isRVAutoCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVAutoCommandBytes); + bool isStandByCommand = checkIthoCommand(&inIthoPacket, ithoMessageStandByCommandBytes); + bool isTimer1Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer1CommandBytes); + bool isTimer2Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer2CommandBytes); + bool isTimer3Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer3CommandBytes); + bool isJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageJoinCommandBytes); + bool isJoin2Command = checkIthoCommand(&inIthoPacket, ithoMessageJoin2CommandBytes); + bool isRVJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVJoinCommandBytes); + bool isLeaveCommand = checkIthoCommand(&inIthoPacket, ithoMessageLeaveCommandBytes); + + //determine command + inIthoPacket.command = IthoUnknown; + if (isHighCommand) inIthoPacket.command = IthoHigh; + if (isRVHighCommand) inIthoPacket.command = IthoHigh; + if (isMediumCommand) inIthoPacket.command = IthoMedium; + if (isRVMediumCommand) inIthoPacket.command = IthoMedium; + if (isLowCommand) inIthoPacket.command = IthoLow; + if (isRVLowCommand) inIthoPacket.command = IthoLow; + if (isRVAutoCommand) inIthoPacket.command = IthoStandby; + if (isStandByCommand) inIthoPacket.command = IthoStandby; + if (isTimer1Command) inIthoPacket.command = IthoTimer1; + if (isTimer2Command) inIthoPacket.command = IthoTimer2; + if (isTimer3Command) inIthoPacket.command = IthoTimer3; + if (isJoinCommand) inIthoPacket.command = IthoJoin; + if (isJoin2Command) inIthoPacket.command = IthoJoin; + if (isRVJoinCommand) inIthoPacket.command = IthoJoin; + if (isLeaveCommand) inIthoPacket.command = IthoLeave; + +#if defined (CRC_FILTER) + uint8_t mLen = 0; + if (isPowerCommand || isHighCommand || isMediumCommand || isLowCommand || isStandByCommand || isTimer1Command || isTimer2Command || isTimer3Command) { + mLen = 11; + } + else if (isJoinCommand || isJoin2Command) { + mLen = 20; + } + else if (isLeaveCommand) { + mLen = 14; + } + else { + return true; + } + if (getCounter2(&inIthoPacket, mLen) != inIthoPacket.dataDecoded[mLen]) { + inIthoPacket.command = IthoUnknown; + return false; + } +#endif + + return true; +} + +bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[]) { + uint8_t offset = 0; + if (itho->deviceType == 28 || itho->deviceType == 24) offset = 2; + for (int i = 4; i < 6; i++) + { + //if (i == 2 || i == 3) continue; //skip byte3 and byte4, rft-rv and co2-auto remote device seem to sometimes have a different number there + if ( (itho->dataDecoded[i + 5 + offset] != commandBytes[i]) && (itho->dataDecodedChk[i + 5 + offset] != commandBytes[i]) ) { + return false; + } + } + return true; +} + +void IthoCC1101::sendCommand(IthoCommand command) +{ + CC1101Packet outMessage; + uint8_t maxTries = sendTries; + uint8_t delaytime = 40; + + //update itho packet data + outIthoPacket.command = command; + outIthoPacket.counter += 1; + + //get message2 bytes + switch (command) + { + case IthoJoin: + createMessageJoin(&outIthoPacket, &outMessage); + break; + + case IthoLeave: + createMessageLeave(&outIthoPacket, &outMessage); + //the leave command needs to be transmitted for 1 second according the manual + maxTries = 30; + delaytime = 4; + break; + + default: + createMessageCommand(&outIthoPacket, &outMessage); + break; + } + + //send messages + for (int i = 0; i < maxTries; i++) + { + + //message2 + initSendMessage(outMessage.length); + sendData(&outMessage); + + finishTransfer(); + delay(delaytime); + } + //initReceive(); SV - I call this from the ESPEasy plugin to prevent crashes +} + +void IthoCC1101::createMessageStart(IthoPacket *itho, CC1101Packet *packet) +{ + + //fixed, set start structure in data buffer manually + for (uint8_t i = 0; i < 7; i++) { + packet->data[i] = 170; + } + packet->data[7] = 171; + packet->data[8] = 254; + packet->data[9] = 0; + packet->data[10] = 179; + packet->data[11] = 42; + packet->data[12] = 171; + packet->data[13] = 42; + + //[start of command specific data] + +} + +void IthoCC1101::createMessageCommand(IthoPacket *itho, CC1101Packet *packet) +{ + + //set start message structure + createMessageStart(itho, packet); + + //set deviceType? (or messageType?), not sure what this is + itho->dataDecoded[0] = itho->deviceType; + + //set deviceID + itho->dataDecoded[1] = itho->deviceId[0]; + itho->dataDecoded[2] = itho->deviceId[1]; + itho->dataDecoded[3] = itho->deviceId[2]; + + //set counter1 + itho->dataDecoded[4] = itho->counter; + + //set command bytes on dataDecoded[5 - 10] + uint8_t *commandBytes = getMessageCommandBytes(itho->command); + for (uint8_t i = 0; i < 6; i++) { + itho->dataDecoded[i + 5] = commandBytes[i]; + } + + //set counter2 + itho->dataDecoded[11] = getCounter2(itho, 11); + + itho->length = 12; + + packet->length = messageEncode(itho, packet); + packet->length += 1; + + //set end byte + packet->data[packet->length] = 172; + packet->length += 1; + + //set end 'noise' + for (uint8_t i = packet->length; i < packet->length + 7; i++) { + packet->data[i] = 170; + } + packet->length += 7; + +} + +void IthoCC1101::createMessageJoin(IthoPacket *itho, CC1101Packet *packet) +{ + + //set start message structure + createMessageStart(itho, packet); + + //set deviceType? (or messageType?) + itho->dataDecoded[0] = itho->deviceType; + + //set deviceID + itho->dataDecoded[1] = itho->deviceId[0]; + itho->dataDecoded[2] = itho->deviceId[1]; + itho->dataDecoded[3] = itho->deviceId[2]; + + //set counter1 + itho->dataDecoded[4] = itho->counter; + + //set command bytes on dataDecoded[5 - ?] + uint8_t *commandBytes = getMessageCommandBytes(itho->command); + for (uint8_t i = 0; i < 6; i++) { + itho->dataDecoded[i + 5] = commandBytes[i]; + } + + //set deviceID + itho->dataDecoded[11] = itho->deviceId[0]; + itho->dataDecoded[12] = itho->deviceId[1]; + itho->dataDecoded[13] = itho->deviceId[2]; + + itho->dataDecoded[14] = 1; + itho->dataDecoded[15] = 16; + itho->dataDecoded[16] = 224; + + //set deviceID + itho->dataDecoded[17] = itho->deviceId[0]; + itho->dataDecoded[18] = itho->deviceId[1]; + itho->dataDecoded[19] = itho->deviceId[2]; + + //set counter2 + itho->dataDecoded[20] = getCounter2(itho, 20); + + itho->length = 21; + + packet->length = messageEncode(itho, packet); + packet->length += 1; + + //set end byte + packet->data[packet->length] = 202; + packet->length += 1; + + //set end 'noise' + for (uint8_t i = packet->length; i < packet->length + 7; i++) { + packet->data[i] = 170; + } + packet->length += 7; + +} + +void IthoCC1101::createMessageLeave(IthoPacket *itho, CC1101Packet *packet) +{ + + //set start message structure + createMessageStart(itho, packet); + + //set deviceType? (or messageType?) + itho->dataDecoded[0] = itho->deviceType; + + //set deviceID + itho->dataDecoded[1] = itho->deviceId[0]; + itho->dataDecoded[2] = itho->deviceId[1]; + itho->dataDecoded[3] = itho->deviceId[2]; + + //set counter1 + itho->dataDecoded[4] = itho->counter; + + //set command bytes on dataDecoded[5 - 10] + uint8_t *commandBytes = getMessageCommandBytes(itho->command); + for (uint8_t i = 0; i < 6; i++) { + itho->dataDecoded[i + 5] = commandBytes[i]; + } + + //set deviceID + itho->dataDecoded[11] = itho->deviceId[0]; + itho->dataDecoded[12] = itho->deviceId[1]; + itho->dataDecoded[13] = itho->deviceId[2]; + + //set counter2 + itho->dataDecoded[14] = getCounter2(itho, 14); + + itho->length = 15; + + packet->length = messageEncode(itho, packet); + packet->length += 1; + + //set end byte + packet->data[packet->length] = 202; + packet->length += 1; + + //set end 'noise' + for (uint8_t i = packet->length; i < packet->length + 7; i++) { + packet->data[i] = 170; + } + packet->length += 7; + +} + +uint8_t* IthoCC1101::getMessageCommandBytes(IthoCommand command) +{ + switch (command) + { + case IthoStandby: + return (uint8_t*)&ithoMessageStandByCommandBytes[0]; + case IthoHigh: + return (uint8_t*)&ithoMessageHighCommandBytes[0]; + case IthoFull: + return (uint8_t*)&ithoMessageFullCommandBytes[0]; + case IthoMedium: + return (uint8_t*)&ithoMessageMediumCommandBytes[0]; + case IthoLow: + return (uint8_t*)&ithoMessageLowCommandBytes[0]; + case IthoTimer1: + return (uint8_t*)&ithoMessageTimer1CommandBytes[0]; + case IthoTimer2: + return (uint8_t*)&ithoMessageTimer2CommandBytes[0]; + case IthoTimer3: + return (uint8_t*)&ithoMessageTimer3CommandBytes[0]; + case IthoJoin: + return (uint8_t*)&ithoMessageJoinCommandBytes[0]; + case IthoLeave: + return (uint8_t*)&ithoMessageLeaveCommandBytes[0]; + default: + return (uint8_t*)&ithoMessageLowCommandBytes[0]; + } +} + +/* + Counter2 is the decimal sum of all bytes in decoded form from + deviceType up to the last byte before counter2 subtracted + from zero. +*/ +uint8_t IthoCC1101::getCounter2(IthoPacket *itho, uint8_t len) { + + uint8_t val = 0; + + for (uint8_t i = 0; i < len; i++) { + val += itho->dataDecoded[i]; + } + + return 0 - val; +} + +uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { + + uint8_t lenOutbuf = 0; + + if ((itho->length * 20) % 8 == 0) { //inData len fits niecly in out buffer length + lenOutbuf = itho->length * 2.5; + } + else { //is this an issue? inData last byte does not fill out buffer length, add 1 out byte extra, padding is done after encode + lenOutbuf = (uint8_t)(itho->length * 2.5) + 0.5; + } + + uint8_t out_bytecounter = 14; //index of Outbuf, start at offset 14, first part of the message is set manually + uint8_t out_bitcounter = 0; //bit position of current outbuf byte + uint8_t out_patterncounter = 0; //bit counter to add 1 0 bit pattern after every 8 bits + uint8_t bitSelect = 4; //bit position of the inData byte (4 - 7, 0 - 3) + uint8_t out_shift = 7; //bit shift inData bit in position of outbuf byte + + //we need to zero the out buffer first cause we are using bitshifts + for (int i = out_bytecounter; i < sizeof(packet->data) / sizeof(packet->data[0]); i++) { + packet->data[i] = 0; + } + + //Serial.println(); + for (uint8_t dataByte = 0; dataByte < itho->length; dataByte++) { + for (uint8_t dataBit = 0; dataBit < 8; dataBit++) { //process a full dataByte at a time resulting in 20 output bits (2.5 bytes) with the pattern 7x6x5x4x 10 3x2x1x0x 10 7x6x5x4x 10 3x2x1x0x 10 etc + if (out_bitcounter == 8) { //check if new byte is needed + out_bytecounter++; + out_bitcounter = 0; + } + + if (out_patterncounter == 8) { //check if we have to start with a 1 0 pattern + out_patterncounter = 0; + packet->data[out_bytecounter] = packet->data[out_bytecounter] | 1 << out_shift; + out_shift--; + out_bitcounter++; + packet->data[out_bytecounter] = packet->data[out_bytecounter] | 0 << out_shift; + if (out_shift == 0) out_shift = 8; + out_shift--; + out_bitcounter++; + } + + if (out_bitcounter == 8) { //check if new byte is needed + out_bytecounter++; + out_bitcounter = 0; + } + + //set the even bit + uint8_t bit = (itho->dataDecoded[dataByte] & (1 << bitSelect)) >> bitSelect; //select bit and shift to bit pos 0 + bitSelect++; + if (bitSelect == 8) bitSelect = 0; + + packet->data[out_bytecounter] = packet->data[out_bytecounter] | bit << out_shift; //shift bit in corect pos of current outbuf byte + out_shift--; + out_bitcounter++; + out_patterncounter++; + + //set the odd bit (inverse of even bit) + bit = ~bit & 0b00000001; + packet->data[out_bytecounter] = packet->data[out_bytecounter] | bit << out_shift; + if (out_shift == 0) out_shift = 8; + out_shift--; + out_bitcounter++; + out_patterncounter++; + + } + + } + if (out_bitcounter < 8) { //add closing 1 0 pattern to fill last packet->data byte and ensure DC balance in the message + for (uint8_t i = out_bitcounter; i < 8; i += 2) { + packet->data[out_bytecounter] = packet->data[out_bytecounter] | 1 << out_shift; + out_shift--; + packet->data[out_bytecounter] = packet->data[out_bytecounter] | 0 << out_shift; + if (out_shift == 0) out_shift = 8; + out_shift--; + } + } + + return out_bytecounter; +} + + +void IthoCC1101::messageDecode(CC1101Packet *packet, IthoPacket *itho) { + + itho->length = 0; + int lenInbuf = packet->length; + + lenInbuf -= STARTBYTE; //correct for sync byte pos + + while (lenInbuf >= 5) { + lenInbuf -= 5; + itho->length += 2; + } + if (lenInbuf >= 3) { + itho->length++; + } + + for (int i = 0; i < sizeof(itho->dataDecoded) / sizeof(itho->dataDecoded[0]); i++) { + itho->dataDecoded[i] = 0; + } + for (int i = 0; i < sizeof(itho->dataDecodedChk) / sizeof(itho->dataDecodedChk[0]); i++) { + itho->dataDecodedChk[i] = 0; + } + + uint8_t out_i = 0; //byte index + uint8_t out_j = 4; //bit index + uint8_t out_i_chk = 0; //byte index + uint8_t out_j_chk = 4; //bit index + uint8_t in_bitcounter = 0; //process per 10 input bits + + for (int i = STARTBYTE; i < packet->length; i++) { + + for (int j = 7; j > -1; j--) { + if (in_bitcounter == 0 || in_bitcounter == 2 || in_bitcounter == 4 || in_bitcounter == 6) { //select input bits for output + uint8_t x = packet->data[i]; //select input byte + x = x >> j; //select input bit + x = x & 0b00000001; + x = x << out_j; //set value for output bit + itho->dataDecoded[out_i] = itho->dataDecoded[out_i] | x; + out_j += 1; //next output bit + if (out_j > 7) out_j = 0; + if (out_j == 4) out_i += 1; + } + if (in_bitcounter == 1 || in_bitcounter == 3 || in_bitcounter == 5 || in_bitcounter == 7) { //select input bits for check output + uint8_t x = packet->data[i]; //select input byte + x = x >> j; //select input bit + x = x & 0b00000001; + x = x << out_j_chk; //set value for output bit + itho->dataDecodedChk[out_i_chk] = itho->dataDecodedChk[out_i_chk] | x; + out_j_chk += 1; //next output bit + if (out_j_chk > 7) out_j_chk = 0; + if (out_j_chk == 4) { + itho->dataDecodedChk[out_i_chk] = ~itho->dataDecodedChk[out_i_chk]; //inverse bits + out_i_chk += 1; + } + } + in_bitcounter += 1; //continue cyling in groups of 10 bits + if (in_bitcounter > 9) in_bitcounter = 0; + } + } +} + +uint8_t IthoCC1101::ReadRSSI() +{ + uint8_t rssi = 0; + uint8_t value = 0; + + rssi = (readRegister(CC1101_RSSI, CC1101_STATUS_REGISTER)); + + if (rssi >= 128) + { + value = 255 - rssi; + value /= 2; + value += 74; + } + else + { + value = rssi / 2; + value += 74; + } + return (value); +} + +bool IthoCC1101::checkID(const uint8_t *id) +{ + for (uint8_t i = 0; i < 3; i++) + if (id[i] != inIthoPacket.deviceId[i]) + return false; + return true; +} + +String IthoCC1101::getLastIDstr(bool ashex) { + String str; + for (uint8_t i = 0; i < 3; i++) { + if (ashex) str += String(inIthoPacket.deviceId[i], HEX); + else str += String(inIthoPacket.deviceId[i]); + if (i < 2) str += ","; + } + return str; +} + +int * IthoCC1101::getLastID() { + static int id[3]; + for (uint8_t i = 0; i < 3; i++) { + id[i] = inIthoPacket.deviceId[i]; + } + return id; +} + +String IthoCC1101::getLastMessagestr(bool ashex) { + String str = "Length=" + String(inMessage.length) + "."; + for (uint8_t i = 0; i < inMessage.length; i++) { + if (ashex) str += String(inMessage.data[i], HEX); + else str += String(inMessage.data[i]); + if (i < inMessage.length - 1) str += ":"; + } + return str; +} + +String IthoCC1101::LastMessageDecoded() { + + String str; + if (inIthoPacket.length > 11) { + str += "Device type?: " + String(inIthoPacket.deviceType); + str += " - CMD: "; + for (int i = 4; i < inIthoPacket.length; i++) { + str += String(inIthoPacket.dataDecoded[i]); + if (i < inIthoPacket.length - 1) str += ","; + } + + } + else { + for (uint8_t i = 0; i < inIthoPacket.length; i++) { + str += String(inIthoPacket.dataDecoded[i]); + if (i < inIthoPacket.length - 1) str += ","; + } + + } + str += "\n"; + return str; + +} diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h new file mode 100644 index 000000000..f4fd7b0da --- /dev/null +++ b/lib/Itho/IthoCC1101.h @@ -0,0 +1,116 @@ +/* + * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra + */ + +#ifndef __ITHOCC1101_H__ +#define __ITHOCC1101_H__ + +#include +#include "CC1101.h" +#include "IthoPacket.h" + + +//pa table settings +const uint8_t ithoPaTableSend[8] = {0x6F, 0x26, 0x2E, 0x8C, 0x87, 0xCD, 0xC7, 0xC0}; +const uint8_t ithoPaTableReceive[8] = {0x6F, 0x26, 0x2E, 0x7F, 0x8A, 0x84, 0xCA, 0xC4}; + +//message command bytes +const uint8_t ithoMessageRVHighCommandBytes[] = {49,224,4,0,0,200}; +const uint8_t ithoMessageHighCommandBytes[] = {34,241,3,0,4,4}; +const uint8_t ithoMessageFullCommandBytes[] = {34,241,3,0,4,4}; +const uint8_t ithoMessageMediumCommandBytes[] = {34,241,3,0,3,4}; +const uint8_t ithoMessageRVMediumCommandBytes[] = {34,241,3,0,3,7}; +const uint8_t ithoMessageLowCommandBytes[] = {34,241,3,0,2,4}; +const uint8_t ithoMessageRVLowCommandBytes[] = {49,224,4,0,0,1}; +const uint8_t ithoMessageRVAutoCommandBytes[] = {34,241,3,0,5,7}; +const uint8_t ithoMessageStandByCommandBytes[] = {0,0,0,0,0,0}; //unkown, tbd +const uint8_t ithoMessageTimer1CommandBytes[] = {34,243,3,0,0,10}; //10 minutes full speed +const uint8_t ithoMessageTimer2CommandBytes[] = {34,243,3,0,0,20}; //20 minutes full speed +const uint8_t ithoMessageTimer3CommandBytes[] = {34,243,3,0,0,30}; //30 minutes full speed +const uint8_t ithoMessageJoinCommandBytes[] = {31,201,12,0,34,241}; +const uint8_t ithoMessageJoin2CommandBytes[] = {31,201,12,99,34,248}; //join command of RFT AUTO Co2 remote +const uint8_t ithoMessageRVJoinCommandBytes[] = {31,201,24,0,49,224}; //join command of RFT-RV +const uint8_t ithoMessageLeaveCommandBytes[] = {31,201,6,0,31,201}; +//itho rft-rv +//unknown, high +//148,216,43,49,224,4,0,0,200,0,3,127,244,78,11,155,154,225,11,96,138 +//148,216,43,49,224,4,0,0,200,0,3,127,51,80,47,233,94,6,189,114,73 + +//low +//148,216,43,49,224,4,0,0,1,0,202,127,242,212,160,123,15,64,7,129,33 +//148,216,43,34,241,3,0,4,4,194,127,255,189,90,107,88,72,115,49,192,105 + +//join +//151,149,65,31,201,24,0,49,224,151,149,65,0,18,160,151,149,65,1,16,224 + + +class IthoCC1101 : protected CC1101 +{ + private: + //receive + CC1101Packet inMessage; //temp storage message2 + IthoPacket inIthoPacket; //stores last received message data + + //send + IthoPacket outIthoPacket; //stores state of "remote" + + //settings + uint8_t sendTries; //number of times a command is send at one button press + + //functions + public: + IthoCC1101(uint8_t counter = 0, uint8_t sendTries = 3); //set initial counter value + ~IthoCC1101(); + + //init + void init() { CC1101::init(); initReceive(); } //init,reset CC1101 + void initReceive(); + uint8_t getLastCounter() { return outIthoPacket.counter; } //counter is increased before sending a command + void setSendTries(uint8_t sendTries) { this->sendTries = sendTries; } + void setDeviceID(uint8_t byte0, uint8_t byte1, uint8_t byte2) { this->outIthoPacket.deviceId[0] = byte0; this->outIthoPacket.deviceId[1] = byte1; this->outIthoPacket.deviceId[2] = byte2;} + + //receive + bool checkForNewPacket(); //check RX fifo for new data + IthoPacket getLastPacket() { return inIthoPacket; } //retrieve last received/parsed packet from remote + IthoCommand getLastCommand() { return inIthoPacket.command; } //retrieve last received/parsed command from remote + uint8_t getLastInCounter() { return inIthoPacket.counter; } //retrieve last received/parsed command from remote + uint8_t ReadRSSI(); + bool checkID(const uint8_t *id); + int * getLastID(); + String getLastIDstr(bool ashex=true); + String getLastMessagestr(bool ashex=true); + String LastMessageDecoded(); + + //send + void sendCommand(IthoCommand command); + protected: + private: + IthoCC1101( const IthoCC1101 &c); + IthoCC1101& operator=( const IthoCC1101 &c); + + //init CC1101 for receiving + void initReceiveMessage(); + + //init CC1101 for sending + void initSendMessage(uint8_t len); + void finishTransfer(); + + //parse received message + bool parseMessageCommand(); + bool checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[]); + + //send + void createMessageStart(IthoPacket *itho, CC1101Packet *packet); + void createMessageCommand(IthoPacket *itho, CC1101Packet *packet); + void createMessageJoin(IthoPacket *itho, CC1101Packet *packet); + void createMessageLeave(IthoPacket *itho, CC1101Packet *packet); + uint8_t* getMessageCommandBytes(IthoCommand command); + uint8_t getCounter2(IthoPacket *itho, uint8_t len); + + uint8_t messageEncode(IthoPacket *itho, CC1101Packet *packet); + void messageDecode(CC1101Packet *packet, IthoPacket *itho); + + +}; //IthoCC1101 + +#endif //__ITHOCC1101_H__ \ No newline at end of file diff --git a/lib/Itho/IthoPacket.h b/lib/Itho/IthoPacket.h new file mode 100644 index 000000000..3a5c3e807 --- /dev/null +++ b/lib/Itho/IthoPacket.h @@ -0,0 +1,49 @@ +/* + * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra + */ + +#ifndef ITHOPACKET_H_ +#define ITHOPACKET_H_ + +enum IthoCommand +{ + IthoUnknown = 0, + + IthoJoin = 1, + IthoLeave = 2, + + IthoStandby = 3, + IthoLow = 4, + IthoMedium = 5, + IthoHigh = 6, + IthoFull = 7, + + IthoTimer1 = 8, + IthoTimer2 = 9, + IthoTimer3 = 10, + + //duco c system remote + DucoStandby = 11, + DucoLow = 12, + DucoMedium = 13, + DucoHigh = 14 +}; + + +class IthoPacket +{ + public: + IthoCommand command; + + uint8_t dataDecoded[32]; + uint8_t dataDecodedChk[32]; + uint8_t length; + + uint8_t deviceType; + uint8_t deviceId[3]; + + uint8_t counter; //0-255, counter is increased on every remote button press +}; + + +#endif /* ITHOPACKET_H_ */ \ No newline at end of file diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino new file mode 100644 index 000000000..c3b19119f --- /dev/null +++ b/src/_P118_Itho.ino @@ -0,0 +1,548 @@ +//####################################################################################################### +//############################## Plugin 118: Itho ventilation unit 868Mhz remote ######################## +//####################################################################################################### + +// author :jodur, 13-1-2018 +// changed :jeroen, 2-11-2019 +// changed :svollebregt, 30-1-2020 - changes to improve stability: volatile decleration of state, +// disable logging within interrupts unles enabled, removed some unused code, +// reduce noInterrupts() blockage of code fragments to prevent crashes +// svollebregt, 16-2-2020 - ISR now sets flag which is checked by 50 per seconds plugin call as +// receive ISR with Ticker was the cause of instability. Inspired by: https://github.com/arnemauer/Ducobox-ESPEasy-Plugin +// svollebregt, 11-04-2020 - Minor changes to make code compatible with latest mega 20200410, removed SYNC1 option for now; +// better to change this value in the Itho-lib code and compile it yourself +// svollebreggt, 13-2-2021 - Now uses rewirtten library made by arjenhiemstra: https://github.com/arjenhiemstra/IthoEcoFanRFT + +// Recommended to disable RF receive logging to minimize code execution within interrupts + +// List of commands: +// 1111 to join ESP8266 with Itho ventilation unit +// 9999 to leaveESP8266 with Itho ventilation unit +// 0 to set Itho ventilation unit to standby +// 1 - set Itho ventilation unit to low speed +// 2 - set Itho ventilation unit to medium speed +// 3 - set Itho ventilation unit to high speed +// 4 - set Itho ventilation unit to full speed +// 13 - set itho to high speed with hardware timer (10 min) +// 23 - set itho to high speed with hardware timer (20 min) +// 33 - set itho to high speed with hardware timer (30 min) + +//List of States: + +// 1 - Itho ventilation unit to lowest speed +// 2 - Itho ventilation unit to medium speed +// 3 - Itho ventilation unit to high speed +// 4 - Itho ventilation unit to full speed +// 13 -Itho to high speed with hardware timer (10 min) +// 23 -Itho to high speed with hardware timer (20 min) +// 33 -Itho to high speed with hardware timer (30 min) + +// Usage for http (not case sensitive): +// http://ip/control?cmd=STATE,1111 +// http://ip/control?cmd=STATE,1 +// http://ip/control?cmd=STATE,2 +// http://ip/control?cmd=STATE,3 + +// usage for example mosquito MQTT +// mosquitto_pub -t /Fan/cmd -m 'state 1111' +// mosquitto_pub -t /Fan/cmd -m 'state 1' +// mosquitto_pub -t /Fan/cmd -m 'state 2' +// mosquitto_pub -t /Fan/cmd -m 'state 3' + + +// This code needs the library made by 'supersjimmie': https://github.com/supersjimmie/IthoEcoFanRFT/tree/master/Master/Itho +// A CC1101 868Mhz transmitter is needed +// See https://gathering.tweakers.net/forum/list_messages/1690945 for more information +// code/idea was inspired by first release of code from 'Thinkpad' + +#include +#include "IthoCC1101.h" +#include "IthoPacket.h" +#include "_Plugin_Helper.h" +#ifdef USES_P118 + +//This extra settings struct is needed because the default settingsstruct doesn't support strings +struct PLUGIN__ExtraSettingsStruct +{ char ID1[9]; + char ID2[9]; + char ID3[9]; +} PLUGIN_118_ExtraSettings; + +IthoCC1101 PLUGIN_118_rf; + +// extra for interrupt handling +bool PLUGIN_118_ITHOhasPacket = false; +int PLUGIN_118_State=1; // after startup it is assumed that the fan is running low +int PLUGIN_118_OldState=1; +int PLUGIN_118_Timer=0; +int PLUGIN_118_LastIDindex = 0; +int PLUGIN_118_OldLastIDindex = 0; +int8_t Plugin_118_IRQ_pin=-1; +bool PLUGIN_118_InitRunned = false; +bool PLUGIN_118_Log = false; + +// volatile for interrupt function +volatile bool PLUGIN_118_Int = false; +//volatile unsigned long PLUGIN_118_Int_time = 0; + +#define PLUGIN_118 +#define PLUGIN_ID_118 118 +#define PLUGIN_NAME_118 "Itho ventilation remote" +#define PLUGIN_VALUENAME1_118 "State" +#define PLUGIN_VALUENAME2_118 "Timer" +#define PLUGIN_VALUENAME3_118 "LastIDindex" + +// Timer values for hardware timer in Fan +#define PLUGIN_118_Time1 10*60 +#define PLUGIN_118_Time2 20*60 +#define PLUGIN_118_Time3 30*60 + +boolean Plugin_118(byte function, struct EventStruct *event, String &string) +{ + boolean success = false; + + switch (function) + { + + case PLUGIN_DEVICE_ADD: + { + Device[++deviceCount].Number = PLUGIN_ID_118; + Device[deviceCount].Type = DEVICE_TYPE_SINGLE; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; + Device[deviceCount].Ports = 0; + Device[deviceCount].PullUpOption = false; + Device[deviceCount].InverseLogicOption = false; + Device[deviceCount].FormulaOption = false; + Device[deviceCount].ValueCount = 3; + Device[deviceCount].SendDataOption = true; + Device[deviceCount].TimerOption = true; + Device[deviceCount].GlobalSyncOption = true; + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_118); + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_118)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_118)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_118)); + break; + } + + case PLUGIN_GET_DEVICEGPIONAMES: + { + event->String1 = formatGpioName_input(F("Interrupt pin (CC1101 GDO2)")); + break; + } + + case PLUGIN_SET_DEFAULTS: + { + PCONFIG(0) = 1; + PCONFIG(1) = 10; + PCONFIG(2) = 87; + PCONFIG(3) = 81; + success = true; + break; + } + + case PLUGIN_INIT: + { + //If configured interrupt pin differs from configured, release old pin first + if ((Settings.TaskDevicePin1[event->TaskIndex]!=Plugin_118_IRQ_pin) && (Plugin_118_IRQ_pin!=-1)) + { + addLog(LOG_LEVEL_DEBUG, F("IO-PIN changed, deatachinterrupt old pin")); + detachInterrupt(Plugin_118_IRQ_pin); + } + LoadCustomTaskSettings(event->TaskIndex, (byte*)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + addLog(LOG_LEVEL_INFO, F("Extra Settings PLUGIN_118 loaded")); + PLUGIN_118_rf.setDeviceID(PCONFIG(1), PCONFIG(2), PCONFIG(3)); //DeviceID used to send commands, can also be changed on the fly for multi itho control, 10,87,81 corresponds with old library + PLUGIN_118_rf.init(); + Plugin_118_IRQ_pin = Settings.TaskDevicePin1[event->TaskIndex]; + pinMode(Plugin_118_IRQ_pin, INPUT); + attachInterrupt(Plugin_118_IRQ_pin, PLUGIN_118_ITHOinterrupt, FALLING); + addLog(LOG_LEVEL_INFO, F("CC1101 868Mhz transmitter initialized")); + PLUGIN_118_rf.initReceive(); + PLUGIN_118_InitRunned=true; + success = true; + break; + } + + case PLUGIN_EXIT: + { + addLog(LOG_LEVEL_INFO, F("EXIT PLUGIN_118")); + //remove interupt when plugin is removed + detachInterrupt(Plugin_118_IRQ_pin); + success = true; + break; + } + + case PLUGIN_ONCE_A_SECOND: + { + //decrement timer when timermode is running + if (PLUGIN_118_State>=10) PLUGIN_118_Timer--; + + //if timer has elapsed set Fan state to low + if ((PLUGIN_118_State>=10) && (PLUGIN_118_Timer<=0)) + { + PLUGIN_118_State=1; + PLUGIN_118_Timer=0; + } + + //Publish new data when vars are changed or init has runned or timer is running (update every 2 sec) + if ((PLUGIN_118_OldState!=PLUGIN_118_State) || ((PLUGIN_118_Timer>0) && (PLUGIN_118_Timer % 2==0)) || (PLUGIN_118_OldLastIDindex!=PLUGIN_118_LastIDindex)|| PLUGIN_118_InitRunned) + { + addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_ONCE_A_SECOND")); + PLUGIN_118_Publishdata(event); + sendData(event); + //reset flag set by init + PLUGIN_118_InitRunned=false; + } + //Remeber current state for next cycle + PLUGIN_118_OldState=PLUGIN_118_State; + PLUGIN_118_OldLastIDindex =PLUGIN_118_LastIDindex; + success = true; + break; + } + + case PLUGIN_FIFTY_PER_SECOND: + { + if (PLUGIN_118_Int) + { + PLUGIN_118_Int = false; // reset flag + PLUGIN_118_ITHOcheck(); + /* + unsigned long time_elapsed = millis() - PLUGIN_118_Int_time; //Disabled as it doesn't appear to be required + if (time_elapsed >= 10) + { + PLUGIN_118_ITHOcheck(); + } + else + { + delay(10-time_elapsed); + PLUGIN_118_ITHOcheck(); + }*/ + } + success = true; + break; + } + + + case PLUGIN_READ: { + // This ensures that even when Values are not changing, data is send at the configured interval for aquisition + addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_READ")); + PLUGIN_118_Publishdata(event); + //sendData(event); //SV - Added to send status every xx secnds as set within plugin + success = true; + break; + } + + case PLUGIN_WRITE: { + String tmpString = string; + String cmd = parseString(tmpString, 1); + String param1 = parseString(tmpString, 2); + if (cmd.equalsIgnoreCase(F("STATE"))) + { + + if (param1.equalsIgnoreCase(F("1111"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoJoin); + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'join' to Itho unit")); + printWebString += F("Sent command for 'join' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("9999"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoLeave); + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'leave' to Itho unit")); + printWebString += F("Sent command for 'leave' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("0"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoStandby); + PLUGIN_118_State=0; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'standby' to Itho unit")); + printWebString += F("Sent command for 'standby' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("1"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoLow); + PLUGIN_118_State=1; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'low speed' to Itho unit")); + printWebString += F("Sent command for 'low speed' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("2"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoMedium); + PLUGIN_118_State=2; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'medium speed' to Itho unit")); + printWebString += F("Sent command for 'medium speed' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("3"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoHigh); + PLUGIN_118_State=3; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'high speed' to Itho unit")); + printWebString += F("Sent command for 'high speed' to Itho unit"); + + success = true; + } + if (param1.equalsIgnoreCase(F("4"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoFull); + PLUGIN_118_State=4; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'full speed' to Itho unit")); + printWebString += F("Sent command for 'full speed' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("13"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer1); + PLUGIN_118_State=13; + PLUGIN_118_Timer=PLUGIN_118_Time1; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 1' to Itho unit")); + printWebString += F("Sent command for 'timer 1' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("23"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer2); + PLUGIN_118_State=23; + PLUGIN_118_Timer=PLUGIN_118_Time2; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 2' to Itho unit")); + printWebString += F("Sent command for 'timer 2' to Itho unit"); + success = true; + } + if (param1.equalsIgnoreCase(F("33"))) + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer3); + PLUGIN_118_State=33; + PLUGIN_118_Timer=PLUGIN_118_Time3; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 3' to Itho unit")); + printWebString += F("Sent command for 'timer 3' to Itho unit"); + success = true; + } + } + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + addFormSubHeader(F("Remote RF Controls")); + addFormTextBox(F("Unit ID remote 1"), F("PLUGIN_118_ID1"), PLUGIN_118_ExtraSettings.ID1, 8); + addFormTextBox(F("Unit ID remote 2"), F("PLUGIN_118_ID2"), PLUGIN_118_ExtraSettings.ID2, 8); + addFormTextBox(F("Unit ID remote 3"), F("PLUGIN_118_ID3"), PLUGIN_118_ExtraSettings.ID3, 8); + addFormCheckBox(F("Enable RF receive log"), F("p118_log"), PCONFIG(0)); + addFormNumericBox(F("Device ID byte 1"), F("p118_deviceid1"), PCONFIG(1), 0, 255); + addFormNumericBox(F("Device ID byte 2"), F("p118_deviceid2"), PCONFIG(2), 0, 255); + addFormNumericBox(F("Device ID byte 3"), F("p118_deviceid3"), PCONFIG(3), 0, 255); + addFormNote(F("Device ID of your ESP, should not be the same as your neighbours ;-). Defaults to 10,87,81 which corresponds to the old Itho code")); + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + strcpy(PLUGIN_118_ExtraSettings.ID1, web_server.arg(F("PLUGIN_118_ID1")).c_str()); + strcpy(PLUGIN_118_ExtraSettings.ID2, web_server.arg(F("PLUGIN_118_ID2")).c_str()); + strcpy(PLUGIN_118_ExtraSettings.ID3, web_server.arg(F("PLUGIN_118_ID3")).c_str()); + SaveCustomTaskSettings(event->TaskIndex, (byte*)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + + PCONFIG(0) = isFormItemChecked(F("p118_log")); + PLUGIN_118_Log = PCONFIG(0); + PCONFIG(1) = getFormItemInt(F("p118_deviceid1"), 10); + PCONFIG(2) = getFormItemInt(F("p118_deviceid2"), 87); + PCONFIG(3) = getFormItemInt(F("p118_deviceid3"), 81); + success = true; + break; + } + } +return success; +} + +ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt() +{ + PLUGIN_118_Int = true; //flag + //PLUGIN_118_Int_time = millis(); //used to register time since interrupt, to make sure we don't read within 10 ms as the RX buffer needs some time to get ready. Update: Disabled as it appear not necessary +} + +void PLUGIN_118_ITHOcheck() +{ + noInterrupts(); + if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, "RF signal received\n");} + if(PLUGIN_118_rf.checkForNewPacket()) + { + IthoCommand cmd = PLUGIN_118_rf.getLastCommand(); + String Id = PLUGIN_118_rf.getLastIDstr(); + + //Move check here to prevent function calling within ISR + byte index = 0; + if (Id == PLUGIN_118_ExtraSettings.ID1){ + index = 1; + } + else if (Id == PLUGIN_118_ExtraSettings.ID2){ + index = 2; + } + else if (Id == PLUGIN_118_ExtraSettings.ID3){ + index = 3; + } + + //int index = PLUGIN_118_RFRemoteIndex(Id); + // IF id is know index should be >0 + if (index>0) + { + if(PLUGIN_118_Log){ + String log = F("Command received from remote-ID: "); + log += Id; + addLog(LOG_LEVEL_DEBUG, log); + } + String log2 = ""; + if(PLUGIN_118_Log){log2 += F("Command received=");} + switch (cmd) + { + case IthoUnknown: + if(PLUGIN_118_Log){log2 += F("unknown\n");} + break; + case IthoStandby: + case DucoStandby: + if(PLUGIN_118_Log){log2 += F("standby\n");} + PLUGIN_118_State = 0; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoLow: + case DucoLow: + if(PLUGIN_118_Log){log2 += F("low\n");} + PLUGIN_118_State = 1; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoMedium: + case DucoMedium: + if(PLUGIN_118_Log){log2 += F("medium\n");} + PLUGIN_118_State = 2; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoHigh: + case DucoHigh: + if(PLUGIN_118_Log){log2 += F("high\n");} + PLUGIN_118_State = 3; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoFull: + if(PLUGIN_118_Log){log2 += F("full\n");} + PLUGIN_118_State = 4; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoTimer1: + if(PLUGIN_118_Log){log2 += +F("timer1\n");} + PLUGIN_118_State = 13; + PLUGIN_118_Timer = PLUGIN_118_Time1; + PLUGIN_118_LastIDindex = index; + break; + case IthoTimer2: + if(PLUGIN_118_Log){log2 += F("timer2\n");} + PLUGIN_118_State = 23; + PLUGIN_118_Timer = PLUGIN_118_Time2; + PLUGIN_118_LastIDindex = index; + break; + case IthoTimer3: + if(PLUGIN_118_Log){log2 += F("timer3\n");} + PLUGIN_118_State = 33; + PLUGIN_118_Timer = PLUGIN_118_Time3; + PLUGIN_118_LastIDindex = index; + break; + case IthoJoin: + if(PLUGIN_118_Log){log2 += F("join\n");} + break; + case IthoLeave: + if(PLUGIN_118_Log){log2 += F("leave\n");} + break; + } + if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, log2);} + } + else { + if(PLUGIN_118_Log){ + String log = F("Device-ID:"); + log += Id; + log += F(" IGNORED"); + addLog(LOG_LEVEL_DEBUG, log); + } + } + } + interrupts(); +} + +void PLUGIN_118_Publishdata(struct EventStruct *event) +{ + UserVar[event->BaseVarIndex]=PLUGIN_118_State; + UserVar[event->BaseVarIndex+1]=PLUGIN_118_Timer; + UserVar[event->BaseVarIndex+2]=PLUGIN_118_LastIDindex; + + String log = F("State: "); + log += UserVar[event->BaseVarIndex]; + addLog(LOG_LEVEL_DEBUG, log); + log = F("Timer: "); + log += UserVar[event->BaseVarIndex+1]; + addLog(LOG_LEVEL_DEBUG, log); + log = F("LastIDindex: "); + log += UserVar[event->BaseVarIndex+2]; + addLog(LOG_LEVEL_DEBUG, log); +} + + +#endif // USES_P118 diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index bb0835465..df729d989 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1133,6 +1133,7 @@ To create/register a plugin, you have to : #define USES_P094 // CUL Reader #define USES_P114 // VEML6075 UVA/UVB sensor #define USES_P115 // Fuel Gauge MAX1704x + #define USES_P118 // Itho ventilation control #endif @@ -1144,14 +1145,14 @@ To create/register a plugin, you have to : #ifndef USES_P027 #define USES_P027 // INA219 #endif - #ifndef USES_P076 + #ifndef USES_P076 #define USES_P076 // HWL8012 in POW r1 #endif - #ifndef USES_P077 + #ifndef USES_P077 // Needs CSE7766 Energy sensor, via Serial RXD 4800 baud 8E1 (GPIO1), TXD (GPIO3) #define USES_P077 // CSE7766 in POW R2 #endif - #ifndef USES_P078 + #ifndef USES_P078 #define USES_P078 // Eastron Modbus Energy meters #endif #ifndef USES_P085 @@ -1163,7 +1164,7 @@ To create/register a plugin, you have to : #ifndef USES_P102 #define USES_P102 // PZEM-004Tv30 #endif - #ifndef USES_P108 + #ifndef USES_P108 #define USES_P108 // DDS238-x ZN MODBUS energy meter (was P224 in the Playground) #endif #ifndef USES_P115 @@ -1179,31 +1180,31 @@ To create/register a plugin, you have to : #ifndef USES_P023 #define USES_P023 // OLED #endif - #ifndef USES_P036 + #ifndef USES_P036 #define USES_P036 // FrameOLED #endif - #ifndef USES_P038 + #ifndef USES_P038 #define USES_P038 // NeoPixel #endif - #ifndef USES_P041 + #ifndef USES_P041 #define USES_P041 // NeoClock #endif - #ifndef USES_P042 + #ifndef USES_P042 #define USES_P042 // Candle #endif - #ifndef USES_P057 + #ifndef USES_P057 #define USES_P057 // HT16K33_LED #endif - #ifndef USES_P070 + #ifndef USES_P070 #define USES_P070 // NeoPixel_Clock #endif - #ifndef USES_P075 + #ifndef USES_P075 #define USES_P075 // Nextion #endif - #ifndef USES_P095 + #ifndef USES_P095 #define USES_P095 // TFT ILI9341 #endif - #ifndef USES_P096 + #ifndef USES_P096 #define USES_P096 // eInk (Needs lib_deps = Adafruit GFX Library, LOLIN_EPD ) #endif #ifndef USES_P099 @@ -1280,7 +1281,7 @@ To create/register a plugin, you have to : #define USES_P142 // RGB-Strip #define USES_P143 // AnyonePresent #define USES_P144 // RC-Switch-TX - #define USES_P145 // Itho + #define USES_P145 // Itho - P118 in the main repo. #define USES_P149 // MHZ19 #define USES_P150 // SDM120C #define USES_P151 // CISA @@ -1401,34 +1402,34 @@ To create/register a plugin, you have to : #define USES_P115 // Fuel gauge MAX1704x #endif #ifndef USES_P116 - #define USES_P116 // + #define USES_P116 // #endif #ifndef USES_P117 - #define USES_P117 // + #define USES_P117 // #endif #ifndef USES_P118 - #define USES_P118 // + #define USES_P118 // Itho ventilation coontrol #endif #ifndef USES_P119 - #define USES_P119 // + #define USES_P119 // #endif #ifndef USES_P120 - #define USES_P120 // + #define USES_P120 // #endif #ifndef USES_P121 - #define USES_P121 // + #define USES_P121 // #endif #ifndef USES_P122 - #define USES_P122 // + #define USES_P122 // #endif #ifndef USES_P123 - #define USES_P123 // + #define USES_P123 // #endif #ifndef USES_P124 - #define USES_P124 // + #define USES_P124 // #endif #ifndef USES_P125 - #define USES_P125 // + #define USES_P125 // #endif // Controllers From 377dd940d393ca9551a2cfe5df7078d905d2b7aa Mon Sep 17 00:00:00 2001 From: svollebregt <42436341+svollebregt@users.noreply.github.com> Date: Sat, 16 Oct 2021 15:03:05 +0200 Subject: [PATCH 04/17] Update _P118_Itho.ino Incorporated and tested all changes suggested by tonhuisman: - Moved ifdef to start - Changed plugin name - Adjusted timer options - Switch statement using Par1 --- src/_P118_Itho.ino | 273 ++++++++++++++++++++++++--------------------- 1 file changed, 145 insertions(+), 128 deletions(-) diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index c3b19119f..7ec0770f3 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -55,11 +55,12 @@ // See https://gathering.tweakers.net/forum/list_messages/1690945 for more information // code/idea was inspired by first release of code from 'Thinkpad' +#ifdef USES_P118 + #include #include "IthoCC1101.h" #include "IthoPacket.h" #include "_Plugin_Helper.h" -#ifdef USES_P118 //This extra settings struct is needed because the default settingsstruct doesn't support strings struct PLUGIN__ExtraSettingsStruct @@ -87,7 +88,7 @@ volatile bool PLUGIN_118_Int = false; #define PLUGIN_118 #define PLUGIN_ID_118 118 -#define PLUGIN_NAME_118 "Itho ventilation remote" +#define PLUGIN_NAME_118 "Remote - Itho ventilation" #define PLUGIN_VALUENAME1_118 "State" #define PLUGIN_VALUENAME2_118 "Timer" #define PLUGIN_VALUENAME3_118 "LastIDindex" @@ -115,7 +116,8 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) Device[deviceCount].FormulaOption = false; Device[deviceCount].ValueCount = 3; Device[deviceCount].SendDataOption = true; - Device[deviceCount].TimerOption = true; + Device[deviceCount].TimerOption = false; + Device[deviceCount].TimerOptional = true; Device[deviceCount].GlobalSyncOption = true; break; } @@ -244,134 +246,149 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) case PLUGIN_WRITE: { String tmpString = string; String cmd = parseString(tmpString, 1); - String param1 = parseString(tmpString, 2); if (cmd.equalsIgnoreCase(F("STATE"))) { - - if (param1.equalsIgnoreCase(F("1111"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoJoin); - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'join' to Itho unit")); - printWebString += F("Sent command for 'join' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("9999"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoLeave); - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'leave' to Itho unit")); - printWebString += F("Sent command for 'leave' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("0"))) + switch(event->Par1) { + case 1111: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoJoin); + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'join' to Itho unit")); + printWebString += F("Sent command for 'join' to Itho unit"); + success = true; + break; + } + case 9999: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoLeave); + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'leave' to Itho unit")); + printWebString += F("Sent command for 'leave' to Itho unit"); + success = true; + break; + } + case 0: { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoStandby); - PLUGIN_118_State=0; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'standby' to Itho unit")); - printWebString += F("Sent command for 'standby' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("1"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoLow); - PLUGIN_118_State=1; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'low speed' to Itho unit")); - printWebString += F("Sent command for 'low speed' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("2"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoMedium); - PLUGIN_118_State=2; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'medium speed' to Itho unit")); - printWebString += F("Sent command for 'medium speed' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("3"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoHigh); - PLUGIN_118_State=3; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'high speed' to Itho unit")); - printWebString += F("Sent command for 'high speed' to Itho unit"); - - success = true; - } - if (param1.equalsIgnoreCase(F("4"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoFull); - PLUGIN_118_State=4; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'full speed' to Itho unit")); - printWebString += F("Sent command for 'full speed' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("13"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoTimer1); - PLUGIN_118_State=13; - PLUGIN_118_Timer=PLUGIN_118_Time1; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 1' to Itho unit")); - printWebString += F("Sent command for 'timer 1' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("23"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoTimer2); - PLUGIN_118_State=23; - PLUGIN_118_Timer=PLUGIN_118_Time2; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 2' to Itho unit")); - printWebString += F("Sent command for 'timer 2' to Itho unit"); - success = true; - } - if (param1.equalsIgnoreCase(F("33"))) - { - noInterrupts(); - PLUGIN_118_rf.sendCommand(IthoTimer3); - PLUGIN_118_State=33; - PLUGIN_118_Timer=PLUGIN_118_Time3; - PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 3' to Itho unit")); - printWebString += F("Sent command for 'timer 3' to Itho unit"); - success = true; + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoStandby); + PLUGIN_118_State=0; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'standby' to Itho unit")); + printWebString += F("Sent command for 'standby' to Itho unit"); + success = true; + break; + } + case 1: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoLow); + PLUGIN_118_State=1; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'low speed' to Itho unit")); + printWebString += F("Sent command for 'low speed' to Itho unit"); + success = true; + break; + } + case 2: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoMedium); + PLUGIN_118_State=2; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'medium speed' to Itho unit")); + printWebString += F("Sent command for 'medium speed' to Itho unit"); + success = true; + break; + } + case 3: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoHigh); + PLUGIN_118_State=3; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'high speed' to Itho unit")); + printWebString += F("Sent command for 'high speed' to Itho unit"); + success = true; + break; + } + case 4: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoFull); + PLUGIN_118_State=4; + PLUGIN_118_Timer=0; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'full speed' to Itho unit")); + printWebString += F("Sent command for 'full speed' to Itho unit"); + success = true; + break; + } + case 13: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer1); + PLUGIN_118_State=13; + PLUGIN_118_Timer=PLUGIN_118_Time1; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 1' to Itho unit")); + printWebString += F("Sent command for 'timer 1' to Itho unit"); + success = true; + break; + } + case 23: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer2); + PLUGIN_118_State=23; + PLUGIN_118_Timer=PLUGIN_118_Time2; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 2' to Itho unit")); + printWebString += F("Sent command for 'timer 2' to Itho unit"); + success = true; + break; + } + case 33: + { + noInterrupts(); + PLUGIN_118_rf.sendCommand(IthoTimer3); + PLUGIN_118_State=33; + PLUGIN_118_Timer=PLUGIN_118_Time3; + PLUGIN_118_LastIDindex = 0; + interrupts(); + PLUGIN_118_rf.initReceive(); + addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 3' to Itho unit")); + printWebString += F("Sent command for 'timer 3' to Itho unit"); + success = true; + break; + } + default: + { + addLog(LOG_LEVEL_INFO, F("Invalid command parameter")); + printWebString += F("Invalid command parameter"); + success = true; + } } } break; From 4361ad35d366a0b6440a0a8a85411f10e848f8b4 Mon Sep 17 00:00:00 2001 From: svollebregt <42436341+svollebregt@users.noreply.github.com> Date: Wed, 20 Oct 2021 21:01:57 +0200 Subject: [PATCH 05/17] Update _P118_Itho.ino Code clean-up following suggestions of TD-er. --- src/_P118_Itho.ino | 83 ++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index 7ec0770f3..b72b4d66e 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -88,7 +88,7 @@ volatile bool PLUGIN_118_Int = false; #define PLUGIN_118 #define PLUGIN_ID_118 118 -#define PLUGIN_NAME_118 "Remote - Itho ventilation" +#define PLUGIN_NAME_118 "Communication - Itho ventilation" #define PLUGIN_VALUENAME1_118 "State" #define PLUGIN_VALUENAME2_118 "Timer" #define PLUGIN_VALUENAME3_118 "LastIDindex" @@ -248,148 +248,118 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) String cmd = parseString(tmpString, 1); if (cmd.equalsIgnoreCase(F("STATE"))) { + noInterrupts(); switch(event->Par1) { - case 1111: + case 1111: //Join command { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoJoin); - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'join' to Itho unit")); - printWebString += F("Sent command for 'join' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'join' to Itho unit")); success = true; break; } - case 9999: + case 9999: //Leave command { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoLeave); - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'leave' to Itho unit")); - printWebString += F("Sent command for 'leave' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'leave' to Itho unit")); success = true; break; } - case 0: + case 0: //Off command { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoStandby); PLUGIN_118_State=0; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'standby' to Itho unit")); - printWebString += F("Sent command for 'standby' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'standby' to Itho unit")); success = true; break; } - case 1: + case 1: //Fan low { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoLow); PLUGIN_118_State=1; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'low speed' to Itho unit")); - printWebString += F("Sent command for 'low speed' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'low speed' to Itho unit")); success = true; break; } - case 2: + case 2: //Fan medium { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoMedium); PLUGIN_118_State=2; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'medium speed' to Itho unit")); - printWebString += F("Sent command for 'medium speed' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'medium speed' to Itho unit")); success = true; break; } - case 3: + case 3: //Fan high { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoHigh); PLUGIN_118_State=3; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'high speed' to Itho unit")); - printWebString += F("Sent command for 'high speed' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'high speed' to Itho unit")); success = true; break; } - case 4: + case 4: //Fan full { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoFull); PLUGIN_118_State=4; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'full speed' to Itho unit")); - printWebString += F("Sent command for 'full speed' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'full speed' to Itho unit")); success = true; break; } - case 13: + case 13: //Timer1 - 10 min { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoTimer1); PLUGIN_118_State=13; PLUGIN_118_Timer=PLUGIN_118_Time1; PLUGIN_118_LastIDindex = 0; - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 1' to Itho unit")); - printWebString += F("Sent command for 'timer 1' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'timer 1' to Itho unit")); success = true; break; } - case 23: + case 23: //Timer2 - 20 min { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoTimer2); PLUGIN_118_State=23; PLUGIN_118_Timer=PLUGIN_118_Time2; PLUGIN_118_LastIDindex = 0; - interrupts(); PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 2' to Itho unit")); - printWebString += F("Sent command for 'timer 2' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'timer 2' to Itho unit")); success = true; break; } - case 33: + case 33: //Timer3 - 30 min { - noInterrupts(); PLUGIN_118_rf.sendCommand(IthoTimer3); PLUGIN_118_State=33; PLUGIN_118_Timer=PLUGIN_118_Time3; PLUGIN_118_LastIDindex = 0; - interrupts(); - PLUGIN_118_rf.initReceive(); - addLog(LOG_LEVEL_INFO, F("Sent command for 'timer 3' to Itho unit")); - printWebString += F("Sent command for 'timer 3' to Itho unit"); + PLUGIN_118_PluginWriteLog(F("Sent command for 'timer 3' to Itho unit")); success = true; break; } default: { - addLog(LOG_LEVEL_INFO, F("Invalid command parameter")); - printWebString += F("Invalid command parameter"); + PLUGIN_118_PluginWriteLog(F("Invalid command parameter")); success = true; } } + interrupts(); } break; } @@ -561,5 +531,10 @@ void PLUGIN_118_Publishdata(struct EventStruct *event) addLog(LOG_LEVEL_DEBUG, log); } +void PLUGIN_118_PluginWriteLog(String log) +{ + addLog(LOG_LEVEL_INFO, log); + printWebString += log; +} #endif // USES_P118 From 26caea93b8558469daa731fbb7183cecb2f8d688 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 18 Nov 2021 23:13:08 +0100 Subject: [PATCH 06/17] [P124] Add plugin Output - I2C Multi Relay --- .../.gitlab-ci.yml | 8 + .../.travis.yml | 21 ++ .../LICENSE | 21 ++ .../README.md | 84 +++++++ .../change_i2c_address/change_i2c_address.ino | 45 ++++ .../eight_channel_relay_control.ino | 95 +++++++ .../four_channel_relay_control.ino | 70 ++++++ .../read_firmware_version.ino | 23 ++ .../keywords.txt | 6 + .../library.properties | 9 + .../multi_channel_relay.cpp | 134 ++++++++++ .../multi_channel_relay.h | 129 ++++++++++ platformio_esp32_envs.ini | 2 +- platformio_esp82xx_base.ini | 2 +- src/_P124_MultiRelay.ino | 231 ++++++++++++++++++ src/src/PluginStructs/P124_data_struct.cpp | 61 +++++ src/src/PluginStructs/P124_data_struct.h | 45 ++++ src/src/WebServer/I2C_Scanner.cpp | 3 + 18 files changed, 987 insertions(+), 2 deletions(-) create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/.gitlab-ci.yml create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/.travis.yml create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/LICENSE create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/README.md create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/examples/change_i2c_address/change_i2c_address.ino create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/examples/eight_channel_relay_control/eight_channel_relay_control.ino create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/examples/four_channel_relay_control/four_channel_relay_control.ino create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/examples/read_firmware_version/read_firmware_version.ino create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/keywords.txt create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/library.properties create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/multi_channel_relay.cpp create mode 100644 lib/Multi_Channel_Relay_Arduino_Library/multi_channel_relay.h create mode 100644 src/_P124_MultiRelay.ino create mode 100644 src/src/PluginStructs/P124_data_struct.cpp create mode 100644 src/src/PluginStructs/P124_data_struct.h diff --git a/lib/Multi_Channel_Relay_Arduino_Library/.gitlab-ci.yml b/lib/Multi_Channel_Relay_Arduino_Library/.gitlab-ci.yml new file mode 100644 index 000000000..5e2b6aa0c --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/.gitlab-ci.yml @@ -0,0 +1,8 @@ +build: + tags: + - nas + script: + - wget -c https://files.seeedstudio.com/arduino/seeed-arduino-ci.sh + - chmod +x seeed-arduino-ci.sh + - bash $PWD/seeed-arduino-ci.sh test + diff --git a/lib/Multi_Channel_Relay_Arduino_Library/.travis.yml b/lib/Multi_Channel_Relay_Arduino_Library/.travis.yml new file mode 100644 index 000000000..202d2e079 --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/.travis.yml @@ -0,0 +1,21 @@ + +language: generic +dist: bionic +sudo: false +cache: + directories: + - ~/arduino_ide + - ~/.arduino15/packages/ + +before_install: + - wget -c https://files.seeedstudio.com/arduino/seeed-arduino-ci.sh + +script: + - chmod +x seeed-arduino-ci.sh + - cat $PWD/seeed-arduino-ci.sh + - bash $PWD/seeed-arduino-ci.sh test + +notifications: + email: + on_success: change + on_failure: change diff --git a/lib/Multi_Channel_Relay_Arduino_Library/LICENSE b/lib/Multi_Channel_Relay_Arduino_Library/LICENSE new file mode 100644 index 000000000..c78cc0bf3 --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Seeed Studio + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/lib/Multi_Channel_Relay_Arduino_Library/README.md b/lib/Multi_Channel_Relay_Arduino_Library/README.md new file mode 100644 index 000000000..f04230ae9 --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/README.md @@ -0,0 +1,84 @@ +# Multi_Channel_Relay_Arduino_Library [![Build Status](https://travis-ci.com/Seeed-Studio/Multi_Channel_Relay_Arduino_Library.svg?branch=master)](https://travis-ci.com/Seeed-Studio/Multi_Channel_Relay_Arduino_Library) +This is the Arduino library for Seeed multi channel relay. + + + + + + + +### How to use this library +you can download from Arduino Library Manager or directlly download from this repository. +Connect the 8-channel Solid state relay v1.0 to Arduino board's I2C port, compile and upload four_channel_relay_control.ino. Open the serial monitor the relay should go as expected some massage should show as below: + +``` +Channel 1 on +Channel 2 on +Channel 3 on +Channel 4 on +Turn all channels on, State: 1111 +Turn 1 3 channels on, State: 101 +Turn 2 4 channels on, State: 1010 +Turn off all channels, State: 0 +``` + +- Upload eight_channel_relay_control.ino, open the serial monitor and show the below logs: + +``` +Channel 1 on +Channel 2 on +Channel 3 on +Channel 4 on +Channel 5 on +Channel 6 on +Channel 7 on +Channel 8 on +Turn all channels on, State: 11111111 +Turn 1 3 5 7 channels on, State: 1010101 +Turn 2 4 6 8 channels on, State: 10101010 +Turn off all channels, State: 0 + +``` + +- This module can be set I2C address by software, open change_i2c_address.ino modify new_i2c_address as you want; compile and upload the sketch, open the serial monitor and show the below logs: + +``` +Scanning... +I2C device found at address 0x11 ! +Found 1 devices +Address 0x21 has been saved to flash. +``` + +- Read firmware version of the module by example read_firmware_version.ino. Serial logs as below: + +``` +firmware version: 0x1 +``` + +### Functionalities +- The relay board is an I2C device, device address is changeable, refer to **changeI2CAddress(uint8_t new_addr, uint8_t old_addr)** . +- Use **getChannelState()** to know the state of every channel. +- Use **getFirmwareVersion()** to recognize firmware burn in the on board MCU. +- Use **channelCtrl(uint8_t state)** to change all channel immediately, the **state** parameter represents channel 1 to 8. +- Use **turn_on_channel(uint8_t channel)** to turn on single channel. +- Use **turn_off_channel(uint8_t channel)** to turn off single channel. +- External functionality **scanI2CDevice()**, use for scan device address. + + + + +---- +This software is written by lambor for seeed studio and is licensed under The MIT License.
+ +Contributing to this software is warmly welcomed. You can do this basically by
+[forking](https://help.github.com/articles/fork-a-repo), committing modifications and then [pulling requests](https://help.github.com/articles/using-pull-requests) (follow the links above
+for operating guide). Adding change log and your contact into file header is encouraged.
+Thanks for your contribution. + +Seeed is a hardware innovation platform for makers to grow inspirations into differentiating products. By working closely with technology providers of all scale, Seeed provides accessible technologies with quality, speed and supply chain knowledge. When prototypes are ready to iterate, Seeed helps productize 1 to 1,000 pcs using in-house engineering, supply chain management and agile manufacture forces. Seeed also team up with incubators, Chinese tech ecosystem, investors and distribution channels to portal Maker startups beyond. + + +[1]:http://wiki.seeedstudio.com/Grove-OLED_Display_0.96inch/ + + + diff --git a/lib/Multi_Channel_Relay_Arduino_Library/examples/change_i2c_address/change_i2c_address.ino b/lib/Multi_Channel_Relay_Arduino_Library/examples/change_i2c_address/change_i2c_address.ino new file mode 100644 index 000000000..8c41134ca --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/examples/change_i2c_address/change_i2c_address.ino @@ -0,0 +1,45 @@ +/** + When running this sketch please make sure there is only one + Multi Channel Relay module connected to your board, otherwise + it may not work to change module address, or cause other + unpredictable issue. + + +*/ + +#include + + +int new_i2c_address = 0x21; + +Multi_Channel_Relay relay; + +void setup() { + uint8_t old_address = 0; + uint8_t retry = 0; + + DEBUG_PRINT.begin(9600); + while (!DEBUG_PRINT); + + // // Set I2C address by default and start relay + relay.begin(); + + /* Scan I2C device detect device address */ + while ((retry++ < 10) && (old_address == 0x00)) { + old_address = relay.scanI2CDevice(); + delay(100); + } + + if ((0x00 == old_address) || (0xff == old_address)) { + while (1); + } + + relay.changeI2CAddress(old_address, new_i2c_address); /* Set I2C address and save to Flash */ + DEBUG_PRINT.print("Address 0x"); + DEBUG_PRINT.print(new_i2c_address, HEX); + DEBUG_PRINT.println(" has been saved to flash."); +} + +void loop() { + +} diff --git a/lib/Multi_Channel_Relay_Arduino_Library/examples/eight_channel_relay_control/eight_channel_relay_control.ino b/lib/Multi_Channel_Relay_Arduino_Library/examples/eight_channel_relay_control/eight_channel_relay_control.ino new file mode 100644 index 000000000..d7f91ab5c --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/examples/eight_channel_relay_control/eight_channel_relay_control.ino @@ -0,0 +1,95 @@ +#include + +/** + channle: 8 7 6 5 4 3 2 1 + state: 0b00000000 -> 0x00 (all off) + state: 0b11111111 -> 0xff (all on) +*/ + +Multi_Channel_Relay relay; + +void setup() { + + DEBUG_PRINT.begin(9600); + while (!DEBUG_PRINT); + + // Set I2C address and start relay + relay.begin(0x11); + + /* Begin Controlling Relay */ + DEBUG_PRINT.println("Channel 1 on"); + relay.turn_on_channel(1); + delay(500); + DEBUG_PRINT.println("Channel 2 on"); + relay.turn_off_channel(1); + relay.turn_on_channel(2); + delay(500); + DEBUG_PRINT.println("Channel 3 on"); + relay.turn_off_channel(2); + relay.turn_on_channel(3); + delay(500); + DEBUG_PRINT.println("Channel 4 on"); + relay.turn_off_channel(3); + relay.turn_on_channel(4); + delay(500); + DEBUG_PRINT.println("Channel 5 on"); + relay.turn_off_channel(4); + relay.turn_on_channel(5); + delay(500); + DEBUG_PRINT.println("Channel 6 on"); + relay.turn_off_channel(5); + relay.turn_on_channel(6); + delay(500); + DEBUG_PRINT.println("Channel 7 on"); + relay.turn_off_channel(6); + relay.turn_on_channel(7); + delay(500); + DEBUG_PRINT.println("Channel 8 on"); + relay.turn_off_channel(7); + relay.turn_on_channel(8); + delay(500); + relay.turn_off_channel(8); + + relay.channelCtrl(CHANNLE1_BIT | + CHANNLE2_BIT | + CHANNLE3_BIT | + CHANNLE4_BIT | + CHANNLE5_BIT | + CHANNLE6_BIT | + CHANNLE7_BIT | + CHANNLE8_BIT); + DEBUG_PRINT.print("Turn all channels on, State: "); + DEBUG_PRINT.println(relay.getChannelState(), BIN); + + delay(2000); + + relay.channelCtrl(CHANNLE1_BIT | + CHANNLE3_BIT | + CHANNLE5_BIT | + CHANNLE7_BIT); + DEBUG_PRINT.print("Turn 1 3 5 7 channels on, State: "); + DEBUG_PRINT.println(relay.getChannelState(), BIN); + + delay(2000); + + relay.channelCtrl(CHANNLE2_BIT | + CHANNLE4_BIT | + CHANNLE6_BIT | + CHANNLE8_BIT); + DEBUG_PRINT.print("Turn 2 4 6 8 channels on, State: "); + DEBUG_PRINT.println(relay.getChannelState(), BIN); + + delay(2000); + + + relay.channelCtrl(0); + DEBUG_PRINT.print("Turn off all channels, State: "); + DEBUG_PRINT.println(relay.getChannelState(), BIN); + + delay(2000); +} + +void loop() { + + +} diff --git a/lib/Multi_Channel_Relay_Arduino_Library/examples/four_channel_relay_control/four_channel_relay_control.ino b/lib/Multi_Channel_Relay_Arduino_Library/examples/four_channel_relay_control/four_channel_relay_control.ino new file mode 100644 index 000000000..7c6b5c052 --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/examples/four_channel_relay_control/four_channel_relay_control.ino @@ -0,0 +1,70 @@ +#include + +/** + channle: 4 3 2 1 + state: 0b0000 -> 0x00 (all off) + state: 0b1111 -> 0x0f (all on) +*/ + +Multi_Channel_Relay relay; + +void setup() { + DEBUG_PRINT.begin(9600); + while (!DEBUG_PRINT); + + // Set I2C address and start relay + relay.begin(0x11); + + /* Begin Controlling Relay */ + DEBUG_PRINT.println("Channel 1 on"); + relay.turn_on_channel(1); + delay(500); + DEBUG_PRINT.println("Channel 2 on"); + relay.turn_off_channel(1); + relay.turn_on_channel(2); + delay(500); + DEBUG_PRINT.println("Channel 3 on"); + relay.turn_off_channel(2); + relay.turn_on_channel(3); + delay(500); + DEBUG_PRINT.println("Channel 4 on"); + relay.turn_off_channel(3); + relay.turn_on_channel(4); + delay(500); + relay.turn_off_channel(4); + + relay.channelCtrl(CHANNLE1_BIT | + CHANNLE2_BIT | + CHANNLE3_BIT | + CHANNLE4_BIT); + DEBUG_PRINT.print("Turn all channels on, State: "); + DEBUG_PRINT.println(relay.getChannelState(), BIN); + + delay(2000); + + relay.channelCtrl(CHANNLE1_BIT | + CHANNLE3_BIT); + DEBUG_PRINT.print("Turn 1 3 channels on, State: "); + DEBUG_PRINT.println(relay.getChannelState(), BIN); + + delay(2000); + + relay.channelCtrl(CHANNLE2_BIT | + CHANNLE4_BIT); + DEBUG_PRINT.print("Turn 2 4 channels on, State: "); + DEBUG_PRINT.println(relay.getChannelState(), BIN); + + delay(2000); + + + relay.channelCtrl(0); + DEBUG_PRINT.print("Turn off all channels, State: "); + DEBUG_PRINT.println(relay.getChannelState(), BIN); + + delay(2000); +} + +void loop() { + + +} diff --git a/lib/Multi_Channel_Relay_Arduino_Library/examples/read_firmware_version/read_firmware_version.ino b/lib/Multi_Channel_Relay_Arduino_Library/examples/read_firmware_version/read_firmware_version.ino new file mode 100644 index 000000000..b1fd29610 --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/examples/read_firmware_version/read_firmware_version.ino @@ -0,0 +1,23 @@ +#include + +#define USE_8_CHANNELS (1) + +Multi_Channel_Relay relay; + +void setup() { + DEBUG_PRINT.begin(9600); + while (!DEBUG_PRINT); + + // Set I2C address and start relay + relay.begin(0x11); + + /* Read firmware version */ + DEBUG_PRINT.print("firmware version: "); + DEBUG_PRINT.print("0x"); + DEBUG_PRINT.print(relay.getFirmwareVersion(), HEX); + DEBUG_PRINT.println(); +} + +void loop() { + +} diff --git a/lib/Multi_Channel_Relay_Arduino_Library/keywords.txt b/lib/Multi_Channel_Relay_Arduino_Library/keywords.txt new file mode 100644 index 000000000..16526100b --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/keywords.txt @@ -0,0 +1,6 @@ +changeI2CAddress KEYWORD2 +getChannelState KEYWORD2 +getFirmwareVersion KEYWORD2 +channelCtrl KEYWORD2 +turn_on_channel KEYWORD2 +turn_off_channel KEYWORD2 diff --git a/lib/Multi_Channel_Relay_Arduino_Library/library.properties b/lib/Multi_Channel_Relay_Arduino_Library/library.properties new file mode 100644 index 000000000..d8ab6cb97 --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/library.properties @@ -0,0 +1,9 @@ +name=Multi Channel Relay Arduino Library +version=1.1.0 +author=Seeed Studio +maintainer=Seeed Studio +sentence=Arduino library to control Multi Channel Rely. +paragraph=Arduino library to control Multi Channel Rely. +category=Device Control +url=https://github.com/Seeed-Studio/Multi_Channel_Relay_Arduino_Library +architectures=* \ No newline at end of file diff --git a/lib/Multi_Channel_Relay_Arduino_Library/multi_channel_relay.cpp b/lib/Multi_Channel_Relay_Arduino_Library/multi_channel_relay.cpp new file mode 100644 index 000000000..ecf5950e4 --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/multi_channel_relay.cpp @@ -0,0 +1,134 @@ +/* + multi_channel_relay.h + Seeed multi channel relay Arduino library + + Copyright (c) 2018 Seeed Technology Co., Ltd. + Author : lambor + Create Time : June 2018 + Change Log : + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include + +Multi_Channel_Relay::Multi_Channel_Relay() { + +} + +void Multi_Channel_Relay::begin(int address) { + // Wire.begin(); Handled by ESPEasy already + channel_state = 0; + _i2cAddr = address; + +} + +uint8_t Multi_Channel_Relay::getFirmwareVersion(void) { + Wire.beginTransmission(_i2cAddr); + Wire.write(CMD_READ_FIRMWARE_VER); + Wire.endTransmission(); + + Wire.requestFrom(_i2cAddr, 1); + //while(!Wire.available()); + return Wire.read(); +} + +void Multi_Channel_Relay::changeI2CAddress(uint8_t old_addr, uint8_t new_addr) { + Wire.beginTransmission(old_addr); + Wire.write(CMD_SAVE_I2C_ADDR); + Wire.write(new_addr); + Wire.endTransmission(); + + _i2cAddr = new_addr; +} + +uint8_t Multi_Channel_Relay::getChannelState(void) { + return channel_state; +} + +void Multi_Channel_Relay::channelCtrl(uint8_t state) { + channel_state = state; + + Wire.beginTransmission(_i2cAddr); + Wire.write(CMD_CHANNEL_CTRL); + Wire.write(channel_state); + Wire.endTransmission(); +} + +void Multi_Channel_Relay::turn_on_channel(uint8_t channel) { + channel_state |= (1 << (channel - 1)); + + Wire.beginTransmission(_i2cAddr); + Wire.write(CMD_CHANNEL_CTRL); + Wire.write(channel_state); + Wire.endTransmission(); +} + +void Multi_Channel_Relay::turn_off_channel(uint8_t channel) { + channel_state &= ~(1 << (channel - 1)); + + Wire.beginTransmission(_i2cAddr); + Wire.write(CMD_CHANNEL_CTRL); + Wire.write(channel_state); + Wire.endTransmission(); +} + +// uint8_t Multi_Channel_Relay::scanI2CDevice(void) { + // byte error = 0, address = 0, result = 0; + // int nDevices; + + // DEBUG_PRINT.println("Scanning..."); + + // nDevices = 0; + // for (address = 1; address <= 127; address++) { + // // The i2c_scanner uses the return value of + // // the Write.endTransmisstion to see if + // // a device did acknowledge to the address. + // Wire.beginTransmission(address); + // error = Wire.endTransmission(); + + // if (error == 0) { + // result = address; + // DEBUG_PRINT.print("I2C device found at address 0x"); + // if (address < 16) { + // DEBUG_PRINT.print("0"); + // } + // DEBUG_PRINT.print(address, HEX); + // DEBUG_PRINT.println(" !"); + + // nDevices++; + // } else if (error == 4) { + // DEBUG_PRINT.print("Unknown error at address 0x"); + // if (address < 16) { + // DEBUG_PRINT.print("0"); + // } + // DEBUG_PRINT.println(address, HEX); + // } + // } + // if (nDevices == 0) { + // DEBUG_PRINT.println("No I2C devices found\n"); + // result = 0x00; + // } else { + // DEBUG_PRINT.print("Found "); + // DEBUG_PRINT.print(nDevices); + // DEBUG_PRINT.print(" devices\n"); + // if (nDevices != 1) { + // result = 0x00; + // } + // } + + // return result; +// return 0; +// } diff --git a/lib/Multi_Channel_Relay_Arduino_Library/multi_channel_relay.h b/lib/Multi_Channel_Relay_Arduino_Library/multi_channel_relay.h new file mode 100644 index 000000000..df27d602b --- /dev/null +++ b/lib/Multi_Channel_Relay_Arduino_Library/multi_channel_relay.h @@ -0,0 +1,129 @@ +/* + multi_channel_relay.cpp + Seeed multi channel relay Arduino library + + Copyright (c) 2018 Seeed Technology Co., Ltd. + Author : lambor + Create Time : June 2018 + Change Log : + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#ifndef MULTI_CHANNEL_RELAY_H +#define MULTI_CHANNEL_RELAY_H + +// Architecture specific include +// #if defined(ARDUINO_ARCH_AVR) +// #define DEBUG_PRINT Serial +// #elif defined(ARDUINO_ARCH_SAM) +// #define DEBUG_PRINT SerialUSB +// #elif defined(ARDUINO_ARCH_SAMD) +// #define DEBUG_PRINT SerialUSB +// #elif defined(ARDUINO_ARCH_STM32F4) +// #define DEBUG_PRINT SerialUSB +// #else +// #pragma message("Not match any architecture.") +// #define DEBUG_PRINT Serial +// #endif + +#include +#include + + +#define CHANNLE1_BIT 0x01 +#define CHANNLE2_BIT 0x02 +#define CHANNLE3_BIT 0x04 +#define CHANNLE4_BIT 0x08 +#define CHANNLE5_BIT 0x10 +#define CHANNLE6_BIT 0x20 +#define CHANNLE7_BIT 0x40 +#define CHANNLE8_BIT 0x80 + +#define CMD_CHANNEL_CTRL 0x10 +#define CMD_SAVE_I2C_ADDR 0x11 +#define CMD_READ_I2C_ADDR 0x12 +#define CMD_READ_FIRMWARE_VER 0x13 + +class Multi_Channel_Relay { + public: + Multi_Channel_Relay(); + + /** + Begin Multi Channel Relay by a I2C address. + @param address can be changed by changeI2CAddress. Please refer to example change_i2c_address + */ + void begin(int address = 0x11); + + /** + @brief Change device address from old_addr to new_addr. + @param new_addr, the address to use. + old_addr, the original address + @return None + */ + void changeI2CAddress(uint8_t new_addr, uint8_t old_addr); + + /** + /brief Get channel state + /return One byte value to indicate channel state + the bits range from 0 to 7 represents channel 1 to 8 + */ + uint8_t getChannelState(void); + + /** + @brief Read firmware version from on board MCU + @param + @return Firmware version in byte + */ + uint8_t getFirmwareVersion(void); + + /** + @brief Control relay channels + @param state, use one Byte to represent 8 channel + @return None + */ + void channelCtrl(uint8_t state); + + /** + @brief Turn on one of 8 channels + @param channel, channel to control with (range form 1 to 8) + @return None + */ + void turn_on_channel(uint8_t channel); + + /** + @brief Turn off on of 8 channels + @param channel, channel to control with (range form 1 to 8) + @return None + */ + void turn_off_channel(uint8_t channel); + + // /** + // @brief Scan I2C device, return the address if there is only one i2c device + // @param + // @return device address + // */ + // uint8_t scanI2CDevice(void); + + private: + int _i2cAddr; // This is the I2C address you want to use + int channel_state; // Value to save channel state +}; + + + +#endif \ No newline at end of file diff --git a/platformio_esp32_envs.ini b/platformio_esp32_envs.ini index 727f5d22b..5be3ef720 100644 --- a/platformio_esp32_envs.ini +++ b/platformio_esp32_envs.ini @@ -10,7 +10,7 @@ lib_ignore = ESP8266WiFi, ESP8266Ping, ESP8266WebServer, ESP8266H [esp32_common] extends = common, core_esp32_3_3_0 -lib_deps = td-er/ESPeasySerial @ 2.0.7, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, Adafruit BusIO, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ArduinoOTA, ESP32HTTPUpdateServer +lib_deps = td-er/ESPeasySerial @ 2.0.7, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, Adafruit BusIO, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ArduinoOTA, ESP32HTTPUpdateServer, Multi Channel Relay Arduino Library lib_ignore = ${esp32_always.lib_ignore}, ESP32_ping, IRremoteESP8266, HeatpumpIR board_build.f_flash = 80000000L board_build.flash_mode = dout diff --git a/platformio_esp82xx_base.ini b/platformio_esp82xx_base.ini index 71d65746c..ef13b0397 100644 --- a/platformio_esp82xx_base.ini +++ b/platformio_esp82xx_base.ini @@ -52,7 +52,7 @@ extends = common board_build.f_cpu = 80000000L build_flags = ${debug_flags.build_flags} ${mqtt_flags.build_flags} -DHTTPCLIENT_1_1_COMPATIBLE=0 build_unflags = -DDEBUG_ESP_PORT -lib_deps = td-er/ESPeasySerial @ 2.0.7, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, Adafruit BusIO, bblanchon/ArduinoJson @ ^6.17.2, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/RABurton ESP8266 Mutex @ ^1.0.2, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ESP8266HTTPUpdateServer +lib_deps = td-er/ESPeasySerial @ 2.0.7, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, Adafruit BusIO, bblanchon/ArduinoJson @ ^6.17.2, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/RABurton ESP8266 Mutex @ ^1.0.2, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ESP8266HTTPUpdateServer, Multi Channel Relay Arduino Library lib_ignore = ${esp82xx_defaults.lib_ignore}, IRremoteESP8266, HeatpumpIR, LittleFS(esp8266), ServoESP32, TinyWireM board = esp12e monitor_filters = esp8266_exception_decoder diff --git a/src/_P124_MultiRelay.ino b/src/_P124_MultiRelay.ino new file mode 100644 index 000000000..30b9faa28 --- /dev/null +++ b/src/_P124_MultiRelay.ino @@ -0,0 +1,231 @@ +#include "_Plugin_Helper.h" + +#ifdef USES_P124 + +// ####################################################################################################### +// ########################### Plugin 124 I2C Multi Relay module ############################### +// ####################################################################################################### + +/** Changelog: + * 2021-11-18 tonhuisman: Implement settings, + * Implement write commands: + * multirelay,on, (channel 1..8, max. accepted as configured) + * multirelay,off, + * multirelay,set, (value in decimal, 0xnn hex or 0bnnnnnnnn binary) + * Add binary state to Devices screen, when plugin enabled. + * 2021-11-17 tonhuisman: Initial plugin development. + */ + +# define PLUGIN_124 +# define PLUGIN_ID_124 124 +# define PLUGIN_NAME_124 "Output - I2C Multi Relay [TESTING]" +# define PLUGIN_VALUENAME1_124 "State" + +# include "./src/PluginStructs/P124_data_struct.h" + + +boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) +{ + boolean success = false; + + switch (function) + { + case PLUGIN_DEVICE_ADD: + { + Device[++deviceCount].Number = PLUGIN_ID_124; + Device[deviceCount].Type = DEVICE_TYPE_I2C; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_SINGLE; + Device[deviceCount].Ports = 0; + Device[deviceCount].PullUpOption = false; + Device[deviceCount].InverseLogicOption = false; + Device[deviceCount].FormulaOption = true; + Device[deviceCount].ValueCount = 1; + Device[deviceCount].SendDataOption = true; + Device[deviceCount].TimerOption = true; + Device[deviceCount].TimerOptional = true; + + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_124); + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_124)); + break; + } + + case PLUGIN_I2C_HAS_ADDRESS: + { + success = (event->Par1 == 0x11); + break; + } + + case PLUGIN_SET_DEFAULTS: + { + ExtraTaskSettings.TaskDeviceValueDecimals[0] = 0; // No decimals needed + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + const __FlashStringHelper *optionsMode2[] = { + F("2 relays"), + F("4 relays"), + F("8 relays") }; + int optionValuesMode2[] { 2, 4, 8 }; + addFormSelector(F("Number of relays"), F("plugin_124_relays"), 3, optionsMode2, optionValuesMode2, P124_CONFIG_RELAY_COUNT, true); + + const __FlashStringHelper *yesNoOptions[] = { + F("No"), + F("Yes") + }; + int yesNoValues[] = { 0, 1 }; + addFormSelector(F("Initialize relays on startup"), + getPluginCustomArgName(P124_FLAGS_INIT_RELAYS), 2, yesNoOptions, yesNoValues, + bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_RELAYS) ? 1 : 0, true); + String label; + + if (bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_RELAYS)) { + for (int i = 0; i < P124_CONFIG_RELAY_COUNT; i++) { + label = F("Relay "); + label += i + 1; + label += F(" initial state (on/off)"); + addFormCheckBox(label, getPluginCustomArgName(i), bitRead(P124_CONFIG_FLAGS, i)); + } + } + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + P124_CONFIG_RELAY_COUNT = getFormItemInt(F("plugin_124_relays")); + uint32_t lSettings = 0u; + bitWrite(lSettings, P124_FLAGS_INIT_RELAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_INIT_RELAYS)) == 1); + + if (lSettings != 0) { + for (int i = 0; i < P124_CONFIG_RELAY_COUNT; i++) { + bitWrite(lSettings, i, isFormItemChecked(getPluginCustomArgName(i))); + } + } + P124_CONFIG_FLAGS = lSettings; + + success = true; + break; + } + + case PLUGIN_INIT: + { + initPluginTaskData(event->TaskIndex, new (std::nothrow) P124_data_struct(PCONFIG(0))); + P124_data_struct *P124_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P124_data) { + return success; + } + + if (P124_data->isInitialized()) { + String log; + log.reserve(46); + log = F("MultiRelay: Initialized, firmware version: "); + log += P124_data->getFirmwareVersion(); + addLog(LOG_LEVEL_INFO, log); + + if (bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_RELAYS)) { + P124_data->channelCtrl(P124_CONFIG_FLAGS & 0xFF); // Set relays state + UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state + } + success = true; + } else { + addLog(LOG_LEVEL_ERROR, F("MultiRelay: Initialization error!")); + } + + break; + } + + case PLUGIN_READ: + { + P124_data_struct *P124_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P124_data) { + return success; + } + + if (P124_data->isInitialized()) { + UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state + success = true; + } + break; + } + + case PLUGIN_WEBFORM_SHOW_VALUES: + { + P124_data_struct *P124_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if ((nullptr != P124_data) && P124_data->isInitialized()) { + uint8_t varNr = 1; // VARS_PER_TASK; + String label = F("Relay state "); + label += P124_CONFIG_RELAY_COUNT; + label += F(".."); + label += 1; + String state = F("0b "); + uint32_t val = UserVar[event->BaseVarIndex]; + val &= 0xff; + val |= (0x1 << P124_CONFIG_RELAY_COUNT); + state += ull2String(val, 2); + state.remove(3, 1); // Delete leading 1 we added + pluginWebformShowValue(event->TaskIndex, varNr++, label, state, true); + + // success = true; + } + break; + } + + case PLUGIN_WRITE: + { + P124_data_struct *P124_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P124_data) { + return success; + } + + # ifdef P124_DEBUG_LOG + addLog(LOG_LEVEL_INFO, string); + String log = F("Par1..3:"); + log += event->Par1; + log += ','; + log += event->Par2; + log += ','; + log += event->Par3; + addLog(LOG_LEVEL_INFO, log); + # endif // ifdef P124_DEBUG_LOG + + String command = parseString(string, 1); + + if (P124_data->isInitialized() && + command.equals(F("multirelay"))) { + String subcommand = parseString(string, 2); + + if (subcommand.equals(F("on"))) { + success = P124_data->turn_on_channel(event->Par2); + } else if (subcommand.equals(F("off"))) { + success = P124_data->turn_off_channel(event->Par2); + } else if (subcommand.equals(F("set"))) { + success = P124_data->channelCtrl(event->Par2); + } + + if (success) { + UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state + } + } + break; + } + } + return success; +} + +#endif // ifdef USES_P124 diff --git a/src/src/PluginStructs/P124_data_struct.cpp b/src/src/PluginStructs/P124_data_struct.cpp new file mode 100644 index 000000000..6ea0cace0 --- /dev/null +++ b/src/src/PluginStructs/P124_data_struct.cpp @@ -0,0 +1,61 @@ +#include "../PluginStructs/P124_data_struct.h" + +#ifdef USES_P124 + +// **************************************************************************/ +// Constructor +// **************************************************************************/ +P124_data_struct::P124_data_struct(uint8_t relayCount) + : _relayCount(relayCount) { + relay = new (std::nothrow) Multi_Channel_Relay(); // Use default address +} + +// **************************************************************************/ +// Destructor +// **************************************************************************/ +P124_data_struct::~P124_data_struct() { + if (isInitialized()) { + delete relay; + relay = nullptr; + } +} + +uint8_t P124_data_struct::getChannelState() { + if (isInitialized()) { + return relay->getChannelState(); + } + return 0u; +} + +uint8_t P124_data_struct::getFirmwareVersion() { + if (isInitialized()) { + return relay->getFirmwareVersion(); + } + return 0u; +} + +bool P124_data_struct::channelCtrl(uint8_t state) { + if (isInitialized()) { + relay->channelCtrl(state); + return true; + } + return false; +} + +bool P124_data_struct::turn_on_channel(uint8_t channel) { + if (isInitialized() && (validChannel(channel))) { + relay->turn_on_channel(channel); + return true; + } + return false; +} + +bool P124_data_struct::turn_off_channel(uint8_t channel) { + if (isInitialized() && (validChannel(channel))) { + relay->turn_off_channel(channel); + return true; + } + return false; +} + +#endif // ifdef USES_P124 diff --git a/src/src/PluginStructs/P124_data_struct.h b/src/src/PluginStructs/P124_data_struct.h new file mode 100644 index 000000000..61a41a3b1 --- /dev/null +++ b/src/src/PluginStructs/P124_data_struct.h @@ -0,0 +1,45 @@ +#ifndef PLUGINSTRUCTS_P124_DATA_STRUCT_H +#define PLUGINSTRUCTS_P124_DATA_STRUCT_H + +#include "../../_Plugin_Helper.h" +#ifdef USES_P124 + +# include + +// # define P124_DEBUG_LOG // Enable for some (extra) logging + +# define P124_CONFIG_RELAY_COUNT PCONFIG(0) +# define P124_CONFIG_FLAGS PCONFIG_LONG(0) + +# define P124_FLAGS_INIT_RELAYS 8 // 0..7 hold the on/off state of each relay + +struct P124_data_struct : public PluginTaskData_base { +public: + + P124_data_struct(uint8_t relayCount); + + P124_data_struct() = delete; + ~P124_data_struct(); + + bool isInitialized() { + return relay != nullptr; + } + + uint8_t getChannelState(); + uint8_t getFirmwareVersion(); + bool channelCtrl(uint8_t state); + bool turn_on_channel(uint8_t channel); + bool turn_off_channel(uint8_t channel); + +private: + + bool validChannel(uint channel) { + return channel > 0 && channel <= _relayCount; + } + + Multi_Channel_Relay *relay = nullptr; + + uint8_t _relayCount; +}; +#endif // ifdef USES_P124 +#endif // ifndef PLUGINSTRUCTS_P124_DATA_STRUCT_H diff --git a/src/src/WebServer/I2C_Scanner.cpp b/src/src/WebServer/I2C_Scanner.cpp index a687f8746..6e89b4f9f 100644 --- a/src/src/WebServer/I2C_Scanner.cpp +++ b/src/src/WebServer/I2C_Scanner.cpp @@ -176,6 +176,9 @@ String getKnownI2Cdevice(uint8_t address) { switch (address) { + case 0x11: + result += F("I2C_MultiRelay"); + break; case 0x20: case 0x21: case 0x22: From 11ea3d55842d5aaa9d32168ac1b794e8c48a72a0 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 19 Nov 2021 20:38:46 +0100 Subject: [PATCH 07/17] [P124] Add optional initialization 'protection' --- src/_P124_MultiRelay.ino | 13 ++++++++++++- src/src/PluginStructs/P124_data_struct.h | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/_P124_MultiRelay.ino b/src/_P124_MultiRelay.ino index 30b9faa28..ccb35f0d7 100644 --- a/src/_P124_MultiRelay.ino +++ b/src/_P124_MultiRelay.ino @@ -7,6 +7,7 @@ // ####################################################################################################### /** Changelog: + * 2021-11-19 tonhuisman: Add protection for single initialization of relays (per reset/boot/powercycle) * 2021-11-18 tonhuisman: Implement settings, * Implement write commands: * multirelay,on, (channel 1..8, max. accepted as configured) @@ -23,6 +24,7 @@ # include "./src/PluginStructs/P124_data_struct.h" +static uint32_t P124_InitializedRelays = 0u; boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) { @@ -91,6 +93,11 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) String label; if (bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_RELAYS)) { + addFormCheckBox(F("Apply initial state always"), + getPluginCustomArgName(P124_FLAGS_INIT_ALWAYS), + bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_ALWAYS)); + addFormNote(F("Disabled: Applied once per restart, Enabled: Applied on every plugin start, like on Submit of this page")); + for (int i = 0; i < P124_CONFIG_RELAY_COUNT; i++) { label = F("Relay "); label += i + 1; @@ -107,6 +114,7 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) P124_CONFIG_RELAY_COUNT = getFormItemInt(F("plugin_124_relays")); uint32_t lSettings = 0u; bitWrite(lSettings, P124_FLAGS_INIT_RELAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_INIT_RELAYS)) == 1); + bitWrite(lSettings, P124_FLAGS_INIT_ALWAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_INIT_ALWAYS)) == 1); if (lSettings != 0) { for (int i = 0; i < P124_CONFIG_RELAY_COUNT; i++) { @@ -135,9 +143,12 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) log += P124_data->getFirmwareVersion(); addLog(LOG_LEVEL_INFO, log); - if (bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_RELAYS)) { + if (bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_RELAYS) && + (!bitRead(P124_InitializedRelays, event->TaskIndex) || + bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_ALWAYS))) { P124_data->channelCtrl(P124_CONFIG_FLAGS & 0xFF); // Set relays state UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state + bitSet(P124_InitializedRelays, event->TaskIndex); // Update initialization status } success = true; } else { diff --git a/src/src/PluginStructs/P124_data_struct.h b/src/src/PluginStructs/P124_data_struct.h index 61a41a3b1..ce9badc9c 100644 --- a/src/src/PluginStructs/P124_data_struct.h +++ b/src/src/PluginStructs/P124_data_struct.h @@ -12,6 +12,7 @@ # define P124_CONFIG_FLAGS PCONFIG_LONG(0) # define P124_FLAGS_INIT_RELAYS 8 // 0..7 hold the on/off state of each relay +# define P124_FLAGS_INIT_ALWAYS 9 // Apply relay initialization on every plugin restart struct P124_data_struct : public PluginTaskData_base { public: From 1c3adb9e0e717d0a24f115e3e8fabeff78013d04 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 21 Nov 2021 21:51:20 +0100 Subject: [PATCH 08/17] [P124] Add configurable I2C addresses, add relay state on exit [P114] Add VEML6075 to I2C scanner --- src/_P124_MultiRelay.ino | 88 ++++++++++++++++++---- src/src/PluginStructs/P124_data_struct.cpp | 25 +++++- src/src/PluginStructs/P124_data_struct.h | 13 +++- src/src/WebServer/I2C_Scanner.cpp | 12 +++ 4 files changed, 119 insertions(+), 19 deletions(-) diff --git a/src/_P124_MultiRelay.ino b/src/_P124_MultiRelay.ino index ccb35f0d7..2e5f29374 100644 --- a/src/_P124_MultiRelay.ino +++ b/src/_P124_MultiRelay.ino @@ -7,6 +7,10 @@ // ####################################################################################################### /** Changelog: + * 2021-11-21 tonhuisman: Implement configurable I2C addresses, limited to 0x11..-x18 range (8 units) though + * the boards support any I2C address from 0x00 to 0x7F + * Add Relay state on exit/disabling of the plugin. + * WARNING: Is also triggered when the task is enabled and the settings are saved! * 2021-11-19 tonhuisman: Add protection for single initialization of relays (per reset/boot/powercycle) * 2021-11-18 tonhuisman: Implement settings, * Implement write commands: @@ -61,15 +65,28 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) break; } - case PLUGIN_I2C_HAS_ADDRESS: + case PLUGIN_SET_DEFAULTS: { - success = (event->Par1 == 0x11); + P124_CONFIG_I2C_ADDRESS = 0x11; // Default I2C address + ExtraTaskSettings.TaskDeviceValueDecimals[0] = 0; // No decimals needed break; } - case PLUGIN_SET_DEFAULTS: + case PLUGIN_I2C_HAS_ADDRESS: + case PLUGIN_WEBFORM_SHOW_I2C_PARAMS: { - ExtraTaskSettings.TaskDeviceValueDecimals[0] = 0; // No decimals needed + // This range of I2C addresses is also maintained in the constructor of P124_data_struct, and must stay consecutive! + const uint8_t i2cAddressValues[] = { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; + + if (function == PLUGIN_WEBFORM_SHOW_I2C_PARAMS) { + addFormSelectorI2C(F("plugin_124_i2caddress"), 8, i2cAddressValues, P124_CONFIG_I2C_ADDRESS); + + addFormCheckBox(F("Change I2C address of board"), F("plugin_124_change_i2c"), false); + addFormNote( + F("Change of address will be stored in the board and retained until changed again. See documentation for change-procedure.")); + } else { + success = intArrayContains(8, i2cAddressValues, event->Par1); + } break; } @@ -82,13 +99,13 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) int optionValuesMode2[] { 2, 4, 8 }; addFormSelector(F("Number of relays"), F("plugin_124_relays"), 3, optionsMode2, optionValuesMode2, P124_CONFIG_RELAY_COUNT, true); - const __FlashStringHelper *yesNoOptions[] = { + const __FlashStringHelper *noYesOptions[] = { F("No"), F("Yes") }; - int yesNoValues[] = { 0, 1 }; + int noYesValues[] = { 0, 1 }; addFormSelector(F("Initialize relays on startup"), - getPluginCustomArgName(P124_FLAGS_INIT_RELAYS), 2, yesNoOptions, yesNoValues, + getPluginCustomArgName(P124_FLAGS_INIT_RELAYS), 2, noYesOptions, noYesValues, bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_RELAYS) ? 1 : 0, true); String label; @@ -105,6 +122,20 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) addFormCheckBox(label, getPluginCustomArgName(i), bitRead(P124_CONFIG_FLAGS, i)); } } + + addFormSelector(F("Reset relays on exit"), + getPluginCustomArgName(P124_FLAGS_EXIT_RELAYS), 2, noYesOptions, noYesValues, + bitRead(P124_CONFIG_FLAGS, P124_FLAGS_EXIT_RELAYS) ? 1 : 0, true); + + if (bitRead(P124_CONFIG_FLAGS, P124_FLAGS_EXIT_RELAYS)) { + for (int i = 0; i < P124_CONFIG_RELAY_COUNT; i++) { + label = F("Relay "); + label += i + 1; + label += F(" exit-state (on/off)"); + addFormCheckBox(label, getPluginCustomArgName(i + P124_FLAGS_EXIT_OFFSET), bitRead(P124_CONFIG_FLAGS, i + P124_FLAGS_EXIT_OFFSET)); + } + addFormNote(F("ATTENTION: These Relay states will be set when the task is enabled and the settings are saved!")); + } success = true; break; } @@ -112,24 +143,40 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { P124_CONFIG_RELAY_COUNT = getFormItemInt(F("plugin_124_relays")); + P124_CONFIG_I2C_ADDRESS = getFormItemInt(F("plugin_124_i2caddress")); uint32_t lSettings = 0u; bitWrite(lSettings, P124_FLAGS_INIT_RELAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_INIT_RELAYS)) == 1); bitWrite(lSettings, P124_FLAGS_INIT_ALWAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_INIT_ALWAYS)) == 1); + bitWrite(lSettings, P124_FLAGS_EXIT_RELAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_EXIT_RELAYS)) == 1); if (lSettings != 0) { - for (int i = 0; i < P124_CONFIG_RELAY_COUNT; i++) { - bitWrite(lSettings, i, isFormItemChecked(getPluginCustomArgName(i))); + for (int i = 0; i < P124_CONFIG_RELAY_COUNT; i++) { // INIT and EXIT states + bitWrite(lSettings, i, isFormItemChecked(getPluginCustomArgName(i))); + bitWrite(lSettings, i + P124_FLAGS_EXIT_OFFSET, isFormItemChecked(getPluginCustomArgName(i + P124_FLAGS_EXIT_OFFSET))); } } P124_CONFIG_FLAGS = lSettings; + if (isFormItemChecked(F("plugin_124_change_i2c"))) { + P124_data_struct *P124_data = new (std::nothrow) P124_data_struct(P124_CONFIG_I2C_ADDRESS, P124_CONFIG_RELAY_COUNT, true); + + if (nullptr != P124_data) { + P124_CONFIG_I2C_ADDRESS++; // Increment, like the Change Address argument does. + + if (P124_CONFIG_I2C_ADDRESS > 0x18) { P124_CONFIG_I2C_ADDRESS = 0x11; } + + delete P124_data; + P124_data = nullptr; + } + } + success = true; break; } case PLUGIN_INIT: { - initPluginTaskData(event->TaskIndex, new (std::nothrow) P124_data_struct(PCONFIG(0))); + initPluginTaskData(event->TaskIndex, new (std::nothrow) P124_data_struct(P124_CONFIG_I2C_ADDRESS, P124_CONFIG_RELAY_COUNT)); P124_data_struct *P124_data = static_cast(getPluginTaskData(event->TaskIndex)); if (nullptr == P124_data) { @@ -146,9 +193,9 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) if (bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_RELAYS) && (!bitRead(P124_InitializedRelays, event->TaskIndex) || bitRead(P124_CONFIG_FLAGS, P124_FLAGS_INIT_ALWAYS))) { - P124_data->channelCtrl(P124_CONFIG_FLAGS & 0xFF); // Set relays state - UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state - bitSet(P124_InitializedRelays, event->TaskIndex); // Update initialization status + P124_data->channelCtrl(get8BitFromUL(P124_CONFIG_FLAGS, P124_FLAGS_INIT_OFFSET)); // Set relays state + UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state + bitSet(P124_InitializedRelays, event->TaskIndex); // Update initialization status } success = true; } else { @@ -158,6 +205,21 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) break; } + case PLUGIN_EXIT: + { + P124_data_struct *P124_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr != P124_data) { + if (P124_data->isInitialized()) { + P124_data->channelCtrl(get8BitFromUL(P124_CONFIG_FLAGS, P124_FLAGS_EXIT_OFFSET)); // Set relays state + UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state + } + addLog(LOG_LEVEL_INFO, F("MultiRelay: Object still alive.")); + } + success = true; + break; + } + case PLUGIN_READ: { P124_data_struct *P124_data = static_cast(getPluginTaskData(event->TaskIndex)); diff --git a/src/src/PluginStructs/P124_data_struct.cpp b/src/src/PluginStructs/P124_data_struct.cpp index 6ea0cace0..aa3954248 100644 --- a/src/src/PluginStructs/P124_data_struct.cpp +++ b/src/src/PluginStructs/P124_data_struct.cpp @@ -5,9 +5,28 @@ // **************************************************************************/ // Constructor // **************************************************************************/ -P124_data_struct::P124_data_struct(uint8_t relayCount) - : _relayCount(relayCount) { - relay = new (std::nothrow) Multi_Channel_Relay(); // Use default address +P124_data_struct::P124_data_struct(int8_t i2c_address, + uint8_t relayCount, + bool changeAddress) + : _i2c_address(i2c_address), _relayCount(relayCount) { + relay = new (std::nothrow) Multi_Channel_Relay(); + + if (isInitialized()) { + relay->begin(_i2c_address); + + if (changeAddress) { + // This increment shpould match with the range of addresses in _P124_MultiRelay.ino PLUGIN_I2C_HAS_ADDRESS + uint8_t _new_address = _i2c_address == 0x18 ? 0x11 : _i2c_address + 1; // Set to next address + relay->changeI2CAddress(_new_address, _i2c_address); + # ifndef BUILD_NO_DEBUG + String log = F("MultiRelay: Change I2C address 0x"); + log += String(_i2c_address, HEX); + log += F(" to 0x"); + log += String(_new_address, HEX); + addLog(LOG_LEVEL_INFO, log); + # endif // ifndef BUILD_NO_DEBUG + } + } } // **************************************************************************/ diff --git a/src/src/PluginStructs/P124_data_struct.h b/src/src/PluginStructs/P124_data_struct.h index ce9badc9c..0f8641e59 100644 --- a/src/src/PluginStructs/P124_data_struct.h +++ b/src/src/PluginStructs/P124_data_struct.h @@ -9,15 +9,21 @@ // # define P124_DEBUG_LOG // Enable for some (extra) logging # define P124_CONFIG_RELAY_COUNT PCONFIG(0) +# define P124_CONFIG_I2C_ADDRESS PCONFIG(7) // As P0 is already in use, pick the last of the range # define P124_CONFIG_FLAGS PCONFIG_LONG(0) -# define P124_FLAGS_INIT_RELAYS 8 // 0..7 hold the on/off state of each relay -# define P124_FLAGS_INIT_ALWAYS 9 // Apply relay initialization on every plugin restart +# define P124_FLAGS_INIT_OFFSET 0 // 0..7 hold the on/off state of each relay +# define P124_FLAGS_INIT_RELAYS 8 // Initialize relays at startup +# define P124_FLAGS_INIT_ALWAYS 9 // Apply relay initialization on every plugin restart +# define P124_FLAGS_EXIT_RELAYS 10 // Initialize relays when the plugin is disabled +# define P124_FLAGS_EXIT_OFFSET 16 // 16..23 hold the on/off exit-state of each relay struct P124_data_struct : public PluginTaskData_base { public: - P124_data_struct(uint8_t relayCount); + P124_data_struct(int8_t i2c_address, + uint8_t relayCount, + bool changeAddress = false); P124_data_struct() = delete; ~P124_data_struct(); @@ -40,6 +46,7 @@ private: Multi_Channel_Relay *relay = nullptr; + int8_t _i2c_address; uint8_t _relayCount; }; #endif // ifdef USES_P124 diff --git a/src/src/WebServer/I2C_Scanner.cpp b/src/src/WebServer/I2C_Scanner.cpp index 6e89b4f9f..8e74945b7 100644 --- a/src/src/WebServer/I2C_Scanner.cpp +++ b/src/src/WebServer/I2C_Scanner.cpp @@ -176,7 +176,19 @@ String getKnownI2Cdevice(uint8_t address) { switch (address) { + case 0x10: + result += F("VEML6075"); + break; case 0x11: + result += F("VEML6075,I2C_MultiRelay"); + break; + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + case 0x18: result += F("I2C_MultiRelay"); break; case 0x20: From e2f445c0bfc409788e053ba177461fff03e7e1a4 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 21 Nov 2021 21:53:30 +0100 Subject: [PATCH 09/17] [P124] Add documentation --- docs/source/Plugin/P124.rst | 144 ++++++++++++++++++ .../Plugin/P124_DeviceConfiguration.png | Bin 0 -> 48484 bytes docs/source/Plugin/P124_I2CAddresses.png | Bin 0 -> 47435 bytes .../Plugin/P124_InitializeRelaysYes.png | Bin 0 -> 10831 bytes .../Plugin/P124_NumberOfRelaysOptions.png | Bin 0 -> 14694 bytes docs/source/Plugin/P124_ResetRelaysYes.png | Bin 0 -> 8763 bytes docs/source/Plugin/P124_commands.repl | 31 ++++ docs/source/Plugin/_Plugin.rst | 1 + docs/source/Plugin/_plugin_categories.repl | 2 +- docs/source/Plugin/_plugin_substitutions.repl | 1 + .../Plugin/_plugin_substitutions_p12x.repl | 12 ++ 11 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 docs/source/Plugin/P124.rst create mode 100644 docs/source/Plugin/P124_DeviceConfiguration.png create mode 100644 docs/source/Plugin/P124_I2CAddresses.png create mode 100644 docs/source/Plugin/P124_InitializeRelaysYes.png create mode 100644 docs/source/Plugin/P124_NumberOfRelaysOptions.png create mode 100644 docs/source/Plugin/P124_ResetRelaysYes.png create mode 100644 docs/source/Plugin/P124_commands.repl create mode 100644 docs/source/Plugin/_plugin_substitutions_p12x.repl diff --git a/docs/source/Plugin/P124.rst b/docs/source/Plugin/P124.rst new file mode 100644 index 000000000..c52a56f6f --- /dev/null +++ b/docs/source/Plugin/P124.rst @@ -0,0 +1,144 @@ +.. include:: ../Plugin/_plugin_substitutions_p12x.repl +.. _P124_page: + +|P124_typename| +================================================== + +|P124_shortinfo| + +Plugin details +-------------- + +Type: |P124_type| + +Name: |P124_name| + +Status: |P124_status| + +GitHub: |P124_github|_ + +Maintainer: |P124_maintainer| + +Used libraries: |P124_usedlibraries| + +Description +----------- + +This plugin is the interface to the Seeed Studio I2C multi relay boards (either solid state or mechanical). + +Each channel can be switched separately, or multiple relays at once, by sending the appropriate commands (see below). + +Supported hardware +------------------ + +* `Seeed Studio Grove SPDT 4 Channel Relay`_ +* `Seeed Studio Grove 2 Channel Solid State Relay`_ +* `Seeed Studio Grove 4 Channel Solid State Relay`_ +* `Seeed Studio Grove 8 Channel Solid State Relay`_ + +.. _Seeed Studio Grove SPDT 4 Channel Relay: https://wiki.seeedstudio.com/Grove-4-Channel_SPDT_Relay/ +.. _Seeed Studio Grove 2 Channel Solid State Relay: https://wiki.seeedstudio.com/Grove-2-Channel_Solid_State_Relay/ +.. _Seeed Studio Grove 4 Channel Solid State Relay: https://wiki.seeedstudio.com/Grove-4-Channel_Solid_State_Relay/ +.. _Seeed Studio Grove 8 Channel Solid State Relay: https://wiki.seeedstudio.com/Grove-8-Channel_Solid_State_Relay/ + +Configuration +------------- + +.. image:: P124_DeviceConfiguration.png + :alt: Device Configuration + +* **Name**: A unique name should be entered here. + +* **Enabled**: The device can be disabled or enabled. When not enabled the device should not use any resources. + +I2C Options +^^^^^^^^^^^^ + +The available settings here depend on the build used. At least the **Force Slow I2C speed** option is available, but selections for the I2C Multiplexer can also be shown. For details see the :ref:`Hardware_page` + +* **I2C Address**: The address the device is using. + +.. image:: P124_I2CAddresses.png + :alt: List of supported I2C addresses + +* **Change I2C address of board**: These boards allow to change their I2C address. Once changed the address will be retained in the board non-volatile memory, until changed again. Currently addresses ``0x11`` to ``0x18`` are supported. + +The procedure to change the address is: + +* Connect only 1 of these boards to the I2C bus (other I2C devices *can* stay connected) +* Select the current board address in the **I2C Address** field. Use the Tools / I2C Scanner feature to retrieve the current board address, if needed. + +.. .. comment + +* Enable the **Change I2C address of board** option. +* Submit the page (saving the settings). This will increment the board' I2C address to the next one in the list. +* Optional: Use the Tools / I2C Scanner feature to verify that the board now has the next address. +* The **I2C Address** of the task will be updated to the new address. +* Repeat above steps until the desired address is set. After reaching the highest address, the default address ``0x11`` will be set. +* Save the settings using the Submit button. + +After changing the address, a next board can be connected that uses another board address, most likely ``0x11``. Each board will require an additional task to be configured. + +Device Settings +^^^^^^^^^^^^^^^ + +* **Number of relays**: Select the number of relays on the board connected. + +.. image:: P124_NumberOfRelaysOptions.png + :alt: Number of relays selection + +When changing the number of relays, the page will be submitted and the settings saved. + +* **Initialize relays on startup**: To initialize the relay on/off state after startup of the plugin, this option can be set to Yes. When changing the setting the page will be submitted and the settings saved. + +.. image:: P124_InitializeRelaysYes.png + :alt: Initialize relays on startup enabled + +* **Apply initial state always**: Off by default, then the initialization state will be sent only once after a restart of the ESP unit. When Enabled the initialization state will be sent on every start of the plugin, like after a Submit of the settings page. + +* **Relay # initial state (on/off)**: The on or off state the relay should be set to on initialization of the plugin. Will show a checkbox for the number of relays configured (2, 4 or 8). + +.. .. comment + +* **Reset relays on exit**: To set the relay on/off state when the plugin is disabled, this option can be set to Yes. When changing the setting the page will be submitted and the settings saved. + +.. image:: P124_ResetRelaysYes.png + :alt: Reset relays on exit enabled + +* **Relay # exit-state (on/off)**: The on or off state the relay should be set to when the plugin is disabled. Will show a checkbox for the number of relays configured (2, 4 or 8). + +.. note:: This will also be applied when Submitting the device settings page! + +Data Acquisition +^^^^^^^^^^^^^^^^ + +The Data Acquisition, Send to Controller and Interval settings are standard available configuration items. Send to Controller only when one or more Controllers are configured. + +Values +^^^^^^ + +There is only 1 value available for this sensor, with the default name ``State``. + +Commands available +------------------ + +.. include:: P124_commands.repl + +.. Events +.. ~~~~~~ + +.. .. include:: P124_events.repl + +Change log +---------- + +.. versionchanged:: 2.0 + ... + + |added| + 2021-11-19 Initial release version. + + + + + diff --git a/docs/source/Plugin/P124_DeviceConfiguration.png b/docs/source/Plugin/P124_DeviceConfiguration.png new file mode 100644 index 0000000000000000000000000000000000000000..5d56221af4cda92bdfb8a61b4b4c1cd7aa24e0c5 GIT binary patch literal 48484 zcmce;2UJt(+BS@5jxtupiV=hq|)~Fzx<}8^s~~L6GxpRy{CE5 zXa0ftsv=B@=@E=p?Y+-Qs(15$QN49*(>?X0tv3<%7smZVOgASPo_b8p%b!gB6x4xx zynX6=u72k~eIH;#e3d!QdE1}aoV8@Du8+L_XV-<(4A2j--3P1J{c!VjYg`YtM_RcU z6BpPJsPK3-7=g8Ej98u)dkKkh8dgN^i58#vVYLt3TC75-z=Uo?{d4udNT>&+K|BzX zbnpAYMWmyD{eI}21RePPyXM0G?84|#JX%xXPwYj-c|vANB^_`&5i}iP6G%(r(hbn& z*m0uJxl5PDY)5&^2bn`6upDX4GEyT~&-|+iiC|<3=ipQ11%|yIKVxI9Ft#Ts^NJe- zQ!=4y|F5Qoh@<|3aSw8O%4|ZXi)+I{WWB$_oBP7c%k;;G!44^Zsm*q z<@S`*DZHJ2dFSf7Vcg4YhkFik{vn9S>7;cH4iway)FUEe;FL}5i7>hZn^ZE$jjWwn z%tjZEY9{T(|I#rPQqK`Y1RxT&>uy z_S_}=oe{SUKQrpWYAe9{#(myttn&*nPp_uM9UhI2MefS#=T6IpHuD5e6aSmx6V&U2{fa*psk#v@$*S z%9lHAi*3)ox+XbLT!#Nm(m@I0b-}iOYrHw_Ab)wE*7@kDfGUH}`}J#ei&^F6|73x~ z)(0IqSh-)35a1FSHVh5taU;$By=uBy&P?S)HugX>RvB$cK_Sp9psN>rtJ(#F#BHAr1ksH ztq(Jv;J$ZqtGo99?81jk^yXD_5ImlaUOm6q22)=>m!AMBuby?zGu8m7*Jpvt!Jp*s zA&QEz7R8q*l3HkN&q@YeG-%|QZ^bU=x?Gr1EMjGx*~jEcU6!d_s(?||N3pEV(QqZD z-+{`Mr()4@Jt`Oclua(~3z*ko>i2T!n-OmDZa}`svr4;`Aj>gDcM@%Vc@9 zB_@jN6}jAy81CL05<4-aD9^)62IoR+8@MV;N-BBaElOMd>ZQ*&h}wt{`8*cuOv_tQ z#g>^%d?8d;=L(;P!Am+7lZu|qkWsr@j&lMDJag53opzCDtwI<(=g2?4M(NYhY+b&7 zheJ+Unl8LOZ-pvqX7iFZE1e!0kDCx0L1Y`TfiWU?LV-LkbS_3g9J7TkUzk^wzlC0< z%_gCYu*;5$iS`(=rrdy#Rv;_k1lanxXejV;6Ik4FsBP`aB7M2Xz*N!Ft4JyRQ>~Z_ zEtZl|^5g3NP*T#sNx(A;^-+6~SWH~BU?L8{e2rcETw7rp8u`rlo2c$^ua>zgIYv-3?a+6M3aD%_7ce zo=K%prE9Tf;`9^=E)8P`F`}VCJ-)+kQ)78Ex8#v~_S>0pPq~^Gw-!Bu%rDW)HdLk= zDA;2MuyY55095C5>vN!D!M;{K~e)3|i zVtV>)a|>f{CfZ(MVMg@6@2k!4k9xYDAuf3=6BofeMOl9C@LtQjh;Yu!`h~i-0!54f zEP}t0H94-F2%S4XtF0BP$Xi5Ieyaf3QsJ0kKFTdQ!`6WXwcVQ}!OH0zse(se7F>&i zMdM5Vq4edP7ZJxJkBRIq>1c_GZXo_k293m@(gI5H_)LxVPu%!qm&45ZW7Qo%9;2CJ0 zy24@(jI?Pr1FTU^U?#c|`g1Dr&|S7{m&I~X)gJmGzJJ{FiPl?I+vy{3j=Im;MjyOT zGj4xr_eYp#ZfDTrkI`h9XT4p6eo^Js7ha`?%vOvbp&2um%%b=Wj&gkJxrK|iOe_$KwxD|)EZziQW+ADk#bHpfKDd*Kony>KTc00zsNHqsln~f&5V{F?Mm@q)6&yc>*7lVL}#wUc7CUROoIVq z>V0RSWyh`zG`en zwr#%tfT9vVi8)`v44$TxZ%lqu_k!S&f9X;*OtO72C~ocmHbkntw-sBkG5~q0Ju&=} zd@i|&RfGdp_89;X-%?1H?w=Jp;WMT4GRGSCIPl z=N(^SG8fMR4sYeXDQAxFBYpAh7hcP6M;Bh|cU%GCGh8CP85uEky>!gzdB`a`x3RZ* z!5FN!yd2P(LwtGzH#hpZ$v5~}zs+`$ovBS+{IHFBsrV#KgHK#9cBF^$!87>%@|mIqqhqCHE__*~=YoD&%mx=C= zgH9yA|CzCij*!q(o>hnL)ojz%abUpDWK1yOgl2)}st@8uQvU&?ZPdSIHu6k|nTB0K zo{zdM1KC=do(Fcnva_3{u{+(<2_#G%njL^$qg4cHih%BrHDqyi5$(gHIl$tfW9Bt; zs<2a4Lb~%}NL#_u{erKKT#)?Mq^5i#I_GGs`!yIBEDv+m@>vNi#miUW?7bE0f_#nA z>&H%vH^;;87(R^roT+*`RBP997CE45ha2G1pj__sn?UAMIN-{awcT@hXaM)yTd*qy zJzZg69o)Ub;$}DPdD_Av)+MQ+$WDweSV=MiqH(L;t!9z$($Tuz`GTFDnJi#b>!Lxf zE2F3aQ|y?kyrgvGnv?uo<7gIL6iDyMM2`aSt9kud@zflgCW+w8B1=6(mYG<&(3KBg zb)Vm9l>)V1{kzub&^y9}sm%sE!-Gceaz2;BJkQJf-a10$87o0AAtF_R0cOG$a8Lx$ z3$l4^RZem4R#y$I+DXsZDaA6GKwqeU4#ccS)Y9!Dc(zvnhk5rwzS1WpjLdQ{FyAWb z!^&)d!rP-VDWR%BE-(I52VZgbSozB6M_`OqQL^cmA}D|*LNV)HVkie=Hl%!UdZ%-#|QBq32xf%^{ALycCa=Q-{2M(7ffaOzCx{>kivetimc}o+A zj3PKfWG^M3ZmZ-RaLfl{xp&r%QJwli-l(;@(xVdG?H|tBG${mh8JRHNSzro{ojo}} z)~*Hwd9~x(LiT6_wLV^FMCnF8c*~aGC9aEeJ~nahRJU$@C@n~V$SF~hIaq9rvXaI@ zn7aCnknBXmVIYXyVmNADUyuJmX-BJ>nbK$sd{{XKsL?z(T^yox;}}R;`S(?VB&2UO zl(Pm(D#3Drmie3ptmMLe7A29GHa0Ty(j2>2Si7)XZ*FZK(4jxP_qCUs3j;F{yY$Qo zrD?1gry=BwrZHhT%=QB<7$?a} z_3B7Q$Fx#dx%s9SHG$4w^m>kqS$Gy@bdiLaqv$m6pDFm`63IUEnxPoKO&B6iv0167 zq4uu^wB`%I_}>_igJ-}cvkPW0_43KHFfO0j&S#B}^BrrXn#L)bkmv}QC!{B7%mCo^0wgPFPaO?B51ZQ&fL z@on|$vJ7-Vkp9Uen47td;C+1ifybL{6DviV>Ug7-)Bu}+L>g}%ZP<~~T6b6!&c4gP z%j{ugd2X`sm zM9O=nW=cgI{;T+&TKY{@xWM;1%4(mh9^d3n@6t5!!qtpl59MdOsCuDF-v&Y1B%$1A zcxB4gRmws{uwF*2n4NN2f{_7|pnS*blRVBb1PL#n&s9LC5ZB*cGO;%+t2orZ%vr8Z zO|8mVr*T6EkLI~wrrKUK#XrmZ^-!iySth#LD8>-I%fr-*zEU{LkzRSeYuvqHExzOQ zi4%TBUq}53xUzOdF=0AG=wUa}F6H z6*^B`bolG4erUuQY_G5-4%ID?OIU8;(5NX&EFmWaSZNktr{)gKlc@Kp-}Vn*kSfif zvccNgaK-}WDPyewGp>;W(bjmlTU1r_sS} zl@}R{CwmLg?95Dbi)IXE5sb7%uIN*P`e!2sHZvVrjW0h>m0-A58lH29e*JY$|LcNx z)#c~B*Lq%P$iI7%b2*3h6!7+X6}h-P40Nx0*;EFaCoaBY*jYMPVcivU?g4@xFA%XP zJgwqTti5sEphvjB=Y`K1u>3<4rG^v0?(Rtf4AWhs~Mo0*B& zd%vV#L(BQ2dp#5O^aN6MtDgd7lDj!z=N_4^x*E(Hu*0OT)WHHUJJjUWhimgc0oJlI zTeOK5Nm?}>%m20{+?{+XV1!2UPp(>%bab%NSFSjI@9Go*Sh?mL16Z!=;j5$W2$&+k zG}>IHEe$75-fjTwBUh0w{xaviMd{o@;2|rYlp$C1Ay{t=<-ap}WYnNZXTEhHq|Nd; zKo^c2zA!*!#Y9A@7zjs|l2~n|HsDs ze+C8LO5grrRwUqzbvZ&1HcOvwj{#AQsRO74=gL?M+|l!R6%IBK;hp)$odpoo>Y$9Z z6HLF|a`7F&yS=vfZlC=uO2(`B$B`q8l8I3Yy!~Xo@1wl2h5A}{F4T0q1V0sQVtRlA zZ3P7tnI+!oM~pNls5>CTxhvlW6CDU|YOar7EZwUKG=u_p z%m8j!4qUk(PSEDB!e4|Ch-nkYrbnKdQ~Oqgq6qV~)3|spsqriN-rxhx-P~cO20wQr48~njvA`LuP13p) z!GHFsEa6p8<(mJY*9|SUs7EZvu#5F|>^D;VW7khy z&3Sn>@W3?(Drf#k34Zg84zN6xl+@$HS}9*JXhAzW8{M)p#-a?dqZ@--DbK^c>_w~Z z`F7LAR?7AAlQ3@EXr&XJu~`?ieaot|0r(t^8(`cq`n)haaS&uGpL#Cqn}}d&VRt;~ z&b&@DOy+-U){A4H-Xvq){&o0@A_OS6R^2ruc!B zxtd=WxY?G~3H_h{bS$tztggz^6V#;tvRVq_mILNjB=e;S65c`#NP<(XCA=7e?|uu?T+zZ{kBMVZ>y8ok1m+yfTg zjm7_Cw+pE9f8ezJC#3<395qlM@QtnqY!0&Uf5yLWhjCMKQfPDD^8+zLW1%8AQmR4_ z4|2G)7T27VxE-1V`-tqS*pDmEa%N`c3sYzxVn+)#5m@<uHV z)t3M3uB}n7?rsc0oW?E>&aFxv{#b`aV2PJ=)8?}@eJ6Y?P0=_z`hp58V}9;zC3rF- zPeI*!&V$TzNH!dlWr_{j`+KHt?q}i)9BbRgI;|8oeuO$!H=RJ52&jRvVRbmpF2uzF zR2(HDXZHHVt-Ysz`JQ5&`xGhwBV8R9;deK8x|NkcQ@tL((jkY#%e&`6`3)pYPzYx_ zf>DI;>A`>6!lDS4H+MWpg#XmjwW;UK_leQ)0}y+2;`3?TtLYN}@XQh3$w#LLOWg{;-aBA=sUYNw_ zU$>sCsmH0sk}q8%-3d4a<9(^FUf45INi#%J>bnsW;Lcb$HwdJP)kw+Mnf0$i!8H_I z;@};V)+cnkOWO?eHiI(5e_=;-!9e+#UHDqAYX-XII&~k`K8%DTcqpu=^uHH9U2q4+ zja}nXppCZ!LTT>DdMVC8sJ`nmU-A)uvqubvc@o}eQP^|(di+yeNW+Mz%5PbxdsgXo z>1eTe)SCU@D+?L?Zx?&Wjnz2f@8g}%sedJ5fBg^h#YKAjJ*)fgQ-}87q1b;OYyBtN z_6Y#5U-4HNvUGHQVWMjle}7d@3u(*rC95|ARjekEw*0PrB;Puw#|kU2k6N6(xEhW< zynV*ppH=OV>~jY|i7$Vx`akK!gtOUOZMQr;-kPXtkb^gXG8%#G|EHtp0UzfeUSj^6 z!xrn&-TF*0fPBY`SD~F2p|fj?f#mdeEr8mu@mV3dfR(fI``bR7XRcbi8}R@tuZC#M zuc^1c9|z?B+`fW`MqLYdW>FFl8WKHFuQ3pb?#a!~o!%zYq?oDMk>UMkAq6^+v6tor zQNL5|TtVAs?yCW@jFpD9swOtoj%ZYX?X$9o;>K=63Flh)X=Cr>3tY*Lqw^}7#s^X$ z&ym>qvH7va(`a`{fQAax>6poUyGDd1n~WTlr=8X`DL5duhre)ejhl8h;asMa!z_iQ z79OHdYt;i(QsK>kD|Oy==#!j`#!JrBE&|b`9^Gwbe-NUzR#H>$T5N4?8OU7UXy?R0 zH&uEuUY+C?jx}~3h@)Lt9zJNm@4tfZNaz;3W^0T3uLSy_DcRwNJK89{4s2vhYt2+T zx;Ls#{wKJSr{Uu<&}?#P_bMWP`Abiq-~X$QiLckJ%g*cQx}FnJst9JTn$BY!|DG4% z!>5C1+lVBkvz@01EuW|B+=j2ugYq6{;vQNR=ZD9NI0N8Hk4iHhjo4S2*$!@wYVs^` zqjva;;738?F%tRx+~4zp1W0%4)ea!>o_}n5vAH>awoa0tYYclggmpcIZ}aY$ug@2M zzQAUMnQH=`P>pc9-ZBkE(U7q*n9^e}bT%k=;SR7vv6k|M@LQjwVcZk zhTgG|Y=~XLtM^8Qi+feG3Nb(F>vU+Sl4mI&)$F!2_aMveFV>x!+gmgZNu zZcj_cJ{+DQDjEyU8#ZFQ$94lwl|>S>_6o=~E}xre$~jdOq%$?|3laGi9;;l5sIB1Q zcZRI`DW4uwkAhTfOFbx?ULG2pjCu-lt$n4(se4WwjedUw z#FOU*Br3L59C~mJS%M>&7gluo-Q2;*(p{MQFgbaqN85|^!fL{(+K8}Zj>Ny0xAsl} z88n-o59uESEU0|$*#os*=uz5qx%uDg$%~3($$vh6$n@pXza~(0k#)U zPG%0Kqxm0#O8-!Eq&BP=>j%PtoWjKj55Pm6K5I)bdwB)s4m6!3#K)-05h z<)U`5J*Jl%DJce=W8pPWYoV0oEqw}~v>#M)MfbwEpQZi7X1DD!?QS2ra^4yK=DOk9 z3oR=lL15SGdJZ+}PV*Yx(U!WVQ*G(!LSf{T=zU%uNL4eUVbjLi)Y@MwzvlI=t^?G5 zaRS99Y?sI32t^x)Z`eQ3oQjf~_wV`+#SJCcSM4upYJ$0L#!?r>@Uei)EMN~E~R zt%KBNEjf%2w$3qykiG|<{5rsG)_*rwePHA3o8c1p{=NuETdg+;kGH(I#&D_eAB1h% z`Nm{cY|G0>et~HQy$6B5UNk9k2e+s0n@ZRBSn8lGOgAp8xZ-SI)?4v*#Ph+(dcoUz zeq9NRU>DJtz6}Z8>iW%)Hp95jrgOR!u&!y9-|Vbu%NvW&M@X(h#`NUI-PiJ4YW(11 zNg8CKiq+H6pLHV|z%%wTRjabpgPE4qeW3C1R!UuZDcJqzjFF=5?h^OR()wwXT2QgSsmiPvZ+MBLHXyElr4et$BYi;R73#;j)2NQaY7VAhJs!mJ z&xxa#pzjbNlD_$6aIZk~!sjt8O$_etAaMzoMA0#Rc&ftyWGT+lZMYv$o_vmZmkK_Z z|E4>`wED&&KtF_>lTM!!JMK6cDqhT{q%T~`-W_=T`4m9Oshn=U1h%hfiQ~bFY%c6od?vN;)W*V9;#9+8m6NnY{B>p>3!G46te0X6-Brmrhxfc_w zcs(^gW)xJ?5`ZfAsAPk5v&U@IkkaYC{^_bow^*irhdy2m5D9{!L+}?F!@9e!ksqoW zcpDi&X*l9Pf#=M;4g!gnSB$hX>OzoiX6i6dZ^qH?XoleZ&cg>v+MlUTvTDs{E8H%; z)XN@|_p8T*GCwvR;{R`WhAx<=>;57yhloMakLC<;r5E$u3*$5F50x?2UV%^QskK@s zIjI3N1~9IhB$@2?gy-M71T^3^4_KaKws(5lo0~u7({NL*B`qu?v1coL*J1p8{pXH% zoPFALr{-(x6;|3!!}X|hE{a&I>;i5%0xq+GDbsY;Z(k-LehqwU65qljJW`e-p9bYh#ijHQxVskU35uB?p z%Vnay<*cR1TANku04Rj}V5F17$D-yxX}ISbGTvm{w`7MCQvFX^*;acg9^;|Z9#nEIC(iQDoeGhY6~O>0q_jzAI|7IqY#j$rAx?dD!Cs@Y0BSCB{pE}Aj5x;%g=QZN z|EvkPvc_+n zi@@q>)B6dk>KlLaky#Wn?aQIo_S|qZC~f?>MYz|YfnDEv>!Hg(hEC1gDf8m9)}3B! zrwhKT_2FYy;gf)C?D=0&a7R#YyQSsrYja0j-=_?gs2TD0Kh_1T5h-Q=#;T#0w@1(R zh7QUyBr?NGB|&V`eo^lWPu7Na0wbrC3h_~kD!&*c(q+}RRO;H_O2IT=e6VwS^_L${ zo{otLH*wYlSGqGcvuHlWz1rHI(e7J&bE4XQ^;*+yFz8>1JNx{3NJ)ciLzkcQVCED4 zrjjI%(JoKvjAl;1f5b(GmM<>3P$vDE(ED+&i1|hDJLz(eU%WAY;Iq7Z;m2loivd6z z(^bStuL};a--SM@i+Ex!E`cMpik!Lod$aa)PCMQCP)~~_M00DTQ!+#S8)5@alY9@d z>$=p0XOldx&g?9IG!1m|^xG0q>pi%#9A3_ov_T^NiFx#3zv#`JNpS{&LR5FeL*$RHNte zVV~CftJx;^NA34_lmv0y=A1b*(^lrxpiU#U$z)I>_5NrwYd4b}fju-+aJZ`o#$`8G zrn52|?y*!$?u&b+KLZg(%w5jR+-^&ilYY^5(A(eI0pyXCxGqC$#2MfIa5DqVE-I8n z#J@T6Z5x@r09QUYKsh(f{+${8WTx&7QdW8JcLAWHEq?<<@_+yHbytbwS4(~9J@U6I z?}ls4e-U1w(3=U9*aq-)U_d`4Zp55|F$(F?4{` z`YIGqSJQ^)HxNVCag;S<|B7RB?49%h5w!L&Q2MgL<{WWhr!Tdm#@JF}o+UqL)e~ zq0|P8IBaZfV7=o@={To-58;V=XhaC^@Qu=p#dqHPq@VI8e0mm6h@f_u zTFbXM#nRpVL{YKvb)A|7KJ1QUGmj-^DPO7JCl)@!-IfMPPvz4ed&}?xRN0pv`K=w0 zN2002b+a0nNV-!5CEwQODgaTRf=B__O{nDBY!z$a^zrw5XQV|veTr^T1GP7+dqY;4 zo3ENXFD{JbXSEtR21`XwpBY`$$JQW&xnTtE}5187>x13vyBQ{ZE@*7y< z@(6Qb{LCbsFuUr9XiPzr;^-@=#WTYe^atFb_|#gJky#oBZn!s*GC)2k8~xc0-2P@g z{l>KPulWv}2Lrc#W0Pc1)F3q`36!@r%CWpU=>U&C#o`GXIY;zIgl)9(FZN^wZC0hp z60@cJ%Ch8(L$9ouIk={D5Vb23-btK_Jw>+`&&uX}BFWOc#zNffm>>_Mmzw*Bm1!fY zsqeLChuR-WUAfb#Eki}=?072X&gnYc-<;rvVW>-jSpKqcH`o=0DR3Wz3UkPen0a-mn|?ReBnt(e`52P4fBk&&lv%B2B`qnjp;H)0OKz5_ zBE;jOuu_6arQ+4Vf#K*T@eN*RP$xt-PPs; zsA=Zw$USj`h5LJbT2{;>Tg@osy9PZ3MaMuNiQHTuHJw~~`QTsSno4g8!tmUsP!KQvk0ZWsvM5y|h2&-<%DGn7Yb5yu_HJ*a%pO756 zmHeEk&D}JW*xz`xq!i7H!BoV#M5&%laRYYc&x-sMg37254d&q~G2bITTBF#G29fFe z{uEeRpK6dv$4^EjFOr#vZn}%TCjEZtZd-=eizF#JJnbVhkfu2d_V!V8owA9ms9&CP zlSo1ax{sK+jCtjIEKW_Xkg5g`>?%Kf@$7Vd45zx$cX@H3!i8(PSnVAr7m*tS|0uH2 zmm+re#vmob^S@R4hGBogTklm2)T<2errP9TZZ)woQ60}W___QQI^0?+Y1f;767YnA zsTs$b+Arp(*AJ>BSD6W?Yw2u%zVB2SzVnS>%2%9op`nw3R<-VpXI}Ok7>V){bVde+ zO_|MGRg#<*-H{OldO`K9_{LmDZ>(Xs=k3;QKs3&lIq%h8llgId=i`(^Y$%@o#Zoa$K>W4H%jm_y!KQ1Hy%4-O2JZ_&XZ+F|{}v-zvnUf!9Bj`S2zA!luV#7c?a&M&r? z>5qJt-yeC<9&=E7ZZApr1aYn*?*~L~d(0O~5X8?{$YZpK?z6g3e6#RHi(O><#iQ1(3@?(P z4Dr&>dN?$0P=2<6;A2@*MdVpHOQU|4-*RrM7U@C5r&D^4)M5|v{2Q6vD)E~|Z@5Wi zU93b@KXfGWB%$|?e#-`UmD()D18l#eprGQqqPe$omgGnd4QXSBvGUUj(WwHfOZi%z zi))$a2BApPW5TF=93RbLA~I|s!?U|ae48d3=d?Irr-oA~BsWR30CF;1_wJEla0$z) zA(=Vu6q)4xQV}{auNAd7nLpyI0yP+x2AejA6Lh1h)af?iPR>+Z%y5YE_w z#FUnKm;B6AZak8SynRWL=x$@rJuo{y5NyM)@gob6gI35Z)7N?NZ}~MU-P<*vkV|{> zVxy*dOZ{p~s~I0K4I{NR*P#sPhSNXTCqpuES8#RuyM6Az8Vn|pMxmyi(SGk9^ zeM2sve}Le)Jrj~#oA-H_Hyqq3(Q~J*H?>+<6=--WR#xDa@;)4xH>25DI}y~_-YL8@ zzxUz8AoI8UR9T)iKECQXTh<(@@SE4rV2&~+;(JMvy1o1vY*IjJ6s!Q<_2jCKK662& znQZ;4f-xDE@By8QQElF)!?9@`KbK1*TzNS&QL{jHP1&2%=WkfBp1OWNsq=C@hWX(m zX^#!^3XWo5<9onTbo%SOZ3gDeX;H23OH#m!zbbTve9GYS(B@LJ(?#zjbP8nM}}KbUViqSf^h zau=4LE8fqJC%msonxbl^*DAXkWq;U`!uKT&z6~f||~+>Z%eV?sk3D$yeMWWk8TC zm#Q!4T$A2m`biNwn%Jj%=ybu}dKHJ6!8i3ak{#0gAEZ4#EprHLC6gjQ5*gZf;RVaFJsx2(g79!_Q>4(i)x6pAa~K8xW9I*=||LU`Hf9j zs2NpT-E&Cq!sC8dq-|3+evUTCL0PlEGy|s1qPQ1_aK?^MIpJN%6v6vC2VuPTa=lwH z&hw(;O~A^=xyuDj1u>1j76kv6Iao>{Djy=hBFRLQnb|vA@Anw^QeUA#{DW47w{pEZ z-`RV>N`;87;q(*6Nwc@cBNUeA6g8_(V{Qam-O{i_L`->EO zmYXo?jTO9PN;iA|r5eosVvye8h>_(4ZI50kNj4mgdV8eqQ|({6X2~2)=DY*m)J9Q; zu<;z2fT<`m_nPz|aM?pgt z)AY`AyJ<|}Uiy1I6UouS^{6&LCIDj%))NDUMWB?t3aFUER&kf~9iLDeePwfFI$PYP zCdnv1GoFDcc(EDI7?Aw~9E!b0Ocscn5aNpw95KBhE+9_+SPLr8d$D=hL#*)|I_Dsi zKIB`~@$*arug;}J+mAd5HFhHjy+9VKk9t_(1k+ z&wxqaU0XbA%sf|L;X<2kWIFRr(VTJNFKnZUSbr#gcs9*`JEMifrj5UT683IvZ>E6; zv@rk~yh(o>b1z6Y#)6j*Yb(D00p?9ooZ^`8g)dQppn6%3CdkMw#hn>zI~S+T2K1n$ zdY?Rk*(u96^6^<(Z_}@qw7aFfkNr5tY?~Yq5#JuO)E89nrvP6Wp>8Vo^s7qq$z9r$ zXP=h5w+U^+;2hZ-bH#E?%AHBd)jCNSSud)pjnP-`Y8GB2nP%It`NMzjD*Ui=${TCE z0hd5`-huVmL;HiNa$b@c6Ki-MwaxW?@=i^Tp6iOz&cVpxkS4$*rD$n>is$Gi6Wtj@ zD^sIvyX`iSC%cvJFWH_U#h!0SnHo*xhqElBxwisJRqo2#`2r__0;xz6^#=3AJji3n zT|W8hY|3~yaVoU}*tWeMbS57Rx@QI_>aKxC0GD_q}(YVk6q z$wSL>O8LpfM7zaHJMob7)`~Q=Y7l?&NXwB_VLSzAOPZY<7e|KAhLy+ZRWa`m@0PVo z$rzbU^=0d~Q?}NptnC*)%LjBOW%#|y=U=6yF?HQ(lV-blkZO$ba0srq$i^jcY2`ko zcdBM&={fVc2nx1;B`EWSwS+#>AbZD0=R`|pKJWJ27t^50rRAM&a5QC^DfV1++}wGE%Pf#q4@MC6Uh_I1bE4s0ZbJ!#ZocBGcO$jr z&#=9E*h|QLsYgmx;tFM#7b&WwOZU!2;#M)t?i#N-O5fag~;!;R#6Ph^Q_wF*L|K?oS zv*ht#)eUElycaF)wbo~EA#x&zunM7k^kd%ZG z*;K==8C3?02FFgW6g{P)jgX}JLSIG5t=it2q!%d$nua#(@OseLEh%G!nZkY~j%TLE z23}^H$PwQ5@o??T8;kC|iJAKA(u>%L;7yEzah!M*33}_y_choX(_-|#W?T`efK~Ap zLw)^TCK+fd*h`^yBAfH5##r{Syi6>h@grYMbJWC|9>h+*cSz9Y8FkD|-@BBm#|xOM zcW3yp?^*ylhLWV*Sj;jBsGsQ`PciGrALo9I@$rn0#>S5`$owe;9H}T-$9(^#n*T#d zTwdSV9s`GMDPqcyxWSc_wyDS3Ts|pVaF2{@D{st~z&lTG0XVlvvl`aAKK&oCvCz++=9x-D>vf zkLIqAnAggmeyRO-i}vCuhwSvITgzJA;41_CzkM+nAfo=?-W&cuyR1Ac;E>x_`LftA zZUX0gzs~5oVW(R)rsE zZ)tJv$Vptf5yKA)1=3z(K;rJ@7-~VAP4+T^<5?jl{lSYh$wJLY%s}p4*HW>E`pq^`ZWjK7ryCJ$e8F)oaiQB+G#P0{|Kmt+qHsVLP zz{F>f3~$d4(cQXxQ}*c*%Ev+E|C0BhW`Om*KUnCyo?Rc-pId-RDD2ov*Qq^=IXjyOpI;nH?qX+a3WvpnBL`g>M-#h=^LkrZ_#%(Y5qngp8ZqWx6A z8~P7IJF-577;-p>KHaw--=Cky1S8od(f3wW0!qQ^NH@mfVAQU}vbxLnwSf1!K?9`h zN`rSM&HaxSCK7Vp`jR%a=1YzpNJqD574S#PY@g6_`+1o=V~;QHe&t5fi)zD_9^S-9 zk$VCEQqhcfXzv8o@U$-O$5Q8J`lrM!FL#(KrG9%T5HvQasa# zh9l_V{Wm;23G=-0WnoyHo0OVOpZG}{^;M?f}t^(r%P#4E9q%)w#9q} zSty^{o#c*3hi&aa+!hF^iwDUl+VLPUr+E1nt}gVU&C{y_F($pmxt7#=Yg}ku z)r&*LUIvLJMXBO_)F`H8&hMNwl(M1n;%gOxj&SMJYFPK5m!{wzp?4S7Y}Gk zYP-%~e3P1fZnOt1fA)G0K7e3Yb64w}SMtTWsNSw7L*m=cGukc&&=_A!c`!A|fREF0 zyU%%SX^Rl@mtpJgxYe|s4|nmvOdxh!Y7dqo&Sw-Y);Z5tCeY=&4a$AbsRCsju6?4}=nXoDEooCxgk61mZp!&VTbxCQt zuwK=hx2Fr>CIxjrTUrWjYf~u|CJd<=A%Z*8ViClmBXLMt^~W;iseff`_-meZ1oLSZ z{?;;|V_5sOQf@!~yAh9YF~I9uz=r*r-T~$f&~N?cKR-=$m9(ouW6x(?B|mhn@_zNV z=K8BVepM{Ks({}a@L#jAK>A_J>(y6`Rt4jl*p=CeykA_uCX88sCrN>v`OSlO+|6$b zRyFo?zMxneUz|C+V|8D@(D?t!dIQh;e|$4y_1XX3Cnfs9Gl1>>|Ly}cuqQaF&%As! zr(r)L(_~Db4nSh*1f~>HK11!)W_cmfG%+7zM|CufA(L_P_ot!ka?U4{?_%00w^|pF z>s&@kob%@JE+G3OE*vYY+P8L8Ww0v2^xs;?X+8ch2+Dao-B4wuDLZI0^B5NOZ}DTu zC>ZJXds1ieMtIle#AhKb{xMQ%yNa@JGqJh21IKab)w0w8rx$;&fVG%^~&)1i)5IVuZ@Desy%~h$fa%C-FK*_&ubXm z+hw00&{J$c0R&rWHl!SVP~gjf?QjMRKbP=bLrQIb)k;D8>`1J5Fj8JM6!EkOe}{2v zq90J+yZdkqry0*XMhNfxYv&i=ZFm{&HHT0yNrbp|Ay{qCSz%L8&VLsI57-0hr6hy< zxdq`_UN;xw#98R`J6M$Rj|tB;Li2F6dHvj4Q$t~dJ44_h^f@!3YyNEsJd{2Jq`*`| zMmRhVkytRIOJUO~dlH_OUe1xxULxfjLvA?B@Zx0z5o0I91oO<;^<_Y+9B%AIvXhup z7PB%7mpdr2=A{Dd?^7l;ieXWn<62AJXTusl;{Vvv*_Ut<>Y`!qeYZ|L*A~6vRjg^K z`=xlyT88)ZJ=!-K;$D`D2ms;rnXWli3-d|vt^2bFwtTOy#=r7B!#-BU+;|p!Co}k28#-R0~C^Hb82qNq|69 zB+*1bM2HX|LMQ_v2_dvp-WAk&=A7?5@9&)Nd(ZcK&-#lWduQ*pSGn(d-Rrup)ps10 zH(pt5;RbPWh(|#qN?bHMvUi+W^qxU5_f$5Ihc%469{7D27x5Z{PfekmFR{luIlZ!- z)%a_?@4pXVrX!cmEWv%pohJV3$RV0KgB^cIenu_ESlzF7+rA*SLoc6| z5_>)sz()Hg64?4bbs-az>MykwS4~T$CiTAoZ}W+dsR}qnozi{Tf6d3V1Vja(JQ6L% z8c7K_psho@9Y52GTmTJ6%9*@6kDi%mj}zSjT*E!fmS47T=_Bi}ZnBofq_D}cR6tz3 z-G2IST%@1*BllOHdw_u{yMLk;P(aW8KN(T~ugIC$fGR$$hso=u1Uh26S=vxj_H^{! zz)%$oGY3&*sA@5&R1vR&`o6TOIBtF=R0K$y+(^*z(#>ut;AGy#E1j?S3F>d*f=Hq~ zN%bZJ`Go2lu?rV@(I_bN-tgHUOQqC+`>DXg|H;x}Xou~#&uD4?)$bB`Z-=t>UA^=J z)PoEu^D}(@DSwW~WfhspZWs<+lMT~@0B`d)Z!KEpVKPnqPYgS1KGE)4vg=i_RY+(@4%0e&}cUzXwtr+kRZ@i%o*lv@NsR4IuW0b*I0A0zP^7j;;=P3ffY#(a^6Y7a?l?HhW zOW6pt@T)7X9{{ILF->2+WGt{{Q$)vy2QVhNcyugabH#HJ<8b_sjp*>15?aBxD>05w z;TuNZoDnytcdJ)oy#_UDlc?F?3O1+TH)ymeCkUM?19!P&dn)psnP86B z!^+zebmn@8jfXdcDlyZZJ>eaGoc8CPfziO%Vq~Oq5un?>$gHcvuZllph(A2NrLpP` zP`&VL`O==zQfEX?yu-BLy!L)fF9@d8q30J1_53-)`z7U4{S`05;oOpg#d<*aMafc< z|Dlo@)mldrgjRVHz^+`APM0t2?X~)kFT{K^<6~H7BA`y4iXonNNX<$m*PbCZE_b{O zKKHh-l*hjYxxUO2*40l+X??+Gfy`RDz-#|bhZG9#$pf-> zo|1gvshbA(JPm^K_qn%Oy9WeYi(~^ndUne*DG|VDqfnI@8{xb^My~Mhs^gC7E3FkyI9J=K?*Qz_jsG@of=ZUSSfT{()@-Tn|8+CoG>qFef2!n7vtW)c8tFyq@ z-WIOehn5%!b=Laf*&qAl-SRRWE{l`{+zw@CIY&USEt-*rOCQ%4xSu`J7$8H&d7Ux9 z+^nKCQgC|`P_#nUp}~?o_&@*&xAo4u8bWD7LQEkmG7k!2Ujv+hl(s3Y*XJNEdXl4C zfZmW2(xonNYCt}UE2nqvIQ{aQ(OruJQnq%|cWYKklVS9eBOOVq1@vL0(fySKX${qq zrNiq0BmKEkmx%F$as-50s5O;p2$<wefBNeI2R?-lo17YlamZ=rSXeT7eJJb z$d(Ez%I%j98Yz0`fz*~7&;E5?>W@I&@1!~lI$~T>iciY8WvJFziXNBA{ctq$b z4`d*gzIcu(;Zh)Pn3I4N>vFb8tJ!|?v%0`v{QvJJj>ZAufYAFk{1FH+_)YXRT>eVlkWR7;Pts&!dtU=K?4B1O5=wiUgq-b{D}KV`}%b;YdqAWB0!1FmC7SX zAygyyXf@Cf%VrbiEqG+ka>%vMykhm$au16)Bsu;oh-A2S*9~d2 zzxg}EIFnOw^HZybIE+x%uiO0ZuW>zpDqV?b*|UD@twz?>ju=<|L!FFn_>NTUi*L6t-Iuy_z$<3 z3#JVB^K*9|{9q)*P#(Xq{+;%L%^&~Notv+bx%|~XKCb&C@`vY}e+-_pdsBNjU6V1e zTvk0(Eikutav=m079;N#$<1}Vp}DVCG&c(u-64b{3-GUYTl2S&W@N73U#c_|&;de_{o} z>X32b+5K=>-!%%Z9!6Ncw1St4ZeFUDj{GhW;0opCt_E3rT$Z`lY1IAhby}ycdy~Uy z{l;tV!7&|FSS)W>s%o}(==~Fg6<4kkBI?{Ei{Dhh%XEAbnl%|E{eg|JTD(7&=5sCY zj=z4IQJk%7m#!UltEwD^vNW$zO-T#-1E(6JXq2u$w1LD;AFf?=4Iu=3RHR6<^`=2t1E48OhS zt5Rip{W7TDTJ%RL*$DgQJFLm}PF`)QKZ-3^-&}7eB}~5Cn-j?2&2TIdynk?uvRb4{ z>F(PsC9=F7s7;mWIHi7Z%&7Jj#KKd0#*7~c9H zvxA4b#TN~GU9+M(TwuUz$ses@&4Z4R(ZC*W)c(Bd~4IIyMcXW@` zV)!9{kuSEZ5L$c3;hnm?pB%Pk2_iD6nC_XIwWslD0kX@f zRLbhCy+w0=IkDDdS=opR^ch*3++j{?1${kLK78?9a-Oqs2Ai3w206gMprQKjeZ=M0 zoQ*j#qgiR0&?`ptP;u`1eb`fKLEVzW2PtjwVykSd>rOZfQ>4cZUvYYG4RLAty87OA zV7|6&Zpts)qb(&!gjZLmR_rhu2!g|)lbG^`7vPfTr7I6MK=N; z{g5HXjzJk9B%-AsdUM)2ykqFW7G_njsHrg=f{o8p$!N_&lnzs!(510*6uqE@E*zNw zK1(OI`PU%mwj3!z!NkRd*jGZ`sS`%Qy{>~l`L4Vhsn(^I!mRzC{6rF7!MnliH^=W@ zaz^Vax@eRb%utN+VKtZp_J_!X;qlP+nS#hH5g=ialbv`qnj;S0<#6vBJW*5 zR*lx!(m->NVy!OId|Q8&5_U(nex*%>7_-wQi=}-2ZNx7-$?v#}ss|VX-uZ5CoLzMO zbNAq3L{y#e2zl{-TgBX4R%+MjC5b5Qb-5BUr+jQ5s*u| zCRHL)vY(GNUiUL#?y7wX*Vf`ZpnsAMXh|SS!>vs_5TSe})hD{mSW`wx%|>JdyW3E! z)5eLXVs=`&zG6TP-@S&v0omF5h?XoB63OiVrkfGY+?{B^mPkmtva#=6K`& zt>bM@t;p8O=&gdi8gBl5u>8OunTVtdqLq(&u>Z*EPAS1*%|i|WWO$bg1bg>2BeAt5 z1cJ;*G0Yy@SRoD!CPC!(`{cxZT}$K>-kC zF@)WJ%A~1Bw#DNW_X@gDn}WmPC+1(yvj_azhRtZKhWjU!+V-Sx&#u4E8Su{7MDw=$ z#kf(z3QHsnMoec%x5hV<7fmnk!ymsMd4c8PH8ZbP2ZEz)z!(Bw)}Ia(dr8r`^WjgWg4vO@Zq5= z_L{-@Cu&8bwPfDJ;Y1&ZfHJbv^Z2(PJaa{H6$;SdH7y;DqQ(sDK-Y@ zeCJxkfF63r2qAV1#zht;{^|(YQQL!9P_loy+EDd7FMhvxNHtPF&FQ8~TnCUJ8sBTH zJ#4{$Z7;jIDPdbqQN{2PY5vOuEJd}4{U9T<&N{apOw*RQoz&Cb2i}{R`tPP$msSdQ z`=Y%DFAg@s6<1@XYQ~d!1F42fS%VMjl%NnhER4_qZ+ST}xT|t?=5}YZ^VGxCmV-id zhMj%UlUOQG637Wx1hzX&Y&uom`yCj)uCD?$#zLj0$QO*atnFXK*8BU*>1hg-hEValY?g!e(Fk$NL)yCr z*YrUE^?Y7DU=Kl>hz|M{r&bKFcR76&^!CPUuT-Ii%4qVsWuX{jIFrDr6dM?S1# zpa00=eKw_8b(Mqg!N7rYTKzM$CoL_N5~sIq)jJfSl4(;$Gl{Zj1I19@EOu{1nLS!^ za)l2$4L~8t!FcyYFqvvJ87F{$Hvf;~06wnw}AeL#SpwnMcI$o?6V-94ntz zu&FPRe%gC-Ilq^^+J<$l>`Q(nn)CYr?QpU1C$bYLC#>rS!Lzj6S0e8>N`^&wqk|DF zofJK`H4Et2#<@qx6ZEC_K=gX=5azSzkJMo}2#2x>B_?9w`?{$8A23(Oe<`sktvv7C zS-3RUg?kiIIee(!)-*dH3$8T@kw2c+GbjHA3*bxm!C$qk$`kH!G>l3xOpA2ue+)QX z?-MAv-cG7hEYNnA`o+*_sg*7W15ER;$~`p~6Zj}D%$Yw7=W5rumz+&)anP#R4J}dT zT=%CumDE*luB=-)FCT$$%iDjw_h~A0SDaG=tb0b#v8T$ZOSNLjofR#07L-WQuel4D8$+rxvE%67%0Vz_XpoUNp9KV#-?5 zSLw5NLk6i4G-S1Z``3n3YOvKA;Q8sM(tr_A71Tn6ODw5@AA7NuFSX>Gn-PGhom4B~ zD`3g3mpODDZMJm5JjeU^JaO_b?+8-2&0$k^VTU-f(ls&T; zq*Y()T|d0$4Def}YvP5^YvLb4EkJR-0lpTLookfgbyu%}5CD`5{{t@{h7V0oU-=$5 z{IznMlJ?K@4=z1iYkScApzH*q^q>9$G0(rgkeZ3$JTdf$-rK5o$8h9kd$(;NTXDKz zjLVxSFpQxr&OoPI$+|XEtym9p(tH4B`2wwN**T`svOpB3KU6VYwLEtC%WKBphE?ln zPxWC(H1-+XkGS1{*3;HzT-C*&Z{Ar zi`m}dsI!+B$#sinu^H|SsTGw-rPAis7Y2od+16yyecKAxfx}jto_iXx zGCU6ym-ptM{VogcT>Otb{qEsFd?ieXrH^6w3_F z77+cc6EBf*Qky~ks!A^at*ZgH(8dCVoI@IqX^K&Z!nY5}BVStG_EK#@q_Li(A6}DHXYHa@xhZkqhU@ z2WN4Cv;6X%lu0F!2zXnzwMeEo;?#SMn2jB0p~tpLKDIvyDUWLmcR$mY{bQ+1+@eg0 zYk*VRue)S`mgkbS9$Ik+!cArD?*zC_;3!|JDyD$VO#3 zeqUa=XDUN4=%;_4^DqF+tlB?!*U}&3sE%f>;w9QN5Wt&Wq}#^eVTm?to~AD_#_WZMP7Yz*zLhne_* z=yevMm__q763_GFU-Jf(zRs!TJl;ErSeaK2Xp*YoSMBXXpw$nv!%vb~S|q%V$0(1ubq?St{Iu31diuGk2}j~|-D znX1|7JoW{1e;)Jtx4l(nm;)zcDXs+x%OXafNo05DfKL{B-?Bn9;JG7(n09&Q+cH(p zcK0(u6<+25mOIrxT-e8l{kXQ4!|ysl#wFZrWNDOT)m=K1&;Yj~e7G_3y7|m*4aVT3 z3YVvE_|3C^ZZW(oGWUX-E2i2H=Uy~m?BE?}Yold_Ob7xDe!+)n9ki*cUN>acPv3V@ zxTBZ81BQU?<78`;I0m)z;Y0Z>=z-bfiAp8U+MiNLj)&4hwmA3K4~!t1KLe+9uJ`C^ zry#Q?GU0Z`nm0GRd7ydoIMnb}6vU;#$CPeVkS5?IUU9O%|I2`5w1whM!$$recdlGA zQLN1*_u_8{?YIVw7iRWm^g;)mjhs@dby@CYoDqBGAugU#a2+wAL2`i+&k&uc^#sjY zyeU|zfo_O81mJ**+=@TMd%Q_?PaNLvXx~CEaqS97UG-==?14LHUF9k|opZIUAaXgX z>egUmYDMNb!$GtFld%=c!~;>B+kJQZj%j)^g;)$bzL$o4;GVQ-*6mJA>wHYGb#8x8 znP`8qh6_??f1_7js$ts&&EC;*7JlzMgsS6LUTj+!!i*}QuFkPb;c%SC76nsTg;5!*vK~}-|Wt|zWBiD;r5)C#)D-BkZk+LU(4~P z{^n`7qYoe4>u}cM2$`jypTHNz+$-ZRQ)>1_7i~>>%gaPK(y$oR_2LW08KXwN?eZHC z`fr<0Rt7DPeW=b1L*#3G87Uyu_u#08 zOhlPz^TR!|`_|%AaMuyIQqA_y%?CCs{;`&^nezg;0+3(=*7aEmK%;@=lGFg{Y?Nj= zz9g3R4qDtP%`256QDH#4Lg_awJ;8LCfFh{BTe=NWN>8jZR65+ip)~7qz|XBdN>eky zMB5@TXwz}2^=6N>(oU}lhQ@)I{QD+d3bJMbojFP8M)r(Pk57Lhq)QiGp#0Y(>5mUu z^G4z(vKGiwg!_dZrFBU9X*7PGztA~z*So94*q~L$O^Z2YwR3X7&=Q5I3@+T-sMGyU z`gh>S5Pb&;FPwzG@oj94EDC5D;p6@cJs=Ew<0tVJ@;{mheFP%5BL9JV7Z;bZgnw@n zZKfn-^6(_S6TyNO#@a9$eSoG*PTjU4m+x zulkL%HuuF7_kKZylW=`RDSUVm5AUxou_1<;2H!rfX|p_v%1PnL+#Bjn@+tGabgI$& zf^mh{j{&`Gvm;R#)Z?QOar}vH?Q9b(mGsdg%&$nSrY^=hid3(TY4I#he-(?%HR?{P zw!yFDY368;KJqU!)zZFVF~tutk^I8`9>cl!S{!#l`wHE?tU?rWsFA}6vk4SZa_wP7 z5ZSTCra$qMG_h*B?7+}+PbkjjK%zGDNX@-CtzJ)wi1#X%A&4-J%uixIS~2OeR3#&# z7KDK=7wMW@kDbew&wi8E1B$wOMf?0%$CU}MhL)qnlsU&|lU!X7{i4ONiEEU6IfsfX zFW~|53hsGw2gK2&lsdL{{va<;uVS^aCR9S}4~+i}r?T(#<4E75D%DtXOt*Jqn|O+0 zkY-6eEg#j9guf{smeuuE-Ga4E2=asUV^oqR3=WpWKA$Kui8L@k;@+3^7`A4#fURV- zS9A*XXN{l?9Bb^*wxkS8->hg`omHhQUgxtpeiOQzC5*mFomyg;NnrXQD=kPBHks+} z%iwfIsBdr(1fcaO<+oglamb8rAu}_RM`NH5II5@8zUFt@|W;k)+;! zMua!6TgX}8I$gC<>2z3tayi4mJ~(=7Wf8}|6vi24{={ZTxWjpNre{{~9mKr;V35Le zKecG&uab`?q)^9)z6uDqZwk$zcgETT(ozW<8C*!t5tVM5XAwg2vdtt!sK=#&b_o-j?KPdpqgv&!a%!UpPd*v(#p`64DE_DDykKQ z?ND_s2}H$r4RlW!2~qLH=r?9v{yuFM*tQdO5E7C#Wk{dp2pyF6v#DcIgUw0^;Y7r1 z^fgJsW|Aa-QKtdjR~t_kD=iI#xMA(R#UC!<$ovCslaIz+*(x&Vhhq`)*yAGOAWn^J zhyMfGMlmCla@bk!kBJ#fuq5WPX~9VCAx+n@Lne#6gH=;_)vZf@)O844OAuXdHR*}xQE=^N=5=L|? zm@3X*I3@kAZ5?q8{~Z^kwz-nL>1t0(!|jycLM|%l9CPlyp-`gMn|s*Y4u7w#3rncv z{$5|({1$9$if$DwPB|7>1?CY`oNfh#s=0|69a-#aMGISLXAp^ zXs%YpCyDx831KX4_m{OP2;x!Y-LjlI8B4jZWQ~L0J)D zCZ4h%t49t_{jsCOGge2*wA;z! zYap@jhUt1dEZY$|t%G768L}+xD-7QFE#)rhTM|skw^fDvA%BRN#e&Xq)T@!gKVxFk z@=Y0SRjicSFqi#yYAM2fTk2VhZe6}Kt~+#9z>oVu+gI(hhNf_VsjiLmBhe?sdUlU7 zoa>~%%CKW_OQ($ES0TWQJ{{ZeGwuTf_SRWjcK2Nc)I>+%DtF8)%$oHgtSaB0wWl1l ztwH?4hj!e_xLcxI!S$*1#meQj*p3N0?_0567+9L&pPCi1+k5855VJ=Ehe~>d7cS9W zdh6|mq9LlX%|*-gqE7q8K)Or(a3<+wzCA3d7M^~cVrIa4IJ$`%HAL-4EKd|Y#n*$s}nS-sz`$ zsd;)K;YGDYjOKpW!lJQ3 z#^8Jrt!8D{Fv+$g`LXfTtC(+hRLL}rgf`m$!K#HDCy%!La-D`=l^hNhC*q2Z_VZNV z7~<)j`6m`z17_kI$}LXwckf0iZ^1^#ZaMNT2@XR?Dc7)+dW6Q>?HBO1yuJ$wEWzQ7 zG&C*7m{I3acb<5g(dvrTg>gl%yv-*}wx(HF-V2J4raXFz379bW5U^}CtcC9Oxiu;X zGHoOEqs<}{fxTdfo8r!{zL8jBYJ*B2r zrucb>6@Def5+DKHOu1uN6S_q3X60wMCSAohr6w5{h`JRit&(eln8k1WChiLkdN++3Y*i+~tSNf77H_>)Egmd)Q2#d3@q;Hw< zgVg%Az*1^ijM8G#{YDY*rK#SdK-K)~=%P@ujDEKG1-kq8#TlU&c|ovHYREj|;9d=x zdL^kfZEwz0PS|dTDv5f31;+FPNcmcR7!b^j##!JI9Od*zMJs zw9~QkM{XUY{iwLbMI(I7qEgGBB{F90(i%s^2=}5tg6Giqgc`nv%Z73_#uA2@$E=T;?L=x&j`;89?mC!M7IgQi0oqHM!KGOY z7D-+Tty<@jDXLVoa(UF&m(ee|V{vsx=yRtl1i3eunWSk|IX?fn(IRib+Vy%BFN5i{ z+Z<`zn!pHs8Sw^Fpr*n3wj;b<;FQrZDEwGWLISu$u0mXVf3|toem|n|st_JHob>n) zZ+~LUIczj{aN|8G1Ewsp=eBK2u9>`r$SY@iN~#Xt_E}nWrW}F6-cVob%`HO}bj5FHF zQb{5oMOze3-6D#L>3cuVxe&4C4@8Q1yQhl=!Zz|X3)yx=)*t_bG_ilun z%h9dr^*D)BEb=RIB^O4-fUpE(P{4P z;G}6=Bm2jzafp1mKjKY{6iAD2B1oodaCzjVqK2*e1aQvnH*h;>5Qc~<*OZ>;(NKRiv)J%=sB3tLb$># z*DE;w(D&NEn=oa>kcn$Q4>M zJWpoCE3YGEzOv2})8$>`IlnMAPbw-shd~4uGUHpxA%kJd&qF+d+n{vfwGgHLq~KC_ zCDkf(&DF`3vVfuh@#q88miLw+r&)}dP3W`3ct6UH^bWH(AKM+_t($1m0XU?;DyAfd z5P8~uv#?~yr;!mo{^9;m_rO=^?kZBWD4P6+IyLV9q^?MVgfwu))=y0;rL?npihN+* zO_W1JR@~}_nN5AK7ge{XU%S0H>#;mhn`vwRxVe&Rq!_KLT;96TfOKMrm^krm@$n(& ziMrGHjTm$Kx9E0!Tw>i5y@EkZ`A$u!2P4{qyVz{}6D=Y`ZSgQKVA*?h)t~9+-j*BX zktQ@|-yLNB_{O&;Ao?j;*gKb7YNoK&lOE<#r0sXg+D+wL&nuz*vq0Mz}eT_X> zbbIJoXtp2ELXPo2j$FZ{u|j@uCKV6Y+D@d~O_)3@20XU8J7sZyu8Z73I9PH6>d$^T zzt~tYrRovs5Vyf`+oq8F|39NYq-lcxBJ$*x9|XC~cK^KZ0nn{Kw~1N7q`@i=qIukk zZb4z;7c$X}R~x=}qO7GNF0B4v_+em!rLf?MBDK2MUp!=Vs(UOXr?_CXip0HHX!d4& zT}yOis5h>S8dNN{H3x}r7Cg4lwiH{C8}Wf6O`@;+P|DR~bE`^Hlg+6NXpZ}t#Q-fv zVewr>Uy8oPGg;?r&YHDAu2rZ0o7}*roOhpSpdnjMP;e#cpxRLN?e#`LPw*|-(r$qtgPIt;(f>rWFFV(M-OilgLPT&g=;NWYy;?8a9C}X6_I`gsPY2{Q$5Hu zYpmY;WrA6zhRoWUTv8(X774w^P4@Cz{Fn}hiJD?@ZK(x)FrTD@KRVZw%3NDh!( z{jRMKCo#sOiV&H+& z-0#a`L&uT1TiJh>XP!o=96!_;Q`3~_A7D(@)0PaGo3&cFMM$b9n-=H0?~^2CTrCSK zEC>cCT?B|mm62y64g4sBwQxm(`v3mA}(>EN5bI z#BbR9cBXGm_F#Itbhc3AJHu+WK&NU+dewQ6tK3IV^3@r+&#|+rYpKjiYYkXK9Rh4 zIvhgXa;OpYX_}|p(`H;v`x|HE)CpWNXTDTqwsnA1nib^ zP+IZ(b|O?)PYbfpgrax#cf-{zklEdFEU}UN1-Z{j=v`WjHsNGxN*${oSy;@86Pr#C zoP;1*y}>0M;;$w7bBJ4VL!i4H|tg{oFcWr;0S~e72$U z0HeAXn!#wen(~Q+{rA-N_1k(~)N`X>vgf)E#J<(5?frUPsv%cx5TBtWkB-r-=EHdl z(p^W2;dx^5G9lYyI|ZW!AM$NmO32?_vniM$j+A06&pu(PeZdT_sa|V8kAfg4LDQ6E zbi8Br7TKa(mj)K5=T=A-v3Eu?wy#@S126uo4EbF+%v!aW)5gDFcPr)iknhN8x>!A3 zX4X;r4cL|BYZQa6LifYbCebm;QYYowm8};v@u@?FHy>pRKkT>2`_h--`nnxHSqMed zVjvZMK_2-+>H(hFfsrA-bd|T(JpVVqHEy%)y9DPu&Kc&#sSH)e!Ey!N6aJd(d zUV5je%l^9MnshG2OVK@i?FtzCH0&LyYoO{w4C>eZgI+m^zT{ICd-P2e@ zgoLHM&L z(eRo| zBRt)ln&(0^2ax=t*0vxEB{RvK5C5K~!rH8kk5y;*ZGZ}0QKCghK9bULNA{i`I@L6g z^E-1`c}k<)MT(OKYPFz05=ZK43vM|Enax=^y^E_Z4(LI;1Xw&6B9 zxgGvg`x6wSk0G-lZx;e^KE3ICdU-~ zQi@4FIqtBZ4$mSi-9yRUR|=BO4Ql{8Tle$YCgA@Jcv+tw8@R;3peUig#R+Nj^hRiF zYV69~MH6##`7hDPFzr+Bv{!jt9 zhJPvb^Yc`d;cdvc*MA!p-;C3b?o3fJP9Oivq^*Y!ao0TZ+FP&ceHp<3aS|IDs^;DT z5HKD4F;Q{M6D9)@Pi(lF1w**(ASL39XV<{+H^<+vc5lISbtYw9yi^N9%iOS_mhNoh z;Hs|M!)C$e>D@Uh^l~j4BfJ8MKXPBcc3b=Mt-b~w-KMZyo}`DKfPL4dfT&Scp$}SQ z7y}ABAbY(ON0pHn`GNSy_^n-tVGs%1(1Ibt+T33kvFv zT@_jvF*^aA3nWoVyZ{%60&jQON%KoWA*q;+#wlx`Z|FP}qtpQR=fC`=T!9GGh5&Ia z)c;e1S5?Z0lH zCL+d?%E(EljK1fQaY@sYjpV;uTJ3X0c~50co9j{RsshGXQ)rGu)!74ynC!rd&BL)| z#`F205(w2hf?b`DWf*)5aCr7{^iwR8MXU|#-iqYV4y^2_;o*qo6S-vLZ2pBxPzsarYV4%r_lR0-5Kc! z6;VY!7X=Ee;f@HUeF;&!6Vx?y78ov9V7GW<_x_FH&kf5#WQG1zxa`bW=*nUlLQQ;+_w7Tr4fFF(VJ%6z_j^K00u53y%$tXZ6A>#FV~kXm$R6L7y=oko6J#(cUJuFjDla``E9s1XhthewqjGPP(OfSl)yWKD&(5C)onfBu?PgYl!D(dRQ?wJf0-iKLCIGoDS*N029$!z|U3kFHXWGX+*O{Dw2e_9eOn zlS*bs(hkQIjy&xwU2=V&eS!5y%6!ST635Cgw~-ykon0kfral*&;Z3`e1|*VgQ;s?< zm@4`WBbdlIMOd|};-;98XVm(1UPTFOUuC9)re?+Mu*rT z+Edv)(r;|xkuls;9aTyv328)Q6QvEmXvXs95_veBxqov(*Rq@LrvSp9`;TWeGG;LL`e}vs)Dwkhlve%z zR2O_{rHb+h_gtl;8!>BY0aEN)d|6|2K3R7HaJ%5JL!Iw~N}me?2UOjLWE;6-31wUB zccvHQ8nr%vW!$_{WsA>nA>#&@paV;BAG<)jD?0?}T^-$3qYWgGO&D7xR)uSL{6Wnw zE=bo)SLng(u}?K4#j{63c%vV=Nrn{CrKVYQDkWx$5+!k$duM8yo}ajhSW<7`@OvOc zCp)5IF@^Gd*J{W766m!{lbH#L)BW@?XC$-;(9NjW_;4JnoZ~K!Fnv!k3s;E)q^QoBKQFae;?)P!YqAPxyRsf|tbS0Ss0#4tJ#B^Ey%?xir2rf63d zbe9rB%Dj+5a z`FROStg`&0&*D~q9{z_SpiGy-F``eFb#6#7B#b5{u3YrArxZ!6*cwDP#sB|VJtq*5${!vL7$ zWgFP!N?g8=bDp+zF!t!l8Y!7(AwthIoq=HgaGg2lWL>N5J|N(RUP<>QLDsEHvRzw1 zBk|FP$hd~)-h(64jfomiOOJzm?K8-Z_3J|Z{Ir%Pek44l_n`{*cy zRNQjmA~+bz<`ex+Ie2m{Ji#k{{*B_NkW;z@k@C-gV4qg9it+4teD<<3@ZaIm+rXMX z6L^2mO!H6Pz5cHy#~jEogim!9lnfz~hDP^u9^=vT78IcrylS$Bk9z+a#06;cN&3-! zNqN%H&;cAlz3#jJ5up+Ii2pK#>;E+4;2asZ{0@q4yWsd&6SN5R?t zwqzZ{C>RZ6K4SM_-^C*E=l==r-eWrhj@WZ?^7lN;+Da?7v@1@Hol1*tp9o;TtH?5bPc|TVQw{NX~}=wUJL{TvyM7DBs~MGTuU$_jcEIC3c`o zWw*<;7=)oR3ckGhmHeegsWQkHPS%aRy~uZp$ItdpI#2-OK3!3%+ycrv8>om6tA1l6~(D&%N3DQdcOx$-y5HqG~?@d%aV@ZBgi&!r;fO%IZoI_ z7oLsbSteu_nytDC)C>62-S6sq2`A-LR<8&;P^|~2=I5gEtC9ebkuBJ05qx0`$!e~) zD@XzbBm!4BjYf@H9^O0DNFA{!tfa~HB2jk~asBqIeon6%EmnGpN~vuEvCoQ`hl-{B zemm%~W7P)0OSa9klRCd$71GI;D2;f_)w$7@X^pIl$B(u3<^Pmhe?rjzA&kE2d`LPt zUs1AAD>|rBK6$$Q59kB~wceczq1^Zx$PU0T+U=9i(HaNbW zvlc6(MaA6`w%j}4Hm;fZ%830tyGO@u9|f1yx79`12`|v8h_0pJ&`&1K8(OyqMdjq*IC(z-qF|qzQkXAgyjed^ zRqUn$j{4){PBiXeaq;fnEI#tInS*m8bLEu z9R-`VSeB}Y=#qr&D45ueT9}#^o1u$D#{~k$)U;_SDq^YK6Xai)FUHJ0jw20u7Vqas z)Wu_jRo&Lr-g?!d)o6h#3Tc#P=8h!Y#E;+CEFbi=?9cC?$v~f|Drjlf9p$+o+VCHJ zo5t$lsQV}SYj^o(OtE>#0L?z-QoLYgu6^-lqp9bP{Zr?+QO~dX^WSUrw?+r3(DEOU z?BJw%9WJmfqjSHOQvbD-7c<~wgYWfv>{-}najE00 zpg-G1Zs@mJ0gDsBRy!r*`iD?+!lDE*HU<53GEO8X2{Yk?;mF{i8eT6aK_0g-zd4{AhJ@${0 zShf3c0TErvp7zS+gad$x{z{_9_kanLaFV3r&eArrvYfZ)sbtl$nGAVV3D5|c6nT$j zP=v~cilccGZ*o&TTnl@#rhODp|zS9gue*O3~DLVO;P3GWK zu?IM!dBySupsXA`?y|R|sRpBTH^a%g-s2&5cVK0z#%hm`pIZ(YG&?LY`KarGZN~VQ z+e218joVc1yBzmR#{SVZA&V^_8%^ngX;0}IkL5PMI!Hk~`3xa?N|sjMdn;%&1UHNF z{MygyfrWu2)K8KqJFU};90AiBF|}&rxq4P&9(o{t;jvurR048E-U$dr94_18B;L~P zzrE;n-K3IRp`!nRt{i;}Y@ezk;p^z~R7LlkD&%EP4`Ai%P9|>;VI4caBO(jciTn61 zKH)blFkEfmBe96(=VhX2yD0TzyDwhCHD&bWpheHzx{%}T9vG?~d((6nM?#&f#>vf7 z&!RY1)Bct)@dz%%4wWASPIk*efTz!s!Fb#|GT~gfOn!-&`BV!hRc3_4W5W3hp~_*` zi;k9N3-#)ukeuFX(M)H#`_0#e4%=>%zct#wQ^X3JsA)~1r4&A+S}xn+^B~z+%M{R| z|Esw#fok&H)~4;T-WIF1l|ewMZB^z7l{u1DoG6ok%z+dlBC{bN0Rm}l5lw(l6+#&+ z3dn32GB8##2@)YkrZA%fh#@2~gb)ZMxgXe`d;fdSS$C~}{p+qjYq40#_kH`@d%yeL zd%wee-e52EwVEr-j<1ACbXSeIR>l(3*UY_GREKeyx z`c+oYtwJRL0>EW1ZENY1f!6-b3+t^P=2xzVke5kSDJfDm&W;wli`+WbyV6K#E+mb;=Kw|*i` zNM^oPO+>jD@~FmcXXdZ{mCM=wzSV=-u;&rdIT#SOwHTi!1%f5yc@j-=@)U~eJ`|-w zND1DMK8(NAn)w>TW2=I`>9pAgc1EHGt~POszjrN_*d16e)1C-vfXnDe@wiJVlZ?lz2o|1aawJS!eLt zA@Yb6RVKsZK@^AnT;Ig>nIq`8}XY>YagmXQn;^&eKc)hQ^Pb z&U|+%$?Cskfq+9$-@(`1Ns_;8y7*U9{I?5*7G~Q^glYu0e+BR|rJ9|YZuvgGBuT9- zE*Gu$bN5eH5|55mx3yTS@#oqme|uW%|KXi4%s=#Zj_Eisdivi?i~Y2p*tqCretYrk zNDzn%=3W}%ZF-X1g`(Y7K4rrO3bNzX5r^Vk8XRN4sTVP7&FE6{MztF5 zR&Q&Ss%{C=Y@_*4Z<=k-c6R}o))~TFL6|fE7#H(Cb9VD6Xy#OGzziO{T`9aTH-BGH zERGMLZBX+lduqU5lF$;Wrh+{^2!m*~?T2DabF& zzW#wW0Q+PdvGMl>(n(k*kamL+&`dpdNn&sH;gw+7OJ8+-n5?BkpN&JnI=-)|Wd~;2 zYcOkek*udTA%{0p_0ufDJ^;w!uM8(}|X zo^_%XL9%H+!55lU+r8dbtd2w?5M6K~dUw#B>bF*Pr=%4l3y(`fOK$ZxyY0{u?9k)= zNpB$W_PXg4$uxpT{#zIeV2gl5+~%9nI|W_xH=X5w__+I1j72HB*yyB#&D3sSAZ2N) z-!_#$y4@PTR^{+>IT3ApZ54v8J^(t|Tk15lk`6v&-oS$97JY*`mFQwZAF1snu50R- z$*>vZ^*q1*`;*;vv;&Rbwy~9afZ2du*+;Ml-`@bKLa+$rXjgy^4iS;xeZ5eNlh1zJ z8NL5kZqc?8FsHXo!-bztn1So(7@3 z4gdx0rC|XM2~<;A*Ir=2Ic^A8NfpoTjJeQgo7wB;Lc!@HF(w6{(AtwTozzcTn z--p;{u<87E^GzmRP`Jl!?A9x_G8HilKf;eET z4Q92V-^2$N?y%bM#rEq2b`R5=b0PPoEfL9(nRMWl>)u0srUf+9BB?!R@coIc))DFj zU(MM*jBBeBZwDAQ^1n5IVWXp)^}(ao1{>I=yHyJc)UD7d!yfe$&Inn6T`USgPZo6_ z{5a}4E@^8LIerNBS|5E8fNe}3eW&h(9d-Y*t!}NL%wO8*gVwvl$^i8hVG-^N+Ww<&C~;zTOX>Tmmm)U9IjDacSH^Vy3;z~v)s(mAi-(o+Y^^+Ij7(Kzh15ltW&G0 z*?a743jDFe;2lDk+ku@eC;!sHpZVW;d?a)#Q$O7Ru=IByS+6Kv*6w$`R*N&!kvs$( zJ?sN?h<6|1ua5$Dgit$N`ezvWU*hs#1i}C5?G0VpvPR67NLbaa#0&K?;uzT+cNV<4 z%DoTKS0qO)=mj65?>u#eJ$xIA3*LUkll29XPjc&xI*x6qaU6S$7fy z$FW*zj#DSty$&TNoHyb8HLq01Wn@uoF&^50!k?l^_@)_-$>~Qh4%6a;vQ?u2uWvWg9eag zI2^+9iKVhwe|taD+bO+nZL-e~+VV>K`2(C9TaBNM&XyB@_n!)(d`6>Gm}qfEH}fCt z*gc#3mPYY95<&bzoF)_N-#P7Bp=~|mZEf_iJB@A4JnaSL9Kjs|gkvJVJMt)A8TwPW z2|cZ}N*un1uv&!rF$codkR{!fI;t3XJSG)2h7Ie#I0J3qgVvDefiw; zl%8q)-OKeT2N_xctg!#_Qm+FL@!Cl(_-S(%g%b3WGgH}-0IVjb8|!7N{%nZ zHR>}?(Jf2gZm-*_okzY6oyvecaKSm{Jv~#l9F0kvPSXxxeIcIL>8sHLOw0L6D>2Ph-r3q24DPl|UxoU*9?#;Lv3N`ssfj0Q~lZ;0J#40P%h9)OJn7vW*JI}VZ zWXNmu8@#VFCAgy>F0Q~h7ZyPBpc5iSNZdZ|oKgI*V?9uZPeXOI7bW{!M>?;7+aSRu4 zWk(p%7cvZJlrfpB4cR$~zF>$$4}_QkQ~-PF()K-|ZItHa0T&pjI+AcY zP99f7vLjzEw!`LrEx3)lvSfnKAJsx&q)1SM0V)xj%jcYu$qmr@CAwzIxHHM+nhQc zQnK5SUV&jYdcJBqxoy{8O2HE+0a$jskvEZCSJHe2eac-yd_)+Nypu$N#v*nDTm}pC zO%WX}+uLeOfyU5f{f|EtZF{61P?u?XGQn1{G5#61ce?3l4uJ%a_ydUqyjj`S(ku=( z6Xu3VCmCFwfta3k=WeY0XNM*sV2sm$+IhVE_hePefm0=1=1_bvFz9)8wV_R$y~&0s zlVn3M)p;3S2)3($>{D#KVL7Ys>ffxYc*6t3O7q`&Po$J)BXs{xvH}fQUAARSQr|ho zum1mxd;gaa<(~y@)Md!l+ny_CgaQ2k9d^{A-D+Skjgl%CN@49f28YPnH!FAA&z1uIOizo!Px_Cp2sg{=WTgJbID zV!*gOou{m59X)_FzKx`03MB0#voxhXcK>mKf>_0r8MMtQL;m=ow(JbT&G`B8dx{O! z7e)%CMV@l~D`Pq5$3F2HVz=JQns_~gAX+@;D})wlap#@smO}&Mb6XQg)EOGGT1ZW( zvqW^p5BOj(pCVkm!?N#d%hF^^+}Q*Wc8hlfYxNsQm0+hNl&O4|Z8zq~3gjj!RY4d- zQZr5H(u}I=H>+RJDBX#TOAY-6*@KwA zEE?%5hs5MaCgh$>c>aWbaB`hAt+}hi7%04xVAm+I0PR{93y0u=-eNY8G7Hi)w#b++ zr9HTqAQuM7nhY$0uC#q+X8h*puC`PS`O*>~Q+tQDwF87kS?=7L6b*f+_Qyb^)|^xL z8)wkeP{<@_(ERxebnFG&W54ONWF6EyTnAg>`umJa`E%ShDT>pIi2aCv z3|=K1E0g9cM!N1-G@UnB8NvK~ucLVz*ffu@Xfg6pRyB#ipe5hNGdt%vq5bQWrYp?} z^$F=O9fW!MvjrZ}?rlbCL)f#ab|CMpqEsucLl(t79lTJ0D%Lkq7XR=Z|ArhZD0l`yLKnlS4i)5`2k@2)#3Myq)OuaGDgpNr0; zy6PE@^wa+47=v4`yIR?j4w~)#TS@-r4e`fceW0-`Zq)CRZ`KDt9`|nlv{CwrsZ(Iu z6qTw+?(#XcK)GLC9`?du^7!?=`J=}8ILDsgy9v=Ts&Wv|ed5kX63W9w590N%D~Ypt z)R1pyVD_Xwodr8)pDoX>>OETNM)3)U21IFY|8p6Rdo`^7q-1`CBAz1`W4_!-))w9P#67yr!`7VR4m6Zu4@!bj0t`4fZz;O_BH%WYO67nk3~?+KP7ud>!@GJYmL-omo0+8nO*}K zo#l@;v4}~5;~jWc> zWN#wSTk?&vHjHkmYzxgv)lx(4q0vyLk~a_2Lb5Xuyn_4pweOOT>YnV)xu1`|_ENt; z&7Z$iT+m}q!ZrWeYuf!8xLQBPsWyQx~ppmYJbbQ=2-0OWI=huY;$+i z%(*`p+@K+2E~Ms5=C;NhfWBE-fax2i{#|Qwz=_Rd?BO3(`1{C=@;G#&SUfm(guDN> zIAU;k^q8<{D~SkuVAFWrrImE)Wp_H1Nu$g~)gWAy&gkIiBAgnxeDuWif+8*QO(I+z zV$^1;We^$?XHr+Jz6Ti++o^3W^W}mZL{Mk*hApEo6ASOIN}XtpijN6f{nU9kaZ+>D;j4yEdbObqagdWPX?t-b6q~j zi4C;@oyCljz{kj4EFeEYM}9vsU`i`+9dDEV^3hpU2WB>i7w88fF_f3A7N66bgb9JhYcX{ro>_q%A4C_DgR+XSQBInSH8VqcHoPb3vb23*@Jj%kAqH@ndX|C> zZ5pMIf3#ISDE6#b^M{BF*tF9l8?GXgz$=j&hTn z5QqmSK2if0NMT>hWuVdEETd5aMg{!l%@g4v&+qvmC-+3 zuS>>6L#q4{ds@rRPm!lU?lIXX;`riS?ne&0&uGZ}Y)A=HeKRA}yV;gT42vXFS-;gN ztSJQmkm7nr>DsMu1W4h93Hp7hWz$+{$wWu|wr)Y6aTyHtKKLv!v)iBM4Nl})(h@Ri0&pYcU~(S3 z?@}LT&ei>`g)Wu}+7g8(Lo7YioDBRD^!5dqR3MNUi zh+hY)U9nMg2C<$*k4}Te2+mko^W7$+B**GGHzwq-{rb3h1O5{x4Z8X=*H2=2oQU2j ztCoBG=<2%mhf|{A0u~;x^)RZ0aTL#x4w>iCd>HJ*u$1&B1^;&HGv%13)dl&T(P*?^ z%4Gct z9&D21YAQ%|3wRaKk=nSvTMwA!t(sR|bToMsBcg(nTA*3Fo`yft%aYbw=hsuR8YRxS zD8M&t?voH^^~ETy!kDnj`6MFZ z#YsUdRvHyyQ(11h3K9?!VrpJEvvmUc(m16wAh6jSBZVDY8^1UJ90EC+*-~dNxRhgB zfrP_+Pq72JhGe1=ONpKOibRV(#2sXHjONSAtoHGT2atAgS0_Bj8(k*HLSM8vx!OX? zd?WCw^;y4NuLe@jY|5E7`q+WdI=%B~aBUr%-0%WoO=DD~VC>?A%*oSDJmWMEJ>4c7 zE79|3mMhU&5&rOr2p#BZ(8VnNQCjQLIuX;wkKU?)D4&*Teo(Wk6Ge9Od{e8@yD067 zQL-<=6xS0n>5BDLi#g8!?u=?N;x#S1Qq~oPe5y_Kk4ayn!*i*qT;d|7l^&&gv>A0-u{kN zmjz?1`b==^>hl8mlerD{mbBJE zDdTq4Pvc_=*&*M)F=o&c%W@np^bogoBN>9hBr#%l5>G?x40okay_$*Uto{}K?pr}g~$eY zzG_8lB66c)+bvx5=`iC3;4q9>pMgRZ+ literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P124_I2CAddresses.png b/docs/source/Plugin/P124_I2CAddresses.png new file mode 100644 index 0000000000000000000000000000000000000000..7a43124e382b7615ba83d99afc17966fdd60f993 GIT binary patch literal 47435 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�r532?;HWRcqA~l^rE1r5c5^`|ZjDAk<+) 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P124_InitializeRelaysYes.png b/docs/source/Plugin/P124_InitializeRelaysYes.png new file mode 100644 index 0000000000000000000000000000000000000000..ea156edcc5c382f1fa79844987f8254e6af11be6 GIT binary patch literal 10831 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 D<~PJL literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P124_ResetRelaysYes.png b/docs/source/Plugin/P124_ResetRelaysYes.png new file mode 100644 index 0000000000000000000000000000000000000000..ec0560021f40135510d9db3eb05ff3d2de8d9fc0 GIT binary patch literal 8763 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 + + "," + + | Switch on the relay at the channel given. Range is 1..8, but limited to the number the plugin is configured for. + + " + " + + | ``multirelay,off,`` + + "," + + | Switch off the relay at the channel given. Range is 1..8, but limited to the number the plugin is configured for. + + " + " + + | ``multirelay,set,`` + + "," + + | Switch on and off according to the bitmap provided, in 8..1 order. The bitmap can be given in decimal (``nn``, 1-3 digits, ``0`` to ``9``), hexadecimal (``0xnn``, 1 or 2 digits, ``0`` to ``f``) or binary (``0bnnnnnnnn``, 1 to 8 digits, ``0`` or ``1``) notation. + + " diff --git a/docs/source/Plugin/_Plugin.rst b/docs/source/Plugin/_Plugin.rst index a643dda81..5ddba4fdd 100644 --- a/docs/source/Plugin/_Plugin.rst +++ b/docs/source/Plugin/_Plugin.rst @@ -140,6 +140,7 @@ There are different released versions of ESP Easy: ":ref:`P113_page`","|P113_status|","P113" ":ref:`P114_page`","|P114_status|","P114" ":ref:`P115_page`","|P115_status|","P115" + ":ref:`P124_page`","|P124_status|","P124" Internal GPIO handling diff --git a/docs/source/Plugin/_plugin_categories.repl b/docs/source/Plugin/_plugin_categories.repl index 30917de4a..59bb7950a 100644 --- a/docs/source/Plugin/_plugin_categories.repl +++ b/docs/source/Plugin/_plugin_categories.repl @@ -19,7 +19,7 @@ .. |Plugin_Light_Lux| replace:: :ref:`P010_page`, :ref:`P015_page`, :ref:`P074_page` .. |Plugin_Motor| replace:: :ref:`P048_page`, :ref:`P079_page` .. |Plugin_Notify| replace:: :ref:`P055_page`, :ref:`P065_page` -.. |Plugin_Output| replace:: :ref:`P029_page`, :ref:`P038_page`, :ref:`P041_page`, :ref:`P042_page`, :ref:`P043_page`, :ref:`P070_page` +.. |Plugin_Output| replace:: :ref:`P029_page`, :ref:`P038_page`, :ref:`P041_page`, :ref:`P042_page`, :ref:`P043_page`, :ref:`P070_page`, :ref:`P124_page` .. |Plugin_Position| replace:: :ref:`P013_page`, :ref:`P082_page` .. |Plugin_Regulator| replace:: :ref:`P021_page` .. |Plugin_RFID| replace:: :ref:`P008_page`, :ref:`P017_page`, :ref:`P040_page`, :ref:`P111_page` diff --git a/docs/source/Plugin/_plugin_substitutions.repl b/docs/source/Plugin/_plugin_substitutions.repl index 5b21be98c..9f0022404 100644 --- a/docs/source/Plugin/_plugin_substitutions.repl +++ b/docs/source/Plugin/_plugin_substitutions.repl @@ -10,3 +10,4 @@ .. include:: ../Plugin/_plugin_substitutions_p09x.repl .. include:: ../Plugin/_plugin_substitutions_p10x.repl .. include:: ../Plugin/_plugin_substitutions_p11x.repl +.. include:: ../Plugin/_plugin_substitutions_p12x.repl diff --git a/docs/source/Plugin/_plugin_substitutions_p12x.repl b/docs/source/Plugin/_plugin_substitutions_p12x.repl new file mode 100644 index 000000000..5288de500 --- /dev/null +++ b/docs/source/Plugin/_plugin_substitutions_p12x.repl @@ -0,0 +1,12 @@ +.. |P124_name| replace:: :cyan:`MultiRelay` +.. |P124_type| replace:: :cyan:`Output` +.. |P124_typename| replace:: :cyan:`Output - I2C Multi Relay` +.. |P124_porttype| replace:: `.` +.. |P124_status| replace:: :yellow:`TESTING D` +.. |P124_github| replace:: P124_MultiTelay.ino +.. _P124_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P124_MyltiRelay.ino +.. |P124_usedby| replace:: `.` +.. |P124_shortinfo| replace:: `Seeed Studio I2C Multi Relay boards` +.. |P124_maintainer| replace:: `tonhuisman` +.. |P124_compileinfo| replace:: `.` +.. |P124_usedlibraries| replace:: `https://github.com/Seeed-Studio/Multi_Channel_Relay_Arduino_Library (modified local copy)` From 21360c48bfff1d89a7f32ff57bb7106b6f017abc Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 21 Nov 2021 21:57:08 +0100 Subject: [PATCH 10/17] [P124] Include in TEST_D build, aad missed documentation file --- docs/source/Reference/Command.rst | 6 ++++++ src/Custom-sample.h | 1 + src/src/CustomBuild/define_plugin_sets.h | 1 + 3 files changed, 8 insertions(+) diff --git a/docs/source/Reference/Command.rst b/docs/source/Reference/Command.rst index d4a5fa359..063c0eaf8 100644 --- a/docs/source/Reference/Command.rst +++ b/docs/source/Reference/Command.rst @@ -663,3 +663,9 @@ P115 :ref:`P115_page` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. include:: ../Plugin/P115_commands.repl + + +P124 :ref:`P124_page` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. include:: ../Plugin/P124_commands.repl diff --git a/src/Custom-sample.h b/src/Custom-sample.h index f2e422bb9..d079feee6 100644 --- a/src/Custom-sample.h +++ b/src/Custom-sample.h @@ -379,6 +379,7 @@ static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = { // #define USES_P115 // MAX1704x // #define USES_P119 // ITG3205 Gyro // #define USES_P120 // ADXL345 Acceleration / Gravity +// #define USES_P124 // I2C MultiRelay // Special plugins needing IR library diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 413094737..fb6beb87d 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1137,6 +1137,7 @@ To create/register a plugin, you have to : #define USES_P094 // CUL Reader #define USES_P114 // VEML6075 UVA/UVB sensor #define USES_P115 // Fuel Gauge MAX1704x + #define USES_P124 // I2C MultiRelay #endif From d953f68981edfaf84adce00732f3fbe8746a8da3 Mon Sep 17 00:00:00 2001 From: svollebregt <42436341+svollebregt@users.noreply.github.com> Date: Tue, 23 Nov 2021 13:17:58 +0100 Subject: [PATCH 11/17] Further code improvements -Moved noInterrupts() part to Itho library - Reduced length of status log message - Reduced noInterrupts() blocked code in ITHOCheck function (handles received RF packages) and made log more efficient --- lib/Itho/IthoCC1101.cpp | 3 ++ platformio.ini | 4 +- src/_P118_Itho.ino | 92 +++++++++++++++++++++-------------------- 3 files changed, 52 insertions(+), 47 deletions(-) diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp index 97d158879..9516b7245 100644 --- a/lib/Itho/IthoCC1101.cpp +++ b/lib/Itho/IthoCC1101.cpp @@ -373,6 +373,7 @@ bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[] void IthoCC1101::sendCommand(IthoCommand command) { + noInterrupts(); //Moved from plugin here to reduce blocking code CC1101Packet outMessage; uint8_t maxTries = sendTries; uint8_t delaytime = 40; @@ -411,9 +412,11 @@ void IthoCC1101::sendCommand(IthoCommand command) finishTransfer(); delay(delaytime); } + interrupts(); //Moved from plugin here to reduce blocking code //initReceive(); SV - I call this from the ESPEasy plugin to prevent crashes } + void IthoCC1101::createMessageStart(IthoPacket *itho, CC1101Packet *packet) { diff --git a/platformio.ini b/platformio.ini index 4fe062eae..88eae6bb9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,8 +20,8 @@ extra_configs = platformio_esp32_envs.ini platformio_special_envs.ini -default_envs = custom_ESP8266_4M1M, custom_ESP32_4M316k -; default_envs = custom_ESP8266_4M1M +;default_envs = custom_ESP8266_4M1M, custom_ESP32_4M316k +default_envs = test_D_ESP8266_4M1M ;default_envs = normal_ESP8266_4M1M ;default_envs = test_beta_ESP8266_4M1M diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index b72b4d66e..6adb872c1 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -12,6 +12,7 @@ // svollebregt, 11-04-2020 - Minor changes to make code compatible with latest mega 20200410, removed SYNC1 option for now; // better to change this value in the Itho-lib code and compile it yourself // svollebreggt, 13-2-2021 - Now uses rewirtten library made by arjenhiemstra: https://github.com/arjenhiemstra/IthoEcoFanRFT +// svollebregt, 11-2021 - Code improvements // Recommended to disable RF receive logging to minimize code execution within interrupts @@ -50,7 +51,7 @@ // mosquitto_pub -t /Fan/cmd -m 'state 3' -// This code needs the library made by 'supersjimmie': https://github.com/supersjimmie/IthoEcoFanRFT/tree/master/Master/Itho +// This code needs the library made by 'arjenhiemstra': https://github.com/arjenhiemstra/IthoEcoFanRFT // A CC1101 868Mhz transmitter is needed // See https://gathering.tweakers.net/forum/list_messages/1690945 for more information // code/idea was inspired by first release of code from 'Thinkpad' @@ -93,7 +94,7 @@ volatile bool PLUGIN_118_Int = false; #define PLUGIN_VALUENAME2_118 "Timer" #define PLUGIN_VALUENAME3_118 "LastIDindex" -// Timer values for hardware timer in Fan +// Timer values for hardware timer in Fan in seconds #define PLUGIN_118_Time1 10*60 #define PLUGIN_118_Time2 20*60 #define PLUGIN_118_Time3 30*60 @@ -142,7 +143,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) break; } - case PLUGIN_SET_DEFAULTS: + case PLUGIN_SET_DEFAULTS: //Set defaults address to the one used in old versions of the library for backwards compatability { PCONFIG(0) = 1; PCONFIG(1) = 10; @@ -248,13 +249,13 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) String cmd = parseString(tmpString, 1); if (cmd.equalsIgnoreCase(F("STATE"))) { - noInterrupts(); + //noInterrupts(); switch(event->Par1) { case 1111: //Join command { PLUGIN_118_rf.sendCommand(IthoJoin); PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'join' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("join")); success = true; break; } @@ -262,7 +263,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) { PLUGIN_118_rf.sendCommand(IthoLeave); PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'leave' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("leave")); success = true; break; } @@ -273,7 +274,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'standby' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("standby")); success = true; break; } @@ -284,7 +285,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'low speed' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("low speed")); success = true; break; } @@ -295,7 +296,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'medium speed' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("medium speed")); success = true; break; } @@ -306,7 +307,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'high speed' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("high speed")); success = true; break; } @@ -317,7 +318,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'full speed' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("full speed")); success = true; break; } @@ -328,7 +329,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) PLUGIN_118_Timer=PLUGIN_118_Time1; PLUGIN_118_LastIDindex = 0; PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'timer 1' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("timer 1")); success = true; break; } @@ -339,7 +340,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) PLUGIN_118_Timer=PLUGIN_118_Time2; PLUGIN_118_LastIDindex = 0; PLUGIN_118_rf.initReceive(); - PLUGIN_118_PluginWriteLog(F("Sent command for 'timer 2' to Itho unit")); + PLUGIN_118_PluginWriteLog(F("timer 2")); success = true; break; } @@ -349,17 +350,18 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) PLUGIN_118_State=33; PLUGIN_118_Timer=PLUGIN_118_Time3; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_PluginWriteLog(F("Sent command for 'timer 3' to Itho unit")); + PLUGIN_118_rf.initReceive(); + PLUGIN_118_PluginWriteLog(F("timer 3")); success = true; break; } default: { - PLUGIN_118_PluginWriteLog(F("Invalid command parameter")); + PLUGIN_118_PluginWriteLog(F("INVALID")); success = true; } } - interrupts(); + //interrupts(); } break; } @@ -370,11 +372,11 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) addFormTextBox(F("Unit ID remote 1"), F("PLUGIN_118_ID1"), PLUGIN_118_ExtraSettings.ID1, 8); addFormTextBox(F("Unit ID remote 2"), F("PLUGIN_118_ID2"), PLUGIN_118_ExtraSettings.ID2, 8); addFormTextBox(F("Unit ID remote 3"), F("PLUGIN_118_ID3"), PLUGIN_118_ExtraSettings.ID3, 8); - addFormCheckBox(F("Enable RF receive log"), F("p118_log"), PCONFIG(0)); + addFormCheckBox(F("Enable RF receive log"), F("p118_log"), PCONFIG(0)); //Makes RF logging optional to reduce clutter in the lof file in RF noisy environments addFormNumericBox(F("Device ID byte 1"), F("p118_deviceid1"), PCONFIG(1), 0, 255); addFormNumericBox(F("Device ID byte 2"), F("p118_deviceid2"), PCONFIG(2), 0, 255); addFormNumericBox(F("Device ID byte 3"), F("p118_deviceid3"), PCONFIG(3), 0, 255); - addFormNote(F("Device ID of your ESP, should not be the same as your neighbours ;-). Defaults to 10,87,81 which corresponds to the old Itho code")); + addFormNote(F("Device ID of your ESP, should not be the same as your neighbours ;-). Defaults to 10,87,81 which corresponds to the old Itho library")); success = true; break; } @@ -395,7 +397,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) break; } } -return success; + return success; } ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt() @@ -406,13 +408,13 @@ ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt() void PLUGIN_118_ITHOcheck() { - noInterrupts(); - if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, "RF signal received\n");} + if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, "RF signal received");} //All logs statements contain if-statement to disable logging to reduce log clutter when many RF sources are present if(PLUGIN_118_rf.checkForNewPacket()) { + noInterrupts(); //Do not disturb while fetching received command from buffer IthoCommand cmd = PLUGIN_118_rf.getLastCommand(); String Id = PLUGIN_118_rf.getLastIDstr(); - + interrupts(); //Move check here to prevent function calling within ISR byte index = 0; if (Id == PLUGIN_118_ExtraSettings.ID1){ @@ -427,91 +429,89 @@ void PLUGIN_118_ITHOcheck() //int index = PLUGIN_118_RFRemoteIndex(Id); // IF id is know index should be >0 + String log2 = ""; if (index>0) { if(PLUGIN_118_Log){ - String log = F("Command received from remote-ID: "); - log += Id; - addLog(LOG_LEVEL_DEBUG, log); + log2 += F("Command received from remote-ID: "); + log2 += Id; + log2 += F(", command: "); + //addLog(LOG_LEVEL_DEBUG, log); } - String log2 = ""; - if(PLUGIN_118_Log){log2 += F("Command received=");} switch (cmd) { case IthoUnknown: - if(PLUGIN_118_Log){log2 += F("unknown\n");} + if(PLUGIN_118_Log){log2 += F("unknown");} break; case IthoStandby: case DucoStandby: - if(PLUGIN_118_Log){log2 += F("standby\n");} + if(PLUGIN_118_Log){log2 += F("standby");} PLUGIN_118_State = 0; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; break; case IthoLow: case DucoLow: - if(PLUGIN_118_Log){log2 += F("low\n");} + if(PLUGIN_118_Log){log2 += F("low");} PLUGIN_118_State = 1; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; break; case IthoMedium: case DucoMedium: - if(PLUGIN_118_Log){log2 += F("medium\n");} + if(PLUGIN_118_Log){log2 += F("medium");} PLUGIN_118_State = 2; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; break; case IthoHigh: case DucoHigh: - if(PLUGIN_118_Log){log2 += F("high\n");} + if(PLUGIN_118_Log){log2 += F("high");} PLUGIN_118_State = 3; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; break; case IthoFull: - if(PLUGIN_118_Log){log2 += F("full\n");} + if(PLUGIN_118_Log){log2 += F("full");} PLUGIN_118_State = 4; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; break; case IthoTimer1: - if(PLUGIN_118_Log){log2 += +F("timer1\n");} + if(PLUGIN_118_Log){log2 += +F("timer1");} PLUGIN_118_State = 13; PLUGIN_118_Timer = PLUGIN_118_Time1; PLUGIN_118_LastIDindex = index; break; case IthoTimer2: - if(PLUGIN_118_Log){log2 += F("timer2\n");} + if(PLUGIN_118_Log){log2 += F("timer2");} PLUGIN_118_State = 23; PLUGIN_118_Timer = PLUGIN_118_Time2; PLUGIN_118_LastIDindex = index; break; case IthoTimer3: - if(PLUGIN_118_Log){log2 += F("timer3\n");} + if(PLUGIN_118_Log){log2 += F("timer3");} PLUGIN_118_State = 33; PLUGIN_118_Timer = PLUGIN_118_Time3; PLUGIN_118_LastIDindex = index; break; case IthoJoin: - if(PLUGIN_118_Log){log2 += F("join\n");} + if(PLUGIN_118_Log){log2 += F("join");} break; case IthoLeave: - if(PLUGIN_118_Log){log2 += F("leave\n");} + if(PLUGIN_118_Log){log2 += F("leave");} break; } - if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, log2);} } else { if(PLUGIN_118_Log){ - String log = F("Device-ID:"); - log += Id; - log += F(" IGNORED"); - addLog(LOG_LEVEL_DEBUG, log); + log2 += F("Device-ID: "); + log2 += Id; + log2 += F(" IGNORED"); } } + if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, log2);} } - interrupts(); } void PLUGIN_118_Publishdata(struct EventStruct *event) @@ -531,8 +531,10 @@ void PLUGIN_118_Publishdata(struct EventStruct *event) addLog(LOG_LEVEL_DEBUG, log); } -void PLUGIN_118_PluginWriteLog(String log) +void PLUGIN_118_PluginWriteLog(String command) { + String log = F("Send Itho command for: "); + log += command; addLog(LOG_LEVEL_INFO, log); printWebString += log; } From fa4b849fa85931460cfe7aa61a4b74349f2e669d Mon Sep 17 00:00:00 2001 From: svollebregt <42436341+svollebregt@users.noreply.github.com> Date: Thu, 25 Nov 2021 17:02:17 +0100 Subject: [PATCH 12/17] Remove noInterrupts() and Itho library improvements Related to #3801: - Removed all noInterrupts(); code -> not needed and tested stable - Initialize CC1101 packet - Removed not used defines from IthoCC1101.ccp --- lib/Itho/CC1101Packet.h | 4 ++-- lib/Itho/IthoCC1101.cpp | 10 ++++------ src/_P118_Itho.ino | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/Itho/CC1101Packet.h b/lib/Itho/CC1101Packet.h index f8c4f01b4..250c63458 100644 --- a/lib/Itho/CC1101Packet.h +++ b/lib/Itho/CC1101Packet.h @@ -15,8 +15,8 @@ class CC1101Packet { public: - uint8_t length; - uint8_t data[128]; + uint8_t length = 0; + uint8_t data[128] = {0}; }; diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp index 9516b7245..9e2d0961f 100644 --- a/lib/Itho/IthoCC1101.cpp +++ b/lib/Itho/IthoCC1101.cpp @@ -1,10 +1,10 @@ /* Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra */ -#define DEBUG 0 +//#define DEBUG 0 -#define BYTE_TO_BINARY_PATTERN "%c,%c,%c,%c,%c,%c,%c,%c," -#define BYTE_TO_BINARY(byte) \ +//#define BYTE_TO_BINARY_PATTERN "%c,%c,%c,%c,%c,%c,%c,%c," +/*#define BYTE_TO_BINARY(byte) \ (byte & 0x80 ? '1' : '0'), \ (byte & 0x40 ? '1' : '0'), \ (byte & 0x20 ? '1' : '0'), \ @@ -12,7 +12,7 @@ (byte & 0x08 ? '1' : '0'), \ (byte & 0x04 ? '1' : '0'), \ (byte & 0x02 ? '1' : '0'), \ - (byte & 0x01 ? '1' : '0') + (byte & 0x01 ? '1' : '0')*/ #include "IthoCC1101.h" #include @@ -373,7 +373,6 @@ bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[] void IthoCC1101::sendCommand(IthoCommand command) { - noInterrupts(); //Moved from plugin here to reduce blocking code CC1101Packet outMessage; uint8_t maxTries = sendTries; uint8_t delaytime = 40; @@ -412,7 +411,6 @@ void IthoCC1101::sendCommand(IthoCommand command) finishTransfer(); delay(delaytime); } - interrupts(); //Moved from plugin here to reduce blocking code //initReceive(); SV - I call this from the ESPEasy plugin to prevent crashes } diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index 6adb872c1..f29d9bf24 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -411,10 +411,9 @@ void PLUGIN_118_ITHOcheck() if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, "RF signal received");} //All logs statements contain if-statement to disable logging to reduce log clutter when many RF sources are present if(PLUGIN_118_rf.checkForNewPacket()) { - noInterrupts(); //Do not disturb while fetching received command from buffer IthoCommand cmd = PLUGIN_118_rf.getLastCommand(); String Id = PLUGIN_118_rf.getLastIDstr(); - interrupts(); + //Move check here to prevent function calling within ISR byte index = 0; if (Id == PLUGIN_118_ExtraSettings.ID1){ From 58803c1c061731453422853eabf17531657c67f9 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 29 Nov 2021 21:36:23 +0100 Subject: [PATCH 13/17] [P124] Add get command and looping that, fix a small settings bug --- docs/source/Plugin/P124.rst | 5 ++- .../Plugin/P124_DeviceConfiguration.png | Bin 48484 -> 53573 bytes docs/source/Plugin/P124_commands.repl | 18 +++++++++ src/_P124_MultiRelay.ino | 38 ++++++++++++++++-- src/src/PluginStructs/P124_data_struct.cpp | 12 ++++++ src/src/PluginStructs/P124_data_struct.h | 11 +++++ 6 files changed, 79 insertions(+), 5 deletions(-) diff --git a/docs/source/Plugin/P124.rst b/docs/source/Plugin/P124.rst index c52a56f6f..3d9ecd1e7 100644 --- a/docs/source/Plugin/P124.rst +++ b/docs/source/Plugin/P124.rst @@ -109,6 +109,9 @@ When changing the number of relays, the page will be submitted and the settings .. note:: This will also be applied when Submitting the device settings page! +* **Loop Channel/Get on read**: When enabled, will update the Channel and Get values with the selected channel and its current state (0=off, 1=on). The channel will be incremented each interval (read action). + + Data Acquisition ^^^^^^^^^^^^^^^^ @@ -117,7 +120,7 @@ The Data Acquisition, Send to Controller and Interval settings are standard avai Values ^^^^^^ -There is only 1 value available for this sensor, with the default name ``State``. +The Values available for this sensor, are ``State``, holding the decimal value of the current channel_bitmap, ``Channel`` and ``Get``, showing the channel and its on/off state (1/0) of the last ``multirelay,get,`` command or when **Loop Channel/Get on read** is checked. Commands available ------------------ diff --git a/docs/source/Plugin/P124_DeviceConfiguration.png b/docs/source/Plugin/P124_DeviceConfiguration.png index 5d56221af4cda92bdfb8a61b4b4c1cd7aa24e0c5..2a7ae2b2baa656f8bae5c680938767cdfe06e04e 100644 GIT binary patch literal 53573 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 CY}3X7 delta 42045 zcmb@u2~<*R|39kdyw%(2Y0^m3PNy<+DxBxje9EabMHBRxk(q*_DNeXM$J3;!B`PTn zm8s>Jnt*~(nV8~GnIotv4k;ojDk2JZ)9}9k`@8qQ)_<*g_gbt)viGx}={tQspQnb$ zKaVW^bWd%a#=lo9DJgYxeKz0!)e*QHn4`9UD>OTd+gEevbE4XZO!*YlhJUn`e?7~f{a=3fa3OwcIOnssKD9k#)vLBP;`%=w zE~i+KpOHHbRIL8_ub0iSU9>J~*?e?tU|pcxt9lR%*w$m0ro;$gm5c!_P`aYT!0DgC z&M0wf0J%_(3*8d@FU3F6K3=Rk@j!IqogWt$m5SN(i)agy%c-*p02h-m<~Qp#`W6ZNoNJr&LV; zrw<8GbTa3_6SNDlGUqi%^?~vRBoUUTq(5Zr&a0o^J z9T48o6JAnmFg^@*I{y8dKG;mPB_O$F&-<2IlTRi0Z@-j%wzd<={j=pz*8$E?0&HeG zqhoL&x5ku?iiq~7Zs?r|V@iO;!a;6C&GdW*CT~3<8LEqZ?M+sE73)Wy&&^p|W*MbFfw69wP*)Z4=+Al-nr4ljIP zU)djh*wdrW{hJxEX*0XFbTSl?IXW5Hw_67E5s;^443yO)qN!qAE{T;}y z-+OPtIKTsz_U=0u6&X-&_<6rUja~t}q~u?0XxQ4I!w1Ut%i{ywBf^G#!uz=q7MBr~ zoopW$XIN|AV)o$L6JA;4FT)J`xBqDD-T~gyUL?HfX`t>ZczBJ!+uOwUFK5^#L0Nat z*ZU_wi+>)Kd9KC-M}$Z7TAQgUGhwx@*bfd>11U$*!<9ZiX4Eep>V(#(X&UGrom`!kKwQDbr({$)s{5O=A>bw~vM^Dg6#v zqqK$}&0H`vLvnaNXjzMy94$H}pR@~-y)=t0mGt{a=@ybc=2h2N1BvV4@>9tYMHXd+ zi{v6Amg*A1J)1*ffC;|5BpZ+n&W6_1aaEL*RI;I)ls3KVW{T<-TB_`1A^C=%6J)S|E zQzn6+=ii#WOj~H^?N8jObZTTgc0y=ux3V4xj9&000lBi+(Aj8t)tJ4{l8Z=9_S)wP zVj!A=^R0yLHFrUOB3G5%Qd&_lPG{IPTs>GTQ;7Os%x$hEY$G2g+m#g{_<;8{Y6Nv ze9ozU(=wq-$9(0)DL&a>8rB=cit-8S@*DQxk7ZB)EsNN>-@%-F5@u1*od3{Hw8$)8 zS4J?D_l{`;v)YtZT&BcKUJxl;mMo0ByjzhC%B3X2DW-80+~Qan9@_y3Lv)F`No^lU zr!%J2i5)*Hy*w5M`ZxK$y~~x>F+r9%AX=Q@6q0duIp0$DdW$fbS3YB}&3!gaT$-sE zRNJurOu^G#!H?n8yU)fK!_Fh&;y&mOYrA6uCW2)A`i#6cmVE|@f#cz5Cy?J3GM;+7W*km%#?U_oBAvvaDw(?U9_#Z>70H7tf zoml8@AXsJ*d|C`w)oG0TEGGc(RfEUJ#|8OCqhyztX&1}5GMQ9prp**4pHJ2YxQ|l( zy6djL*cJ&)tHqk&rDO@cPAm_!U0Dx+D{l_f5Gv?9(&!WSM~^7QYv%EpFsUeM=i zTXN;m0sy+;|7zup^YZb~S#3s5jZj6_v_RuE3qZ>X#|`s>J(AMwo!CD1yAmaUjLDJ8 z`vf4|L9YmNc1r_bKU#5TMd~69TK3K8_9^;>`IiCu@7ss!CjpE8?Mr5)hWt8V#ua3qSkGU7k)H( z;t8AEOVOe%1QAb%vJt|BSev#gDCI)e(#*M<_{)H267$~1gnuG@jKk!w zeTIi*?1Y?(nxnMbOYNdE8Z*{lS{L$KWZO2rU)3i0*xmc3Qnw(AJ-MGvZ|j)*ccU{g5F&n1pBKTowQ|4T^pI3`Sln|}>cB~3 z!+M1ntxcWMixkcI@Y3}83yq(+vmaxpnKF`OU(sl?S?Vn6K?sK^4~nk2D*1U+K3M(mMR0&G4(#XQ>qT$>RnN_T*@!s2`~@xF2_EbX#U%J zD|6)45zkrsr~@vQEwziMEE4`Fg!Wd+f@o3T#&Zt zI}4f!+HTKGGVEr6R`MOGwtOTvNdEBH!WxZRIy|3K)o}SId4R0{+TU_1%A}ms0>*_+ z{qxP#_lDPI)F=ToANLfW-;1E=VPf^g!If)YQpW!nB{x$?YK(Ul8|V~8HcGf}K0@bf zp|QZ)uvPT}Xx~ZFJhueK)z(eNXh*U{IkEbhIr;FkM4-;&pN*`hE}b&*yD8}@>(#f5 z1Xbs`PFs2H2GMnfEdKlUJgYX z%YEZ_;679P`k1Byuv9J|}tQT(}U0lx!Ugik;O4 zLZoYUH3PZJ19mTTCWc>7&L-uv^YP&J22O)vWO&q9z2ES5+~CDYAIm;3EOW7;qTUcH zSU#1kCI8#Qu(31t*7%< zk~5_B;S=q911a@rUOxd^#?9L9w6$Y(9@Icng`|^$3Fp^}sX@c0|NameUL*(wY&t~A z(rZr#)OyW1;+%x%CF|4cVtul_a{5NFtcGQV-G^8^?n1KZa+&-}@aC^(dTJ>f6Wwe+ z2<9IzkD2h}_gP%zS8}VP0)+3h;XOwlC4?^Jl=!wCoQ5Jjd^$fjI6tV(Inh+zW%Yix6U( zJJy?(7G2xT#EqV_JIUnMcQ?$LK=qfF0@^dHp4`CCj(&dQckOAP?bZbcGuznP!?qel z;u8!_Ue#K$Gc%M2ohI&=WnOI&t{iKX3Ymkd7`u5_yH44!x36a21H9ycMT#x+z}S~gAZyy_6|Ltn~v#>a-8TgvmpeuS2lzV zpox;wei;R->_>>f`)#0Vj zl=HlpOOGXaz`RycBiEIcHo@j8DSi6Yh4reAkf)-ne1g!fas|MNv@**9ez}l&ema*1;|^Wu=#oTv@M0QgFtZ4Dzu9MMCrai(%a&Jl&Sqo4;M>{+Ea!H0gni95cL@s` zJahmNV{RUBPt47C5EFBk6V1V*)p|#>dBodPj9zDsV0(Ky8yrn-6a=ao1_U*nRu+=Ny zTAd2LDU9cDG~6B@G5~#}#cD7x$2#)E@=UJW+p8=w zzC2eZEBHs3SbFPd$@1t&aF*4ASEgQsJPWLWS6@gs$T!y$0V`Iy%N>hnm6RgRrec(? zJ!FUj<&~0NpJlWtbZ|*skY5bUjmx{oRAP95*7l2|D#ocC*+(<^IC+C^2eGmLrpwzE zE||{~^Xvl4mxCoWdOSxaX_Jyt(qD?yoNKo<&g{&YaE~X?Hg0Z)gyNb}`$lCU783|v zwo6;zRz5LL@e~()0vrZ-{b@q*bv*-cD8VFWyOkG`PjM=l+Riy(rSjIsIkMeA*gtBc zq4cm2ANTWF+c$CnbA>`0Z_hRJiJ3Ve8f#StOPHE*9bxZi9gTilcSPw%4s_F|-zBj5 zSzp`Ow`!X=-x8zC79M#Er z1+ruMRUoJQta9VB*$e`2E!kIa9q(;f)86=n2*vey%kMo=> zrCKJ*T6R%bB;2kmk?`+rzX9#eoEd{=Il8bHzM3=8J(!+#*GzA9mAyYlY7(bWQJjX! z4Kg^9i1e_~6}-RQs{Lq#eL~s7hT8toGFpIbKmwy*gfVJMYpy-C5Z-%>cdN0B{Q$ns zD#C##!(`VYnUkK8Ac8>IjMPwi#{t0|v_WRoY{E3vQ=N#J8D9VjT%HT!t58Ga?5FBx zG}NJY1$Wd_|5Ec8_+1aS-V4*`ncnPPoT_rUlD6kyPKLW0BDm;vkWVjJDDxd&=G()h zY!sIL0=sN}(tQz00ZY`9ZHiZUon;B)Up$*Fwd13%zrJAVXkJ`;uy2X8RFRTWp1E4{ zhAt7)@A)SUuy->fK26_qFx|H}9aCW(ZG=(vGD9$z^JX~Gi_cWYJ#$wP+fE%nekuR! z%sz&e)hx@$3*&}^9`EIFP3Rj;(lNYYechSKEr;NHXc-5XyOKV3vZ-gC(n3mgAG_=F zR+s-=k2l;}YF{-}J4Y#Gdq6`Y`P3LvW-_=*0n2kNWu>~CNO+b0r#|~X{2*!!!?HRX z8>4AUBz%Rv3@)o?QZW(3`>A-FYu?4cM9^zrhKmuQ7~4+lyF>yJ^Jt3Q+?X;1xo&W7 z`{w&tjjzM^W|_pYXrv2cRLnsLOm4JRz;}qsvOPL#M z{zP{krZ+tu)1(zmorj{W(8~t2puQRGz{W;rcKwUb{6ZYpS`$8daL=AugU4V>5CzlY zWxn6{EYFB{>jdY|OvV!c%x1dfS@>+6&n}H(ej27c z?);j16(i$~?t;hf>#Y2f&7xKdrI5l4A#W}W z?AF*1#Qi+8AmEI3INPCY7eB>~ftj?K6QmFiW32*RZPz1(6J1y3-=1UAo@*DZ=f!|_$eNBETU=D2|C zT=pv3I94MWs73s$#-^H|w9D-m_lBK-n-t6@)+_e4(hVNi-;HhN>i%bMgdw?$DwcS| zRf+p9V0lj;7ROh2MNG|@_r_rTd#dZn`G49LK$DLB_qm3j^gDeDEa?DhDZ017K-Y0S z?Q+c8i7S~et^{gdb)s=ZhYN`tr**+Cqoky9JFJ=d1&0x|_GVz3mdDuCq28$apl0f` zurIqX8auz=jCFn zTa;oG6&>5ElWg2e}F$l%sf_v->iLLJJ;4IYzPFjIM&e%C6Yy6aUwB8cO!MEfhJBzp6M_yn5YSt+ zz|XF&AJQ?w1N4CJuqS9l=sZHQ#y$|#)B+f@-lBnMp@~p_Ekde75)X2?j3!uSa_lxO zk|Q>wJm%x_)6D53RL(FKN77CCJN&9E%^7^Y6zC)y-t7w^pPM^{;@wvOo%l+-j zmz7JIcLRjxxS0a{4_NrV^j-rJZc!f;bBlw{L{*p^n7Xy9=PB^kYi2`D$$xI!90~LE zUB{nnU^)sW-X~^Qn&+W}5I4k0$k4M)zh(#vZ zmJ@81Y>V;!!CuVnu6mAVZ&_cvFS(aELK~}{iYHG5R3dwkwRnyy%55MxmWs`s0j|4k z?mqSF58`F^Q>XxnhB?jq-`d#jQC#Sgg1BF;O9}Tc=@db7>d3gD5Y7~ql~3&IB7WM$ zrV5rew%tqc|D~&AL)Yma^Q(CYj2{~lo=xGarw&Vv<`*MY54^d~aY12wKc_&;WCnDD zN5|QP#l(F%5CB_;`t?Vj8y|pyJCAL0is`&&vyHXZ+rqoRRj_*qc;cEFO8@hFAAr?hlg~|L4}Tm2|v% z4CTTF^38yg$o?-C6~Nrikuru6no93PO+edY{JB98H9#{tZTo}&Jaw&-icc84Y1;gl z>2P6-q5eildibxs*bXEl2d7G`;lk1|P1k9A0mm>hp5!IB;T!yT^i=LmBsWIcJy(b5 zU>ACl`;jii8wxd4|Kv$N694KF`y&nDq*wc>y|V`TyptVhBWz^ZcO3qXb*e)uMr;wO zy#GgQA%lu-yTd-@1`-TJ->$d5C;!7l?fE}s&GPkmJ3(Vo`qZXFc(3mMBQmV9TRb9~ zo9IwPhCOQtAuZ{C6pecDJ_WMA>35yOIX2N<)_{y2IX~&9NU!h5owm5luJB6oy$L$6 zFYgqgGPNrHOvYyWP4|yAC#V@_5)FM=_3GfOj+|q?Qa(VGSp4R+$!2tiK_e85C&vmD zK?h)9byYVRN=_*$?N&N{{K$FrJ+~i(=z$h(eMu1tn$0cJ6_$PDHW)9~{0`A>+@kP< z-`)q(6&_!Gnr3Yibb4MA5E>FSK-V0Iz;tD0Wle1nYEjMA9Vq^NXY6uy?Z#eMQ7-j?E*Aad|Z#3idr?XR=pJTsElw*%QRP8?CAg82^Kqb-jwqvwFGG;BscY= z^7bjx1tpdKS!kK=+cpETtMPc@>6&@4e$Z!nOz#guH3K!0$`V+Cjg3`cx0Ya9J8YV94vci1Lc>6vHxf7Wys*`eLXV;fLB)WGi>W7$d-+moa+jTrr4b_;XuKNhjyMq98zEtQ8 z&{9QKI@5lV)bx3()?@g(2$KCM9e>}tASXO#finOt^C~m%XH@l+rME&GBHzFZJ!oxy z3;stS;xRJi{p`1;L4!CX?MfR+7v>zDnr~>xnW>fJWSJn}4gs)}#1`*15j{uz*`=2q zW}yX!=}O@g-HH%Q)m-Vt0l1>0&vnA-9Eh5}Zyx$wU~M0L=O|WUte)5lmbSD{MPxhoh(F-Y@yL)DL zHnr`=6Il!SW5BiSVdGxUm`*U{TP8NvT!g@4bGYedoRj%Mx_ptJ-GX1<(XwT1O(~bS zJwyTde0oGX0#UOs@}h2dad2=l@(B`FQ?1XbeOF#96#tjnRK{tCUd_Xe%fpbu*Y z??-^}?UvGm_l}|q@g$49()LS#ZDT#qo16VGIe8lB(m{})TTd8Q7?Tz)(8Tw$=59Vn zF4%T`Nc|vS`^aXW-cwh_j54N5EWT|j1P#xY?LKzl65`0x;$%tXwScLGu~*jxKsS++ z)Hs-m;e7}y`a|8BR<~?o5agcVtsnbvUa2v0exmoeM&k0KGB$ z4M3-Ok-*WhHU&|th<@7|5_Ecub3ipj8c+Rh?TBJLJDz%R`mhug|GeKYmlw3Ya;}6T zTWEzk;<~vJk^(S$6JCWh=SkV#(kK3tj)N*NOgEDIS=u*j9_Ntm5O?_Ex%2+7t{bg# zX<7~mg2JxrJ5_490wPWCC@VcPeoHDQPZ+^pc%PjOQPaZKZCGEEQnRP*IGEfkSzia? z&VqQVdzh-%A*{Rw$2001Xvn~bmWEN280qwF92oN)aoQdsm%G2{m3Yi;FmgM-aokM2 z*wE3s?T>x-XP(qlFx2FI=sdfgd+oehFtIauJ6oZ>{^eic694@@Si6=QZwQfS<)&=3SpO4g zi%yOSg&ouM;^C!0f?@XoaI|iw`JT|$l)d~^1Fyw4>fBWQk_rrO|AKDazwI^;fTHPw z*K}TOA)DlYtxw&8_Sp>k?o%6(+^18S9dcyHl*(@oHjJh91?RAmD|Tc0GLz1$IZc(9 z{KpbCDMA(NC!@RduyxRB#}zf};*PIQ6ErX;3d#fYb+hFdx59bp{}QWcPYLQ$};U^?Wy3lL+VauVO?HT zC7%;UFZjGgg-CitOVDnCmdoccfFXu$Qnpwq-gP{BYJu96$DR$m=B2+w|K~0^zkg+2Wxc-a}0yHY88ZJN`D_eNbvM17M z2FhTZQz}H$!t)V!t)sC!iKKZ*FlY2>JR_=Z+VYzflJs~tq`Z?zZk+>SNB`wBlIzia zOp?*tX-f(%G5yrHeWM6rjWAZ-MB6o5oKe;EMQ(^{FBe&TJDt{Za(y2DX8Xf8Nc|o@T z)||=_OLGCD$YgHS z({UirIE`g;(jjg42*>QdMfRcp#^GP`-yHDYp2l8o7NhTKoM}Gnm4r9}#^D=ZO8_PJ zJOakm9IP}AfKR`FZEUW?nWSR7_%L#Iq$$yrrNc|ZD2l%@Md6$JdaAd|zg0BePBLUH zK8^&_iSM^;4*|RTJZTk@dm=PExJlLjM=KA0qImv-&FaeM@*wZn*mV_mZgIHgZ|IchV@jhQtqo6 zdK(-1Fz{9X0$()!G6*7GS~k{6s|`VWm}?**-DyWUqgaCX+Yf0MwmwyxWY?I_lzO$oqdOT04+%*#3Nk6Lt_F|J#BEEkiV(eQt8P_d{8<4I=O{omR8& z1Sch6+7QY0kR(w&9`|2vUIcsavLD<-{>-yyA%%4->pCt;52QclZ759S7^}ji(^{E* zm$7cFeSY|)ycOwgQuoK%{KhZ(KMKeLyn^*P1D|CjbN_Dev>X6Q=8k+$YVEZEM^(%T zJ=9|xaiKqYU;cUS{_Y3+3D@-Wl_`WAz2jhiczTA)s)VijamoY&QD0lRjlk6Z9cU;^ zWvAEOVXGD16?aQ_gNae}Ezb1p4l9)tm!cee;=UK8AV{jjbstg|r+xp!Uul@${5%Qv z_N&9+=fnItwCtH-@cB10-{9s?<{I9RH7fVMJpeWYTGrR<_J92f{NMllvZK)X>kdBn z9{pRnciq*-|CFgO3v*-p6zTH859r?vz3ErH88Hz5wcwOZE%LmqKuv=y zOBduy6edDbAlm2FRfVkPtkH^j7u(2jbTt4qXgb46;HLf421LcpfluDj63K$1a>dO{ zTlo>NnO+J>JRrR)TA6FpA^JHbVs&egnj^)XKQYh@c_&-SSn;H;EXFJdY6RD|k_6Vw zwM2(dC2xQ!nfzoPObKAR3^C3`%dPrWBL{?+SamolAhM=UU?fXYpIxna{dU-youQ@O zG#WWreGtg!^a3@3bmte+aZc+_(qoNKYzY3ujD_`uMiN@?7pWCPZ2#+%LGr8csTqF~ zmeyuwBirN}!}Pqg5E*m3wq1+FL*A5Z>}QMFYc7`Z67nA6)U_)?HO@@k%aO&9mUDMM1t{!>kLLII9 zLFc*$#U6g@&NWq!CDRK-0*~T1eIgCU%)+3N>`?^RskA3TBZP&xEl1nTR<`Nvd~DSl zT69hwTU>&*5Z<1iWRhkSE>V+jrxeRv4xT?fY{|UG9lD)TqcSqX!1){PN}vt^lmjcH zyFH+-uhueeOiAB~+HCI)#QDV}uJ{BG(xMX~*_$JsODYnz`+NE9enCCwu)&D1g)#oc zks@czsJyYlZ7R9AB>Cdhy;9IPs9ZD{tQz6pUd4|&$+QvAtjzjGP^8)QdHA^KATQ$= zTKk9BFhxKlt%(@=hD?`<0H=BZk}-(3BT!h$adVt7j?olsaXngi_X z$LA&+Wt|fLRlC&PIb%G(W9xoOfQF%b_n>;PJg9GI3ANx{=e*$9AwOE-MUIUW3*<9B z7>51y;4>g{f>_8vRmBX&wSHE+htrA*7prt1CdJ52pE}xHu1e}%28J=4ld5D?hqgP} zncZ!S6`us@MQK9Ym#|B-Gh6x49y=P+DIGgqeXx^xE5`IeFm(E3NaFsrq9nd~jaC^W zF`=$q7|2L!SW!cX#}~qiNTy}->H+QHs5jyp{h<+yF;546yFAvdZj)=O%NWN$Tn_W4 zmyZz*flbbJ$&PHG5i$VNwBu5mB8ux&Shy@3W|>%{hqkHv0wZDrT(pC$n`Cu%yUluU zK-F<;Cr8^#wjgBMh&1b%;RQ0~pf8pwxb&ytGmP=2DU5X$*Qn85wrsyJ&Oq`tT{Z1! zNp)0*xbeni+e}%OJ4wnl@o0QidET!<{P$!eeahlIPZztL?g96T@JEs!G(SKHGG-%(QzBAWbj?MmeOmIFg$B96v7K(jFJW6pUJMv z&(8z60}xJ2^z)d0(imD2u61$jyr_>hK(qtqImC?UD(8ZU*nq`JSe1{)*aDzo#VH6C zui>b0vn%0(ieJzinYHY!ne8otO3ZKljKpNE(!mte*#xP+Z9yHd{7-34GD&6Bmx1(x zODuMZk5tOHVjvXe-hTvEHYe*=q~j-m$fS8nBdU|>?x@AQTeQQTg?OGQ#e@?+HU=`Z zhN0fR>M*`-Y$<(-?;(+d40Ikgbst0Ic+K-Cm&xUW+Nvd|+|EqpL~|{}B$b;Br)rqJmwA5tVq*I%0pCxY=~CCu!l>DF z-){U9J}?rA5VS`Gh4IZr)@5YZc~3Oft2!yP{MbxYTM8~U)`|UQegmh-=k^dE;JSC( z-r>-CX~;!S>w{gMd-J{z*M*6|FT z;sU~jW1VYA=8m=Sp^arxaR$Sr5MzuyBDQHw8LvdC5dGiF+S4%+ zZ~@KkjP1VxX%*q532Ae=_|?3(ziC-*jAVg6bU5M!Vb&x=8CHci-K?yl>g^A$m*{&k z)|y$bE)OJ>m=Tp-3CGQED5nt`%ug#96I3j$HFnWeoTdj~(JLj}q&YuJyL_8wQ9xNE zRdzTcl;Ce+u)2xPdm!9MAX!{eE=;l9uKY7W)zVkljSyiOtNe-}wv1NpB!pTStc})@ zzH?1gn_&}*tW4Hb+uoSU5VxpH(g+h)>OYH_xr8;=_R8G^SL?u^(ANkrt2-Ia;YlMS_u zY*!QYePT8xkCCSH`q21(bM;0pHdzy#*fqXG>)WkLQ1#Hs*`gG@bvUyw1IQL_;FZ^~C zaC@9Z;qg)aXnEo4#`j+;ct01$X7`-wGIZLKET#fO;yM_au%XMwLN>WF=NS>(Qc|BI z@ozu1i2&UcVP2_%12uPCvG|pg5%cn&5<2}>5&k9*RPrRbtdz$|m~+Z657!EXable0 z#@alii6s(xBB*q7gORy+v-zh#8ah7WUatA{Yt8ptbmm7n6xWBH`)o7}6=@{l<`H{> z_o~fcEu&Nd7R+mg1sqDNA_I!~@@h>hLE9Of^%ciouejkDc*DbOmld=YNPZ2(%rvNh zR8#QxTZ-=~!2B?Wbc~w6c5749`L@i2#T(JQuu!lH6NCIa!a3BEF_VNa_9wDKO#80Q zS0)LyB5(s)w^o2d7J^5gyCyz$`>h(?Kz7M$I4Urm9d1Y%8)XwQ>EO#>?gBXtr*@JF zzlg-y=B-`PUt_+0v=euLSGxyIO)MUr3U*r8z{pD2s_#ZM*A$4lqrk6}u4$k> zSo4FCstLulf8N~(DzYI1n z)3?VQo8M9G!O)Lv!51Cczze2y|CxDJ-hjIA=<1^hw<+iYXtUFOeDMuML8|o0oLhn~ zZqAX74f!?h^D+Y(a#D1n9emyiEJXT}V{7K4@Cj8hh}-OYX_pFH1Kd7$axVH0TAGD5 zh1Ijo#pv>)-JBh$ePtqq|Uib6_67KS4Zdja^n38%bx`7?Q zhn5|T&wRddb%UFyFnno&-1-377lhBgWHnd0r_4n3pdp+X=gtYnK-9!Xa*se@{oVP9 zMPS*Xk>km{eMC|FD>V*{A>r;tiOu9E*D@?cr4*rzza!C;hzZ-=g^CjhX!8ds!RYB= z`>qP;;Z{%v=U~WTF{F8n2mFQB;}A1$IYAl`*aUUhmeYp!a`@gRh&P-MNXPkepI+;Q zy>{rgi$2+(<8o!L2V;>6ln3HWy9=_cXmlHVXl?oPg9ViO5vbZ$4z)A;h`IPn^Ya`0 zF)jPbb89UQ%ZEJgOBTGICze+&tE7IflfXj+3?+5db8fFvQqPWdL1j;0?j#0~j4E&K zJBvtitBvgLcwkDEZan#I%6u#FJmsft*LVyU1sMMlWH)UtHB;jree1jk9(J+xetm+ZFu&_JIEhJb<<3 zrk4t3lY(*A0+we=vwwws1xeW7U;z-z`Rl+tP1%UFZC746t$(}$ch$gB3RolhDmCa+LcVx+I83n6hRC3 zF=kX(%fxOnRtCI3<uHSYG+wRii3{xAG z)lkre=Qwrmv(gQQZ7~4!*YT_5lVKo$NGgbp@ni~bPe?}-`IZAL8mh0r(&$ z#M$^CydbT$x|xdc-Ih>#Z=|Gr2>T?Tc$4+_L?5Uc@bu*vO|hP}jga16t`W_@UH2ji zF^keDOt6D>px7wWB;;*?_@dM1cZER}E$M|QOq1kwb z$RG=-F*6ckJy`-Tq3`JlJ&PYFhotvGI+td|3ir=OCFG9iQG1!xo$*hK{>)rqyg&FdpW&Me z-)A7}KNA1g)ZP<+!Uu5Ibo9PeE1qqMT1FIT8R>l~7_(U+!u^i)jD~m?r=S8LM1w|H zX8D{b(SP&)jDbx*+N-==c8=v3qvGZEw(|s>G*VJw>j{Cm-~g|%0+?Py`nI$4ev=sf zo|4*;&=&(uy?^&p!Clkjs9mb zxts&MXR|rYUkguOIt?fTq6@EukwSY};6kb9L zlnS*fz^4Db{?QK9On~QQA{F4TLb?MEGNj%0yIKea3V0Nvr5m&5lcV)9%l8bypT%}R zaH+uoemOerSLGp9)AyP^kp|o-gWPqg=`D z0s}9r>4``JDnygaCt`BVv~2q{igB92mcp9+nAydUWl$M86AsRq0*CX8Zl)6G8W)Q9 zdIj3TFt^!H@P5spV8856^qj9K67ceR(0}gNLi|=>^X3dZ7^6N540#5jJDrnh^+&H> z0$<#)%YXn)(&;|PQ|K$N&pwrW5zPp4tv9XeM}0j}3Wb0f>O0cctWQKvwF*4h@2dC2 zpl!jaL(New&fZ$e{_{2wm(=_;7B6a*w;^^~>Zug71KG2v?tLx4O^#(4IMZ@~t>TR@ z_@Utz%no#I|E;TSmo;Kd*<=o@3$optz87&?}JMj%(Rw>ht=f zS^$+spP$j@Nc;8#M_b=~YzQWLMl+R-FGP1c#QvgrQPBE^As%lIToL|wppW!|NF8J( zI|ZZU5FdJ-B2$@LNCzhkN81)S`)i&3smk*kJU&aTuZwI3*<_`#<=Se;0(9eId>ClOP@#nZwrfAmld5nYa7 zkA$T-o(}(Dz4S%&UO?J|K6|Ud-rEn-EweV5I_Y=)1m|-~FEE5^TET|9=YkTr*Qu4I zbook8@=DLsd%AK2NSq>XW+@g&{{Sz1^aHv6?#++GP&8bl0eUss%jfqnzc=hlRRI^7 z#+d~N^P%9OBMm!QR!<2(qOOv&6jJKL1kr^W!#02TyPH*?Uav^3Esj&%S)g|6bQJ4? z3qB3s3{;$DRf`)w1fPG`+3@Mg!08OobY{z3&rl2j2|Mp7+T%@{w6bbUp>Yg82Oh#L zTDqvM>XN{wK6%(|K+@QbI(^slfsZLVYd?Z<1`&h;Gs?2)$KK_4AD2&bQD}9s@ZhD{ zuHF#zwI&oaaMRn;RLsi0+=|`nn+*IM*Vh6mzRGItc=bz@vt2XPiY)1qNUqKPoP0$u z47RSX!JUBL{W=4n2oh1@dLi564Z z*&mV5P8^sml3M)B%z&q=irELU2q9Bs@RpNEuP&T#kqW*)lkK~)v^d!EOM?P%N};(_(<#% z!;yvWufgVnETPxIF!@jB2E0nUz1P$gME^k&b}P&KJj-$0WejAv{cat<+QeXK!7vn7 zfZR7R;TdYGx3SP+TNXFC+kjUFF3=?~9e(WAkP}e&MeH1jmi;(le{3) zH$i|c`r8j+x`|0woVHidR%{#v#qdxeJF}Sj&+m%(0w%C-T2tU!jK~V< zw~68qr5i^Tne0BWfL8Ez#VTU3jS#sAEUFjkK=u7u1?MPTK#BT=u@u6xny&I7;lPea zkO@@z!34fiiG62{TY3R9fr`wDD@qkl%?=<2>XntEzKT7MErY_-Ooen`!J!P$BYy+m zA+{d1q0zBK@wJ=Z0zAR9eU(VZQG}#C@_K(Zu zOT{%Y%T1?4!z&~clgU2s8$3fpLb|ReEPYDFbqEVdwdPi+HsPpKEVj&xg_~U*nWch? zLHSg_bBo3VPAg)-)NH(h(^39;{t2h&A}=@D%h^pq&Zl97GLKg12JNM^BuN@m3_`l^%*` zPC+P!&D51CZ6v(&CYxFXXNoT^(iQ)^!KDht{%KnV^-ZKqIokGRM`^Y#x-iLHka zH7*WD)i)8rQ4QVjXweS5R=QtE#>t%GfLj1FtkP39|L<)KVd{7iM+K*oo+Ws9I6#=1E4}%f(@JU;~YYvf?4eh1NXx zw_W6pUQ26Dbor2Z^@K+9`ZP?pFhAN+2Ww#V=dihmO`UbB>ZDdg=-yT@vhj&7c_4d; zCsm;zoo!B@8+YXlc)bzy+gf_GP1ZWJ6~o((o?~SPuW>82ym5SXXzJuW0>0qkjx?P+n@_{zJ13DX0- zzVRh&n_HnmE?RMq#C)tV~WeqZnDqp7vhTtmAkiJpFbZ@a<@pH=BNG!(czKH zy^KCyUMafiZXt{sN=r=j3NRlGSphPY_ft;)hHjS`A7nFdE2q*a9;!$H_bJ>Gipu5_zt|sf*9s`A(<9b;czIdB?FB%E=YJL--xBy0TbiH9 zt{02$@Vjj2I~Ux`52?d+3rr{37WB~P$!%R~D@>gx0ad4heot2fyQqZYKDD7C%wz4S^JGCkM>EZe zSI~`GX}Q{mLBzWhLEE2Z2Ll!`Q9s@l!EC0RUd3{63U;umo_sqBURM|kch*hC#LdJ{ zXi!$tF@>b^VsBmf!#HLEcx0p!kbhmh{B#T{<&ufROpMpl(ooZP&wp3foO-Q2Yt4EasSo!wWU_)XlD%8B413j z{V`gl{%00C>AZ05gFf>L!gd%!k0Pzh6}#2P+r()vcW-SY+K zGPYQUgu`{v7N0M&M*O=b%*L3H@19iY+LOK`r|~X(#5ZHpn6K@x#!X^ocp_mmf{_#5 z8Q(^rcDM}Ul9l&+ZB*2t)={iLN|SXl<~`KqjyJb9+(Zc61?X(Y-v(eip`1xbBAVt1 zT@!RHir0siu1QTceMiLB)g{;1@XlFd4=y*`l@^9nYBH~*CdMz@X@wM?tQStz6PIX5 z5-%$8Ns~LhPJH*?D>p3fZR&~~sXgB=1Q9Y}fh_+&TQB#IjwR6lX1>l z?YSk@6Gtng+7#@qbSC_jCS{#$x9=thO615_Zn0XO13S??#HP1=d{sXHIlj8eQnP4d#UXjf`e3ByOIa7g1IXVumTTjX+wlLNt?Jtqg30%bZG1;}P^vW|-0KD7Aw=y(-F(WD1*!H21cU2#s2kppvlsCQ4TzmKRr`g-)XGh|$R45@W@&|Do{ z$Da*q&5SpBDv>Wl92n;tDJi*t35-mU?(BfZd*#$VFSk1X-z<=QpQ_Io>r7&b{rZ*# z^Vu*3@9ox<)%pE@QlXi)NF^v#u*#I`_33Ax>x!FaZq}Xqjf|96@ikE3XSi&Nt`1AO z+gn)r@HBDu9~%YYh;89?+i*R$taRT=J+*FR{_XJ}czY}))iL6HD{<#xsf~O8l0Cxf zL~IAqDs^dUQ0Dwk#7r|pL}uD0#1BVM;|#{-AAr4DziyM#GIcNS%G9~61paJ;QnS$P z2Zo-}`#SaS7*5{o8L%m4DKmnFJY+@hZ1FTFECx1n&yRI+onxBd zMZ)mI?(`ARzx8Y8b$Fe=4t=O)Qggq--H6+dvHChXRB!onv6|G$=+eUUe@e2= zJZx-AO~E`3f>c1bwD7ENRPo4_ti0S`h;g+K&^`BQhYz$Yz#PB_1s+|*RNzFFN z?H&ARYLQP2Eu6|Y^R#0tf_*Y&32P0Rr|l1U&@z3*l63}=o5?&fPx^s9oiY}I>r5gw zoN27J%(*wddT&7`)exDZSoP#{0XJy7@j_uK@GNalD7`DwPV-U98O(CY-c#3i<@7sk zFH=$dovQ8{z0Ti)ujCc_sA9CK#65MSi({UDrB(-x8NK>je3SGKi7NLl5M7RDmPhns z_179ya##7N`EYxRRt{pQX3N81_q~|;l^kDDl>Ma@V#A7AY=*~UD7Crr*cyW`>t~S=C^n{rA}@=dynl93FUjsOL^?-sFo= zv|BPK8SHXM4kMFIK`OcT4(_wOo&wL%Y6s?j{Xz!!JK+_sIcmL)xvN9CpnFHHr?0O>2V#|mGs|Q z6RATbeqE~{h=mSmg0#YPkbteHNvPCDxP_$wx|g}N8-8f`a--P^B^S+C$)McDWiE$5 z<}1X2G~E}`sMAM~aP@)7)tv4d`HtFkVV?1tA~X$qz0OhlhDMnaJo{PAYVPTF9tKmG zCVO~SaE!Sn_!g&}^>ZplW|rY{rip`s*a{)p9AGNG^prHK0`{V>E4P(wDT_G$PBUg> zuRY@UHt~m^ze20xnyz@99m@Hs!X<8HOSx;HQ`c|1bD2f=&yl_xqXnwfzt$k(9w9r? zhD+EibcN&C{u)TjgxajEq^t99Q*n}LRGI7l>4QImzvvnI!9PoVuTR$q&@g+~bGHm6RguZbvxGjJ0w7njPSNTZ1MrI(D`cU-eLO-~!v0bh^bioR34 zZDUMfaFKb!JmS8c~71$*pIX?wdug zEvg2#O4-I1i$Iet|NW-a$O^ps!w;#sz>7ReLumb~Mj@7s*IsFzctOiP?Q&oHC>78K zeE7L7A`n>I3zc2*!aVPtVazfB07WJ@A_3s92&6?Fu#gbf$ z61YO@kV)h~-^j&m?0&9dG(di11kF7P^RFvmUOgVr+2Cp)^XahX^qz_0As+H4;Eu}3 zbnH4w#3kHpVro`qH(WTI@EB#){r(2+W!u@^n$*$z)h-WT@!CH6yTvSB-jWyGR!#3Y zlJ9Lmm0wct>Ke-qrSSs|e(ea?*08FrlNmP~rtJ3?$PZHFk!Yp;>>SN<$KW0wYP^t% zQ2z)e(`r<_>VHllIO?T^Zgn1R9GOJ7ea2VlUVo#nlY+^nWua`#v~F&A^_SMo69~f> zQA#dF7fmTfMQQw{#LG@+?*2OB7;T}v)3Ay6=bg(JOqA=hh=U!sgXOOw;ssfQ8H0!s zXCtSSx04cMlg&ExL{#(ImJ)Nc>s66S=WltG>e&Y#u=8qx1lFNSRapr)ZBa zq2YnUYx$n7#B$gEz*LE6`w>svxiht{LhIbCl|_-q>KBhRNk=5s0&70X1~CI|4}1d|8PBclxoGtq z6Vsf1C(!4V%AN5FdjlbRdHCKK(hgG98&m2=|7-7ziMYuti(xg_nQAFZxI+CpzHkH3 zI7|e^5)HAV;+lF05hcS4;b=G6q|dKi(XTs*xqjVCZlX!+?!n}f*VWGyo{TpM@SQ)j zmV9|Q8{R_vp~8J~$xMRY}~hCFvzfb@wtzR|?Q?<*n%f_o`W;GB`o zVs%tfHRT^v6Rs0%-6n$(!$AXH+MfjzERbZHs~vkjOEbIS=bc*h@2bRwbj33d`(*me z_#3QhvuZx4U!Bc>m>eZ%1x>lSZY1QjT5IZ&e+p0p@A9qmO9pbFjBK zN@N}`OyCJ)@+x^;(xd&+CD68%*GpMw$FY`X=z7_CoUhoUkcF-kZoaoicK^C3 z1@1bEQhBuF^8kWK_rHMq2beoPAodPz|HQON0k{>*Pq-D`jnb|6mk7kZQHwhj1r<{4 z8qz}gLklIrbfO#TKKOR^Hi$huu*^{FbpypGWpcsemLH@+1E8&K(9KxW8L1*?O?Hl1 zuUBS>18(CFt%pdMM>K5KEQJ&KW`^b4jQ zXKt8hObtPhJZ!_N`6(h#l zB_{p!+S}+lV)SIm#(6_R^Z+XNM=b#j*0Sj_OGW{7`2s@6q+rY4yRdJabvVx+cntF+ zoCIG*RG=niJ5a-QlGg`s*)Y-+ewkbsnLhb&Q&onC4EyJL1t z1WT748RxzU!&#{(>dYT~lozKx=p_~|y@;jqBa9;pljiTQne@Y9M0C`$Ajrj=qQ&vt z$%Q}pUD^-?boH{1`*iPRi01RS{a6`k!STr~=aA>&l9h1UHBzCRL-pn7s6Yipj{-S$ zQ8Xc?fu&P8x>TTFEopibCLS9OivJy_w%@um((jmBU5k11fNxWmh)y*~gOjZlqI#1$ zZi*&k5Bb8jw%8;D`zsAMt0mD4G|FS2(n?Gs4Gb{2cja#kJ2ToLkbcAZM5mB{(G1SO zF{kymCuP9>vZHNuB`VoVeNPs6t=IvkAo?bmzDhL{BM*bfBR^HpYIbOU2D|@*k(YK9 z@Frf8PomBG+|t7Y+qhOVSbB3E)c3Cb>|M*fYAud~my-TIGbGlJBn%EyBYbfK0`^y( zjM|MV*5QGwP!-j{E+l$eO$pns0?D45|CvPDLBoE;Wj*-NNxd*!9_Cse z1jYA{4A6`OP&_{Rm05ql#V(7Mu9FQ)1PqgINcqSXIH(+8k*A|Z+f>j3TEt82HF3gb zg1B%+_c3;;KAs{{Sse*=Yq9ecy+4nGh&=VK+56M3EVV7zd(#mLEhmJ=!R$w}y#arX zZ4^<%NJpII{-n({hlpb?nHEjf>uI@8>zSKq=hbps z-{Wab!~A-mF>5UZs#_jQ#t9K#CA7F^RSMeJg%E3zOWUlM1 zADqhJ{_VYS)BySOQk`2v+;sI#%H!K9zlVCO=pJ_-yrEdWcQF5mxot;YWq(U|4d;)> z`nJ~~k0rcSv_f|*vJ5J~r#yYy5xb?w64h?CJJG?ko?dI~{f|x3fiV zdV^?|7dEQgB)1RkPCK#s0pgi!OBSM(?2E{u9dmKh98Y6|GugBe{uHQ{EIj{6tz5WJ zuXZts{8R;PEFJsdmUz?%)r>3Tn^Djf-edFO!vPmAjHHBLFqRPH*@H_g)G4+F3_-ccvml>kX1{rhUTCFrS6j7TPiQR6*Mx zi=JH^JKVX-qa>?)Hrz<&83x;Hg^(w zX|Jx6^qcz(1kreX&$Sg}{fr<2Piuut?;xLwj}3uarq?dv{o6CPIR5i|V=>0m2A2kR{B3F{I=XH>&(oIKJKiKR>} z4tTN`rhL}EI>;(jBEiY;Vy?#{3lhft5agD~@sENPw<62&>r3az6+@JeaL>JH#LiGy)M#aC z`HbuJ+NF$nr`_flo6ZDk*z<^2%|&}P+28eE>ESzN^o|NX)DbWs&z-Lr7Y_~RnD-y> z#~VuosGy0Y(m#Cz@G<9FqB)}*^Q3_I%E&kGEW#syzb)~ne)!`r+^!q28*_?q&L5%t zp7B2;#Y0;thdM`3oI^H8Jw1%9<8O#S=B|ae2BS^T1=)%r=zI=6OpTLxyv1ft5a|&6 zD>7A4H;cOI_ejbzVXRAT8fv~oP+vu#RY^jN5EC4S_Nx=5MRJnTCEboCw!6EXNq>9v zC6Yy5+}T~^;0$6yDnWQr*#^QLw1^@%54~GtpO4u3Cpty6!^=ezZJZp`8DYZ78z;;k zwrxZZ!gwwD0)8Q9ccp4OXH93gtZ3j}y(_DG${>9y4ep0Fo;HAVn!SWqO?^dH9M`{R z9qZ;}#3J8XJ>T7EHl%|SpdKl<#IJhi z*~W>=S$Q9W28^sZyF-!~Hb!=(k~nms+@J9#Mv8=$S8~~czrN3no}X_qOFEZvd%?oK zqZwa5M)4dgzda3g^cXy>gN#m2hg~6@*Yl*L`d-2}@D41jCkVeZ_6hCr&LgMv)(D}n;ev62>>5r8t&L8(9_23u7(T%o9Jn;mA`Sru- z$E{B)wOxYlYA8-#9&1AtY?<^a=uKIyY4B?9_kHBGxI7Il5=tu0HKT)y=i@txp`+p4 zr=gx9T?h*PTByo!Qb>h|3ar*#OESAw8CVi1n)(ac`VJmy&7{t4!rD)C_><(*d(B>b z=y61KZW?0*;xGZQ=JMR`NNc;zg7Wc;P1NX__jkt!M!v=l)DohF(Zp9|`b@xsh7wHz z#=y0u5u(qkr1UV~lw3p(w3773Eje|M=Qa(!T!HOKzjk}&qbHYEpJiiL+E&9cQjUhH zR&{PPAeC(^zvJ2CD|Yq0Lv*lbSu4%^cam)P(?zi70%YNr;$lNxQpS!px=c`PDh z@5+&-K(4Pu5-{)P(Ul+NnIUJGuP{^G=0yqjEII6%k{xgA zDj6WN&+7&nU%x)@g;>qs(&6R23ZZS3yaKB4?H|~lHf?^$_8Y0ID5G1nHudfM#L*-{ zuC10qYgi)m{(P6~!dH*%Ky1rX-$JYBL^fuc_hIsKa~d=BM`uFWM7<3y-IiwZq|5>T z(^m_3D&zi^iInfwD8GRSU_D=4X{x5fJR=?AHaKqI6nZyPHL=vdUkSryd3=fWZg2gi zOT-KzjMjSM&EwV%6%`kMp%L78vEc_Nk~F9$b6x@-&4fVYzPRXtGP$lSKs0W2dSE&< zx2#A~OW@orHhVQA(;gie=8Nkh2bYO#%s~*BiHa?@sc0$6k9bd#283naS8()MoZ9l# zWOFhVk?V1GB~Y7MT$ZWqM>=frMArG5vsOK*#i85yZGKQ|?%PkKr_ik@Nx1TZRR(9F z>S;Ru<=0LFe*hMgUZmTGnRjLwX>uD7#y0LnjkETVs~*jT?cLVaHmlQ)V4!1LJaT;& zrvmC*Y(c_c)wVez2IVGpmf@arkO zGS*<&H?^8ZYU@lb7>D<*`DGMAR|E$vq_LDI79eR=fc5Kd-CKhT*GlMZ(ws^TtJHdJ zfEf=Wf`TnI&bynJMy)GUFIL@}Cq7$S%d1^_pA|HJLUR~9u~7u}AcNB_RhawH>JaPdi4&h&rHI@$ho^X}~H;xwh zWiM78Ogx$VAk8%|g4XRn3Wm|O8B!8(G!p3$?RiKcwiPc0S{hgVG;u|*-r zb-g92bvvY86FiPp%Dukt?cYT%@!5A68mv@JSV&O8K+W_ccm`kS!`lhf1y#IZnxg#> zZg?j=Y(2Ckqjk>{AEaK9|3hVL7&L>)-^Tj8D$5$Jc0#Wy=22^6K%g;EUq?J{Zq{kx z79p;kZCzO$xJwWdarI1upeO`X2MiQWsbbb+!&mpcBjMgDoJHMi)F`^1AxEkT&hu<_ z)Vw==h&fR3b%4|NOlN#q#P2P4ZB1XDdei)-Uw11xzAyaIRs_ACpkD_SL`pdK9~2&> z=09!u2wM-yJ=p`K^CW`5Ro}tCc5{ghanavOi5wefo|Z{G^aeyEHvqMcRvh~ir`8*W z*-zI4uw=VZrOxIe_a0jEKE@R#@>abjXg#JUlai-Hy_(1YOqa=`9@9h)`LzS$VuJX zKRafrW)^iz%{)eay#NXFYiS%%nEf_>V!LAIe+do>rFB=E2WCE{K!C~q_n0E6TO$2_ z$FBbxg8UB=NO1T1&5>GEi!-2H=Ti+>Fa0X%OOUfhpOtvP%dW+OyM;lOs*}z*p`t}@ zaJ=X=|HpKhkDm&+cy2`yaT#wh6rnZW_285bWU+eJa1b-0zA5(;xli{xsQV|%9B@v? zWt7NNPiRIY} zO%$R(EfXlT(Tc%>QKR>!pSui_F9e3O{o_7$YF!y3gZlVt$|vUGzej1W-`4LZpPTv; z2|nb2ACg?{8P@NIgnoA=Yy5j+CwL8aR2dP;b9L&|Vnozj; z(Wa1YQKS@5X8(Y>_X{#>y}FUk(yDP<9?IY6p{PE^erD9bzXsF8&9(gWWYR*M4Lp%Buh<% zYk!V@FzwT%3U1!d61+cPQSfCx!S6Ei7qijIurg2->bwHxnbaCQu?5m0{psqGL*`V5 zQl@UA$NvCaQwd$PAPO|thrykHOEGjW%K9hTF@qujcMyQq2N{gYe{Q)ZmAZH-z=5Yz zjbuH4Ybk5`WmMXM8oHXBCtVrh(NfdtblvC@TD&xSw~mDq#=h`*vlk-V?qGxe`U@0a z0bn)L$@NhE`e)DLRYTTAWjQSe4RVnW0t{{6HuinPz&8aj-1iyU4j-#E>uQQ&4O6G6 zX?~2ElWJ_|D)TK_ks4IBNfmIkl10tdy(!Xi(K5eK`ccRcthsHAMla%U0%~oDYUuEh zeH9Hg0$X>Zy_jXL#BZjt2+|XXx=g=jI46 zm!e&Yc@O>>oOS^JmbkcDT5c>qNKH3fUw8afXZ%oic^hx9?18FlK*jtO0!Ja4t*iCQ2j_1}OHxI)4eM;?aMdq*};ADe0##QtM`LY1yr8Fu}grejCqk9wIySkX=YQa+msas9~eO)md?xoFKO&pW1(tWb`5Q z<2n4xUq4M)$vRq*6}9eeNWqpsH-706pi}%uSpl_#z8&w1H#G+oh=VaRk21M#li*aZ zPjJyMXi)pr7vA)ziJSVmOr$DCij4dR3>rpI4NEHN$XM(m3bfd!;87_i^VE#P0SXEn zx|RpY-Bk&eu8_>&yn4eSeHJhRU!Vkg!hVkvj8W2?5S^*9YYW~c=H_1xm)m}O%gcZN!h6Y3YALMn8Z8;CAauC0-C2BU&NTu4Z!WCtadTfPDQT9fe?sSAmj9zf0pD|sBzA^o?rj$RsAo64|4Odkzsp1+5uRg_ds)0-26$C zk%$LY9Ic{pTu!hG{@IgjVCKv_?$&vp; zocDv}hx{Y+xrzWNG=8rc)qzY_VaL8$?sYniKC^1=Bw zu#KapKTW$7@V)AjF=vjzh{FTDa_+oilOw(dCa#1$fL40>Nw|w9op#^!F!1QgDwbfayDdC`bFXCzaH1)cp-p!td&7ie;Tvzh zB6FfQLS=tK)b50Xn!3wWm&?doOVfG(MRVnc=c2R2elF%ZQkFz`RT$WFT&_m8*YSQ{xg7Y3JC8Yel2~+~7{jcKq&)qTf)3UAQhpMkh|TdQ+_6a3IwvVd*SX%rWn3*yi550t%g|?-y#z3APBYTcDIA{K9_JLMUyI%=0=tz-I3Sm#bz5$mB>97IA38dK+ADI1_EI&E z^q*e7Z;bLf2vtxBf-xft#WcXpai(Xz1~PwbCphd923_HUJ?6&2fc&;R!_;`d}Rj*;)t)JsZdB z;^PjzT3xBmMmJ4i%Toh3?t)9d|Ho5)ztQUMOg0sz2Aa;;5Ah6@olx{4TCje|b)17}Kh955q#2WqDt6Tq%@T7s{=qUrf3Ij5{B zT|Zs%O@}_~bQ?zW@o4DM)CW$IA&GFI^&>Ww6hkLPi9O`rn!?iy6F1?@8x0)(2vX9` ziKt#lA^p%V>0Mj}6L(=YD+(hcxBdj*vZz?_Dj(rHNzP$R-ZW2n98a*H3243elx^s&p! zx=#$f5X{-5VYg*1FL?s^@Z<(^yJ^46Ijk9waSi&Qk+oZB?{tm!qmBDc?kYDrkh>9c zVq3|j^p>gU=5}zr=2}9%`|wnQ{06J~{-%cD7*R5}F7VJqSm9#lMS;L_TyY}uV@Soc z2)kPzR2}?|IHCc-oTHiH-?i!mnIA&}=~72#%ekBw7_oodTp#_wL*?^B%AdR^sYfj(l7Nax+R19hA7|ooxT?UOUyHON#4B#egf8f zmU3)=QUUOBzU^`VCzr^4|H&aK%6pXg;nKJNHvsWDB98kOf$h5E8BNm)>2OSa4T%8) zxY`W1IEi2knbo>@+JtnvL`zZ7>(|V_(`BiqHtf!oxvJDyTH3O$%4Kp`5P1qkQj+Qlir3$kZ!8!Lm`mZiqh)1rH5 zfvmUHxrw6bP=zwUm;ZFBTm-wq+om6qpVc@#Z*t3x=#`T9RzeXKs-?F8X?6X&-9x$r z(58gsKkAgrUEV94K%h)|4>e5W}T>K4p1r!!51gP6CI(R z80bBFTnb<6*LmIQa=rb}) zTyZZHaum}g>eCgyX^RuIQUvxfOYnrOVl#;w|6mc1G4Qr=u=|vpn&dLS7wXiYFD^uP zNW_6cBO7qI8UNsCAhN8Mq(@O4#3yuf1Pm-R1wXQHyoo$%*S(e|H;94mDC36hB>qk> znk?4dlvI$r_@awzW}a$ry92fm;`s481E86^7FkJs-$?`%A{^3;hf5Zw+8LVJ-Y1TC z4Hf>J-*}Qg{63r_an@Tue~pv_t?5D)JR+m&PXvt$t@$y~i0APOrLYP^iKyg3327jn zv5C=0;Vng+=Dv|DsTdHkB<*UtAV#Izf9$yDvpCwsWB)LJ3&@(CP^RCn*6=sIbH&7S zlK*ipXpFORqep8b3LeMR>vi;{( zaq4b@lA>K|N^zl-HF2giRe6BIH|@^qTA)dcMJ--y-J4M31Mw?~Lmp_j=&}i~6n@%p zl6NrvHRTeVcTgmTiBI!CN|qG+$j)#+Xe6ZE-Kqqa{GOy6$D%vh4YoqMqKgwq)3 z2r)!o)#)Ue^QZx#WGFo1QFd#`ZTI9sOZKy-1idln*h8~!i@C2Vvr87(`AShzB z#|wm&xZW`frE!FDud)Md@j=max8zW#WUvudB8diG8OA8h%mYKX*)elht7_B>K3q6F zmw`Q5Th!ijXlls?-PQ5IuXVZ+1>HS4T))dNgU(ty4w#WC7vlMA3q310n@qjr576DW zliei&ym#8eoza16V}*YaY#|h3QI`W2GNBMyr^Wmq6DHpyE#sgi?}d-~_hZevySWrW zJ6AC3OC`A^oYKa6&5_zko4SOAo#J-gg!Y3}3sD-}Zs6GkM?PtJB}53unCs%@6)%B{ zFJn^$Xnr6H5qy}TP zAZEnLs$)$>7ZW4cPo*MF1br!N87xo zeL0OD5~S`jp&EK@+bAr#B$nRXppc@ z7st_W%2(Ik`6}vA`8P`n{Cm#n1FuvZ<}Xf^W#|rKCZlsK&~%BFm&9Id9;P0@Tq-w6 zPr$4xI01t1kxF?d(N<_6U`NTzhFKN2V&wq!{@lYBEkiKn?r);2Qk6Y&YcZF+Jb}h< zIF-C3lzH4;J|Y|H!+rR!BjI;#AhTAe$yhY^X{GSVE>h$4Ztn}Y){LQCtni6jKW3)K zvze^V+QbNFi^;QfIJrf#J;b(T1i+D^NnD04R2U2@cV|Or@cKm}kjj0NH0NS2@i}_# zQ!|`k^AQfE-R|eZRKu^A9IMC?=rrYA1bvcURcc9Z`A% z#nmR;L9;rn)#27dj)L<>JX7jPs7|juxncW+^QF`sAX)_OAs=<7H zy&5H>|<{Da;oG7Ac)HCmpA4{Y`y>Q)^8dv-RtCVeJtaw1)6?;`knD zPuS!u5H-r`_B9YEluRGK9W&uK_9i;-M$ z#EX{ws=FWNm$Op^JTC$DVp}vHT@1Pr_m7iuCyqM`@8oswyyl*pAp!R00NjSjz9IOP z-^&*M9Di#Of`zOdif6TurX-^QcKM7}P21x$q5n8Ve+hdao+%?YfePs+Hx*{YuK?Q# ze~IZ89Wjrh+YJUt&?~u4a3bWkVn~{T7-WgZ;Z7qt;GDGmFXx1pp)I5qND%yauQj=z zKbR!wP6DknE&xgKDu>u{ba+|D%5@Tz#!&zis3gVD6#`#;HOm&IP|g7AdJMn7iyyNT zF&Oa^w*YS4y|_kTw8QzAVqs*jGP$;y+KIcMaS3H|;_;cVB8*H) z$gMb)#O~h>mh3y0ZC8U0MqcjH`q#5+w~cpx)Z6H071SX={M)zMg|54v8d<9({c`ri zkcTeajDBv2A#iAEXR$XyXG_R)Z4+~CazoMpvdsAd$?M_UM>~4gbM)_T`bZ<)G9PE= zQqb(l&iMog_;VQPS$SM=kdFiKSp45Y1}i1@B0mMfj0rGZbw}%tr4K~BV9vbdQk@j3 z`~#e+Bv@og+~R32;BsL?C|w|4+?u~!Vk7c9Cvh9%KP53kRe|m!tB`*p%$m$JhcN`4 zsuXm*Q$3{5ISrr#NJk6N@*t~mz#;{L`HmZr0XZ>j3tBbIy6#x;;aWDKS_i+HdT|{fu0jBee^R&rK6f$W%*nKi1S<0A`yOks7 z5u7p)NY|~|I9X#g@=q-VIowho>1mzNHxFpXa>u5nT@Bs`x=H|DEvJdzJEsa*5!}Vz zYF5g7@e7?8PW*~JcQQqBBIO7>{$0G2GFMy>Cib7a!X^&`-m22u#e1Zn*1x6infZz|{n?u=t~9&S1}Sn{b+xq|;KivfidjX`4ptj?EoSr|rJ^@pcZE z8m1R~5PIGjC-UG<%;&lwQ>(kzh=p6i!_nw2Usl{Ek2{qM1~uj@Wka+_#XdzhUNqXS zzmU29LdNG8_D9@Y(|)=%fo8P)3pD`Rm79XTIj@87WOniY?!5oR+0G58$&OJ@4#X%m zwswOZ(ot<#+j!2fbV6=J73uT0sF&MhjYUi$5tLSRS~)$qm|%8XuWkT&tKcwX9$~G8 zAF6&)zqS)r!cD%`vX6V>HoZV92&4(Am>gftSpP003mDr(&}uo?RI9KU@A3@|4YzVcow$1+ zcC!Dh-bsDt5#=Zkc2UJBcZ(~n+T1Q#un`|o2a;03FgdQr(vY?`Qjf*_?WZ8EyluU0 zq^@%Joy*`0>-Wo{r7di*l+NH-#3_+o2yc^0v4J+5Fs6&wmIu+a886js*B;Mpxw6(a z=c88_S6Igk0h?8`fyZ%whavLU8(a6I^m|N`2MLY0;P+M@Uyg=PCV+3QISmrE^Ln)l zR!pgHzaAAf4Yi(f*-0TnHcjFT6CmE<|COETy zS)sY^Cc#l?d^EdTX>-8K6pU~zd_*;B3Kn-36wQhr{xsrO)<^tL>4+mZje`~l2 zRRRXEAQ^2(yRRs%|F@(3e_T@!rGmXEv;H0&7{=R(oAb~uDtJ{p=#YkVhi>-E`l$Us zEad;Nj-!TpaneJE1pIky$^l@(!)zGvYW{#$dT#_+TI0}fU}iGsZ;e^cuPdpN_*FOV z%L66ycGBts`HPGSkiq&7+NTrnQw4n?3E?sN8qdI+gBb7v{=h?VW0mC9v?~%U-4{oJ zI;n4!ZLLM}6FTO6kXYykpa#49u#(AI@N?4xND?>)FLk#FZM9kNv^!W;Ye0M1>cqo6 zi@6j|NZeH zpYbGE!a*SKpDykTb^^EEHL_ZfI@u9D2+oUf2Fv2p#k&2_UD^xqeYN7xM(O|5&VT>T zf3Ru)Pme<~|JcO;+w*l@ySP|L$Yxb^i!)d-h!ez}rc=xW%k+D2m?-)(O~q4<&O?Xg zE^P+0Vi^V`4-zki^y)6@SHR(j!X5!3I-&4!p$qucC?A+O(Ts}(@)Ulp8_r*9>^1Iy z3h6*)M{fY>-O|=MpKKM<@C;DsMrfga=hxBrSh*^9$W zxOj`Y6PDsFX5}OT-+4(SOfej5h?rh8rF)4dA<F(bT~4j<$TMqbFHUEEyE35rL5cu7hM`q;zAfCg={R0 zt90*baX!C`Uv4@hPpvXmNYprSzy2?`ac|rYy|^+hMe491r*vKV$@GO@oa0ZI>wo0L zb2+d(zd4lcGMscW)Yxp-OEx6^=7^;Jl%fi{M+*P-r-xx;pWpqo@bSe(nM7j=$8QyF zFpG30^`ln9i@M8q4`3k9;r3(A&pEk%R5NmN3a3eNb~qVLZ~u7mWlYaRM&^auC<|_! z2qbZzyiw@@oeVvq==+-<1BpAec_+uVjaYQNehs5@p{52s07jpEr*)U(&fSei6sc_= zReG`Bw!YevfbDp0gRGMdxm+>w4A;&GZ*9OEb4n^k^X}g| z(rnXPo~THDwv=*$;^}y!6^Rwd#O# zqwHJSXh#L2B(0#U0$d?b<{4`U(R!Zzna!lgumni(y<{H0U_I`Lz{2 zlt9)6?^Ee}nu6x8<@?t@dUs%_R`T>Yt&3VC6?xkU@JrXj^%QRT)2;RD^ptyX*YZThxtb42fT|m zspHEa64v2E+YzaBJBpc4PczN9H?fS-=>M;^GmmQO+V?oEOZ$4U)%M9yK&iJy8H4~& zMgz1}RAf>F!VpLyB0>T*CP09|>GfV5LPESK1Z65HAVa_)WPlLrkVM8@84^eW*hC32 z1cVR*NRpS{uD8~EYrXaUcUTCh2eF-t^2k2At(h<7FK4^#O35_i z7{8*?My}QEyqhn9&836^MKt>>a{79>b4ADJL=Hx<@(KUoVH~@53Qb}4%obEy=sF}Ay=m%%g`zd z`I}&_$Hdr-bVr27p5mZemYJA#4^;ovv1kC;j|z!OA=X=J3$^d$^BALOu%oz)_q+!( zV0`uDIs;_MXG==gp+C?HbmVC?^Tsi*rntFfas6u!V<5Y;>(9{I#*c;l8lL_;4zTd4J$|m-(+fac#NHzc5!qb%byBiu$;r)u% z^&0oRF|lx~3yKpdRuG6E63DD)Uq25qR_Rbe*O`H^r`R>^kFSaD>G`a&U?*eBp%@i{scyN z5%_H-w%QB@_0UiA(DO?mK%GmP#mrlrsi`6?X(i8Y=lwuBZxX6`3zYk18{bC_3x za})7H6uT_nalKr1F$ob8eLffvSeq7~_gW?M%Or{N;9qsp)QQy*#GGZ@2n)7*uz=@P~@_FJN3^K#e z08)gD%05*y9%f&l=CChk&zCckzMF%cpbc5Q^&KAaQTR50#Hrn?cJ&83P_?&52el>g zD=C!Y>Nis~_4A>fjf=-)yyfb!Ui&)&0Qscb0U!noTM#x}1Sruy~G>j;WWmemV^)ut9{x9Br* z@*bIQLIiko;<51Y!nFS-s1`Z5(#?|-IgBKU1Dx6r@zRO^yq#hRarjc>V>VHNM==}r z)1#19rkBfm(#?f+3n1`~i(|$84Uf(7D6#5f8N8?+NS17A80-0Q`8BzNN*RB!(iUb7 zYwF~`QO=S}LHN|)`h7G8A7GTHT};F6Y;V=|0@k(K9h|Jd!UVgQ!@)YM7atZT5sOiZ z%KCcCJ9Ini<1b6=D{w!(vK=jqRlcik9ZGbolF_#LyVh2d!tNx`5|58q?>V4)>s!9z z0LV^T;0wMbA>ggaKv!ZuLH&Le)uqKXEbs;;#WmqlD`Oy25a~qtQYZfw3BCpwXV!fP zH?>f;-7Ed+WHltnov}FF8-CKfw0=CX!%RE;wxB{S-I?|`exgm4z|bV~9~R=VAmK1? zuhLEz8?ALLmvO19tK-v$s!yJYb(TWPu z2jdcvRV60!27zl%Wt9c}Iu*W}jd#m8)GmNlY)m_@S0`fq zTv%f~sc165zf@Px5F39JwzMdjjF-U;(jt>z;gtX?hVSZ|79RsofFWL$n<2x(3 ztMNK?8AotHJv3R|KkkEC{Q6Klj^%KogX*ws&3nPCSFz5}IV51qq?vpg=%C10SKY`9_Zgmwrw(=FF)Ko$1y5?bAR5 z3z$z(#K(YWJbk3+f+S+31o47)mAGf-zQQU6paG34*6G%+#QC^;xry$yx9W&kz@0t) zq&Iqoroj$_+3tt^u20h+fw`EPyqHGHI7HcTPzY=XygtwT#=h^b_8HbiEdgeXrfbg` zr{hd+a!RR|U6D}xlA|+;)R^%$$!vw=GA7<>BcpvPf)LNzg}hHsAXMQILFT*-7Y8m8 z{ygE5rf+Qg&&Js53?n>kJXBkj9LA8+u1DiL?^w&>kxNM=*@u+20Dl**e1$IU-)#bb z@$2LpQr~{YO%jQhgd$#r`X2EQ(-LKI9G&PPkBMUit_2Dbz@1$%)Ut&jMD%1;QRz{h zJ<)%?;-RyDM=9 zm~j}4(-BKfyoDSmE;0E+CUu$?aTgE#M@Nfm^apDU*-X|Y7j(;?Nlw`eY_q~0JIOB= zlwdYZs`T(@V{kYdQtm(3H#Z1{df@u03Aj=N`&wFz!+{Bo0()K~{Q7m*)FkKw#(c`S z>|ibMv|kW>49x$*&BdhAEvhr;K~v@zZ^ zjAUqTig}0V;mC8(b=Ho03SeFN#*8rXD1^1|noNaWJ}ctZYdpF(l50e)tG}-<=u0_| zXF3F*H>$v+g%^Y8Xw4$i zp-9tj@0|Pou=#h_jPmD_kGc7PA02cMA&e$CnVxQ>G+az;TfBSVqbJuA)&Q?}+>NGA z0?ka89!PtKMu?0&X#ms9;X{#$pcaZS50}9ahT(9E%}sAMjJ7S4CF6WAV+&IYjZioX zl9nhn(q#Wa*8Ab$SDr3r3uXu&O?oBT4B_0mB&J_4M8KE7EexZAzR>a)oI{Q+{EgPb zj8d>#9G1TjmXe$RPk~BI)297>tR)nW;PR*x8W&jz%}||k_fm$55$Yop*)`GjZ!tsi z^08Rau?}lnZRlqi3R&7qiD&m+C9Zb1j{e# z(kX8hywFdZd!F4c6CiOhomZ;ljp;g6jXI*ICp`l&RYNYp;i;|JP?bm7EuOh8k0!FZ zHH7N1km23R8%{^yPFUEM^}Q~GZ9)vEAr$O>Q8KDy+iSiFZXasEzIc|OCKmbjw^&Aw z#_Vt1dH!)Mz$oZoh$(9%@UhZRXH`L1-0L`2LFeWtHbzCb`HkWH$zzW)R+!-2ZYaST zh;aCsTbH}h^JX)j(5VhCK^P(NVV{~jZ@Y@MICSG${Pw;_lO+<9Ur(L`O+zMp^5^1r0kxFN}ogavEQIs^~jrQ;G3z9TukW-^M8IH_Odgms3_-{aw;) zAYgL)?Wq`oDgOzmfnqi(&_e6PP9>=EsHb{Q zv`F4gDUVl*Z+D#V;Pi}Zc(`#Ic@t4ic;x{T^=s@@~u_g zf893;JL7dgW={wtHN5_}Ph8E7H3+Rue%_-1B~n*zT^iUME$Vt|0=DBYcFSgiIKV;l zPbR^+?H`*!$v@GbMo7DPwC-GRNBa1u`y7_mVmMe*R6OP`!}yyh|;CqXZ~GVI!hg>aFri6E2x=hs)mGCZ%AIKFN9WO zlu~v4+3F4G3-#muXB~c5k8O1puxl}T_jfefiA3Wb-y|q7>{Q1OKVJJ!`EgEE diff --git a/docs/source/Plugin/P124_commands.repl b/docs/source/Plugin/P124_commands.repl index 734f03235..c39dba848 100644 --- a/docs/source/Plugin/P124_commands.repl +++ b/docs/source/Plugin/P124_commands.repl @@ -29,3 +29,21 @@ | Switch on and off according to the bitmap provided, in 8..1 order. The bitmap can be given in decimal (``nn``, 1-3 digits, ``0`` to ``9``), hexadecimal (``0xnn``, 1 or 2 digits, ``0`` to ``f``) or binary (``0bnnnnnnnn``, 1 to 8 digits, ``0`` or ``1``) notation. " + " + + | ``multirelay,get,`` + + "," + + | Get the current state of the requested channel (checked for being in range 1 to the number of relays configured), and put that in the Channel and Get values, so it will be available to rules or a configured controller. + + " + " + + | ``multirelay,loop,0|1`` + + "," + + | Disable or enable getting the next channel state on every Interval read. Corresponds to the **Loop Channel/Get on read** option in the device settings, but does not check/uncheck that setting. + + " diff --git a/src/_P124_MultiRelay.ino b/src/_P124_MultiRelay.ino index 2e5f29374..4e802ed84 100644 --- a/src/_P124_MultiRelay.ino +++ b/src/_P124_MultiRelay.ino @@ -25,6 +25,8 @@ # define PLUGIN_ID_124 124 # define PLUGIN_NAME_124 "Output - I2C Multi Relay [TESTING]" # define PLUGIN_VALUENAME1_124 "State" +# define PLUGIN_VALUENAME2_124 "Channel" +# define PLUGIN_VALUENAME3_124 "Get" # include "./src/PluginStructs/P124_data_struct.h" @@ -45,7 +47,7 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) Device[deviceCount].PullUpOption = false; Device[deviceCount].InverseLogicOption = false; Device[deviceCount].FormulaOption = true; - Device[deviceCount].ValueCount = 1; + Device[deviceCount].ValueCount = 3; Device[deviceCount].SendDataOption = true; Device[deviceCount].TimerOption = true; Device[deviceCount].TimerOptional = true; @@ -62,6 +64,8 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_GET_DEVICEVALUENAMES: { strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_124)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_124)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_124)); break; } @@ -69,6 +73,8 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) { P124_CONFIG_I2C_ADDRESS = 0x11; // Default I2C address ExtraTaskSettings.TaskDeviceValueDecimals[0] = 0; // No decimals needed + ExtraTaskSettings.TaskDeviceValueDecimals[1] = 0; // No decimals needed + ExtraTaskSettings.TaskDeviceValueDecimals[2] = 0; // No decimals needed break; } @@ -136,6 +142,11 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) } addFormNote(F("ATTENTION: These Relay states will be set when the task is enabled and the settings are saved!")); } + + addFormCheckBox(F("Loop Channel/Get on read"), + getPluginCustomArgName(P124_FLAGS_LOOP_GET), + bitRead(P124_CONFIG_FLAGS, P124_FLAGS_LOOP_GET)); + success = true; break; } @@ -146,8 +157,9 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) P124_CONFIG_I2C_ADDRESS = getFormItemInt(F("plugin_124_i2caddress")); uint32_t lSettings = 0u; bitWrite(lSettings, P124_FLAGS_INIT_RELAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_INIT_RELAYS)) == 1); - bitWrite(lSettings, P124_FLAGS_INIT_ALWAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_INIT_ALWAYS)) == 1); + bitWrite(lSettings, P124_FLAGS_INIT_ALWAYS, isFormItemChecked(getPluginCustomArgName(P124_FLAGS_INIT_ALWAYS))); bitWrite(lSettings, P124_FLAGS_EXIT_RELAYS, getFormItemInt(getPluginCustomArgName(P124_FLAGS_EXIT_RELAYS)) == 1); + bitWrite(lSettings, P124_FLAGS_LOOP_GET, isFormItemChecked(getPluginCustomArgName(P124_FLAGS_LOOP_GET))); if (lSettings != 0) { for (int i = 0; i < P124_CONFIG_RELAY_COUNT; i++) { // INIT and EXIT states @@ -197,6 +209,7 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state bitSet(P124_InitializedRelays, event->TaskIndex); // Update initialization status } + P124_data->setLoopState(bitRead(P124_CONFIG_FLAGS, P124_FLAGS_LOOP_GET)); // Loop state success = true; } else { addLog(LOG_LEVEL_ERROR, F("MultiRelay: Initialization error!")); @@ -230,7 +243,14 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) if (P124_data->isInitialized()) { UserVar[event->BaseVarIndex] = P124_data->getChannelState(); // Get relays state - success = true; + + if (P124_data->isLoopEnabled()) { + uint8_t chan = P124_data->getNextLoop(); + uint8_t data = P124_data->getChannelState() & (1 << (chan - 1)); + UserVar[event->BaseVarIndex + 1] = chan; + UserVar[event->BaseVarIndex + 2] = (data ? 1 : 0); + } + success = true; } break; } @@ -240,7 +260,7 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) P124_data_struct *P124_data = static_cast(getPluginTaskData(event->TaskIndex)); if ((nullptr != P124_data) && P124_data->isInitialized()) { - uint8_t varNr = 1; // VARS_PER_TASK; + uint8_t varNr = 3; // VARS_PER_TASK; String label = F("Relay state "); label += P124_CONFIG_RELAY_COUNT; label += F(".."); @@ -289,6 +309,16 @@ boolean Plugin_124(uint8_t function, struct EventStruct *event, String& string) success = P124_data->turn_off_channel(event->Par2); } else if (subcommand.equals(F("set"))) { success = P124_data->channelCtrl(event->Par2); + } else if (subcommand.equals(F("get")) && (event->Par2 > 0) && (event->Par2 <= P124_CONFIG_RELAY_COUNT)) { + uint8_t data = P124_data->getChannelState() & (1 << (event->Par2 - 1)); + UserVar[event->BaseVarIndex + 1] = event->Par2; + UserVar[event->BaseVarIndex + 2] = (data ? 1 : 0); + + success = true; + } else if (subcommand.equals(F("loop")) && (event->Par2 >= 0) && (event->Par2 <= 1)) { + P124_data->setLoopState(event->Par2 == 1); + + success = true; } if (success) { diff --git a/src/src/PluginStructs/P124_data_struct.cpp b/src/src/PluginStructs/P124_data_struct.cpp index aa3954248..17670bc67 100644 --- a/src/src/PluginStructs/P124_data_struct.cpp +++ b/src/src/PluginStructs/P124_data_struct.cpp @@ -77,4 +77,16 @@ bool P124_data_struct::turn_off_channel(uint8_t channel) { return false; } +uint8_t P124_data_struct::getNextLoop() { + if (_loopEnabled) { + _getLoop++; + + if (_getLoop > _relayCount) { + _getLoop = 1; + } + return _getLoop; + } + return 0; +} + #endif // ifdef USES_P124 diff --git a/src/src/PluginStructs/P124_data_struct.h b/src/src/PluginStructs/P124_data_struct.h index 0f8641e59..b55e3ce38 100644 --- a/src/src/PluginStructs/P124_data_struct.h +++ b/src/src/PluginStructs/P124_data_struct.h @@ -16,6 +16,7 @@ # define P124_FLAGS_INIT_RELAYS 8 // Initialize relays at startup # define P124_FLAGS_INIT_ALWAYS 9 // Apply relay initialization on every plugin restart # define P124_FLAGS_EXIT_RELAYS 10 // Initialize relays when the plugin is disabled +# define P124_FLAGS_LOOP_GET 11 // Loop the state of the channels into Channel/Get values # define P124_FLAGS_EXIT_OFFSET 16 // 16..23 hold the on/off exit-state of each relay struct P124_data_struct : public PluginTaskData_base { @@ -37,6 +38,14 @@ public: bool channelCtrl(uint8_t state); bool turn_on_channel(uint8_t channel); bool turn_off_channel(uint8_t channel); + uint8_t getNextLoop(); + bool isLoopEnabled() { + return _loopEnabled; + } + + void setLoopState(bool state) { + _loopEnabled = state; + } private: @@ -48,6 +57,8 @@ private: int8_t _i2c_address; uint8_t _relayCount; + uint8_t _getLoop = 0; + bool _loopEnabled = false; }; #endif // ifdef USES_P124 #endif // ifndef PLUGINSTRUCTS_P124_DATA_STRUCT_H From 6fcbdeb461133a4befafbb67cf37ddf769f0bcff Mon Sep 17 00:00:00 2001 From: TD-er Date: Fri, 3 Dec 2021 16:13:20 +0100 Subject: [PATCH 14/17] [Password] Add clearPassword command As suggested here: https://github.com/letscontrolit/ESPEasy/issues/3860 --- docs/source/Plugin/P000_commands.repl | 11 ++++++++++- src/src/Commands/InternalCommands.cpp | 1 + src/src/Commands/Settings.cpp | 14 ++++++++++++++ src/src/Commands/Settings.h | 1 + src/src/DataStructs/SecurityStruct.cpp | 10 ++++++++++ src/src/DataStructs/SecurityStruct.h | 2 ++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/source/Plugin/P000_commands.repl b/docs/source/Plugin/P000_commands.repl index cf850b393..18ea544b5 100644 --- a/docs/source/Plugin/P000_commands.repl +++ b/docs/source/Plugin/P000_commands.repl @@ -50,6 +50,14 @@ See also ``AccessInfo``. ``ClearAccessBlock``" + + " + ClearPassword"," + :red:`Internal`"," + Clear the password of the unit. + See also ``Password``. + + ``ClearPassword,``" " ClearRTCram"," :red:`Internal`"," @@ -287,7 +295,8 @@ " Password"," :red:`Internal`"," - Set the password of the unit + Set the password of the unit. + See also ``ClearPassword``. ``Password,``" " diff --git a/src/src/Commands/InternalCommands.cpp b/src/src/Commands/InternalCommands.cpp index a1584ca58..6a1a1fe85 100644 --- a/src/src/Commands/InternalCommands.cpp +++ b/src/src/Commands/InternalCommands.cpp @@ -268,6 +268,7 @@ bool executeInternalCommand(command_case_data & data) } case 'c': { COMMAND_CASE_R( "clearaccessblock", Command_AccessInfo_Clear, 0); // Network Command + COMMAND_CASE_R( "clearpasword", Command_Settings_Password_Clear, 1); // Settings.h COMMAND_CASE_R( "clearrtcram", Command_RTC_Clear, 0); // RTC.h COMMAND_CASE_R( "config", Command_Task_RemoteConfig, -1); // Tasks.h COMMAND_CASE_R("controllerdisable", Command_Controller_Disable, 1); // Controller.h diff --git a/src/src/Commands/Settings.cpp b/src/src/Commands/Settings.cpp index 61b2bf293..0e16e99b8 100644 --- a/src/src/Commands/Settings.cpp +++ b/src/src/Commands/Settings.cpp @@ -62,6 +62,20 @@ String Command_Settings_Password(struct EventStruct *event, const char* Line) ); } +String Command_Settings_Password_Clear(struct EventStruct *event, const char* Line) +{ + const String storedPassword = SecuritySettings.getPassword(); + if (storedPassword.length() > 0) { + // There is a password set, so we must check it. + const String password = parseStringKeepCase(Line, 2); + if (!storedPassword.equals(password)) { + return return_command_failed(); + } + ZERO_FILL(SecuritySettings.Password); + } + return return_command_success(); +} + const __FlashStringHelper * Command_Settings_Save(struct EventStruct *event, const char* Line) { SaveSettings(); diff --git a/src/src/Commands/Settings.h b/src/src/Commands/Settings.h index b6dc63a0c..bf39baf2a 100644 --- a/src/src/Commands/Settings.h +++ b/src/src/Commands/Settings.h @@ -7,6 +7,7 @@ String Command_Settings_Build(struct EventStruct *event, const char* Line); String Command_Settings_Unit(struct EventStruct *event, const char* Line); String Command_Settings_Name(struct EventStruct *event, const char* Line); String Command_Settings_Password(struct EventStruct *event, const char* Line); +String Command_Settings_Password_Clear(struct EventStruct *event, const char* Line); const __FlashStringHelper * Command_Settings_Save(struct EventStruct *event, const char* Line); const __FlashStringHelper * Command_Settings_Load(struct EventStruct *event, const char* Line); const __FlashStringHelper * Command_Settings_Print(struct EventStruct *event, const char* Line); diff --git a/src/src/DataStructs/SecurityStruct.cpp b/src/src/DataStructs/SecurityStruct.cpp index c9caba1d8..2a81ea4be 100644 --- a/src/src/DataStructs/SecurityStruct.cpp +++ b/src/src/DataStructs/SecurityStruct.cpp @@ -68,3 +68,13 @@ bool SecurityStruct::hasWiFiCredentials(SecurityStruct::WiFiCredentialsSlot slot } return false; } + +String SecurityStruct::getPassword() const { + String res; + const size_t passLength = strnlen(Password, sizeof(Password)); + res.reserve(passLength); + for (size_t i = 0; i < passLength; ++i) { + res += Password[i]; + } + return res; +} \ No newline at end of file diff --git a/src/src/DataStructs/SecurityStruct.h b/src/src/DataStructs/SecurityStruct.h index 49c66acc1..fca718e48 100644 --- a/src/src/DataStructs/SecurityStruct.h +++ b/src/src/DataStructs/SecurityStruct.h @@ -27,6 +27,8 @@ struct SecurityStruct bool hasWiFiCredentials(WiFiCredentialsSlot slot) const; + String getPassword() const; + char WifiSSID[32]; char WifiKey[64]; char WifiSSID2[32]; From 74c311ef95273f3f2505ee63a9605a5e340e2280 Mon Sep 17 00:00:00 2001 From: TD-er Date: Fri, 3 Dec 2021 16:14:25 +0100 Subject: [PATCH 15/17] [Doc] Add Notepad++ colourise ESPEasy Rules language --- docs/source/Rules/Rules.rst | 4 ++ misc/Notepad++/ESP_Easy__rules.xml | 64 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 misc/Notepad++/ESP_Easy__rules.xml diff --git a/docs/source/Rules/Rules.rst b/docs/source/Rules/Rules.rst index d735938f1..6baf9fb11 100644 --- a/docs/source/Rules/Rules.rst +++ b/docs/source/Rules/Rules.rst @@ -7,6 +7,10 @@ Introduction Along with ESP Easy R108, a new feature was enabled, named Rules. Rules can be used to create very simple flows to control devices on your ESP. +.. note:: + To assist writing rules, one may prefer to use an editor like Notepad++ which supports user defined languages to colorize the text. + See the ``Misc/Notepad++`` folder for a Notepad++ language definition which can be used to colorize rules. + Enable Rules ------------ diff --git a/misc/Notepad++/ESP_Easy__rules.xml b/misc/Notepad++/ESP_Easy__rules.xml new file mode 100644 index 000000000..5ebc41408 --- /dev/null +++ b/misc/Notepad++/ESP_Easy__rules.xml @@ -0,0 +1,64 @@ + + + + + + + + 00// 01 02 03// 04 + + + + + + + + > < = <= >= <> != + - * / % ! , # + and or + do + + endon + If + Else ElseIf + Endif + + + + AcessInfo Background Build ClearAccessBlock ClearPassword ClearRTCam Config ControllerDisable ControllerEnable DateTime Debug DeepSleep Delay DNS DST EraseSDKwifi ExecuteRules Gateway I2Cscanner IP Let Load Logentry LogPortStatus LoopTimerSet LoopTimerSet_ms MemInfo MemInfoDetail Name Password Publish Reboot Reset ResetFlashWriteCounter Save SendTo SendToHTTP SendToUDP Settings Subnet Subscribe TaskClear TaskClearAll TaskDisable TaskEnable TaskRun TaskValueSet TaskValueSetAndRun TimerPause TimerResume TimerSet TimerSet_ms TimeZone UdpPort UdpTest Unit UseNTP WdConfig WdRead WifiAPkey WifiAllowAP WifiAPMode WifiConnect WifiDisconnect WifiKey WifiKey2 WifiScan WifiSSID WifiSSID2 WifiSTAMode + Clock#Time= GPIO# Login#Failed MQTT#Connected MQTT#Disconnected MQTTimport#Connected MQTTimport#Disconnected Rules#Timer= System#Boot System#BootMode System#Sleep System#Wake TaskExit# TaskInit# Time#Initialized Time#Set WiFi#APmodeDisabled WiFi#APmodeEnabled WiFi#ChangedAccesspoint WiFi#ChangedWiFichannel WiFi#Connected + Event AsyncEvent + on + + /control?cmd= + GPIO GPIOToggle LongPulse LongPulse_mS Monitor UnMonitor Pulse PWM Servo Status MCPGPIO MCPGPIOToggle MCPLongPulse MCPLongPulse_ms MCPPulse Status,MCP Monitor,MCP UnMonitor,MCP MonitorRange;MCP UnMonitorRange,MCP MCPGPIORange MCPGPIOPattern MCPMode MCPModeRange PCFGPIO PCFGPIOToggle PCFLongPulse PCFLongPulse_ms PCFPulse Status,PCF Monitor,PCF UnMonitor,PCF MonitorRange,PCF UnMonitorRange,PCF PCFGPIORange PCFMode Tone RTTTL + ResetPulseCounter SetPulseCounterTotal LogPulseStatistic LCDCmd LCD OledFramedCmd OledFramedCmd,Display OledFramedCmd,Frame MotorShieldCmd,DCMotor MotorShieldCmd,Stepper Sensair_SetRelay PMSX003,Wake PMSX003,Sleep PMSX003,Reset Play Vol Eq Mode Repeat HLWCalibrate HLWReset WemosMotorShieldCMD LolinMotorShieldCMD HeatPumpir MitsubishiHP,temperature MitsubishiHP,power MitsubishiHP,mode MitsubishiHP,fan MitsubishiHP,vane MitsubishiHP,widevane Culreader_write TFTcmd TFT Touch,Rot WakeOnLan Max1704xclearalert + 00[ 01 02] 03{ 04 05} 06( 07 08) 09' 10 11' 12" 13 14" 15` 16 17` 18% 19 20% 21 22 23 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e19df0879f533ca9a7c563beabe4a7b4e91ae349 Mon Sep 17 00:00:00 2001 From: TD-er Date: Sun, 12 Dec 2021 16:16:42 +0100 Subject: [PATCH 16/17] [ClearPassword] Fix typo in command --- src/src/Commands/InternalCommands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/Commands/InternalCommands.cpp b/src/src/Commands/InternalCommands.cpp index 6a1a1fe85..94fd6a5c4 100644 --- a/src/src/Commands/InternalCommands.cpp +++ b/src/src/Commands/InternalCommands.cpp @@ -268,7 +268,7 @@ bool executeInternalCommand(command_case_data & data) } case 'c': { COMMAND_CASE_R( "clearaccessblock", Command_AccessInfo_Clear, 0); // Network Command - COMMAND_CASE_R( "clearpasword", Command_Settings_Password_Clear, 1); // Settings.h + COMMAND_CASE_R( "clearpassword", Command_Settings_Password_Clear, 1); // Settings.h COMMAND_CASE_R( "clearrtcram", Command_RTC_Clear, 0); // RTC.h COMMAND_CASE_R( "config", Command_Task_RemoteConfig, -1); // Tasks.h COMMAND_CASE_R("controllerdisable", Command_Controller_Disable, 1); // Controller.h From f8d3fcfc4c1a6b79bd322fab09e44859f55dd8c7 Mon Sep 17 00:00:00 2001 From: TD-er Date: Sun, 12 Dec 2021 18:39:53 +0100 Subject: [PATCH 17/17] [PIO] Restore default envs --- platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 88eae6bb9..4fe062eae 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,8 +20,8 @@ extra_configs = platformio_esp32_envs.ini platformio_special_envs.ini -;default_envs = custom_ESP8266_4M1M, custom_ESP32_4M316k -default_envs = test_D_ESP8266_4M1M +default_envs = custom_ESP8266_4M1M, custom_ESP32_4M316k +; default_envs = custom_ESP8266_4M1M ;default_envs = normal_ESP8266_4M1M ;default_envs = test_beta_ESP8266_4M1M