From 7ac83486ed2dc4d61c31a6e2aad39dc74db5c2e1 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 27 Dec 2021 15:42:12 +0100 Subject: [PATCH 01/33] [P118] Make CS pin configurable, resolve some compiler warnings --- lib/Itho/CC1101.cpp | 10 +++--- lib/Itho/CC1101.h | 4 ++- lib/Itho/IthoCC1101.cpp | 22 ++++++------ lib/Itho/IthoCC1101.h | 6 ++-- src/_P118_Itho.ino | 78 +++++++++++++++++++++++------------------ 5 files changed, 66 insertions(+), 54 deletions(-) diff --git a/lib/Itho/CC1101.cpp b/lib/Itho/CC1101.cpp index 903a6cab7a..a5dd99ee63 100644 --- a/lib/Itho/CC1101.cpp +++ b/lib/Itho/CC1101.cpp @@ -5,11 +5,11 @@ #include "CC1101.h" // default constructor -CC1101::CC1101() +CC1101::CC1101(int8_t CSpin) : _CSpin(CSpin) { - SPI.begin(); + // SPI.begin(); // Done by ESPEasy #ifdef ESP8266 - pinMode(SS, OUTPUT); + pinMode(_CSpin, OUTPUT); #endif } //CC1101 @@ -21,11 +21,11 @@ CC1101::~CC1101() /***********************/ // SPI helper functions select() and deselect() inline void CC1101::select(void) { - digitalWrite(SS, LOW); + digitalWrite(_CSpin, LOW); } inline void CC1101::deselect(void) { - digitalWrite(SS, HIGH); + digitalWrite(_CSpin, HIGH); } void CC1101::spi_waitMiso() diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h index b30e0162a0..ec729e71c7 100644 --- a/lib/Itho/CC1101.h +++ b/lib/Itho/CC1101.h @@ -168,7 +168,7 @@ class CC1101 //functions public: - CC1101(); + CC1101(int8_t CSpin = SS); ~CC1101(); //spi @@ -194,6 +194,8 @@ class CC1101 // SPI helper functions void select(void); void deselect(void); + + int8_t _CSpin = SS; protected: uint8_t readRegister(uint8_t address); diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp index 9e2d0961fc..7eb5d67849 100644 --- a/lib/Itho/IthoCC1101.cpp +++ b/lib/Itho/IthoCC1101.cpp @@ -40,7 +40,7 @@ #define MDMCFG2 0x02 //16bit sync word / 16bit specific // default constructor -IthoCC1101::IthoCC1101(uint8_t counter, uint8_t sendTries) : CC1101() +IthoCC1101::IthoCC1101(int8_t CSpin, uint8_t counter, uint8_t sendTries) : CC1101(CSpin) { this->outIthoPacket.counter = counter; this->sendTries = sendTries; @@ -629,14 +629,14 @@ uint8_t IthoCC1101::getCounter2(IthoPacket *itho, uint8_t len) { uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { - uint8_t lenOutbuf = 0; + // uint8_t lenOutbuf = 0; // Inhinit unused code to avoid compiler warning - 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; - } + // 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 @@ -645,7 +645,7 @@ uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { 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++) { + for (uint32_t i = out_bytecounter; i < sizeof(packet->data) / sizeof(packet->data[0]); i++) { packet->data[i] = 0; } @@ -723,10 +723,10 @@ void IthoCC1101::messageDecode(CC1101Packet *packet, IthoPacket *itho) { itho->length++; } - for (int i = 0; i < sizeof(itho->dataDecoded) / sizeof(itho->dataDecoded[0]); i++) { + for (uint32_t 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++) { + for (uint32_t i = 0; i < sizeof(itho->dataDecodedChk) / sizeof(itho->dataDecodedChk[0]); i++) { itho->dataDecodedChk[i] = 0; } diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h index f4fd7b0dad..48ba8384bc 100644 --- a/lib/Itho/IthoCC1101.h +++ b/lib/Itho/IthoCC1101.h @@ -56,12 +56,12 @@ class IthoCC1101 : protected CC1101 //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(int8_t CSpin = SS, uint8_t counter = 0, uint8_t sendTries = 3); //set initial counter value ~IthoCC1101(); - + //init void init() { CC1101::init(); initReceive(); } //init,reset CC1101 void initReceive(); diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index f29d9bf24c..1c5f8b5c93 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -70,7 +70,7 @@ struct PLUGIN__ExtraSettingsStruct char ID3[9]; } PLUGIN_118_ExtraSettings; -IthoCC1101 PLUGIN_118_rf; +IthoCC1101 *PLUGIN_118_rf = nullptr; // extra for interrupt handling bool PLUGIN_118_ITHOhasPacket = false; @@ -109,7 +109,7 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) case PLUGIN_DEVICE_ADD: { Device[++deviceCount].Number = PLUGIN_ID_118; - Device[deviceCount].Type = DEVICE_TYPE_SINGLE; + Device[deviceCount].Type = DEVICE_TYPE_SPI2; Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; Device[deviceCount].Ports = 0; Device[deviceCount].PullUpOption = false; @@ -140,11 +140,14 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) case PLUGIN_GET_DEVICEGPIONAMES: { event->String1 = formatGpioName_input(F("Interrupt pin (CC1101 GDO2)")); + event->String2 = formatGpioName_output(F("CS pin (CC1101 CSN)")); break; } case PLUGIN_SET_DEFAULTS: //Set defaults address to the one used in old versions of the library for backwards compatability { + PIN(0) = -1; // Interrupt pin undefined by default + PIN(1) = 15; // CS pin use the previous default of SS/gpio 15 PCONFIG(0) = 1; PCONFIG(1) = 10; PCONFIG(2) = 87; @@ -163,15 +166,18 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) } 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; + PLUGIN_118_rf = new (std::nothrow) IthoCC1101(PIN(1)); + if (nullptr != PLUGIN_118_rf) { + 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; } @@ -180,6 +186,10 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) addLog(LOG_LEVEL_INFO, F("EXIT PLUGIN_118")); //remove interupt when plugin is removed detachInterrupt(Plugin_118_IRQ_pin); + if (nullptr != PLUGIN_118_rf) { + delete PLUGIN_118_rf; + PLUGIN_118_rf = nullptr; + } success = true; break; } @@ -253,104 +263,104 @@ boolean Plugin_118(byte function, struct EventStruct *event, String &string) switch(event->Par1) { case 1111: //Join command { - PLUGIN_118_rf.sendCommand(IthoJoin); - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->sendCommand(IthoJoin); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("join")); success = true; break; } case 9999: //Leave command { - PLUGIN_118_rf.sendCommand(IthoLeave); - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->sendCommand(IthoLeave); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("leave")); success = true; break; } case 0: //Off command { - PLUGIN_118_rf.sendCommand(IthoStandby); + PLUGIN_118_rf->sendCommand(IthoStandby); PLUGIN_118_State=0; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("standby")); success = true; break; } case 1: //Fan low { - PLUGIN_118_rf.sendCommand(IthoLow); + PLUGIN_118_rf->sendCommand(IthoLow); PLUGIN_118_State=1; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("low speed")); success = true; break; } case 2: //Fan medium { - PLUGIN_118_rf.sendCommand(IthoMedium); + PLUGIN_118_rf->sendCommand(IthoMedium); PLUGIN_118_State=2; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("medium speed")); success = true; break; } case 3: //Fan high { - PLUGIN_118_rf.sendCommand(IthoHigh); + PLUGIN_118_rf->sendCommand(IthoHigh); PLUGIN_118_State=3; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("high speed")); success = true; break; } case 4: //Fan full { - PLUGIN_118_rf.sendCommand(IthoFull); + PLUGIN_118_rf->sendCommand(IthoFull); PLUGIN_118_State=4; PLUGIN_118_Timer=0; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("full speed")); success = true; break; } case 13: //Timer1 - 10 min { - PLUGIN_118_rf.sendCommand(IthoTimer1); + PLUGIN_118_rf->sendCommand(IthoTimer1); PLUGIN_118_State=13; PLUGIN_118_Timer=PLUGIN_118_Time1; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("timer 1")); success = true; break; } case 23: //Timer2 - 20 min { - PLUGIN_118_rf.sendCommand(IthoTimer2); + PLUGIN_118_rf->sendCommand(IthoTimer2); PLUGIN_118_State=23; PLUGIN_118_Timer=PLUGIN_118_Time2; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("timer 2")); success = true; break; } case 33: //Timer3 - 30 min { - PLUGIN_118_rf.sendCommand(IthoTimer3); + PLUGIN_118_rf->sendCommand(IthoTimer3); PLUGIN_118_State=33; PLUGIN_118_Timer=PLUGIN_118_Time3; PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf.initReceive(); + PLUGIN_118_rf->initReceive(); PLUGIN_118_PluginWriteLog(F("timer 3")); success = true; break; @@ -409,10 +419,10 @@ ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt() 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()) + if(PLUGIN_118_rf->checkForNewPacket()) { - IthoCommand cmd = PLUGIN_118_rf.getLastCommand(); - String Id = PLUGIN_118_rf.getLastIDstr(); + IthoCommand cmd = PLUGIN_118_rf->getLastCommand(); + String Id = PLUGIN_118_rf->getLastIDstr(); //Move check here to prevent function calling within ISR byte index = 0; From 08daa62a411c4ca7a5376a1a09e0aa37ea2e6c23 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 27 Dec 2021 22:28:13 +0100 Subject: [PATCH 02/33] [P118] Restructure using Plugin_data_struct, allow 3 simultaneous instances, code reformatted --- src/_P118_Itho.ino | 726 +++++++-------------- src/src/PluginStructs/P118_data_struct.cpp | 389 +++++++++++ src/src/PluginStructs/P118_data_struct.h | 71 ++ 3 files changed, 714 insertions(+), 472 deletions(-) create mode 100644 src/src/PluginStructs/P118_data_struct.cpp create mode 100644 src/src/PluginStructs/P118_data_struct.h diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index 1c5f8b5c93..55a5d49492 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -1,18 +1,25 @@ -//####################################################################################################### -//############################## Plugin 118: Itho ventilation unit 868Mhz remote ######################## -//####################################################################################################### +// ####################################################################################################### +// ############################## Plugin 118: Itho ventilation unit 868Mhz remote ######################## +// ####################################################################################################### -// author :jodur, 13-1-2018 +// 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; +// 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 +// svollebreggt, 13-2-2021 - Now uses rewirtten library made by arjenhiemstra: +// https://github.com/arjenhiemstra/IthoEcoFanRFT // svollebregt, 11-2021 - Code improvements +// tonhuisman, 27-12-2021 - Add setting for CS pin +// tonhuisman, 27-12-2021 - Split into P118_data_struct to enable multiple instances, reduce memory footprint +// - Allow 3 simultaneous instances, each using an interrupt and CS +// - Remove unused code, reformat source using Uncrustify // Recommended to disable RF receive logging to minimize code execution within interrupts @@ -28,7 +35,7 @@ // 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: +// List of States: // 1 - Itho ventilation unit to lowest speed // 2 - Itho ventilation unit to medium speed @@ -58,34 +65,13 @@ #ifdef USES_P118 -#include -#include "IthoCC1101.h" -#include "IthoPacket.h" #include "_Plugin_Helper.h" - -//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 = nullptr; - -// 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; +#include "./src/PluginStructs/P118_data_struct.h" // volatile for interrupt function -volatile bool PLUGIN_118_Int = false; -//volatile unsigned long PLUGIN_118_Int_time = 0; +volatile bool PLUGIN_118_Int[P118_INTERUPT_HANDLER_COUNT] = { false, false, false }; +int8_t PLUGIN_118_Task[P118_INTERUPT_HANDLER_COUNT]; +bool PLUGIN_118_Task_Initialized = false; #define PLUGIN_118 #define PLUGIN_ID_118 118 @@ -94,458 +80,254 @@ volatile bool PLUGIN_118_Int = false; #define PLUGIN_VALUENAME2_118 "Timer" #define PLUGIN_VALUENAME3_118 "LastIDindex" -// 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 - -boolean Plugin_118(byte function, struct EventStruct *event, String &string) +boolean Plugin_118(uint8_t 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_SPI2; - 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 = false; - Device[deviceCount].TimerOptional = 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)")); - event->String2 = formatGpioName_output(F("CS pin (CC1101 CSN)")); - break; - } + boolean success = false; - case PLUGIN_SET_DEFAULTS: //Set defaults address to the one used in old versions of the library for backwards compatability - { - PIN(0) = -1; // Interrupt pin undefined by default - PIN(1) = 15; // CS pin use the previous default of SS/gpio 15 - 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 = new (std::nothrow) IthoCC1101(PIN(1)); - if (nullptr != PLUGIN_118_rf) { - 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); - if (nullptr != PLUGIN_118_rf) { - delete PLUGIN_118_rf; - PLUGIN_118_rf = nullptr; - } - success = true; - break; - } - - case PLUGIN_ONCE_A_SECOND: + switch (function) { - //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_DEVICE_ADD: + { + Device[++deviceCount].Number = PLUGIN_ID_118; + Device[deviceCount].Type = DEVICE_TYPE_SPI2; + 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 = false; + Device[deviceCount].TimerOptional = true; + Device[deviceCount].GlobalSyncOption = 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_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_118); + break; + } - case PLUGIN_WRITE: { - String tmpString = string; - String cmd = parseString(tmpString, 1); - if (cmd.equalsIgnoreCase(F("STATE"))) - { - //noInterrupts(); - switch(event->Par1) { - case 1111: //Join command - { - PLUGIN_118_rf->sendCommand(IthoJoin); - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("join")); - success = true; - break; - } - case 9999: //Leave command - { - PLUGIN_118_rf->sendCommand(IthoLeave); - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("leave")); - success = true; - break; - } - case 0: //Off command - { - PLUGIN_118_rf->sendCommand(IthoStandby); - PLUGIN_118_State=0; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("standby")); - success = true; - break; - } - case 1: //Fan low - { - PLUGIN_118_rf->sendCommand(IthoLow); - PLUGIN_118_State=1; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("low speed")); - success = true; - break; - } - case 2: //Fan medium - { - PLUGIN_118_rf->sendCommand(IthoMedium); - PLUGIN_118_State=2; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("medium speed")); - success = true; - break; - } - case 3: //Fan high - { - PLUGIN_118_rf->sendCommand(IthoHigh); - PLUGIN_118_State=3; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("high speed")); - success = true; - break; - } - case 4: //Fan full - { - PLUGIN_118_rf->sendCommand(IthoFull); - PLUGIN_118_State=4; - PLUGIN_118_Timer=0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("full speed")); - success = true; - break; - } - case 13: //Timer1 - 10 min - { - PLUGIN_118_rf->sendCommand(IthoTimer1); - PLUGIN_118_State=13; - PLUGIN_118_Timer=PLUGIN_118_Time1; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("timer 1")); - success = true; - break; - } - case 23: //Timer2 - 20 min - { - PLUGIN_118_rf->sendCommand(IthoTimer2); - PLUGIN_118_State=23; - PLUGIN_118_Timer=PLUGIN_118_Time2; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("timer 2")); - success = true; - break; - } - case 33: //Timer3 - 30 min - { - PLUGIN_118_rf->sendCommand(IthoTimer3); - PLUGIN_118_State=33; - PLUGIN_118_Timer=PLUGIN_118_Time3; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); - PLUGIN_118_PluginWriteLog(F("timer 3")); - success = true; - break; - } - default: - { - PLUGIN_118_PluginWriteLog(F("INVALID")); - success = true; - } - } - //interrupts(); - } - 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)); //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 library")); - success = true; - 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_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; -} + case PLUGIN_GET_DEVICEGPIONAMES: + { + event->String1 = formatGpioName_input(F("Interrupt pin (CC1101 GDO2)")); + event->String2 = formatGpioName_output(F("CS pin (CC1101 CSN)")); + break; + } -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 + case PLUGIN_SET_DEFAULTS: // Set defaults address to the one used in old versions of the library for backwards compatability + { + PIN(0) = -1; // Interrupt pin undefined by default + PIN(1) = 15; // CS pin use the previous default of SS/gpio 15 + PCONFIG(0) = 1; + PCONFIG(1) = 10; + PCONFIG(2) = 87; + PCONFIG(3) = 81; + success = true; + break; + } + + case PLUGIN_INIT: + { + #ifdef P118_DEBUG_LOG + addLog(LOG_LEVEL_INFO, F("INIT PLUGIN_118")); + #endif // ifdef P118_DEBUG_LOG + initPluginTaskData(event->TaskIndex, new (std::nothrow) P118_data_struct(PCONFIG(0))); + P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P118_data) { + return success; + } + success = P118_data->plugin_init(event); // Part 1 + + if (success) { + if (!PLUGIN_118_Task_Initialized) { + for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT; i++) { + PLUGIN_118_Task[i] = -1; + PLUGIN_118_Int[i] = false; + } + PLUGIN_118_Task_Initialized = true; + } + int8_t offset = -1; + + for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT && offset == -1; i++) { + if (PLUGIN_118_Task[i] == -1) { // Find a free spot + offset = i; + } + } + + if (offset > -1) { + PLUGIN_118_Task[offset] = event->TaskIndex; + pinMode(PIN(0), INPUT); + + switch (offset) { // Add more interrupt handlers when needed + case 0: + attachInterrupt(PIN(0), PLUGIN_118_ITHOinterrupt0, FALLING); + break; + case 1: + attachInterrupt(PIN(0), PLUGIN_118_ITHOinterrupt1, FALLING); + break; + case 2: + attachInterrupt(PIN(0), PLUGIN_118_ITHOinterrupt2, FALLING); + break; + default: + break; + } + P118_data->plugin_init_part2(); // Start the data flow + addLog(LOG_LEVEL_INFO, F("CC1101 868Mhz transmitter initialized")); + } else { + clearPluginTaskData(event->TaskIndex); // Destroy initialized data, init failed + addLog(LOG_LEVEL_ERROR, F("CC1101 initialization failed!")); + success = false; + } + } + break; + } + + case PLUGIN_EXIT: + { + #ifdef P118_DEBUG_LOG + addLog(LOG_LEVEL_INFO, F("EXIT PLUGIN_118")); + #endif // ifdef P118_DEBUG_LOG + P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P118_data) { + return success; + } + int8_t offset = -1; + + for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT && offset == -1; i++) { + if (PLUGIN_118_Task[i] == event->TaskIndex) { + offset = i; + } + } + + // detach interupt when plugin is 'removed' + if (offset > -1) { + detachInterrupt(PIN(0)); + } + + success = P118_data->plugin_exit(event); + break; + } + + case PLUGIN_ONCE_A_SECOND: + { + P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P118_data) { + return success; + } + success = P118_data->plugin_once_a_second(event); + + break; + } + + case PLUGIN_FIFTY_PER_SECOND: + { + int8_t offset = -1; + + // Find matching interrupt flag + for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT && offset == -1; i++) { + if (PLUGIN_118_Task[i] == event->TaskIndex) { + offset = i; + } + } + + if ((offset > -1) && PLUGIN_118_Int[offset]) + { + P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P118_data) { + return success; + } + success = P118_data->plugin_fifty_per_second(event); + + PLUGIN_118_Int[offset] = false; // reset flag + } + break; + } + + + case PLUGIN_READ: { + P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P118_data) { + return success; + } + success = P118_data->plugin_read(event); + + break; + } + + case PLUGIN_WRITE: { + P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P118_data) { + return success; + } + success = P118_data->plugin_write(event, string); + break; + } + + case PLUGIN_WEBFORM_LOAD: + { + PLUGIN__ExtraSettingsStruct PLUGIN_118_ExtraSettings; + LoadCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + 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)); // 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 library")); + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + PLUGIN__ExtraSettingsStruct PLUGIN_118_ExtraSettings; + 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")); + + 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; } -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()) - { - 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 - String log2 = ""; - if (index>0) - { - if(PLUGIN_118_Log){ - log2 += F("Command received from remote-ID: "); - log2 += Id; - log2 += F(", command: "); - //addLog(LOG_LEVEL_DEBUG, log); - } - switch (cmd) - { - case IthoUnknown: - if(PLUGIN_118_Log){log2 += F("unknown");} - break; - case IthoStandby: - case DucoStandby: - 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");} - PLUGIN_118_State = 1; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; - break; - case IthoMedium: - case DucoMedium: - 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");} - PLUGIN_118_State = 3; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; - break; - case IthoFull: - 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");} - PLUGIN_118_State = 13; - PLUGIN_118_Timer = PLUGIN_118_Time1; - PLUGIN_118_LastIDindex = index; - break; - case IthoTimer2: - 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");} - PLUGIN_118_State = 33; - PLUGIN_118_Timer = PLUGIN_118_Time3; - PLUGIN_118_LastIDindex = index; - break; - case IthoJoin: - if(PLUGIN_118_Log){log2 += F("join");} - break; - case IthoLeave: - if(PLUGIN_118_Log){log2 += F("leave");} - break; - } - } - else { - if(PLUGIN_118_Log){ - log2 += F("Device-ID: "); - log2 += Id; - log2 += F(" IGNORED"); - } - } - if(PLUGIN_118_Log){addLog(LOG_LEVEL_DEBUG, log2);} - } +ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt0() { + PLUGIN_118_Int[0] = true; // flag 0 } -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); +ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt1() { + PLUGIN_118_Int[1] = true; // flag 1 } -void PLUGIN_118_PluginWriteLog(String command) -{ - String log = F("Send Itho command for: "); - log += command; - addLog(LOG_LEVEL_INFO, log); - printWebString += log; +ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt2() { + PLUGIN_118_Int[2] = true; // flag 2 } #endif // USES_P118 diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp new file mode 100644 index 0000000000..a6a4865dd4 --- /dev/null +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -0,0 +1,389 @@ +#include "../PluginStructs/P118_data_struct.h" + +#ifdef USES_P118 + +// **************************************************************************/ +// Constructor +// **************************************************************************/ +P118_data_struct::P118_data_struct(uint8_t logData) + : PLUGIN_118_Log(logData) {} + +// **************************************************************************/ +// Destructor +// **************************************************************************/ +P118_data_struct::~P118_data_struct() { + if (isInitialized()) { + delete PLUGIN_118_rf; + PLUGIN_118_rf = nullptr; + } +} + +bool P118_data_struct::plugin_init(struct EventStruct *event) { + bool success = false; + + LoadCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + addLog(LOG_LEVEL_INFO, F("Extra Settings PLUGIN_118 loaded")); + + PLUGIN_118_rf = new (std::nothrow) IthoCC1101(PIN(1)); + + if (nullptr != PLUGIN_118_rf) { + 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(); + + // Next is to connect the interrupt, + // then plugin_init_part2 is to be called (below) + success = true; + } + return success; +} + +void P118_data_struct::plugin_init_part2() { + if (nullptr != PLUGIN_118_rf) { + PLUGIN_118_rf->initReceive(); + PLUGIN_118_InitRunned = true; + } +} + +bool P118_data_struct::plugin_exit(struct EventStruct *event) { + // remove interupt when plugin is removed + detachInterrupt(Plugin_118_IRQ_pin); + + if (nullptr != PLUGIN_118_rf) { + delete PLUGIN_118_rf; + PLUGIN_118_rf = nullptr; + } + return true; +} + +bool P118_data_struct::plugin_once_a_second(struct EventStruct *event) { + // 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")); + 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; + return true; +} + +bool P118_data_struct::plugin_fifty_per_second(struct EventStruct *event) { + // PLUGIN_118_Int = false; // reset flag + ITHOcheck(); + + /* + unsigned long time_elapsed = millis() - PLUGIN_118_Int_time; //Disabled as it doesn't appear to be required + if (time_elapsed >= 10) + { + ITHOcheck(); + } + else + { + delay(10-time_elapsed); + ITHOcheck(); + }*/ + return true; +} + +bool P118_data_struct::plugin_read(struct EventStruct *event) { + // 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")); + PublishData(event); + + // sendData(event); //SV - Added to send status every xx secnds as set within plugin + return true; +} + +bool P118_data_struct::plugin_write(struct EventStruct *event, const String& string) { + bool success = false; + String tmpString = string; + String cmd = parseString(tmpString, 1); + + if (cmd.equalsIgnoreCase(F("STATE"))) + { + // noInterrupts(); + switch (event->Par1) { + case 1111: // Join command + { + PLUGIN_118_rf->sendCommand(IthoJoin); + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("join")); + success = true; + break; + } + case 9999: // Leave command + { + PLUGIN_118_rf->sendCommand(IthoLeave); + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("leave")); + success = true; + break; + } + case 0: // Off command + { + PLUGIN_118_rf->sendCommand(IthoStandby); + PLUGIN_118_State = 0; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = 0; + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("standby")); + success = true; + break; + } + case 1: // Fan low + { + PLUGIN_118_rf->sendCommand(IthoLow); + PLUGIN_118_State = 1; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = 0; + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("low speed")); + success = true; + break; + } + case 2: // Fan medium + { + PLUGIN_118_rf->sendCommand(IthoMedium); + PLUGIN_118_State = 2; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = 0; + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("medium speed")); + success = true; + break; + } + case 3: // Fan high + { + PLUGIN_118_rf->sendCommand(IthoHigh); + PLUGIN_118_State = 3; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = 0; + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("high speed")); + success = true; + break; + } + case 4: // Fan full + { + PLUGIN_118_rf->sendCommand(IthoFull); + PLUGIN_118_State = 4; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = 0; + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("full speed")); + success = true; + break; + } + case 13: // Timer1 - 10 min + { + PLUGIN_118_rf->sendCommand(IthoTimer1); + PLUGIN_118_State = 13; + PLUGIN_118_Timer = PLUGIN_118_Time1; + PLUGIN_118_LastIDindex = 0; + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("timer 1")); + success = true; + break; + } + case 23: // Timer2 - 20 min + { + PLUGIN_118_rf->sendCommand(IthoTimer2); + PLUGIN_118_State = 23; + PLUGIN_118_Timer = PLUGIN_118_Time2; + PLUGIN_118_LastIDindex = 0; + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("timer 2")); + success = true; + break; + } + case 33: // Timer3 - 30 min + { + PLUGIN_118_rf->sendCommand(IthoTimer3); + PLUGIN_118_State = 33; + PLUGIN_118_Timer = PLUGIN_118_Time3; + PLUGIN_118_LastIDindex = 0; + PLUGIN_118_rf->initReceive(); + PluginWriteLog(F("timer 3")); + success = true; + break; + } + default: + { + PluginWriteLog(F("INVALID")); + + // success = true; + } + } + + // interrupts(); + } + return success; +} + +void P118_data_struct::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()) { + 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 + String log2 = ""; + + if (index > 0) { + if (PLUGIN_118_Log) { + log2 += F("Command received from remote-ID: "); + log2 += Id; + log2 += F(", command: "); + + // addLog(LOG_LEVEL_DEBUG, log); + } + + switch (cmd) { + case IthoUnknown: + + if (PLUGIN_118_Log) { log2 += F("unknown"); } + break; + case IthoStandby: + case DucoStandby: + + 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"); } + PLUGIN_118_State = 1; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoMedium: + case DucoMedium: + + 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"); } + PLUGIN_118_State = 3; + PLUGIN_118_Timer = 0; + PLUGIN_118_LastIDindex = index; + break; + case IthoFull: + + 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"); } + PLUGIN_118_State = 13; + PLUGIN_118_Timer = PLUGIN_118_Time1; + PLUGIN_118_LastIDindex = index; + break; + case IthoTimer2: + + 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"); } + PLUGIN_118_State = 33; + PLUGIN_118_Timer = PLUGIN_118_Time3; + PLUGIN_118_LastIDindex = index; + break; + case IthoJoin: + + if (PLUGIN_118_Log) { log2 += F("join"); } + break; + case IthoLeave: + + if (PLUGIN_118_Log) { log2 += F("leave"); } + break; + } + } else { + if (PLUGIN_118_Log) { + log2 += F("Device-ID: "); + log2 += Id; + log2 += F(" IGNORED"); + } + } + + if (PLUGIN_118_Log) { addLog(LOG_LEVEL_DEBUG, log2); } + } +} + +void P118_data_struct::PublishData(struct EventStruct *event) { + UserVar[event->BaseVarIndex] = PLUGIN_118_State; + UserVar[event->BaseVarIndex + 1] = PLUGIN_118_Timer; + UserVar[event->BaseVarIndex + 2] = PLUGIN_118_LastIDindex; + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + 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); + } +} + +void P118_data_struct::PluginWriteLog(const String& command) { + String log = F("Send Itho command for: "); + + log += command; + addLog(LOG_LEVEL_INFO, log); + printWebString += log; +} + +#endif // ifdef USES_P118 diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h new file mode 100644 index 0000000000..790164cc05 --- /dev/null +++ b/src/src/PluginStructs/P118_data_struct.h @@ -0,0 +1,71 @@ +#ifndef PLUGINSTRUCTS_P118_DATA_STRUCT_H +#define PLUGINSTRUCTS_P118_DATA_STRUCT_H + +#include "../../_Plugin_Helper.h" +#ifdef USES_P118 + +# include "../../ESPEasy-Globals.h" + +# include +# include "IthoCC1101.h" +# include "IthoPacket.h" + +# define P118_DEBUG_LOG // Enable for some (extra) logging + +# define P118_INTERUPT_HANDLER_COUNT 3 // This should only be changed if the Interrupt handlers are also adjusted + +// 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 + +// 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]; +}; + +struct P118_data_struct : public PluginTaskData_base { +public: + + P118_data_struct(uint8_t logData); + + P118_data_struct() = delete; + ~P118_data_struct(); + + bool plugin_init(struct EventStruct *event); + void plugin_init_part2(); + bool plugin_exit(struct EventStruct *event); + bool plugin_once_a_second(struct EventStruct *event); + bool plugin_fifty_per_second(struct EventStruct *event); + bool plugin_read(struct EventStruct *event); + bool plugin_write(struct EventStruct *event, + const String & string); + void ITHOcheck(); + void PublishData(struct EventStruct *event); + void PluginWriteLog(const String& command); + + bool isInitialized() { + return PLUGIN_118_rf != nullptr; + } + +private: + + IthoCC1101 *PLUGIN_118_rf = nullptr; + + // 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; + + PLUGIN__ExtraSettingsStruct PLUGIN_118_ExtraSettings; +}; +#endif // ifdef USES_P118 +#endif // ifndef PLUGINSTRUCTS_P118_DATA_STRUCT_H From c2b7a8c08e2754d3d5fc7a24bd497f84e4369eed Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 27 Dec 2021 22:50:34 +0100 Subject: [PATCH 03/33] [P118] Add library.properties and reference it --- lib/Itho/library.properties | 8 ++++++++ platformio_esp32_envs.ini | 2 +- platformio_esp82xx_base.ini | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 lib/Itho/library.properties diff --git a/lib/Itho/library.properties b/lib/Itho/library.properties new file mode 100644 index 0000000000..83af3f69e7 --- /dev/null +++ b/lib/Itho/library.properties @@ -0,0 +1,8 @@ +name=IthoCC1101 +version=1.0.0 +author=Itho +maintainer=Itho +sentence=Itho communication library +paragraph=This is a library for the Arduino IDE that helps interface with Itho ventilation equipment via CC1101 868 MHz. +category=Sensors +architectures=* diff --git a/platformio_esp32_envs.ini b/platformio_esp32_envs.ini index 87d78bf85f..a8f7f9937d 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.8, 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, FrogmoreScd30, Multi Channel Relay Arduino Library +lib_deps = td-er/ESPeasySerial @ 2.0.8, 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, FrogmoreScd30, Multi Channel Relay Arduino Library, IthoCC1101 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 77f8c46bdf..83ad2330b6 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.8, 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, FrogmoreScd30, Multi Channel Relay Arduino Library +lib_deps = td-er/ESPeasySerial @ 2.0.8, 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, FrogmoreScd30, Multi Channel Relay Arduino Library, IthoCC1101 lib_ignore = ${esp82xx_defaults.lib_ignore}, IRremoteESP8266, HeatpumpIR, LittleFS(esp8266), ServoESP32, TinyWireM board = esp12e monitor_filters = esp8266_exception_decoder From b2268009622238284e888f314bcee847ed37dfc4 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 28 Dec 2021 10:19:57 +0100 Subject: [PATCH 04/33] [P118] Use more descriptive names for a few variables/constants --- lib/Itho/CC1101.h | 4 ++-- lib/Itho/IthoCC1101.h | 2 +- src/_P118_Itho.ino | 12 ++++++------ src/src/PluginStructs/P118_data_struct.h | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h index ec729e71c7..d428169514 100644 --- a/lib/Itho/CC1101.h +++ b/lib/Itho/CC1101.h @@ -168,7 +168,7 @@ class CC1101 //functions public: - CC1101(int8_t CSpin = SS); + CC1101(int8_t CSpin = PIN_SPI_SS); ~CC1101(); //spi @@ -195,7 +195,7 @@ class CC1101 void select(void); void deselect(void); - int8_t _CSpin = SS; + int8_t _CSpin = PIN_SPI_SS; protected: uint8_t readRegister(uint8_t address); diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h index 48ba8384bc..ca91c38eb9 100644 --- a/lib/Itho/IthoCC1101.h +++ b/lib/Itho/IthoCC1101.h @@ -59,7 +59,7 @@ class IthoCC1101 : protected CC1101 //functions public: - IthoCC1101(int8_t CSpin = SS, uint8_t counter = 0, uint8_t sendTries = 3); //set initial counter value + IthoCC1101(int8_t CSpin = PIN_SPI_SS, uint8_t counter = 0, uint8_t sendTries = 3); //set initial counter value ~IthoCC1101(); //init diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index 55a5d49492..c3a1f0f08f 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -124,10 +124,10 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) break; } - case PLUGIN_SET_DEFAULTS: // Set defaults address to the one used in old versions of the library for backwards compatability + case PLUGIN_SET_DEFAULTS: // Set defaults address to the one used in old versions of the library for backwards compatability { - PIN(0) = -1; // Interrupt pin undefined by default - PIN(1) = 15; // CS pin use the previous default of SS/gpio 15 + PIN(0) = -1; // Interrupt pin undefined by default + PIN(1) = PIN_SPI_SS; // CS pin use the previous default of PIN_SPI_SS/gpio 15 PCONFIG(0) = 1; PCONFIG(1) = 10; PCONFIG(2) = 87; @@ -236,7 +236,7 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) { int8_t offset = -1; - // Find matching interrupt flag + // Find matching interrupt flag for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT && offset == -1; i++) { if (PLUGIN_118_Task[i] == event->TaskIndex) { offset = i; @@ -281,7 +281,7 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD: { - PLUGIN__ExtraSettingsStruct PLUGIN_118_ExtraSettings; + PLUGIN_118_ExtraSettingsStruct PLUGIN_118_ExtraSettings; LoadCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); addFormSubHeader(F("Remote RF Controls")); addFormTextBox(F("Unit ID remote 1"), F("PLUGIN_118_ID1"), PLUGIN_118_ExtraSettings.ID1, 8); @@ -300,7 +300,7 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { - PLUGIN__ExtraSettingsStruct PLUGIN_118_ExtraSettings; + PLUGIN_118_ExtraSettingsStruct PLUGIN_118_ExtraSettings; 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()); diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index 790164cc05..c696e0b10c 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -20,7 +20,7 @@ # define PLUGIN_118_Time3 30 * 60 // This extra settings struct is needed because the default settingsstruct doesn't support strings -struct PLUGIN__ExtraSettingsStruct { +struct PLUGIN_118_ExtraSettingsStruct { char ID1[9]; char ID2[9]; char ID3[9]; @@ -65,7 +65,7 @@ struct P118_data_struct : public PluginTaskData_base { bool PLUGIN_118_InitRunned = false; bool PLUGIN_118_Log = false; - PLUGIN__ExtraSettingsStruct PLUGIN_118_ExtraSettings; + PLUGIN_118_ExtraSettingsStruct PLUGIN_118_ExtraSettings; }; #endif // ifdef USES_P118 #endif // ifndef PLUGINSTRUCTS_P118_DATA_STRUCT_H From cba7800570fd08596a03bf738dced07b257a2c39 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 28 Dec 2021 11:18:31 +0100 Subject: [PATCH 05/33] [P118] Fix missing #define --- lib/Itho/CC1101.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h index d428169514..cc33dbe35d 100644 --- a/lib/Itho/CC1101.h +++ b/lib/Itho/CC1101.h @@ -9,6 +9,9 @@ #include "CC1101Packet.h" #include // On Arduino, SPI pins are predefined +#ifndef PIN_SPI_SS +#define PIN_SPI_SS (15) +#endif /* Type of transfers */ #define CC1101_WRITE_BURST 0x40 From 2690fd8deed5e7bea08d5c7264784f8191df15aa Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 28 Dec 2021 11:46:27 +0100 Subject: [PATCH 06/33] [Build] Fix compiler warnings --- src/src/WebServer/CustomPage.cpp | 2 +- src/src/WebServer/LoadFromFS.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/src/WebServer/CustomPage.cpp b/src/src/WebServer/CustomPage.cpp index b7114b2432..06e096cbb5 100644 --- a/src/src/WebServer/CustomPage.cpp +++ b/src/src/WebServer/CustomPage.cpp @@ -137,7 +137,7 @@ bool handle_custom(const String& path) { if (dataFile) { // Read the file per line and serve per line to reduce amount of memory needed. - int available = dataFile.available(); + uint32_t available = dataFile.available(); String line; line.reserve(128); while (available > 0) { diff --git a/src/src/WebServer/LoadFromFS.cpp b/src/src/WebServer/LoadFromFS.cpp index 8ff857fb64..984b0cd9cd 100644 --- a/src/src/WebServer/LoadFromFS.cpp +++ b/src/src/WebServer/LoadFromFS.cpp @@ -152,7 +152,7 @@ size_t streamFromFS(String path, bool htmlEscape) { return bytesStreamed; } - int available = f.available(); + uint32_t available = f.available(); String escaped; while (available > 0) { uint32_t chunksize = 64; From b6900f58bd5a18ab6ccffcce9ffb2525a884ddd8 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 28 Dec 2021 14:01:52 +0100 Subject: [PATCH 07/33] [P118] Refactor interrupt handling --- src/_P118_Itho.ino | 98 ++-------------------- src/src/PluginStructs/P118_data_struct.cpp | 45 +++++----- src/src/PluginStructs/P118_data_struct.h | 9 +- 3 files changed, 35 insertions(+), 117 deletions(-) diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index c3a1f0f08f..3fed283117 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -20,6 +20,7 @@ // tonhuisman, 27-12-2021 - Split into P118_data_struct to enable multiple instances, reduce memory footprint // - Allow 3 simultaneous instances, each using an interrupt and CS // - Remove unused code, reformat source using Uncrustify +// tonhuisman, 28-12-2021 - Move interrupt handling to Plugin_data_struct, lifting the limit on nr. of plugins // Recommended to disable RF receive logging to minimize code execution within interrupts @@ -68,11 +69,6 @@ #include "_Plugin_Helper.h" #include "./src/PluginStructs/P118_data_struct.h" -// volatile for interrupt function -volatile bool PLUGIN_118_Int[P118_INTERUPT_HANDLER_COUNT] = { false, false, false }; -int8_t PLUGIN_118_Task[P118_INTERUPT_HANDLER_COUNT]; -bool PLUGIN_118_Task_Initialized = false; - #define PLUGIN_118 #define PLUGIN_ID_118 118 #define PLUGIN_NAME_118 "Communication - Itho ventilation" @@ -147,49 +143,8 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) if (nullptr == P118_data) { return success; } - success = P118_data->plugin_init(event); // Part 1 - - if (success) { - if (!PLUGIN_118_Task_Initialized) { - for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT; i++) { - PLUGIN_118_Task[i] = -1; - PLUGIN_118_Int[i] = false; - } - PLUGIN_118_Task_Initialized = true; - } - int8_t offset = -1; - - for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT && offset == -1; i++) { - if (PLUGIN_118_Task[i] == -1) { // Find a free spot - offset = i; - } - } - - if (offset > -1) { - PLUGIN_118_Task[offset] = event->TaskIndex; - pinMode(PIN(0), INPUT); - - switch (offset) { // Add more interrupt handlers when needed - case 0: - attachInterrupt(PIN(0), PLUGIN_118_ITHOinterrupt0, FALLING); - break; - case 1: - attachInterrupt(PIN(0), PLUGIN_118_ITHOinterrupt1, FALLING); - break; - case 2: - attachInterrupt(PIN(0), PLUGIN_118_ITHOinterrupt2, FALLING); - break; - default: - break; - } - P118_data->plugin_init_part2(); // Start the data flow - addLog(LOG_LEVEL_INFO, F("CC1101 868Mhz transmitter initialized")); - } else { - clearPluginTaskData(event->TaskIndex); // Destroy initialized data, init failed - addLog(LOG_LEVEL_ERROR, F("CC1101 initialization failed!")); - success = false; - } - } + success = P118_data->plugin_init(event); + break; } @@ -203,18 +158,6 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) if (nullptr == P118_data) { return success; } - int8_t offset = -1; - - for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT && offset == -1; i++) { - if (PLUGIN_118_Task[i] == event->TaskIndex) { - offset = i; - } - } - - // detach interupt when plugin is 'removed' - if (offset > -1) { - detachInterrupt(PIN(0)); - } success = P118_data->plugin_exit(event); break; @@ -234,26 +177,13 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_FIFTY_PER_SECOND: { - int8_t offset = -1; + P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - // Find matching interrupt flag - for (uint8_t i = 0; i < P118_INTERUPT_HANDLER_COUNT && offset == -1; i++) { - if (PLUGIN_118_Task[i] == event->TaskIndex) { - offset = i; - } + if (nullptr == P118_data) { + return success; } + success = P118_data->plugin_fifty_per_second(event); - if ((offset > -1) && PLUGIN_118_Int[offset]) - { - P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - - if (nullptr == P118_data) { - return success; - } - success = P118_data->plugin_fifty_per_second(event); - - PLUGIN_118_Int[offset] = false; // reset flag - } break; } @@ -287,7 +217,7 @@ boolean Plugin_118(uint8_t 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)); // Makes RF logging optional to reduce clutter in the lof file + addFormCheckBox(F("Enable RF receive log"), F("p118_log"), PCONFIG(0)); // Makes RF logging optional to reduce clutter in the log 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); @@ -318,16 +248,4 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) return success; } -ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt0() { - PLUGIN_118_Int[0] = true; // flag 0 -} - -ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt1() { - PLUGIN_118_Int[1] = true; // flag 1 -} - -ICACHE_RAM_ATTR void PLUGIN_118_ITHOinterrupt2() { - PLUGIN_118_Int[2] = true; // flag 2 -} - #endif // USES_P118 diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index a6a4865dd4..e6bcbb52b8 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -31,23 +31,22 @@ bool P118_data_struct::plugin_init(struct EventStruct *event) { // multi itho control, 10,87,81 corresponds with old library PLUGIN_118_rf->init(); - // Next is to connect the interrupt, - // then plugin_init_part2 is to be called (below) - success = true; - } - return success; -} + attachInterruptArg(digitalPinToInterrupt(Plugin_118_IRQ_pin), + reinterpret_cast(ISR_ithoCheck), + this, + FALLING); -void P118_data_struct::plugin_init_part2() { - if (nullptr != PLUGIN_118_rf) { PLUGIN_118_rf->initReceive(); PLUGIN_118_InitRunned = true; + + success = true; } + return success; } bool P118_data_struct::plugin_exit(struct EventStruct *event) { // remove interupt when plugin is removed - detachInterrupt(Plugin_118_IRQ_pin); + detachInterrupt(digitalPinToInterrupt(Plugin_118_IRQ_pin)); if (nullptr != PLUGIN_118_rf) { delete PLUGIN_118_rf; @@ -86,20 +85,11 @@ bool P118_data_struct::plugin_once_a_second(struct EventStruct *event) { } bool P118_data_struct::plugin_fifty_per_second(struct EventStruct *event) { - // PLUGIN_118_Int = false; // reset flag - ITHOcheck(); - - /* - unsigned long time_elapsed = millis() - PLUGIN_118_Int_time; //Disabled as it doesn't appear to be required - if (time_elapsed >= 10) - { - ITHOcheck(); - } - else - { - delay(10-time_elapsed); - ITHOcheck(); - }*/ + if (PLUGIN_118_Int) { + ITHOcheck(); + PLUGIN_118_Int = false; // reset flag + } + return true; } @@ -386,4 +376,13 @@ void P118_data_struct::PluginWriteLog(const String& command) { printWebString += log; } +// **************************************************************************/ +// Interrupt handler +// **************************************************************************/ +void ICACHE_RAM_ATTR P118_data_struct::ISR_ithoCheck(P118_data_struct *self) { + noInterrupts(); // Disable interrupts + self->PLUGIN_118_Int = true; + interrupts(); // Re-enable interrupts +} + #endif // ifdef USES_P118 diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index c696e0b10c..41bed47aa6 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -10,9 +10,7 @@ # include "IthoCC1101.h" # include "IthoPacket.h" -# define P118_DEBUG_LOG // Enable for some (extra) logging - -# define P118_INTERUPT_HANDLER_COUNT 3 // This should only be changed if the Interrupt handlers are also adjusted +// # define P118_DEBUG_LOG // Enable for some (extra) logging // Timer values for hardware timer in Fan in seconds # define PLUGIN_118_Time1 10 * 60 @@ -35,7 +33,6 @@ struct P118_data_struct : public PluginTaskData_base { ~P118_data_struct(); bool plugin_init(struct EventStruct *event); - void plugin_init_part2(); bool plugin_exit(struct EventStruct *event); bool plugin_once_a_second(struct EventStruct *event); bool plugin_fifty_per_second(struct EventStruct *event); @@ -66,6 +63,10 @@ struct P118_data_struct : public PluginTaskData_base { bool PLUGIN_118_Log = false; PLUGIN_118_ExtraSettingsStruct PLUGIN_118_ExtraSettings; + + volatile bool PLUGIN_118_Int = false; + + static void ISR_ithoCheck(P118_data_struct *self); }; #endif // ifdef USES_P118 #endif // ifndef PLUGINSTRUCTS_P118_DATA_STRUCT_H From 82eba3bda18d99a89b2ad3385778a5355c7c12f6 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 3 Jan 2022 20:51:48 +0100 Subject: [PATCH 08/33] [P118] Attempt to avoid startup crash --- src/_P118_Itho.ino | 1 + src/src/PluginStructs/P118_data_struct.cpp | 53 +++++++++++----------- src/src/PluginStructs/P118_data_struct.h | 2 +- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index 3fed283117..1be4dd9d70 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -21,6 +21,7 @@ // - Allow 3 simultaneous instances, each using an interrupt and CS // - Remove unused code, reformat source using Uncrustify // tonhuisman, 28-12-2021 - Move interrupt handling to Plugin_data_struct, lifting the limit on nr. of plugins +// tonhuisman, 03-01-2022 - Review source after structural-crash report, fix interrupt handler // Recommended to disable RF receive logging to minimize code execution within interrupts diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index e6bcbb52b8..558353269c 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -22,7 +22,9 @@ bool P118_data_struct::plugin_init(struct EventStruct *event) { bool success = false; LoadCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + # ifdef P118_DEBUG_LOG addLog(LOG_LEVEL_INFO, F("Extra Settings PLUGIN_118 loaded")); + # endif // ifdef P118_DEBUG_LOG PLUGIN_118_rf = new (std::nothrow) IthoCC1101(PIN(1)); @@ -70,7 +72,9 @@ bool P118_data_struct::plugin_once_a_second(struct EventStruct *event) { 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) { + # ifdef P118_DEBUG_LOG addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_ONCE_A_SECOND")); + # endif // ifdef P118_DEBUG_LOG PublishData(event); sendData(event); @@ -95,7 +99,9 @@ bool P118_data_struct::plugin_fifty_per_second(struct EventStruct *event) { bool P118_data_struct::plugin_read(struct EventStruct *event) { // This ensures that even when Values are not changing, data is send at the configured interval for aquisition + # ifdef P118_DEBUG_LOG addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_READ")); + # endif // ifdef P118_DEBUG_LOG PublishData(event); // sendData(event); //SV - Added to send status every xx secnds as set within plugin @@ -109,7 +115,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str if (cmd.equalsIgnoreCase(F("STATE"))) { - // noInterrupts(); switch (event->Par1) { case 1111: // Join command { @@ -218,12 +223,8 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str default: { PluginWriteLog(F("INVALID")); - - // success = true; } } - - // interrupts(); } return success; } @@ -252,13 +253,13 @@ void P118_data_struct::ITHOcheck() { // int index = PLUGIN_118_RFRemoteIndex(Id); // IF id is know index should be >0 - String log2 = ""; + String log; if (index > 0) { if (PLUGIN_118_Log) { - log2 += F("Command received from remote-ID: "); - log2 += Id; - log2 += F(", command: "); + log += F("Command received from remote-ID: "); + log += Id; + log += F(", command: "); // addLog(LOG_LEVEL_DEBUG, log); } @@ -266,12 +267,12 @@ void P118_data_struct::ITHOcheck() { switch (cmd) { case IthoUnknown: - if (PLUGIN_118_Log) { log2 += F("unknown"); } + if (PLUGIN_118_Log) { log += F("unknown"); } break; case IthoStandby: case DucoStandby: - if (PLUGIN_118_Log) { log2 += F("standby"); } + if (PLUGIN_118_Log) { log += F("standby"); } PLUGIN_118_State = 0; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; @@ -279,7 +280,7 @@ void P118_data_struct::ITHOcheck() { case IthoLow: case DucoLow: - if (PLUGIN_118_Log) { log2 += F("low"); } + if (PLUGIN_118_Log) { log += F("low"); } PLUGIN_118_State = 1; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; @@ -287,7 +288,7 @@ void P118_data_struct::ITHOcheck() { case IthoMedium: case DucoMedium: - if (PLUGIN_118_Log) { log2 += F("medium"); } + if (PLUGIN_118_Log) { log += F("medium"); } PLUGIN_118_State = 2; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; @@ -295,57 +296,59 @@ void P118_data_struct::ITHOcheck() { case IthoHigh: case DucoHigh: - if (PLUGIN_118_Log) { log2 += F("high"); } + if (PLUGIN_118_Log) { log += F("high"); } PLUGIN_118_State = 3; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; break; case IthoFull: - if (PLUGIN_118_Log) { log2 += F("full"); } + if (PLUGIN_118_Log) { log += F("full"); } PLUGIN_118_State = 4; PLUGIN_118_Timer = 0; PLUGIN_118_LastIDindex = index; break; case IthoTimer1: - if (PLUGIN_118_Log) { log2 += +F("timer1"); } + if (PLUGIN_118_Log) { log += +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"); } + if (PLUGIN_118_Log) { log += 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"); } + if (PLUGIN_118_Log) { log += 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"); } + if (PLUGIN_118_Log) { log += F("join"); } break; case IthoLeave: - if (PLUGIN_118_Log) { log2 += F("leave"); } + if (PLUGIN_118_Log) { log += F("leave"); } break; } } else { if (PLUGIN_118_Log) { - log2 += F("Device-ID: "); - log2 += Id; - log2 += F(" IGNORED"); + log += F("Device-ID: "); + log += Id; + log += F(" IGNORED"); } } - if (PLUGIN_118_Log) { addLog(LOG_LEVEL_DEBUG, log2); } + if (PLUGIN_118_Log) { + addLog(LOG_LEVEL_DEBUG, log); + } } } @@ -380,9 +383,7 @@ void P118_data_struct::PluginWriteLog(const String& command) { // Interrupt handler // **************************************************************************/ void ICACHE_RAM_ATTR P118_data_struct::ISR_ithoCheck(P118_data_struct *self) { - noInterrupts(); // Disable interrupts self->PLUGIN_118_Int = true; - interrupts(); // Re-enable interrupts } #endif // ifdef USES_P118 diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index 41bed47aa6..673b5474ba 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -10,7 +10,7 @@ # include "IthoCC1101.h" # include "IthoPacket.h" -// # define P118_DEBUG_LOG // Enable for some (extra) logging +# define P118_DEBUG_LOG // Enable for some (extra) logging // Timer values for hardware timer in Fan in seconds # define PLUGIN_118_Time1 10 * 60 From 12c86e0bfe138d20ab9ab6737b49c4eba7e9bc23 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 8 Jan 2022 13:47:06 +0100 Subject: [PATCH 09/33] [P118] Attempt to avoid startup crash --- src/src/PluginStructs/P118_data_struct.cpp | 2 +- src/src/PluginStructs/P118_data_struct.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index 558353269c..8f3cf6ea87 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -382,7 +382,7 @@ void P118_data_struct::PluginWriteLog(const String& command) { // **************************************************************************/ // Interrupt handler // **************************************************************************/ -void ICACHE_RAM_ATTR P118_data_struct::ISR_ithoCheck(P118_data_struct *self) { +void P118_data_struct::ISR_ithoCheck(P118_data_struct *self) { self->PLUGIN_118_Int = true; } diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index 673b5474ba..42d7983375 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -66,7 +66,7 @@ struct P118_data_struct : public PluginTaskData_base { volatile bool PLUGIN_118_Int = false; - static void ISR_ithoCheck(P118_data_struct *self); + static void ISR_ithoCheck(P118_data_struct *self) ICACHE_RAM_ATTR; }; #endif // ifdef USES_P118 #endif // ifndef PLUGINSTRUCTS_P118_DATA_STRUCT_H From eeb655de87c949286f3515b0d62cea8145c1f7b7 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 8 Jan 2022 14:49:04 +0100 Subject: [PATCH 10/33] [P118] Add entry to Custom-sample.h --- src/Custom-sample.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Custom-sample.h b/src/Custom-sample.h index fcd4e7d49f..0e34cefe84 100644 --- a/src/Custom-sample.h +++ b/src/Custom-sample.h @@ -378,6 +378,7 @@ static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = { // #define USES_P114 // VEML6075 // #define USES_P115 // MAX1704x // #define USES_P117 // SCD30 +// #define USES_P118 // Itho // #define USES_P119 // ITG3205 Gyro // #define USES_P120 // ADXL345 Acceleration / Gravity // #define USES_P124 // I2C MultiRelay From 174dee8cfb7725929e861fc0a19077e9e0c18cd5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 8 Jan 2022 20:59:39 +0100 Subject: [PATCH 11/33] [P118] Reformat CC1101 library using Uncrustify --- lib/Itho/CC1101.cpp | 400 +++++++++++---------- lib/Itho/CC1101.h | 356 ++++++++++--------- lib/Itho/CC1101Packet.h | 10 +- lib/Itho/IthoCC1101.cpp | 761 +++++++++++++++++++++------------------- lib/Itho/IthoCC1101.h | 244 +++++++------ lib/Itho/IthoPacket.h | 58 +-- 6 files changed, 966 insertions(+), 863 deletions(-) diff --git a/lib/Itho/CC1101.cpp b/lib/Itho/CC1101.cpp index a5dd99ee63..f44b48a237 100644 --- a/lib/Itho/CC1101.cpp +++ b/lib/Itho/CC1101.cpp @@ -7,86 +7,86 @@ // default constructor CC1101::CC1101(int8_t CSpin) : _CSpin(CSpin) { - // SPI.begin(); // Done by ESPEasy -#ifdef ESP8266 - pinMode(_CSpin, OUTPUT); -#endif -} //CC1101 + // SPI.begin(); // Done by ESPEasy + #ifdef ESP8266 + pinMode(_CSpin, OUTPUT); + #endif // ifdef ESP8266 +} // CC1101 // default destructor CC1101::~CC1101() -{ -} //~CC1101 +{} // ~CC1101 /***********************/ + // SPI helper functions select() and deselect() inline void CC1101::select(void) { - digitalWrite(_CSpin, LOW); + digitalWrite(_CSpin, LOW); } inline void CC1101::deselect(void) { - digitalWrite(_CSpin, HIGH); + digitalWrite(_CSpin, HIGH); } void CC1101::spi_waitMiso() { - while(digitalRead(MISO) == HIGH) yield(); + while (digitalRead(MISO) == HIGH) { yield(); } } void CC1101::init() { - reset(); + 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(); + 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 CC1101::writeCommand(uint8_t command) { - uint8_t result; - - select(); - spi_waitMiso(); - result = SPI.transfer(command); - deselect(); - - return result; + uint8_t result; + + select(); + spi_waitMiso(); + result = SPI.transfer(command); + deselect(); + + return result; } -void CC1101::writeRegister(uint8_t address, uint8_t data) +void CC1101::writeRegister(uint8_t address, uint8_t data) { - select(); - spi_waitMiso(); - SPI.transfer(address); - SPI.transfer(data); - deselect(); + 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 val; + + select(); + spi_waitMiso(); + SPI.transfer(address); + val = SPI.transfer(0); + deselect(); + + return val; } uint8_t CC1101::readRegisterMedian3(uint8_t address) @@ -102,175 +102,185 @@ uint8_t CC1101::readRegisterMedian3(uint8_t address) 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) - + 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) + 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; + 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 +// 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); - } + 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) +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(); + 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) +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(); + 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) +// 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; + 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. +// 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)); + 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; i < length; i++) { + writeRegister(CC1101_TXFIFO, packet->data[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 index cc33dbe35d..6b52d93dd4 100644 --- a/lib/Itho/CC1101.h +++ b/lib/Itho/CC1101.h @@ -8,106 +8,120 @@ #include #include "CC1101Packet.h" #include + // On Arduino, SPI pins are predefined #ifndef PIN_SPI_SS -#define PIN_SPI_SS (15) -#endif +# define PIN_SPI_SS (15) +#endif // ifndef PIN_SPI_SS /* Type of transfers */ -#define CC1101_WRITE_BURST 0x40 -#define CC1101_READ_SINGLE 0x80 -#define CC1101_READ_BURST 0xC0 +#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 +#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 +#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 +#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 +#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 +#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 @@ -115,98 +129,108 @@ #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 +#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 + 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 + 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: -class CC1101 -{ - protected: - - //functions - public: - CC1101(int8_t CSpin = PIN_SPI_SS); - ~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); - - int8_t _CSpin = PIN_SPI_SS; - - 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__ + // functions + +public: + + CC1101(int8_t CSpin = PIN_SPI_SS); + ~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); + + int8_t _CSpin = PIN_SPI_SS; + +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 index 250c63458b..f96390d7e1 100644 --- a/lib/Itho/CC1101Packet.h +++ b/lib/Itho/CC1101Packet.h @@ -12,11 +12,11 @@ #define CC1101_DATA_LEN CC1101_BUFFER_LEN - 3 -class CC1101Packet -{ - public: - uint8_t length = 0; - uint8_t data[128] = {0}; +class CC1101Packet { +public: + + uint8_t length = 0; + uint8_t data[128] = { 0 }; }; diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp index 7eb5d67849..05b5a2ade4 100644 --- a/lib/Itho/IthoCC1101.cpp +++ b/lib/Itho/IthoCC1101.cpp @@ -1,148 +1,149 @@ /* 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_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')*/ + (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 +// #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 +// #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 +// #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 +// 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 +#define MDMCFG2 0x02 // 16bit sync word / 16bit specific // default constructor IthoCC1101::IthoCC1101(int8_t CSpin, uint8_t counter, uint8_t sendTries) : CC1101(CSpin) { this->outIthoPacket.counter = counter; - this->sendTries = sendTries; + this->sendTries = sendTries; this->outIthoPacket.deviceId[0] = 33; this->outIthoPacket.deviceId[1] = 66; this->outIthoPacket.deviceId[2] = 99; this->outIthoPacket.deviceType = 22; - -} //IthoCC1101 +} // IthoCC1101 // default destructor IthoCC1101::~IthoCC1101() -{ -} //~IthoCC1101 +{} // ~IthoCC1101 void IthoCC1101::initSendMessage(uint8_t len) { - //finishTransfer(); + // finishTransfer(); writeCommand(CC1101_SIDLE); delayMicroseconds(1); - writeRegister(CC1101_IOCFG0 , 0x2E); + writeRegister(CC1101_IOCFG0, 0x2E); delayMicroseconds(1); - writeRegister(CC1101_IOCFG1 , 0x2E); + 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 - */ + 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 + 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. + 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_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. - writeRegister(CC1101_PKTLEN , len); + // 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() @@ -150,8 +151,8 @@ void IthoCC1101::finishTransfer() writeCommand(CC1101_SIDLE); delayMicroseconds(1); - writeRegister(CC1101_IOCFG0 , 0x2E); - writeRegister(CC1101_IOCFG1 , 0x2E); + writeRegister(CC1101_IOCFG0, 0x2E); + writeRegister(CC1101_IOCFG1, 0x2E); writeCommand(CC1101_SIDLE); writeCommand(CC1101_SPWD); @@ -160,121 +161,125 @@ void IthoCC1101::finishTransfer() 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 - */ + 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); + 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); + // 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); + // 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(); + // wait for calibration to finish + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) { yield(); } - writeRegister(CC1101_MCSM0 , 0x18); //no auto calibrate + 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. + 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(); + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_RX) { yield(); } initReceiveMessage(); } -void IthoCC1101::initReceiveMessage() +void IthoCC1101::initReceiveMessage() { uint8_t marcState; - writeCommand(CC1101_SIDLE); //idle + writeCommand(CC1101_SIDLE); // idle - //set datarate - writeRegister(CC1101_MDMCFG4 , 0x5A); // set kBaud - writeRegister(CC1101_MDMCFG3 , 0x83); // set kBaud - writeRegister(CC1101_DEVIATN , 0x50); + // 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_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); + // 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 + 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 + if (marcState == CC1101_MARCSTATE_RXFIFO_OVERFLOW) { // RX_OVERFLOW + writeCommand(CC1101_SFRX); // flush RX buffer + } } } @@ -287,57 +292,73 @@ bool IthoCC1101::checkForNewPacket() { } bool IthoCC1101::parseMessageCommand() { - messageDecode(&inMessage, &inIthoPacket); - //deviceType of message type? - inIthoPacket.deviceType = inIthoPacket.dataDecoded[0]; + // deviceType of message type? + inIthoPacket.deviceType = inIthoPacket.dataDecoded[0]; - //deviceID + // deviceID inIthoPacket.deviceId[0] = inIthoPacket.dataDecoded[1]; inIthoPacket.deviceId[1] = inIthoPacket.dataDecoded[2]; inIthoPacket.deviceId[2] = inIthoPacket.dataDecoded[3]; - //counter1 + // 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 + 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) + + 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) { + + if (isPowerCommand || isHighCommand || isMediumCommand || isLowCommand || isStandByCommand || isTimer1Command || isTimer2Command || + isTimer3Command) { mLen = 11; } else if (isJoinCommand || isJoin2Command) { @@ -349,22 +370,26 @@ bool IthoCC1101::parseMessageCommand() { else { return true; } + if (getCounter2(&inIthoPacket, mLen) != inIthoPacket.dataDecoded[mLen]) { inIthoPacket.command = IthoUnknown; return false; } -#endif +#endif // if defined(CRC_FILTER) return true; } bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[]) { uint8_t offset = 0; - if (itho->deviceType == 28 || itho->deviceType == 24) offset = 2; + + 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]) ) { + // 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; } } @@ -374,14 +399,14 @@ bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[] void IthoCC1101::sendCommand(IthoCommand command) { CC1101Packet outMessage; - uint8_t maxTries = sendTries; + uint8_t maxTries = sendTries; uint8_t delaytime = 40; - //update itho packet data - outIthoPacket.command = command; + // update itho packet data + outIthoPacket.command = command; outIthoPacket.counter += 1; - //get message2 bytes + // get message2 bytes switch (command) { case IthoJoin: @@ -390,8 +415,9 @@ void IthoCC1101::sendCommand(IthoCommand command) case IthoLeave: createMessageLeave(&outIthoPacket, &outMessage); - //the leave command needs to be transmitted for 1 second according the manual - maxTries = 30; + + // the leave command needs to be transmitted for 1 second according the manual + maxTries = 30; delaytime = 4; break; @@ -400,107 +426,103 @@ void IthoCC1101::sendCommand(IthoCommand command) break; } - //send messages + // send messages for (int i = 0; i < maxTries; i++) { - - //message2 + // message2 initSendMessage(outMessage.length); sendData(&outMessage); finishTransfer(); delay(delaytime); } - //initReceive(); SV - I call this from the ESPEasy plugin to prevent crashes -} + // 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 + // 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[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] - + // [start of command specific data] } void IthoCC1101::createMessageCommand(IthoPacket *itho, CC1101Packet *packet) { - - //set start message structure + // set start message structure createMessageStart(itho, packet); - //set deviceType? (or messageType?), not sure what this is + // set deviceType? (or messageType?), not sure what this is itho->dataDecoded[0] = itho->deviceType; - //set deviceID + // set deviceID itho->dataDecoded[1] = itho->deviceId[0]; itho->dataDecoded[2] = itho->deviceId[1]; itho->dataDecoded[3] = itho->deviceId[2]; - //set counter1 + // set counter1 itho->dataDecoded[4] = itho->counter; - //set command bytes on dataDecoded[5 - 10] + // 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 + // set counter2 itho->dataDecoded[11] = getCounter2(itho, 11); itho->length = 12; - packet->length = messageEncode(itho, packet); + packet->length = messageEncode(itho, packet); packet->length += 1; - //set end byte + // set end byte packet->data[packet->length] = 172; - packet->length += 1; + packet->length += 1; - //set end 'noise' + // 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 + // set start message structure createMessageStart(itho, packet); - //set deviceType? (or messageType?) + // set deviceType? (or messageType?) itho->dataDecoded[0] = itho->deviceType; - //set deviceID + // set deviceID itho->dataDecoded[1] = itho->deviceId[0]; itho->dataDecoded[2] = itho->deviceId[1]; itho->dataDecoded[3] = itho->deviceId[2]; - //set counter1 + // set counter1 itho->dataDecoded[4] = itho->counter; - //set command bytes on dataDecoded[5 - ?] + // 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 + // set deviceID itho->dataDecoded[11] = itho->deviceId[0]; itho->dataDecoded[12] = itho->deviceId[1]; itho->dataDecoded[13] = itho->deviceId[2]; @@ -509,105 +531,103 @@ void IthoCC1101::createMessageJoin(IthoPacket *itho, CC1101Packet *packet) itho->dataDecoded[15] = 16; itho->dataDecoded[16] = 224; - //set deviceID + // set deviceID itho->dataDecoded[17] = itho->deviceId[0]; itho->dataDecoded[18] = itho->deviceId[1]; itho->dataDecoded[19] = itho->deviceId[2]; - //set counter2 + // set counter2 itho->dataDecoded[20] = getCounter2(itho, 20); itho->length = 21; - packet->length = messageEncode(itho, packet); + packet->length = messageEncode(itho, packet); packet->length += 1; - //set end byte + // set end byte packet->data[packet->length] = 202; - packet->length += 1; + packet->length += 1; - //set end 'noise' + // 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 + // set start message structure createMessageStart(itho, packet); - //set deviceType? (or messageType?) + // set deviceType? (or messageType?) itho->dataDecoded[0] = itho->deviceType; - //set deviceID + // set deviceID itho->dataDecoded[1] = itho->deviceId[0]; itho->dataDecoded[2] = itho->deviceId[1]; itho->dataDecoded[3] = itho->deviceId[2]; - //set counter1 + // set counter1 itho->dataDecoded[4] = itho->counter; - //set command bytes on dataDecoded[5 - 10] + // 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 + // set deviceID itho->dataDecoded[11] = itho->deviceId[0]; itho->dataDecoded[12] = itho->deviceId[1]; itho->dataDecoded[13] = itho->deviceId[2]; - //set counter2 + // set counter2 itho->dataDecoded[14] = getCounter2(itho, 14); itho->length = 15; - packet->length = messageEncode(itho, packet); + packet->length = messageEncode(itho, packet); packet->length += 1; - //set end byte + // set end byte packet->data[packet->length] = 202; - packet->length += 1; + packet->length += 1; - //set end 'noise' + // 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) +uint8_t * IthoCC1101::getMessageCommandBytes(IthoCommand command) { switch (command) { case IthoStandby: - return (uint8_t*)&ithoMessageStandByCommandBytes[0]; + return (uint8_t *)&ithoMessageStandByCommandBytes[0]; case IthoHigh: - return (uint8_t*)&ithoMessageHighCommandBytes[0]; + return (uint8_t *)&ithoMessageHighCommandBytes[0]; case IthoFull: - return (uint8_t*)&ithoMessageFullCommandBytes[0]; + return (uint8_t *)&ithoMessageFullCommandBytes[0]; case IthoMedium: - return (uint8_t*)&ithoMessageMediumCommandBytes[0]; + return (uint8_t *)&ithoMessageMediumCommandBytes[0]; case IthoLow: - return (uint8_t*)&ithoMessageLowCommandBytes[0]; + return (uint8_t *)&ithoMessageLowCommandBytes[0]; case IthoTimer1: - return (uint8_t*)&ithoMessageTimer1CommandBytes[0]; + return (uint8_t *)&ithoMessageTimer1CommandBytes[0]; case IthoTimer2: - return (uint8_t*)&ithoMessageTimer2CommandBytes[0]; + return (uint8_t *)&ithoMessageTimer2CommandBytes[0]; case IthoTimer3: - return (uint8_t*)&ithoMessageTimer3CommandBytes[0]; + return (uint8_t *)&ithoMessageTimer3CommandBytes[0]; case IthoJoin: - return (uint8_t*)&ithoMessageJoinCommandBytes[0]; + return (uint8_t *)&ithoMessageJoinCommandBytes[0]; case IthoLeave: - return (uint8_t*)&ithoMessageLeaveCommandBytes[0]; + return (uint8_t *)&ithoMessageLeaveCommandBytes[0]; default: - return (uint8_t*)&ithoMessageLowCommandBytes[0]; + return (uint8_t *)&ithoMessageLowCommandBytes[0]; } } @@ -615,9 +635,8 @@ uint8_t* IthoCC1101::getMessageCommandBytes(IthoCommand command) 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++) { @@ -628,7 +647,6 @@ uint8_t IthoCC1101::getCounter2(IthoPacket *itho, uint8_t len) { } uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { - // uint8_t lenOutbuf = 0; // Inhinit unused code to avoid compiler warning // if ((itho->length * 20) % 8 == 0) { //inData len fits niecly in out buffer length @@ -638,68 +656,72 @@ uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { // 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 + 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 + // we need to zero the out buffer first cause we are using bitshifts for (uint32_t i = out_bytecounter; i < sizeof(packet->data) / sizeof(packet->data[0]); i++) { packet->data[i] = 0; } - //Serial.println(); + // 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 + 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; + 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; + + if (out_shift == 0) { out_shift = 8; } out_shift--; out_bitcounter++; } - if (out_bitcounter == 8) { //check if new byte is needed + 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 + // 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 + 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; + // 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; + + 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 + + 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; + + if (out_shift == 0) { out_shift = 8; } out_shift--; } } @@ -707,18 +729,17 @@ uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { return out_bytecounter; } - void IthoCC1101::messageDecode(CC1101Packet *packet, IthoPacket *itho) { - itho->length = 0; int lenInbuf = packet->length; - lenInbuf -= STARTBYTE; //correct for sync byte pos + lenInbuf -= STARTBYTE; // correct for sync byte pos while (lenInbuf >= 5) { - lenInbuf -= 5; + lenInbuf -= 5; itho->length += 2; } + if (lenInbuf >= 3) { itho->length++; } @@ -726,89 +747,101 @@ void IthoCC1101::messageDecode(CC1101Packet *packet, IthoPacket *itho) { for (uint32_t i = 0; i < sizeof(itho->dataDecoded) / sizeof(itho->dataDecoded[0]); i++) { itho->dataDecoded[i] = 0; } + for (uint32_t 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 + 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 + 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; + 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 + + 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; + 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; + 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; + 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 rssi = 0; uint8_t value = 0; rssi = (readRegister(CC1101_RSSI, CC1101_STATUS_REGISTER)); if (rssi >= 128) { - value = 255 - rssi; + value = 255 - rssi; value /= 2; value += 74; } else { - value = rssi / 2; + value = rssi / 2; value += 74; } - return (value); + return value; } bool IthoCC1101::checkID(const uint8_t *id) { - for (uint8_t i = 0; i < 3; i++) - if (id[i] != inIthoPacket.deviceId[i]) + 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 += ","; + 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]; } @@ -817,34 +850,36 @@ int * IthoCC1101::getLastID() { 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 += ":"; + 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 += ","; - } + 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 += ","; - } + if (i < inIthoPacket.length - 1) { str += ","; } + } } str += "\n"; return str; - } diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h index ca91c38eb9..aca4255de2 100644 --- a/lib/Itho/IthoCC1101.h +++ b/lib/Itho/IthoCC1101.h @@ -1,5 +1,5 @@ /* - * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra + * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra */ #ifndef __ITHOCC1101_H__ @@ -10,107 +10,141 @@ #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(int8_t CSpin = PIN_SPI_SS, 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 +// 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(int8_t CSpin = PIN_SPI_SS, + 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__ diff --git a/lib/Itho/IthoPacket.h b/lib/Itho/IthoPacket.h index 3a5c3e8075..2ceccda12b 100644 --- a/lib/Itho/IthoPacket.h +++ b/lib/Itho/IthoPacket.h @@ -1,49 +1,49 @@ /* - * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra + * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra */ #ifndef ITHOPACKET_H_ #define ITHOPACKET_H_ enum IthoCommand -{ +{ IthoUnknown = 0, - - IthoJoin = 1, + + IthoJoin = 1, IthoLeave = 2, - + IthoStandby = 3, - IthoLow = 4, - IthoMedium = 5, - IthoHigh = 6, - IthoFull = 7, - + IthoLow = 4, + IthoMedium = 5, + IthoHigh = 6, + IthoFull = 7, + IthoTimer1 = 8, IthoTimer2 = 9, IthoTimer3 = 10, - - //duco c system remote + + // duco c system remote DucoStandby = 11, - DucoLow = 12, - DucoMedium = 13, - DucoHigh = 14 + 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 +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 +#endif /* ITHOPACKET_H_ */ From 942f9ab148a2dbaad6021000b2b11f7c10bc7e8a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 11 Feb 2022 16:43:40 +0100 Subject: [PATCH 12/33] [P118] Log optimization --- src/src/PluginStructs/P118_data_struct.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index 8f3cf6ea87..a4a84355fa 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -347,7 +347,7 @@ void P118_data_struct::ITHOcheck() { } if (PLUGIN_118_Log) { - addLog(LOG_LEVEL_DEBUG, log); + addLogMove(LOG_LEVEL_DEBUG, log); } } } @@ -362,12 +362,14 @@ void P118_data_struct::PublishData(struct EventStruct *event) { log += UserVar[event->BaseVarIndex]; addLog(LOG_LEVEL_DEBUG, log); - log = F("Timer: "); + log.clear(); + log += F("Timer: "); log += UserVar[event->BaseVarIndex + 1]; addLog(LOG_LEVEL_DEBUG, log); - log = F("LastIDindex: "); + log.clear(); + log += F("LastIDindex: "); log += UserVar[event->BaseVarIndex + 2]; - addLog(LOG_LEVEL_DEBUG, log); + addLogMove(LOG_LEVEL_DEBUG, log); } } From 2965de33e87586dbaea29ea50f75f0d20aceb1e3 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 16 Jun 2022 22:11:50 +0200 Subject: [PATCH 13/33] [P118] Code optimizations --- src/_P118_Itho.ino | 106 ++++++++++----------- src/src/PluginStructs/P118_data_struct.cpp | 20 ++-- src/src/PluginStructs/P118_data_struct.h | 5 + 3 files changed, 65 insertions(+), 66 deletions(-) diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index ae8e7bcab9..e675983ac6 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -69,15 +69,15 @@ #ifdef USES_P118 -#include "_Plugin_Helper.h" -#include "./src/PluginStructs/P118_data_struct.h" +# include "_Plugin_Helper.h" +# include "./src/PluginStructs/P118_data_struct.h" -#define PLUGIN_118 -#define PLUGIN_ID_118 118 -#define PLUGIN_NAME_118 "Communication - Itho ventilation" -#define PLUGIN_VALUENAME1_118 "State" -#define PLUGIN_VALUENAME2_118 "Timer" -#define PLUGIN_VALUENAME3_118 "LastIDindex" +# define PLUGIN_118 +# define PLUGIN_ID_118 118 +# define PLUGIN_NAME_118 "Communication - Itho ventilation" +# define PLUGIN_VALUENAME1_118 "State" +# define PLUGIN_VALUENAME2_118 "Timer" +# define PLUGIN_VALUENAME3_118 "LastIDindex" boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) { @@ -123,46 +123,44 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) break; } - case PLUGIN_SET_DEFAULTS: // Set defaults address to the one used in old versions of the library for backwards compatability + case PLUGIN_SET_DEFAULTS: // Set defaults address to the one used in old versions of the library for backwards compatability { - PIN(0) = -1; // Interrupt pin undefined by default - PIN(1) = PIN_SPI_SS; // CS pin use the previous default of PIN_SPI_SS/gpio 15 - PCONFIG(0) = 1; - PCONFIG(1) = 10; - PCONFIG(2) = 87; - PCONFIG(3) = 81; - success = true; + PIN(0) = -1; // Interrupt pin undefined by default + PIN(1) = PIN_SPI_SS; // CS pin use the previous default of PIN_SPI_SS/gpio 15 + P118_CONFIG_LOG = 1; + P118_CONFIG_DEVID1 = 10; + P118_CONFIG_DEVID2 = 87; + P118_CONFIG_DEVID3 = 81; + success = true; break; } case PLUGIN_INIT: { - #ifdef P118_DEBUG_LOG + # ifdef P118_DEBUG_LOG addLog(LOG_LEVEL_INFO, F("INIT PLUGIN_118")); - #endif // ifdef P118_DEBUG_LOG - initPluginTaskData(event->TaskIndex, new (std::nothrow) P118_data_struct(PCONFIG(0))); + # endif // ifdef P118_DEBUG_LOG + initPluginTaskData(event->TaskIndex, new (std::nothrow) P118_data_struct(P118_CONFIG_LOG)); P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P118_data) { - return success; + if (nullptr != P118_data) { + success = P118_data->plugin_init(event); } - success = P118_data->plugin_init(event); break; } case PLUGIN_EXIT: { - #ifdef P118_DEBUG_LOG + # ifdef P118_DEBUG_LOG addLog(LOG_LEVEL_INFO, F("EXIT PLUGIN_118")); - #endif // ifdef P118_DEBUG_LOG + # endif // ifdef P118_DEBUG_LOG P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P118_data) { - return success; + if (nullptr != P118_data) { + success = P118_data->plugin_exit(event); } - success = P118_data->plugin_exit(event); break; } @@ -170,10 +168,9 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) { P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P118_data) { - return success; + if (nullptr != P118_data) { + success = P118_data->plugin_once_a_second(event); } - success = P118_data->plugin_once_a_second(event); break; } @@ -182,10 +179,9 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) { P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P118_data) { - return success; + if (nullptr != P118_data) { + success = P118_data->plugin_fifty_per_second(event); } - success = P118_data->plugin_fifty_per_second(event); break; } @@ -194,10 +190,9 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_READ: { P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P118_data) { - return success; + if (nullptr != P118_data) { + success = P118_data->plugin_read(event); } - success = P118_data->plugin_read(event); break; } @@ -205,10 +200,10 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WRITE: { P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P118_data) { - return success; + if (nullptr != P118_data) { + success = P118_data->plugin_write(event, string); } - success = P118_data->plugin_write(event, string); + break; } @@ -217,14 +212,15 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) PLUGIN_118_ExtraSettingsStruct PLUGIN_118_ExtraSettings; LoadCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); 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)); // Makes RF logging optional to reduce clutter in the log 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); + addFormTextBox(F("Unit ID remote 1"), F("pID1"), PLUGIN_118_ExtraSettings.ID1, 8); + addFormTextBox(F("Unit ID remote 2"), F("pID2"), PLUGIN_118_ExtraSettings.ID2, 8); + addFormTextBox(F("Unit ID remote 3"), F("pID3"), PLUGIN_118_ExtraSettings.ID3, 8); + addFormCheckBox(F("Enable RF receive log"), F("plog"), P118_CONFIG_LOG); // Makes RF logging optional to reduce clutter in the log + // file + // in RF noisy environments + addFormNumericBox(F("Device ID byte 1"), F("pdevid1"), P118_CONFIG_DEVID1, 0, 255); + addFormNumericBox(F("Device ID byte 2"), F("pdevid2"), P118_CONFIG_DEVID2, 0, 255); + addFormNumericBox(F("Device ID byte 3"), F("pdevid3"), P118_CONFIG_DEVID3, 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 library")); success = true; @@ -234,17 +230,17 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { PLUGIN_118_ExtraSettingsStruct PLUGIN_118_ExtraSettings; - 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()); + strcpy(PLUGIN_118_ExtraSettings.ID1, web_server.arg(F("pID1")).c_str()); + strcpy(PLUGIN_118_ExtraSettings.ID2, web_server.arg(F("pID2")).c_str()); + strcpy(PLUGIN_118_ExtraSettings.ID3, web_server.arg(F("pID3")).c_str()); SaveCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); - PCONFIG(0) = isFormItemChecked(F("p118_log")); + P118_CONFIG_LOG = isFormItemChecked(F("plog")); - PCONFIG(1) = getFormItemInt(F("p118_deviceid1"), 10); - PCONFIG(2) = getFormItemInt(F("p118_deviceid2"), 87); - PCONFIG(3) = getFormItemInt(F("p118_deviceid3"), 81); - success = true; + P118_CONFIG_DEVID1 = getFormItemInt(F("pdevid1"), 10); + P118_CONFIG_DEVID2 = getFormItemInt(F("pdevid2"), 87); + P118_CONFIG_DEVID3 = getFormItemInt(F("pdevid3"), 81); + success = true; break; } } diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index a4a84355fa..abf41c1318 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -29,8 +29,9 @@ bool P118_data_struct::plugin_init(struct EventStruct *event) { PLUGIN_118_rf = new (std::nothrow) IthoCC1101(PIN(1)); if (nullptr != PLUGIN_118_rf) { - 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->setDeviceID(P118_CONFIG_DEVID1, P118_CONFIG_DEVID2, P118_CONFIG_DEVID3); // 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(); attachInterruptArg(digitalPinToInterrupt(Plugin_118_IRQ_pin), @@ -50,10 +51,9 @@ bool P118_data_struct::plugin_exit(struct EventStruct *event) { // remove interupt when plugin is removed detachInterrupt(digitalPinToInterrupt(Plugin_118_IRQ_pin)); - if (nullptr != PLUGIN_118_rf) { - delete PLUGIN_118_rf; - PLUGIN_118_rf = nullptr; - } + delete PLUGIN_118_rf; + PLUGIN_118_rf = nullptr; + return true; } @@ -104,16 +104,14 @@ bool P118_data_struct::plugin_read(struct EventStruct *event) { # endif // ifdef P118_DEBUG_LOG PublishData(event); - // sendData(event); //SV - Added to send status every xx secnds as set within plugin return true; } bool P118_data_struct::plugin_write(struct EventStruct *event, const String& string) { - bool success = false; - String tmpString = string; - String cmd = parseString(tmpString, 1); + bool success = false; + String cmd = parseString(string, 1); - if (cmd.equalsIgnoreCase(F("STATE"))) + if (cmd.equals(F("state"))) { switch (event->Par1) { case 1111: // Join command diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index 42d7983375..403503cc71 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -12,6 +12,11 @@ # define P118_DEBUG_LOG // Enable for some (extra) logging +# define P118_CONFIG_LOG PCONFIG(0) +# define P118_CONFIG_DEVID1 PCONFIG(1) +# define P118_CONFIG_DEVID2 PCONFIG(2) +# define P118_CONFIG_DEVID3 PCONFIG(3) + // Timer values for hardware timer in Fan in seconds # define PLUGIN_118_Time1 10 * 60 # define PLUGIN_118_Time2 20 * 60 From 6981e46965599ecdd01f404df75a8aa45606c509 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 21 Jun 2022 20:46:17 +0200 Subject: [PATCH 14/33] [P118] Minor code optimizations --- src/_P118_Itho.ino | 1 + src/src/PluginStructs/P118_data_struct.cpp | 17 +++++++---------- src/src/PluginStructs/P118_data_struct.h | 5 +++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index e675983ac6..6362361b7d 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -22,6 +22,7 @@ // - Remove unused code, reformat source using Uncrustify // tonhuisman, 28-12-2021 - Move interrupt handling to Plugin_data_struct, lifting the limit on nr. of plugins // tonhuisman, 03-01-2022 - Review source after structural-crash report, fix interrupt handler +// tonhuisman, 21-06-2022 - Minor improvements // Recommended to disable RF receive logging to minimize code execution within interrupts diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index abf41c1318..663cb20eab 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -12,10 +12,8 @@ P118_data_struct::P118_data_struct(uint8_t logData) // Destructor // **************************************************************************/ P118_data_struct::~P118_data_struct() { - if (isInitialized()) { - delete PLUGIN_118_rf; - PLUGIN_118_rf = nullptr; - } + delete PLUGIN_118_rf; + PLUGIN_118_rf = nullptr; } bool P118_data_struct::plugin_init(struct EventStruct *event) { @@ -29,9 +27,8 @@ bool P118_data_struct::plugin_init(struct EventStruct *event) { PLUGIN_118_rf = new (std::nothrow) IthoCC1101(PIN(1)); if (nullptr != PLUGIN_118_rf) { - PLUGIN_118_rf->setDeviceID(P118_CONFIG_DEVID1, P118_CONFIG_DEVID2, P118_CONFIG_DEVID3); // DeviceID used to send commands, can also be - // changed on the fly for - // multi itho control, 10,87,81 corresponds with old library + // 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->setDeviceID(P118_CONFIG_DEVID1, P118_CONFIG_DEVID2, P118_CONFIG_DEVID3); PLUGIN_118_rf->init(); attachInterruptArg(digitalPinToInterrupt(Plugin_118_IRQ_pin), @@ -51,9 +48,6 @@ bool P118_data_struct::plugin_exit(struct EventStruct *event) { // remove interupt when plugin is removed detachInterrupt(digitalPinToInterrupt(Plugin_118_IRQ_pin)); - delete PLUGIN_118_rf; - PLUGIN_118_rf = nullptr; - return true; } @@ -355,6 +349,8 @@ void P118_data_struct::PublishData(struct EventStruct *event) { UserVar[event->BaseVarIndex + 1] = PLUGIN_118_Timer; UserVar[event->BaseVarIndex + 2] = PLUGIN_118_LastIDindex; + # ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("State: "); @@ -369,6 +365,7 @@ void P118_data_struct::PublishData(struct EventStruct *event) { log += UserVar[event->BaseVarIndex + 2]; addLogMove(LOG_LEVEL_DEBUG, log); } + # endif // ifndef BUILD_NO_DEBUG } void P118_data_struct::PluginWriteLog(const String& command) { diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index 403503cc71..bc2b99dd98 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -44,6 +44,9 @@ struct P118_data_struct : public PluginTaskData_base { bool plugin_read(struct EventStruct *event); bool plugin_write(struct EventStruct *event, const String & string); + +private: + void ITHOcheck(); void PublishData(struct EventStruct *event); void PluginWriteLog(const String& command); @@ -52,8 +55,6 @@ struct P118_data_struct : public PluginTaskData_base { return PLUGIN_118_rf != nullptr; } -private: - IthoCC1101 *PLUGIN_118_rf = nullptr; // extra for interrupt handling From 1d499c4a9a3a0c632f8d4e919f152318d6c0e2db Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 10 Aug 2022 20:44:07 +0200 Subject: [PATCH 15/33] [P118] Itho library limit formerly endless loops to max. 3 seconds --- lib/Itho/CC1101.cpp | 10 +++++++-- lib/Itho/CC1101.h | 1 + lib/Itho/IthoCC1101.cpp | 47 +++++++++++++++++++++++++++++------------ lib/Itho/IthoCC1101.h | 1 + 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/lib/Itho/CC1101.cpp b/lib/Itho/CC1101.cpp index f44b48a237..94d2133076 100644 --- a/lib/Itho/CC1101.cpp +++ b/lib/Itho/CC1101.cpp @@ -259,8 +259,11 @@ void CC1101::sendData(CC1101Packet *packet) while (index < packet->length) { // check if there is free space in the fifo + uint32_t maxWait = millis() + 3000; // Wait for max. 3 seconds + while ((txStatus = (readRegisterMedian3(CC1101_TXBYTES | CC1101_STATUS_REGISTER) & CC1101_BITS_RX_BYTES_IN_FIFO)) > - (CC1101_DATA_LEN - 2)) {} + (CC1101_DATA_LEN - 2) && + millis() < maxWait) {} // calculate how many bytes we can send length = (CC1101_DATA_LEN - txStatus); @@ -276,11 +279,14 @@ void CC1101::sendData(CC1101Packet *packet) } // wait until transmission is finished (TXOFF_MODE is expected to be set to 0/IDLE or TXFIFO_UNDERFLOW) + uint32_t maxWait = millis() + 3000; // Wait for max. 3 seconds + 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)); + while ((MarcState != CC1101_MARCSTATE_IDLE) && (MarcState != CC1101_MARCSTATE_TXFIFO_UNDERFLOW) && + millis() < maxWait); } diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h index 6b52d93dd4..7b5064822d 100644 --- a/lib/Itho/CC1101.h +++ b/lib/Itho/CC1101.h @@ -1,5 +1,6 @@ /* * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 + * tonhuisman: Fixed perpetual blocking while loops by limiting these to 3000 msec. */ #ifndef __CC1101_H__ diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp index b1b5f9d6fd..fc2ebea9ab 100644 --- a/lib/Itho/IthoCC1101.cpp +++ b/lib/Itho/IthoCC1101.cpp @@ -188,7 +188,12 @@ void IthoCC1101::initReceive() writeCommand(CC1101_SCAL); // wait for calibration to finish - while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) { yield(); } + uint32_t maxWait = millis() + 3000; // Wait for max. 3 seconds + + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE && + millis() < maxWait) { + yield(); + } writeRegister(CC1101_FSCAL2, 0x00); writeRegister(CC1101_MCSM0, 0x18); // no auto calibrate @@ -233,7 +238,12 @@ void IthoCC1101::initReceive() writeCommand(CC1101_SCAL); // wait for calibration to finish - while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE) { yield(); } + maxWait = millis() + 3000; // Wait for max. 3 seconds + + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE && + millis() < maxWait) { + yield(); + } writeRegister(CC1101_MCSM0, 0x18); // no auto calibrate @@ -246,7 +256,12 @@ void IthoCC1101::initReceive() writeCommand(CC1101_SRX); - while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_RX) { yield(); } + maxWait = millis() + 3000; // Wait for max. 3 seconds + + while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_RX && + millis() < maxWait) { + yield(); + } initReceiveMessage(); } @@ -272,10 +287,14 @@ void IthoCC1101::initReceiveMessage() writeRegister(CC1101_MDMCFG2, MDMCFG2); writeRegister(CC1101_PKTCTRL1, 0x00); - writeCommand(CC1101_SRX); // switch to RX state + 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) + uint32_t maxWait = millis() + 3000; // Wait for max. 3 seconds + + while (((marcState = + readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) & CC1101_BITS_MARCSTATE) != CC1101_MARCSTATE_RX && + millis() < maxWait) { if (marcState == CC1101_MARCSTATE_RXFIFO_OVERFLOW) { // RX_OVERFLOW writeCommand(CC1101_SFRX); // flush RX buffer @@ -648,17 +667,19 @@ uint8_t IthoCC1101::getCounter2(IthoPacket *itho, uint8_t len) { uint8_t IthoCC1101::messageEncode(IthoPacket *itho, CC1101Packet *packet) { // FIXME TD-er: lenOutbuf not used???? + /* - uint8_t lenOutbuf = 0; + 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 + 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; - } - */ + 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 diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h index ecb43f45ee..e212374235 100644 --- a/lib/Itho/IthoCC1101.h +++ b/lib/Itho/IthoCC1101.h @@ -1,5 +1,6 @@ /* * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra + * tonhuisman: Fixed perpetual blocking while loops by limiting these to 3000 msec. */ #ifndef __ITHOCC1101_H__ From 3e4550240a22369007755a15cb41d12e210f793c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 10 Aug 2022 20:45:05 +0200 Subject: [PATCH 16/33] [P118] Fix bugs found during testing, rename variables, clean up source --- src/src/PluginStructs/P118_data_struct.cpp | 300 +++++++++++---------- src/src/PluginStructs/P118_data_struct.h | 51 ++-- 2 files changed, 187 insertions(+), 164 deletions(-) diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index 663cb20eab..67af8b7d78 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -5,39 +5,45 @@ // **************************************************************************/ // Constructor // **************************************************************************/ -P118_data_struct::P118_data_struct(uint8_t logData) - : PLUGIN_118_Log(logData) {} +P118_data_struct::P118_data_struct(int8_t csPin, + int8_t irqPin, + bool logData, + bool rfLog) + : _csPin(csPin), _irqPin(irqPin), _log(logData), _rfLog(rfLog) {} // **************************************************************************/ // Destructor // **************************************************************************/ P118_data_struct::~P118_data_struct() { - delete PLUGIN_118_rf; - PLUGIN_118_rf = nullptr; + delete _rf; + _rf = nullptr; } bool P118_data_struct::plugin_init(struct EventStruct *event) { bool success = false; - LoadCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + LoadCustomTaskSettings(event->TaskIndex, (uint8_t *)&_ExtraSettings, sizeof(_ExtraSettings)); # ifdef P118_DEBUG_LOG - addLog(LOG_LEVEL_INFO, F("Extra Settings PLUGIN_118 loaded")); + addLog(LOG_LEVEL_INFO, F("ITHO: Extra Settings PLUGIN_118 loaded")); # endif // ifdef P118_DEBUG_LOG - PLUGIN_118_rf = new (std::nothrow) IthoCC1101(PIN(1)); + _rf = new (std::nothrow) IthoCC1101(_csPin); - if (nullptr != PLUGIN_118_rf) { + if (nullptr != _rf) { // 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->setDeviceID(P118_CONFIG_DEVID1, P118_CONFIG_DEVID2, P118_CONFIG_DEVID3); - PLUGIN_118_rf->init(); - - attachInterruptArg(digitalPinToInterrupt(Plugin_118_IRQ_pin), - reinterpret_cast(ISR_ithoCheck), - this, - FALLING); - - PLUGIN_118_rf->initReceive(); - PLUGIN_118_InitRunned = true; + _rf->setDeviceID(P118_CONFIG_DEVID1, P118_CONFIG_DEVID2, P118_CONFIG_DEVID3); + _rf->init(); + + if (validGpio(_irqPin)) { + attachInterruptArg(digitalPinToInterrupt(_irqPin), + reinterpret_cast(ISR_ithoCheck), + this, + FALLING); + } else { + addLog(LOG_LEVEL_ERROR, F("ITHO: Interrupt pin disabled, sending is OK, not receiving data!")); + } + _rf->initReceive(); + _InitRunned = true; success = true; } @@ -46,46 +52,48 @@ bool P118_data_struct::plugin_init(struct EventStruct *event) { bool P118_data_struct::plugin_exit(struct EventStruct *event) { // remove interupt when plugin is removed - detachInterrupt(digitalPinToInterrupt(Plugin_118_IRQ_pin)); + if (validGpio(_irqPin)) { + detachInterrupt(digitalPinToInterrupt(_irqPin)); + } return true; } bool P118_data_struct::plugin_once_a_second(struct EventStruct *event) { // decrement timer when timermode is running - if (PLUGIN_118_State >= 10) { PLUGIN_118_Timer--; } + if (_State >= 10) { _Timer--; } // if timer has elapsed set Fan state to low - if ((PLUGIN_118_State >= 10) && (PLUGIN_118_Timer <= 0)) + if ((_State >= 10) && (_Timer <= 0)) { - PLUGIN_118_State = 1; - PLUGIN_118_Timer = 0; + _State = 1; + _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) + if ((_OldState != _State) || ((_Timer > 0) && (_Timer % 2 == 0)) || + (_OldLastIDindex != _LastIDindex) || _InitRunned) { # ifdef P118_DEBUG_LOG - addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_ONCE_A_SECOND")); + addLog(LOG_LEVEL_DEBUG, F("ITHO: UPDATE by PLUGIN_ONCE_A_SECOND")); # endif // ifdef P118_DEBUG_LOG PublishData(event); sendData(event); // reset flag set by init - PLUGIN_118_InitRunned = false; + _InitRunned = false; } // Remeber current state for next cycle - PLUGIN_118_OldState = PLUGIN_118_State; - PLUGIN_118_OldLastIDindex = PLUGIN_118_LastIDindex; + _OldState = _State; + _OldLastIDindex = _LastIDindex; return true; } bool P118_data_struct::plugin_fifty_per_second(struct EventStruct *event) { - if (PLUGIN_118_Int) { + if (_Int) { ITHOcheck(); - PLUGIN_118_Int = false; // reset flag + _Int = false; // reset flag } return true; @@ -94,7 +102,7 @@ bool P118_data_struct::plugin_fifty_per_second(struct EventStruct *event) { bool P118_data_struct::plugin_read(struct EventStruct *event) { // This ensures that even when Values are not changing, data is send at the configured interval for aquisition # ifdef P118_DEBUG_LOG - addLog(LOG_LEVEL_DEBUG, F("UPDATE by PLUGIN_READ")); + addLog(LOG_LEVEL_DEBUG, F("ITHO: UPDATE by PLUGIN_READ")); # endif // ifdef P118_DEBUG_LOG PublishData(event); @@ -105,116 +113,109 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str bool success = false; String cmd = parseString(string, 1); - if (cmd.equals(F("state"))) - { + if (cmd.equals(F("state"))) { + success = true; + switch (event->Par1) { case 1111: // Join command { - PLUGIN_118_rf->sendCommand(IthoJoin); - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoJoin); + _rf->initReceive(); PluginWriteLog(F("join")); - success = true; break; } case 9999: // Leave command { - PLUGIN_118_rf->sendCommand(IthoLeave); - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoLeave); + _rf->initReceive(); PluginWriteLog(F("leave")); - success = true; break; } case 0: // Off command { - PLUGIN_118_rf->sendCommand(IthoStandby); - PLUGIN_118_State = 0; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoStandby); + _State = 0; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); PluginWriteLog(F("standby")); - success = true; break; } case 1: // Fan low { - PLUGIN_118_rf->sendCommand(IthoLow); - PLUGIN_118_State = 1; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoLow); + _State = 1; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); PluginWriteLog(F("low speed")); - success = true; break; } case 2: // Fan medium { - PLUGIN_118_rf->sendCommand(IthoMedium); - PLUGIN_118_State = 2; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoMedium); + _State = 2; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); PluginWriteLog(F("medium speed")); - success = true; break; } case 3: // Fan high { - PLUGIN_118_rf->sendCommand(IthoHigh); - PLUGIN_118_State = 3; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoHigh); + _State = 3; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); PluginWriteLog(F("high speed")); - success = true; break; } case 4: // Fan full { - PLUGIN_118_rf->sendCommand(IthoFull); - PLUGIN_118_State = 4; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoFull); + _State = 4; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); PluginWriteLog(F("full speed")); - success = true; break; } case 13: // Timer1 - 10 min { - PLUGIN_118_rf->sendCommand(IthoTimer1); - PLUGIN_118_State = 13; - PLUGIN_118_Timer = PLUGIN_118_Time1; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoTimer1); + _State = 13; + _Timer = PLUGIN_118_Time1; + _LastIDindex = 0; + _rf->initReceive(); PluginWriteLog(F("timer 1")); - success = true; break; } case 23: // Timer2 - 20 min { - PLUGIN_118_rf->sendCommand(IthoTimer2); - PLUGIN_118_State = 23; - PLUGIN_118_Timer = PLUGIN_118_Time2; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoTimer2); + _State = 23; + _Timer = PLUGIN_118_Time2; + _LastIDindex = 0; + _rf->initReceive(); PluginWriteLog(F("timer 2")); - success = true; break; } case 33: // Timer3 - 30 min { - PLUGIN_118_rf->sendCommand(IthoTimer3); - PLUGIN_118_State = 33; - PLUGIN_118_Timer = PLUGIN_118_Time3; - PLUGIN_118_LastIDindex = 0; - PLUGIN_118_rf->initReceive(); + _rf->sendCommand(IthoTimer3); + _State = 33; + _Timer = PLUGIN_118_Time3; + _LastIDindex = 0; + _rf->initReceive(); PluginWriteLog(F("timer 3")); - success = true; break; } default: { PluginWriteLog(F("INVALID")); + success = true; + break; } } } @@ -222,132 +223,138 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str } void P118_data_struct::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()) { - IthoCommand cmd = PLUGIN_118_rf->getLastCommand(); - String Id = PLUGIN_118_rf->getLastIDstr(); + bool _dbgLog = _log && loglevelActiveFor(LOG_LEVEL_DEBUG); + + if (_dbgLog) { + addLog(LOG_LEVEL_DEBUG, "ITHO: RF signal received"); // All logs statements contain if-statement to disable logging to + } // reduce log clutter when many RF sources are present + + if (_rf->checkForNewPacket()) { + IthoCommand cmd = _rf->getLastCommand(); + String Id = _rf->getLastIDstr(); + + if (_rfLog && loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("ITHO: Received ID: "); + log += Id; + log += F("; raw cmd: "); + log += cmd; + addLog(LOG_LEVEL_INFO, log); + } // Move check here to prevent function calling within ISR byte index = 0; - if (Id == PLUGIN_118_ExtraSettings.ID1) { + if (Id == _ExtraSettings.ID1) { index = 1; } - else if (Id == PLUGIN_118_ExtraSettings.ID2) { + else if (Id == _ExtraSettings.ID2) { index = 2; } - else if (Id == PLUGIN_118_ExtraSettings.ID3) { + else if (Id == _ExtraSettings.ID3) { index = 3; } - // int index = PLUGIN_118_RFRemoteIndex(Id); - // IF id is know index should be >0 String log; if (index > 0) { - if (PLUGIN_118_Log) { + if (_dbgLog) { log += F("Command received from remote-ID: "); log += Id; log += F(", command: "); - - // addLog(LOG_LEVEL_DEBUG, log); } switch (cmd) { case IthoUnknown: - if (PLUGIN_118_Log) { log += F("unknown"); } + if (_dbgLog) { log += F("unknown"); } break; case IthoStandby: case DucoStandby: - if (PLUGIN_118_Log) { log += F("standby"); } - PLUGIN_118_State = 0; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; + if (_dbgLog) { log += F("standby"); } + _State = 0; + _Timer = 0; + _LastIDindex = index; break; case IthoLow: case DucoLow: - if (PLUGIN_118_Log) { log += F("low"); } - PLUGIN_118_State = 1; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; + if (_dbgLog) { log += F("low"); } + _State = 1; + _Timer = 0; + _LastIDindex = index; break; case IthoMedium: case DucoMedium: - if (PLUGIN_118_Log) { log += F("medium"); } - PLUGIN_118_State = 2; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; + if (_dbgLog) { log += F("medium"); } + _State = 2; + _Timer = 0; + _LastIDindex = index; break; case IthoHigh: case DucoHigh: - if (PLUGIN_118_Log) { log += F("high"); } - PLUGIN_118_State = 3; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; + if (_dbgLog) { log += F("high"); } + _State = 3; + _Timer = 0; + _LastIDindex = index; break; case IthoFull: - if (PLUGIN_118_Log) { log += F("full"); } - PLUGIN_118_State = 4; - PLUGIN_118_Timer = 0; - PLUGIN_118_LastIDindex = index; + if (_dbgLog) { log += F("full"); } + _State = 4; + _Timer = 0; + _LastIDindex = index; break; case IthoTimer1: - if (PLUGIN_118_Log) { log += +F("timer1"); } - PLUGIN_118_State = 13; - PLUGIN_118_Timer = PLUGIN_118_Time1; - PLUGIN_118_LastIDindex = index; + if (_dbgLog) { log += +F("timer1"); } + _State = 13; + _Timer = PLUGIN_118_Time1; + _LastIDindex = index; break; case IthoTimer2: - if (PLUGIN_118_Log) { log += F("timer2"); } - PLUGIN_118_State = 23; - PLUGIN_118_Timer = PLUGIN_118_Time2; - PLUGIN_118_LastIDindex = index; + if (_dbgLog) { log += F("timer2"); } + _State = 23; + _Timer = PLUGIN_118_Time2; + _LastIDindex = index; break; case IthoTimer3: - if (PLUGIN_118_Log) { log += F("timer3"); } - PLUGIN_118_State = 33; - PLUGIN_118_Timer = PLUGIN_118_Time3; - PLUGIN_118_LastIDindex = index; + if (_dbgLog) { log += F("timer3"); } + _State = 33; + _Timer = PLUGIN_118_Time3; + _LastIDindex = index; break; case IthoJoin: - if (PLUGIN_118_Log) { log += F("join"); } + if (_dbgLog) { log += F("join"); } break; case IthoLeave: - if (PLUGIN_118_Log) { log += F("leave"); } + if (_dbgLog) { log += F("leave"); } break; } } else { - if (PLUGIN_118_Log) { + if (_dbgLog) { log += F("Device-ID: "); log += Id; log += F(" IGNORED"); } } - if (PLUGIN_118_Log) { + if (_dbgLog) { addLogMove(LOG_LEVEL_DEBUG, log); } } } void P118_data_struct::PublishData(struct EventStruct *event) { - UserVar[event->BaseVarIndex] = PLUGIN_118_State; - UserVar[event->BaseVarIndex + 1] = PLUGIN_118_Timer; - UserVar[event->BaseVarIndex + 2] = PLUGIN_118_LastIDindex; + UserVar[event->BaseVarIndex] = _State; + UserVar[event->BaseVarIndex + 1] = _Timer; + UserVar[event->BaseVarIndex + 2] = _LastIDindex; # ifndef BUILD_NO_DEBUG @@ -372,7 +379,10 @@ void P118_data_struct::PluginWriteLog(const String& command) { String log = F("Send Itho command for: "); log += command; - addLog(LOG_LEVEL_INFO, log); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, log); + } printWebString += log; } @@ -380,7 +390,7 @@ void P118_data_struct::PluginWriteLog(const String& command) { // Interrupt handler // **************************************************************************/ void P118_data_struct::ISR_ithoCheck(P118_data_struct *self) { - self->PLUGIN_118_Int = true; + self->_Int = true; } #endif // ifdef USES_P118 diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index bc2b99dd98..b144eed91d 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -12,15 +12,22 @@ # define P118_DEBUG_LOG // Enable for some (extra) logging +# if defined(LIMIT_BUILD_SIZE) && defined(P118_DEBUG_LOG) +# undef P118_DEBUG_LOG +# endif // if defined(LIMIT_BUILD_SIZE) && defined(P118_DEBUG_LOG) + +# define P118_CSPIN PIN(1) +# define P118_IRQPIN PIN(0) # define P118_CONFIG_LOG PCONFIG(0) # define P118_CONFIG_DEVID1 PCONFIG(1) # define P118_CONFIG_DEVID2 PCONFIG(2) # define P118_CONFIG_DEVID3 PCONFIG(3) +# define P118_CONFIG_RF_LOG PCONFIG(4) // 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 +# define PLUGIN_118_Time1 10 * 60 +# define PLUGIN_118_Time2 20 * 60 +# define PLUGIN_118_Time3 30 * 60 // This extra settings struct is needed because the default settingsstruct doesn't support strings struct PLUGIN_118_ExtraSettingsStruct { @@ -32,7 +39,10 @@ struct PLUGIN_118_ExtraSettingsStruct { struct P118_data_struct : public PluginTaskData_base { public: - P118_data_struct(uint8_t logData); + P118_data_struct(int8_t csPin, + int8_t irqPin, + bool logData, + bool rfLog); P118_data_struct() = delete; ~P118_data_struct(); @@ -52,25 +62,28 @@ struct P118_data_struct : public PluginTaskData_base { void PluginWriteLog(const String& command); bool isInitialized() { - return PLUGIN_118_rf != nullptr; + return _rf != nullptr; } - IthoCC1101 *PLUGIN_118_rf = nullptr; + IthoCC1101 *_rf = nullptr; // 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; - - PLUGIN_118_ExtraSettingsStruct PLUGIN_118_ExtraSettings; - - volatile bool PLUGIN_118_Int = false; + bool _ITHOhasPacket = false; + int _State = 1; // after startup it is assumed that the fan is running low + int _OldState = 1; + int _Timer = 0; + int _LastIDindex = 0; + int _OldLastIDindex = 0; + bool _InitRunned = false; + + int8_t _csPin = -1; + int8_t _irqPin = -1; + bool _log = false; + bool _rfLog = false; + + PLUGIN_118_ExtraSettingsStruct _ExtraSettings; + + volatile bool _Int = false; static void ISR_ithoCheck(P118_data_struct *self) ICACHE_RAM_ATTR; }; From 0c7ccee9c8c8dd5470f64cb2d137444cd8a15d01 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 10 Aug 2022 20:45:20 +0200 Subject: [PATCH 17/33] [P118] Update documentation --- docs/source/Plugin/P118.rst | 33 +++++++++++------- .../Plugin/P118_DeviceConfiguration.png | Bin 50483 -> 49289 bytes docs/source/Plugin/P118_commands.repl | 10 +++--- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/docs/source/Plugin/P118.rst b/docs/source/Plugin/P118.rst index b3cc44bf6f..f10ee12d66 100644 --- a/docs/source/Plugin/P118.rst +++ b/docs/source/Plugin/P118.rst @@ -24,7 +24,7 @@ Used libraries: |P118_usedlibraries| Description ----------- -This plugin enables communication with an Itho RFT fan (and some Duco fans) through a CC1101 868 MHz transceiver. It allows controlling the fan speed by simulating a Itho remote, and can also monitor for commands send by other remotes. +This plugin enables communication with an Itho RFT fan (and some Duco fans) through a CC1101 868 MHz transceiver. It allows controlling the fan speed by simulating an Itho remote, and can also monitor for commands sent by other remotes. Wiring ------ @@ -32,7 +32,7 @@ Wiring .. image:: P118_CC1101.jpg :alt: CC1101 868 MHz transceiver -The CC1101 transceiver is controlled via SPI and also requires another pin for the interrupt to let the ESP know a data packet arrived. Make sure to enable SPI under the ESPEasy hardware configuration. +The CC1101 transceiver is controlled via SPI and also requires an pin for the interrupt to let the ESP know a data packet has arrived. Make sure to enable SPI under the ESPEasy hardware configuration. .. code-block:: none @@ -63,17 +63,26 @@ Configuration Sensor ^^^^^^ -* **GPIO <- Interrupt pin** Input interrupt pin of the ESPEasy, should be connected to CC1101 GDO2. Don't use ESP boot related pints (GPIO0, GPIO2, GPIO15) or GPIO16 (no interrupt support). +* **GPIO <- Interrupt pin (CC1101 GDO2)**: Input interrupt pin of the ESPEasy, should be connected to CC1101 GDO2. Don't use ESP boot related pints (GPIO0, GPIO2, GPIO15) or GPIO16 (no interrupt support). When this setting is set to ``- None -`` it will be iognored, but receiving the messages sent from other controllers is not possible. Sending commands to the ITHO ventilation unit is still possible. -Device Settings -^^^^^^^^^^^^^^^ +* **GPIO -> CS pin (CC1101 CSN)**: The output pin to connect to the CS/CSN line of the CC1101 controller. The previous fixed setting of GPIO 15 is filled by default, but can be changed if multiple tranceiver units can be connected to the ESP. -* **Unit ID remote X** ID of another remote that controls your Itho fan. When set it allows the plugin to monitor for commands and act on them. The ID consist out of 3 numbers separated by ','. Up to 3 remotes are supported, +Remote RF Controls +^^^^^^^^^^^^^^^^^^ + +* **Unit ID remote 1..3**: ID of another remote that controls your Itho fan. When set it allows the plugin to monitor for commands and act on them. The ID consist out of 3 numbers separated by ','. Up to 3 remotes are supported. After the controller is joined with the ventilation unit, and enabling the **Enable minimal RF INFO log** option below, these ID's can be extracted from the logs at the INFO level by pressing a button on the remote. Most remotes will repeat a command ca. 3 times, in case there is disturbance in communication, so that should be easily recognizable. + +* **Enable RF DEBUG log**: When enabled all recieved RF packages will be shown in the ESPEasy log at DEBUG level. Useful to determine the ID of other remotes and the messages they send. Disable when not necessary to reduce writing to the log. + +* **Enable minimal RF INFO lgo**: When enabled, this will write the ID and raw command sent from any device using the same frequency and protocol. Can be used to determine entries for **Unit ID remote 1..3**. Disable when not necessary to reduce writing to the log. -* **Enable RF receive log** When enabled all recieved RF packages will be shown in the ESPEasy log under debug. Useful to determine the ID of other remotes. Disable when not necessary to reduce writing to the log. +* **Device ID byte 1..3**: Device ID of the ESPEasy plugin, defaults to 10,87,81. The plugin acts as a remote and can have a unique ID. Can be changed from default if you have interference with neighbours. After a change you should again join the ESP with the fan. -* **Device ID byte X** ID of the ESPEasy plugin, defaults to 10,87,81. The plugin acts as a remote and can have a unique ID. Can be changed from default if you have interference with neighbours. After a change you should again join the ESP with the fan. +Data Acquisition +^^^^^^^^^^^^^^^^ + +This group of settings, **Single event with all values** and **Send to Controller** settings are standard available configuration items. Send to Controller is only visible when one or more Controllers are configured. Values ^^^^^^ @@ -131,12 +140,12 @@ Change log ---------- .. versionchanged:: 2.0 - ... + . |added| - 2021-12-26 Initial release version. - - + 2022-08-10: Made multi-instance compatible, CS pin configurable and interrupt pin usage optional. + |added| + 2021-12-26 Initial release version. diff --git a/docs/source/Plugin/P118_DeviceConfiguration.png b/docs/source/Plugin/P118_DeviceConfiguration.png index 331df9db38923d23750c3b681b005abdaa538920..f7d06c54d867f2f152f30659a6a516ef21d67409 100644 GIT binary patch literal 49289 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 literal 50483 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 Date: Wed, 10 Aug 2022 20:45:36 +0200 Subject: [PATCH 18/33] [P118] Fix bugs found during testing, rename variables, clean up source --- src/_P118_Itho.ino | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index 6362361b7d..0ea7e1bfca 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -23,6 +23,8 @@ // tonhuisman, 28-12-2021 - Move interrupt handling to Plugin_data_struct, lifting the limit on nr. of plugins // tonhuisman, 03-01-2022 - Review source after structural-crash report, fix interrupt handler // tonhuisman, 21-06-2022 - Minor improvements +// tonhuisman, 10-08-2022 - Fix bugs, add 3 second limit to formerly perpetual while loops in IthoCC1101 library +// Restructure source somewhat, rename variables, clean up stuff generally // Recommended to disable RF receive logging to minimize code execution within interrupts @@ -126,12 +128,13 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_SET_DEFAULTS: // Set defaults address to the one used in old versions of the library for backwards compatability { - PIN(0) = -1; // Interrupt pin undefined by default - PIN(1) = PIN_SPI_SS; // CS pin use the previous default of PIN_SPI_SS/gpio 15 - P118_CONFIG_LOG = 1; + P118_IRQPIN = -1; // Interrupt pin undefined by default + P118_CSPIN = PIN_SPI_SS; // CS pin use the previous default of PIN_SPI_SS/gpio 15 + P118_CONFIG_LOG = 0; // RF DEBUG log disabled P118_CONFIG_DEVID1 = 10; P118_CONFIG_DEVID2 = 87; P118_CONFIG_DEVID3 = 81; + P118_CONFIG_RF_LOG = 1; // RF INFO log enabled success = true; break; } @@ -141,11 +144,19 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) # ifdef P118_DEBUG_LOG addLog(LOG_LEVEL_INFO, F("INIT PLUGIN_118")); # endif // ifdef P118_DEBUG_LOG - initPluginTaskData(event->TaskIndex, new (std::nothrow) P118_data_struct(P118_CONFIG_LOG)); - P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr != P118_data) { - success = P118_data->plugin_init(event); + if (validGpio(P118_CSPIN) && (P118_IRQPIN != P118_CSPIN)) { + initPluginTaskData(event->TaskIndex, new (std::nothrow) P118_data_struct(P118_CSPIN, + P118_IRQPIN, + P118_CONFIG_LOG == 1, + P118_CONFIG_RF_LOG == 1)); + P118_data_struct *P118_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr != P118_data) { + success = P118_data->plugin_init(event); + } + } else { + addLog(LOG_LEVEL_ERROR, F("ITHO: CS pin not correctly configured, plugin can not start!")); } break; @@ -211,19 +222,21 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD: { PLUGIN_118_ExtraSettingsStruct PLUGIN_118_ExtraSettings; - LoadCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + LoadCustomTaskSettings(event->TaskIndex, reinterpret_cast(&PLUGIN_118_ExtraSettings), sizeof(PLUGIN_118_ExtraSettings)); addFormSubHeader(F("Remote RF Controls")); addFormTextBox(F("Unit ID remote 1"), F("pID1"), PLUGIN_118_ExtraSettings.ID1, 8); addFormTextBox(F("Unit ID remote 2"), F("pID2"), PLUGIN_118_ExtraSettings.ID2, 8); addFormTextBox(F("Unit ID remote 3"), F("pID3"), PLUGIN_118_ExtraSettings.ID3, 8); - addFormCheckBox(F("Enable RF receive log"), F("plog"), P118_CONFIG_LOG); // Makes RF logging optional to reduce clutter in the log - // file - // in RF noisy environments + + addFormCheckBox(F("Enable RF DEBUG log"), F("plog"), P118_CONFIG_LOG); // Makes RF logging optional to reduce clutter in + // the log + addFormCheckBox(F("Enable minimal RF INFO log"), F("prflog"), P118_CONFIG_RF_LOG); // Log only the received Device ID's at INFO level + addFormNumericBox(F("Device ID byte 1"), F("pdevid1"), P118_CONFIG_DEVID1, 0, 255); addFormNumericBox(F("Device ID byte 2"), F("pdevid2"), P118_CONFIG_DEVID2, 0, 255); addFormNumericBox(F("Device ID byte 3"), F("pdevid3"), P118_CONFIG_DEVID3, 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 library")); + 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; } @@ -234,9 +247,10 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) strcpy(PLUGIN_118_ExtraSettings.ID1, web_server.arg(F("pID1")).c_str()); strcpy(PLUGIN_118_ExtraSettings.ID2, web_server.arg(F("pID2")).c_str()); strcpy(PLUGIN_118_ExtraSettings.ID3, web_server.arg(F("pID3")).c_str()); - SaveCustomTaskSettings(event->TaskIndex, (byte *)&PLUGIN_118_ExtraSettings, sizeof(PLUGIN_118_ExtraSettings)); + SaveCustomTaskSettings(event->TaskIndex, reinterpret_cast(&PLUGIN_118_ExtraSettings), sizeof(PLUGIN_118_ExtraSettings)); - P118_CONFIG_LOG = isFormItemChecked(F("plog")); + P118_CONFIG_LOG = isFormItemChecked(F("plog")); + P118_CONFIG_RF_LOG = isFormItemChecked(F("prflog")); P118_CONFIG_DEVID1 = getFormItemInt(F("pdevid1"), 10); P118_CONFIG_DEVID2 = getFormItemInt(F("pdevid2"), 87); From 5f591cee68a1c059a49e26f7e32240e88fff94bb Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 11 Aug 2022 16:21:05 +0200 Subject: [PATCH 19/33] [P118] Fix ESP32 support, shorten lib time-out, improve Devices overview display --- lib/Itho/CC1101.cpp | 19 +++++---- lib/Itho/CC1101.h | 11 +++++- lib/Itho/IthoCC1101.cpp | 12 +++--- lib/Itho/IthoCC1101.h | 46 +++++++++++----------- src/_P118_Itho.ino | 17 ++++++++ src/src/PluginStructs/P118_data_struct.cpp | 46 ++++++++++++++++------ src/src/PluginStructs/P118_data_struct.h | 2 + 7 files changed, 102 insertions(+), 51 deletions(-) diff --git a/lib/Itho/CC1101.cpp b/lib/Itho/CC1101.cpp index 94d2133076..b9ef6d5ac6 100644 --- a/lib/Itho/CC1101.cpp +++ b/lib/Itho/CC1101.cpp @@ -5,12 +5,10 @@ #include "CC1101.h" // default constructor -CC1101::CC1101(int8_t CSpin) : _CSpin(CSpin) +CC1101::CC1101(int8_t CSpin, int8_t MISOpin) : _CSpin(CSpin), _MISOpin(MISOpin) { - // SPI.begin(); // Done by ESPEasy - #ifdef ESP8266 + // SPI.begin(); // Done already by ESPEasy pinMode(_CSpin, OUTPUT); - #endif // ifdef ESP8266 } // CC1101 // default destructor @@ -30,7 +28,11 @@ inline void CC1101::deselect(void) { void CC1101::spi_waitMiso() { - while (digitalRead(MISO) == HIGH) { yield(); } + uint32_t maxWait = millis() + ITHO_MAX_WAIT; // Wait for max. x seconds + + while (digitalRead(_MISOpin) == HIGH && millis() < maxWait) { + yield(); + } } void CC1101::init() @@ -259,11 +261,12 @@ void CC1101::sendData(CC1101Packet *packet) while (index < packet->length) { // check if there is free space in the fifo - uint32_t maxWait = millis() + 3000; // Wait for max. 3 seconds + uint32_t maxWait = millis() + ITHO_MAX_WAIT; // Wait for max. x seconds while ((txStatus = (readRegisterMedian3(CC1101_TXBYTES | CC1101_STATUS_REGISTER) & CC1101_BITS_RX_BYTES_IN_FIFO)) > (CC1101_DATA_LEN - 2) && - millis() < maxWait) {} + millis() < maxWait) { + } // calculate how many bytes we can send length = (CC1101_DATA_LEN - txStatus); @@ -279,7 +282,7 @@ void CC1101::sendData(CC1101Packet *packet) } // wait until transmission is finished (TXOFF_MODE is expected to be set to 0/IDLE or TXFIFO_UNDERFLOW) - uint32_t maxWait = millis() + 3000; // Wait for max. 3 seconds + uint32_t maxWait = millis() + ITHO_MAX_WAIT; // Wait for max. x seconds do { diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h index 7b5064822d..44ce434e63 100644 --- a/lib/Itho/CC1101.h +++ b/lib/Itho/CC1101.h @@ -1,6 +1,7 @@ /* * Author: Klusjesman, modified bij supersjimmie for Arduino/ESP8266 - * tonhuisman: Fixed perpetual blocking while loops by limiting these to 3000 msec. + * 2022-08-10 tonhuisman: Fixed perpetual blocking while loops by limiting these to 3000 msec. + * 2022-08-11 tonhuisman: Change 3000 to #define ITHO_MAX_WAIT for easy adjustment */ #ifndef __CC1101_H__ @@ -15,6 +16,10 @@ # define PIN_SPI_SS (15) #endif // ifndef PIN_SPI_SS +#ifndef ITHO_MAX_WAIT +# define ITHO_MAX_WAIT 1000 // Wait no longer than this nr of milliseconds +#endif // ifndef ITHO_MAX_WAIT + /* Type of transfers */ #define CC1101_WRITE_BURST 0x40 #define CC1101_READ_SINGLE 0x80 @@ -186,7 +191,8 @@ class CC1101 { public: - CC1101(int8_t CSpin = PIN_SPI_SS); + CC1101(int8_t CSpin = PIN_SPI_SS, + int8_t MISOpin = MISO); ~CC1101(); // spi @@ -223,6 +229,7 @@ class CC1101 { void deselect(void); int8_t _CSpin = PIN_SPI_SS; + int8_t _MISOpin; protected: diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp index fc2ebea9ab..cbb7936a92 100644 --- a/lib/Itho/IthoCC1101.cpp +++ b/lib/Itho/IthoCC1101.cpp @@ -42,7 +42,7 @@ #define MDMCFG2 0x02 // 16bit sync word / 16bit specific // default constructor -IthoCC1101::IthoCC1101(int8_t CSpin, uint8_t counter, uint8_t sendTries) : CC1101(CSpin) +IthoCC1101::IthoCC1101(int8_t CSpin, int8_t MISOpin, uint8_t counter, uint8_t sendTries) : CC1101(CSpin, MISOpin) { this->outIthoPacket.counter = counter; this->sendTries = sendTries; @@ -188,7 +188,7 @@ void IthoCC1101::initReceive() writeCommand(CC1101_SCAL); // wait for calibration to finish - uint32_t maxWait = millis() + 3000; // Wait for max. 3 seconds + uint32_t maxWait = millis() + ITHO_MAX_WAIT; // Wait for max. x seconds while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE && millis() < maxWait) { @@ -238,7 +238,7 @@ void IthoCC1101::initReceive() writeCommand(CC1101_SCAL); // wait for calibration to finish - maxWait = millis() + 3000; // Wait for max. 3 seconds + maxWait = millis() + ITHO_MAX_WAIT; // Wait for max. x seconds while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_IDLE && millis() < maxWait) { @@ -256,7 +256,7 @@ void IthoCC1101::initReceive() writeCommand(CC1101_SRX); - maxWait = millis() + 3000; // Wait for max. 3 seconds + maxWait = millis() + ITHO_MAX_WAIT; // Wait for max. x seconds while ((readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) != CC1101_MARCSTATE_RX && millis() < maxWait) { @@ -287,10 +287,10 @@ void IthoCC1101::initReceiveMessage() writeRegister(CC1101_MDMCFG2, MDMCFG2); writeRegister(CC1101_PKTCTRL1, 0x00); - writeCommand(CC1101_SRX); // switch to RX state + writeCommand(CC1101_SRX); // switch to RX state // Check that the RX state has been entered - uint32_t maxWait = millis() + 3000; // Wait for max. 3 seconds + uint32_t maxWait = millis() + ITHO_MAX_WAIT; // Wait for max. x seconds while (((marcState = readRegisterWithSyncProblem(CC1101_MARCSTATE, CC1101_STATUS_REGISTER)) & CC1101_BITS_MARCSTATE) != CC1101_MARCSTATE_RX && diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h index e212374235..664b6a89ea 100644 --- a/lib/Itho/IthoCC1101.h +++ b/lib/Itho/IthoCC1101.h @@ -1,6 +1,7 @@ /* * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra - * tonhuisman: Fixed perpetual blocking while loops by limiting these to 3000 msec. + * 2022-08-10 tonhuisman: Fixed perpetual blocking while loops by limiting these to 3000 msec. + * 2022-08-11 tonhuisman: Change 3000 to #define ITHO_MAX_WAIT for easy adjustment */ #ifndef __ITHOCC1101_H__ @@ -24,13 +25,13 @@ 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 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 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 @@ -64,13 +65,14 @@ class IthoCC1101 : protected CC1101 { public: IthoCC1101(int8_t CSpin = PIN_SPI_SS, + int8_t MISOpin = MISO, uint8_t counter = 0, uint8_t sendTries = 3); // set initial counter value ~IthoCC1101(); // init void init() { - CC1101::init(); + CC1101::init(); initReceive(); } // init,reset CC1101 @@ -103,7 +105,7 @@ class IthoCC1101 : protected CC1101 { uint8_t ReadRSSI(); bool checkID(const uint8_t *id); - int * getLastID(); + int* getLastID(); String getLastIDstr(bool ashex = true); String getLastMessagestr(bool ashex = true); String LastMessageDecoded(); @@ -131,22 +133,22 @@ class IthoCC1101 : protected CC1101 { 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); + void createMessageStart(IthoPacket *itho, + CC1101Packet *packet); + void createMessageCommand(IthoPacket *itho, + CC1101Packet *packet); + void createMessageJoin(IthoPacket *itho, + CC1101Packet *packet); + void createMessageLeave(IthoPacket *itho, + CC1101Packet *packet); const uint8_t* getMessageCommandBytes(IthoCommand command); - uint8_t getCounter2(IthoPacket *itho, - uint8_t len); + uint8_t getCounter2(IthoPacket *itho, + uint8_t len); - uint8_t messageEncode(IthoPacket *itho, - CC1101Packet *packet); - void messageDecode(CC1101Packet *packet, - IthoPacket *itho); + uint8_t messageEncode(IthoPacket *itho, + CC1101Packet *packet); + void messageDecode(CC1101Packet *packet, + IthoPacket *itho); }; // IthoCC1101 #endif // __ITHOCC1101_H__ diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index 0ea7e1bfca..e19284cbe1 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -25,6 +25,12 @@ // tonhuisman, 21-06-2022 - Minor improvements // tonhuisman, 10-08-2022 - Fix bugs, add 3 second limit to formerly perpetual while loops in IthoCC1101 library // Restructure source somewhat, rename variables, clean up stuff generally +// tonhuisman, 11-08-2022 - Fix issue with ESP32 support, the MISO pin was predefined, but not matching the ESPEasy +// actual configuration. +// Added time-out check (5s) to initialization, usually an indication of incorrect hardware +// configuration, defective or disconnected board. +// Reduced time-out checks in IthoCC1101 library to 1 second (from 3) +// Improved display of GPIO pins in Devices page // Recommended to disable RF receive logging to minimize code execution within interrupts @@ -126,6 +132,17 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) break; } + case PLUGIN_WEBFORM_SHOW_GPIO_DESCR: + { + string = F("GDO2: "); + string += formatGpioLabel(P118_IRQPIN, false); + string += event->String1; + string += F("CSN: "); + string += formatGpioLabel(P118_CSPIN, false); + success = true; + break; + } + case PLUGIN_SET_DEFAULTS: // Set defaults address to the one used in old versions of the library for backwards compatability { P118_IRQPIN = -1; // Interrupt pin undefined by default diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index 67af8b7d78..6ea10a725c 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -27,25 +27,45 @@ bool P118_data_struct::plugin_init(struct EventStruct *event) { addLog(LOG_LEVEL_INFO, F("ITHO: Extra Settings PLUGIN_118 loaded")); # endif // ifdef P118_DEBUG_LOG - _rf = new (std::nothrow) IthoCC1101(_csPin); + int8_t spi_pins[3]; + uint32_t startInit = 0; + + if (Settings.getSPI_pins(spi_pins) && validGpio(spi_pins[1])) { + startInit = millis(); + _rf = new (std::nothrow) IthoCC1101(_csPin, spi_pins[1]); // Pass CS and MISO + } else { + addLog(LOG_LEVEL_ERROR, F("ITHO: SPI configuration not correct!")); + } if (nullptr != _rf) { + success = true; + // DeviceID used to send commands, can also be changed on the fly for multi itho control, 10,87,81 corresponds with old library _rf->setDeviceID(P118_CONFIG_DEVID1, P118_CONFIG_DEVID2, P118_CONFIG_DEVID3); _rf->init(); - - if (validGpio(_irqPin)) { - attachInterruptArg(digitalPinToInterrupt(_irqPin), - reinterpret_cast(ISR_ithoCheck), - this, - FALLING); - } else { - addLog(LOG_LEVEL_ERROR, F("ITHO: Interrupt pin disabled, sending is OK, not receiving data!")); + uint32_t finishInit = millis(); + + if (finishInit - startInit > P118_TIMEOUT_LIMIT) { + String log = F("ITHO: Init duration was: "); + log += finishInit - startInit; + log += F("msec. suggesting that the CC1101 board is not (correctly) connected."); + addLog(LOG_LEVEL_ERROR, log); + success = false; } - _rf->initReceive(); - _InitRunned = true; - success = true; + if (success) { + if (validGpio(_irqPin)) { + attachInterruptArg(digitalPinToInterrupt(_irqPin), + reinterpret_cast(ISR_ithoCheck), + this, + FALLING); + addLog(LOG_LEVEL_INFO, F("ITHO: Interrupts enabled.")); + } else { + addLog(LOG_LEVEL_ERROR, F("ITHO: Interrupt pin disabled, sending is OK, not receiving data!")); + } + _rf->initReceive(); + _InitRunned = true; + } } return success; } @@ -234,7 +254,7 @@ void P118_data_struct::ITHOcheck() { String Id = _rf->getLastIDstr(); if (_rfLog && loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("ITHO: Received ID: "); + String log = F("ITHO: Received from ID: "); log += Id; log += F("; raw cmd: "); log += cmd; diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index b144eed91d..f7c0577139 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -16,6 +16,8 @@ # undef P118_DEBUG_LOG # endif // if defined(LIMIT_BUILD_SIZE) && defined(P118_DEBUG_LOG) +# define P118_TIMEOUT_LIMIT 5000 // If initialization takes > 5 seconds, most likely the hardware is not correctly connected + # define P118_CSPIN PIN(1) # define P118_IRQPIN PIN(0) # define P118_CONFIG_LOG PCONFIG(0) From b04e938e7a1e4c02e684829744ce957ebeed16be Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 11 Aug 2022 16:22:06 +0200 Subject: [PATCH 20/33] [Devices] Fine-tune display of custom GPIO configuration --- src/src/WebServer/DevicesPage.cpp | 29 ++++++++++++++++------------- src/src/WebServer/DevicesPage.h | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/src/WebServer/DevicesPage.cpp b/src/src/WebServer/DevicesPage.cpp index 02e6635d33..2809a9ddab 100644 --- a/src/src/WebServer/DevicesPage.cpp +++ b/src/src/WebServer/DevicesPage.cpp @@ -612,6 +612,9 @@ void handle_devicess_ShowAllTasksTable(uint8_t page) if (validDeviceIndex(DeviceIndex)) { if (Settings.TaskDeviceDataFeed[x] == 0) { + String description; + bool pluginHasGPIODescription = pluginWebformShowGPIOdescription(x, F("
"), description); + bool showpin1 = false; bool showpin2 = false; bool showpin3 = false; @@ -620,18 +623,19 @@ void handle_devicess_ShowAllTasksTable(uint8_t page) case DEVICE_TYPE_I2C: { format_I2C_pin_description(x); + html_BR(); break; } case DEVICE_TYPE_SPI3: - showpin3 = true; + showpin3 = !pluginHasGPIODescription; // Fall Through case DEVICE_TYPE_SPI2: - showpin2 = true; + showpin2 = !pluginHasGPIODescription; // Fall Through case DEVICE_TYPE_SPI: - format_SPI_pin_description(spi_gpios, x); + format_SPI_pin_description(spi_gpios, x, !pluginHasGPIODescription); break; case DEVICE_TYPE_ANALOG: { @@ -680,9 +684,7 @@ void handle_devicess_ShowAllTasksTable(uint8_t page) case DEVICE_TYPE_CUSTOM0: { showpin1 = true; - String description; - - if (pluginWebformShowGPIOdescription(x, F("
"), description) || (Device[DeviceIndex].Type == DEVICE_TYPE_CUSTOM0)) { + if (pluginHasGPIODescription || (Device[DeviceIndex].Type == DEVICE_TYPE_CUSTOM0)) { addHtml(description); showpin1 = false; showpin2 = false; @@ -716,13 +718,12 @@ void handle_devicess_ShowAllTasksTable(uint8_t page) } // Allow for tasks to show their own specific GPIO pins. - if (!Device[DeviceIndex].isCustom()) { - String description; - - if (pluginWebformShowGPIOdescription(x, F("
"), description)) { + if (!Device[DeviceIndex].isCustom() && + pluginHasGPIODescription) { + if (showpin1 || showpin2 || showpin3) { html_BR(); - addHtml(description); } + addHtml(description); } } } @@ -824,7 +825,7 @@ void format_I2C_pin_description(taskIndex_t x) } } -void format_SPI_pin_description(int8_t spi_gpios[3], taskIndex_t x) +void format_SPI_pin_description(int8_t spi_gpios[3], taskIndex_t x, bool showCSpin) { if (Settings.InitSPI > static_cast(SPI_Options_e::None)) { for (int i = 0; i < 3; ++i) { @@ -837,7 +838,9 @@ void format_SPI_pin_description(int8_t spi_gpios[3], taskIndex_t x) } html_BR(); } - Label_Gpio_toHtml(F("CS"), formatGpioLabel(Settings.TaskDevicePin1[x], false)); + if (showCSpin) { + Label_Gpio_toHtml(F("CS"), formatGpioLabel(Settings.TaskDevicePin1[x], false)); + } } } diff --git a/src/src/WebServer/DevicesPage.h b/src/src/WebServer/DevicesPage.h index 33710041bf..525f0ffb14 100644 --- a/src/src/WebServer/DevicesPage.h +++ b/src/src/WebServer/DevicesPage.h @@ -46,7 +46,7 @@ void format_SPI_port_description(int8_t spi_gpios[3]); void format_I2C_pin_description(taskIndex_t x); -void format_SPI_pin_description(int8_t spi_gpios[3], taskIndex_t x); +void format_SPI_pin_description(int8_t spi_gpios[3], taskIndex_t x, bool showCSpin = true); From 0ec45936b6a1b98a3a800d701cceedbd75fbffad Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 11 Aug 2022 16:22:41 +0200 Subject: [PATCH 21/33] [P116] Add Devices list GPIO overview --- src/_P116_ST77xx.ino | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/_P116_ST77xx.ino b/src/_P116_ST77xx.ino index e2af9db700..95591239c6 100644 --- a/src/_P116_ST77xx.ino +++ b/src/_P116_ST77xx.ino @@ -77,6 +77,26 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) break; } + case PLUGIN_WEBFORM_SHOW_GPIO_DESCR: + { + string = F("CS: "); + string += formatGpioLabel(PIN(0), false); + string += event->String1; // contains the NewLine sequence + string += F("DC: "); + string += formatGpioLabel(PIN(1), false); + string += event->String1; + string += F("RES: "); + string += formatGpioLabel(PIN(2), false); + string += event->String1; + string += F("Btn: "); + string += formatGpioLabel(P116_CONFIG_BUTTON_PIN, false); + string += event->String1; + string += F("Bckl: "); + string += formatGpioLabel(P116_CONFIG_BACKLIGHT_PIN, false); + success = true; + break; + } + case PLUGIN_SET_DEFAULTS: { # ifdef ESP32 From 70b4e23ebcfbe5501e0c232bd5fd9488f91d75e0 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 11 Aug 2022 19:55:03 +0200 Subject: [PATCH 22/33] [P118] [Build] Restore Itho communication in ESP32 builds --- src/src/CustomBuild/define_plugin_sets.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 606608ab80..2de27a889c 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1277,8 +1277,7 @@ To create/register a plugin, you have to : #define USES_P115 // Fuel Gauge MAX1704x #define USES_P117 // SCD30 // Disable Itho when using second heap as it no longer fits. - // Disable Itho for ESP32 as it does not (yet) work on ESP32 IDF4.4 - #if !defined(USE_SECOND_HEAP) && !defined(ESP32) + #if !defined(USE_SECOND_HEAP) #define USES_P118 // Itho ventilation control #endif #define USES_P124 // I2C MultiRelay @@ -1631,8 +1630,7 @@ To create/register a plugin, you have to : #define USES_P117 // SCD30 #endif #ifndef USES_P118 - // Does not (yet) work well on ESP32 with IDF 4.4 - // #define USES_P118 // Itho ventilation coontrol + #define USES_P118 // Itho ventilation coontrol #endif #ifndef USES_P119 #define USES_P119 // ITG3205 Gyro From e28e52c125a18be505b6d75d2394f8b20cc8841e Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 11 Aug 2022 19:59:16 +0200 Subject: [PATCH 23/33] [Build] Remove NeoPixel plugins from DISPLAY builds (cherry picked from commit 6074be2397148861d3a02cb055d8e6a32694aa3b) --- src/src/CustomBuild/define_plugin_sets.h | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 2de27a889c..f41019d1ce 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1356,21 +1356,21 @@ To create/register a plugin, you have to : #ifndef USES_P036 #define USES_P036 // FrameOLED #endif - #ifndef USES_P038 - #define USES_P038 // NeoPixel - #endif - #ifndef USES_P041 - #define USES_P041 // NeoClock - #endif - #ifndef USES_P042 - #define USES_P042 // Candle - #endif + // #ifndef USES_P038 + // #define USES_P038 // NeoPixel + // #endif + // #ifndef USES_P041 + // #define USES_P041 // NeoClock + // #endif + // #ifndef USES_P042 + // #define USES_P042 // Candle + // #endif #ifndef USES_P057 #define USES_P057 // HT16K33_LED #endif - #ifndef USES_P070 - #define USES_P070 // NeoPixel_Clock - #endif + // #ifndef USES_P070 + // #define USES_P070 // NeoPixel_Clock + // #endif #ifndef USES_P075 #define USES_P075 // Nextion #endif From 61c1f2b9cdeadaeb6978de974ab33b78ba7a8ba5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 18 Aug 2022 17:41:21 +0200 Subject: [PATCH 24/33] [Itho Lib] Merge Orcon functions from #4099 --- lib/Itho/IthoCC1101.cpp | 245 ++++++++++++++++++++++++++++++++++++---- lib/Itho/IthoCC1101.h | 39 ++++++- lib/Itho/IthoPacket.h | 13 ++- 3 files changed, 270 insertions(+), 27 deletions(-) diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp index cbb7936a92..e2461e18bb 100644 --- a/lib/Itho/IthoCC1101.cpp +++ b/lib/Itho/IthoCC1101.cpp @@ -324,21 +324,32 @@ bool IthoCC1101::parseMessageCommand() { // 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); + const bool isHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageHighCommandBytes); + const bool isRVHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVHighCommandBytes); + const bool isMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageMediumCommandBytes); + const bool isRVMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVMediumCommandBytes); + const bool isLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageLowCommandBytes); + const bool isRVLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVLowCommandBytes); + const bool isRVAutoCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVAutoCommandBytes); + const bool isStandByCommand = checkIthoCommand(&inIthoPacket, ithoMessageStandByCommandBytes); + const bool isTimer1Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer1CommandBytes); + const bool isTimer2Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer2CommandBytes); + const bool isTimer3Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer3CommandBytes); + const bool isJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageJoinCommandBytes); + const bool isJoin2Command = checkIthoCommand(&inIthoPacket, ithoMessageJoin2CommandBytes); + const bool isRVJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVJoinCommandBytes); + const bool isLeaveCommand = checkIthoCommand(&inIthoPacket, ithoMessageLeaveCommandBytes); + + const bool isOrconStandByCommand = checkIthoCommand(&inIthoPacket, orconMessageStandByCommandBytes); + const bool isOrconLowCommand = checkIthoCommand(&inIthoPacket, orconMessageLowCommandBytes); + const bool isOrconMediumCommand = checkIthoCommand(&inIthoPacket, orconMessageMediumCommandBytes); + const bool isOrconFullCommand = checkIthoCommand(&inIthoPacket, orconMessageFullCommandBytes); + const bool isOrconAutoCommand = checkIthoCommand(&inIthoPacket, orconMessageAutoCommandBytes); + const bool isOrconTimer0Command = checkIthoCommand(&inIthoPacket, orconMessageTimer0CommandBytes); + const bool isOrconTimer1Command = checkIthoCommand(&inIthoPacket, orconMessageTimer1CommandBytes); + const bool isOrconTimer2Command = checkIthoCommand(&inIthoPacket, orconMessageTimer2CommandBytes); + const bool isOrconTimer3Command = checkIthoCommand(&inIthoPacket, orconMessageTimer3CommandBytes); + const bool isOrconAutoCO2Command = checkIthoCommand(&inIthoPacket, orconMessageAutoCO2CommandBytes); // determine command inIthoPacket.command = IthoUnknown; @@ -373,7 +384,31 @@ bool IthoCC1101::parseMessageCommand() { if (isLeaveCommand) { inIthoPacket.command = IthoLeave; } -#if defined(CRC_FILTER) + if (_enableOrcon) { + if (isOrconStandByCommand) { inIthoPacket.command = OrconStandBy; } + + if (isOrconLowCommand) { inIthoPacket.command = OrconLow; } + + if (isOrconMediumCommand) { inIthoPacket.command = OrconMedium; } + + if (isOrconFullCommand) { inIthoPacket.command = OrconHigh; } + + if (isOrconAutoCommand) { inIthoPacket.command = OrconAuto; } + + if (isOrconTimer0Command) { inIthoPacket.command = OrconTimer0; } + + if (isOrconTimer1Command) { inIthoPacket.command = OrconTimer1; } + + if (isOrconTimer2Command) { inIthoPacket.command = OrconTimer2; } + + if (isOrconTimer3Command) { inIthoPacket.command = OrconTimer3; } + + if (isOrconAutoCO2Command) { inIthoPacket.command = OrconAutoCO2; } + } + + #if defined(CRC_FILTER) + + // TODO nl0pvm: make this orcon proof uint8_t mLen = 0; if (isPowerCommand || isHighCommand || isMediumCommand || isLowCommand || isStandByCommand || isTimer1Command || isTimer2Command || @@ -394,7 +429,7 @@ bool IthoCC1101::parseMessageCommand() { inIthoPacket.command = IthoUnknown; return false; } -#endif // if defined(CRC_FILTER) + #endif // if defined(CRC_FILTER) return true; } @@ -402,12 +437,30 @@ bool IthoCC1101::parseMessageCommand() { bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[]) { uint8_t offset = 0; + // this is quite hacky as not even the opcode is checked for itho. Because of that orcon 31E0 messages are wrongly recognised as itho + // standby messages. + // TODO nl0pvm: FIX THIS :D + + // first byte is the header of the message, this determines the structure of the rest of the message + // The bits are used as follows <00TTAAPP> + // 00 - Unused + // TT - Message type + // AA - Present DeviceID fields + // PP - Present Params + if ((itho->deviceType == 28) || (itho->deviceType == 24)) { offset = 2; } - for (int i = 4; i < 6; i++) + // for (int i = 4; i < 6; i++) + // for Orcon: the code above makes that only 3 bytes (byte 4, 5 and 6) are checked. That gives false positves + for (int i = 0; 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 + // this is required for differentiating between Orcon and Itho commands. However I don't know what the reason was to comment this out. + // thus this needs to be verified by Itho users + 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; } @@ -415,7 +468,7 @@ bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[] return true; } -void IthoCC1101::sendCommand(IthoCommand command) +void IthoCC1101::sendCommand(IthoCommand command, uint8_t srcId[3], uint8_t destId[3]) { CC1101Packet outMessage; uint8_t maxTries = sendTries; @@ -440,6 +493,23 @@ void IthoCC1101::sendCommand(IthoCommand command) delaytime = 4; break; + case OrconStandBy: + case OrconLow: + case OrconMedium: + case OrconHigh: + case OrconAuto: + case OrconTimer0: + case OrconTimer1: + case OrconTimer2: + case OrconTimer3: + case OrconAutoCO2: + + if (_enableOrcon) { + maxTries = 1; + createOrconMessageCommand(&outIthoPacket, &outMessage, srcId, destId); + } + break; + default: createMessageCommand(&outIthoPacket, &outMessage); break; @@ -476,6 +546,65 @@ void IthoCC1101::createMessageStart(IthoPacket *itho, CC1101Packet *packet) // [start of command specific data] } +void IthoCC1101::createOrconMessageCommand(IthoPacket *itho, CC1101Packet *packet, uint8_t srcId[3], uint8_t destId[3]) +{ + // set start message structure + createMessageStart(itho, packet); + + // first byte is the header of the message, this determines the structure of the rest of the message + // The bits are used as follows <00TTAAPP> + // 00 - Unused + // TT - Message type + // AA - Present DeviceID fields + // PP - Present Params + uint8_t header = 0b00011100; + + itho->dataDecoded[0] = header; // 00TTAAPP + // set source deviceID + itho->dataDecoded[1] = srcId[0]; + itho->dataDecoded[2] = srcId[1]; + itho->dataDecoded[3] = srcId[2]; + + // set destination deviceID + itho->dataDecoded[4] = destId[0]; + itho->dataDecoded[5] = destId[1]; + itho->dataDecoded[6] = destId[2]; + + const uint8_t *commandBytes = getMessageCommandBytes(itho->command); + + for (uint8_t i = 0; i < getMessageCommandLength(itho->command); i++) { + itho->dataDecoded[i + 7] = commandBytes[i]; + } + + itho->length = 7 + 1 + getMessageCommandLength(itho->command); + + itho->dataDecoded[itho->length - 1] = getCRC(itho, itho->length - 1); + itho->length += 1; + + packet->length = messageEncode(itho, packet) - 2; // delete the last two itho bytes (0x55, 0x95) so we can reuse messageEncode() without + // modifications + + // set compex orcon specific end bytes + packet->data[packet->length] = 0xAC; + packet->length += 1; + packet->data[packet->length] = 0xAA; + packet->length += 1; + packet->data[packet->length] = 0xBF; + packet->length += 1; + packet->data[packet->length] = 0x0E; + packet->length += 1; +} + +uint8_t IthoCC1101::getCRC(IthoPacket *itho, uint8_t len) { + uint8_t val = 0; + + for (uint8_t i = 0; i < len; i++) { + val += itho->dataDecoded[i]; + } + + return 0x100 - (val & 0xFF); +} + void IthoCC1101::createMessageCommand(IthoPacket *itho, CC1101Packet *packet) { // set start message structure @@ -645,11 +774,85 @@ const uint8_t * IthoCC1101::getMessageCommandBytes(IthoCommand command) return &ithoMessageJoinCommandBytes[0]; case IthoLeave: return &ithoMessageLeaveCommandBytes[0]; + + case OrconStandBy: + return &orconMessageStandByCommandBytes[0]; + case OrconLow: + return &orconMessageLowCommandBytes[0]; + case OrconMedium: + return &orconMessageMediumCommandBytes[0]; + case OrconHigh: + return &orconMessageFullCommandBytes[0]; + case OrconAuto: + return &orconMessageAutoCommandBytes[0]; + case OrconTimer0: + return &orconMessageTimer0CommandBytes[0]; + case OrconTimer1: + return &orconMessageTimer1CommandBytes[0]; + case OrconTimer2: + return &orconMessageTimer2CommandBytes[0]; + case OrconTimer3: + return &orconMessageTimer3CommandBytes[0]; + case OrconAutoCO2: + return &orconMessageAutoCO2CommandBytes[0]; + default: return &ithoMessageLowCommandBytes[0]; } } +uint8_t IthoCC1101::getMessageCommandLength(IthoCommand command) +{ + switch (command) + { + case IthoStandby: + return sizeof(ithoMessageStandByCommandBytes) / sizeof(uint8_t); + case IthoHigh: + return sizeof(ithoMessageHighCommandBytes) / sizeof(uint8_t); + case IthoFull: + return sizeof(ithoMessageFullCommandBytes) / sizeof(uint8_t); + case IthoMedium: + return sizeof(ithoMessageMediumCommandBytes) / sizeof(uint8_t); + case IthoLow: + return sizeof(ithoMessageLowCommandBytes) / sizeof(uint8_t); + case IthoTimer1: + return sizeof(ithoMessageTimer1CommandBytes) / sizeof(uint8_t); + case IthoTimer2: + return sizeof(ithoMessageTimer2CommandBytes) / sizeof(uint8_t); + case IthoTimer3: + return sizeof(ithoMessageTimer3CommandBytes) / sizeof(uint8_t); + case IthoJoin: + return sizeof(ithoMessageJoinCommandBytes) / sizeof(uint8_t); + case IthoLeave: + return sizeof(ithoMessageLeaveCommandBytes) / sizeof(uint8_t); + + case OrconStandBy: + return sizeof(orconMessageStandByCommandBytes) / sizeof(uint8_t); + case OrconLow: + return sizeof(orconMessageLowCommandBytes) / sizeof(uint8_t); + case OrconMedium: + return sizeof(orconMessageMediumCommandBytes) / sizeof(uint8_t); + case OrconHigh: + return sizeof(orconMessageFullCommandBytes) / sizeof(uint8_t); + case OrconAuto: + return sizeof(orconMessageAutoCommandBytes) / sizeof(uint8_t); + case OrconTimer0: + return sizeof(orconMessageTimer0CommandBytes) / sizeof(uint8_t); + case OrconTimer1: + return sizeof(orconMessageTimer1CommandBytes) / sizeof(uint8_t); + case OrconTimer2: + return sizeof(orconMessageTimer2CommandBytes) / sizeof(uint8_t); + case OrconTimer3: + return sizeof(orconMessageTimer3CommandBytes) / sizeof(uint8_t); + case OrconAutoCO2: + return sizeof(orconMessageAutoCO2CommandBytes) / sizeof(uint8_t); + + + default: + return sizeof(ithoMessageLowCommandBytes) / sizeof(uint8_t); + } +} + /* Counter2 is the decimal sum of all bytes in decoded form from deviceType up to the last byte before counter2 subtracted diff --git a/lib/Itho/IthoCC1101.h b/lib/Itho/IthoCC1101.h index 664b6a89ea..0cd9f111e4 100644 --- a/lib/Itho/IthoCC1101.h +++ b/lib/Itho/IthoCC1101.h @@ -17,6 +17,19 @@ const uint8_t ithoPaTableSend[8] = { 0x6F, 0x26, 0x2E, 0x8C, 0x87, 0xCD, 0xC7 const uint8_t ithoPaTableReceive[8] = { 0x6F, 0x26, 0x2E, 0x7F, 0x8A, 0x84, 0xCA, 0xC4 }; // message command bytes + +const uint8_t orconMessageStandByCommandBytes[] = { 34, 241, 3, 0, 0, 4 }; +const uint8_t orconMessageLowCommandBytes[] = { 34, 241, 3, 0, 1, 4 }; +const uint8_t orconMessageMediumCommandBytes[] = { 34, 241, 3, 0, 2, 4 }; +const uint8_t orconMessageFullCommandBytes[] = { 34, 241, 3, 0, 3, 4 }; +const uint8_t orconMessageAutoCommandBytes[] = { 34, 241, 3, 0, 4, 4 }; + +const uint8_t orconMessageTimer0CommandBytes[] = { 34, 243, 7, 0, 82, 12, 0, 4, 4, 4 }; // Timer 12*60 minuten @ speed 0 +const uint8_t orconMessageTimer1CommandBytes[] = { 34, 243, 7, 0, 18, 60, 1, 4, 4, 4 }; // Timer 60 minuten @ speed 1 +const uint8_t orconMessageTimer2CommandBytes[] = { 34, 243, 7, 0, 82, 13, 2, 4, 4, 4 }; // Timer 13*60 minuten @ speed 2 +const uint8_t orconMessageTimer3CommandBytes[] = { 34, 243, 7, 0, 18, 60, 3, 4, 4, 4 }; // Timer 60 minuten @ speed 3 +const uint8_t orconMessageAutoCO2CommandBytes[] = { 34, 243, 7, 0, 18, 60, 4, 4, 4, 4 }; // Timer 60 minuten @ speed auto + 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 }; @@ -111,7 +124,13 @@ class IthoCC1101 : protected CC1101 { String LastMessageDecoded(); // send - void sendCommand(IthoCommand command); + void sendCommand(IthoCommand command, + uint8_t srcId[3] = 0, + uint8_t destId[3] = 0); + + void enableOrcon(bool state) { + _enableOrcon = state; + } protected: @@ -133,15 +152,24 @@ class IthoCC1101 : protected CC1101 { const uint8_t commandBytes[]); // send - void createMessageStart(IthoPacket *itho, - CC1101Packet *packet); - void createMessageCommand(IthoPacket *itho, - CC1101Packet *packet); + void createMessageStart(IthoPacket *itho, + CC1101Packet *packet); + void createMessageCommand(IthoPacket *itho, + CC1101Packet *packet); + void createOrconMessageCommand(IthoPacket *itho, + CC1101Packet *packet, + uint8_t srcId[3], + uint8_t destId[3]); + uint8_t getCRC(IthoPacket *itho, + uint8_t len); void createMessageJoin(IthoPacket *itho, CC1101Packet *packet); void createMessageLeave(IthoPacket *itho, CC1101Packet *packet); const uint8_t* getMessageCommandBytes(IthoCommand command); + + uint8_t getMessageCommandLength(IthoCommand command); + uint8_t getCounter2(IthoPacket *itho, uint8_t len); @@ -149,6 +177,7 @@ class IthoCC1101 : protected CC1101 { CC1101Packet *packet); void messageDecode(CC1101Packet *packet, IthoPacket *itho); + bool _enableOrcon = false; }; // IthoCC1101 #endif // __ITHOCC1101_H__ diff --git a/lib/Itho/IthoPacket.h b/lib/Itho/IthoPacket.h index 2ceccda12b..0e90339060 100644 --- a/lib/Itho/IthoPacket.h +++ b/lib/Itho/IthoPacket.h @@ -26,7 +26,18 @@ enum IthoCommand DucoStandby = 11, DucoLow = 12, DucoMedium = 13, - DucoHigh = 14 + DucoHigh = 14, + + OrconStandBy = 100, + OrconLow = 101, + OrconMedium = 102, + OrconHigh = 103, + OrconAuto = 104, + OrconTimer0 = 110, + OrconTimer1 = 111, + OrconTimer2 = 112, + OrconTimer3 = 113, + OrconAutoCO2 = 114 }; From 62a9b0beb9012b15d00e766194e152b4387b6dac Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 18 Aug 2022 17:41:48 +0200 Subject: [PATCH 25/33] [P118] Merge Orcon functions from #4099 --- src/_P118_Itho.ino | 39 ++- src/src/PluginStructs/P118_data_struct.cpp | 310 ++++++++++++++++++++- src/src/PluginStructs/P118_data_struct.h | 32 ++- 3 files changed, 372 insertions(+), 9 deletions(-) diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index e19284cbe1..e99e51daaf 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -31,6 +31,10 @@ // configuration, defective or disconnected board. // Reduced time-out checks in IthoCC1101 library to 1 second (from 3) // Improved display of GPIO pins in Devices page +// tonhuisman, 18-08-2022 - Merge Orcon related code from PR #4099 (https://github.com/letscontrolit/ESPEasy/pull/4099) +// Orcon code can be partially disabled by setting P118_FEATURE_ORCON 0 in P118_data_struc.h +// Support for orcon must be enabled in settings, to avoid possible interference with Itho. +// Re-enabled timer support for Orcon, as it is only a status update, NOT a ventilator update // Recommended to disable RF receive logging to minimize code execution within interrupts @@ -46,6 +50,17 @@ // 23 - set itho to high speed with hardware timer (20 min) // 33 - set itho to high speed with hardware timer (30 min) +// 100 - set Orcon ventilation unit to standby +// 101 - set Orcon ventilation unit to low speed +// 102 - set Orcon ventilation unit to medium speed +// 103 - set Orcon ventilation unit to high speed +// 104 - set Orcon ventilation unit to Auto +// 111 - set Orcon to standby with hardware timer (12 hours) +// 111 - set Orcon to low speed with hardware timer (1 hour) +// 112 - set Orcon to medium speed with hardware timer (13 hours) +// 113 - set Orcon to high speed with hardware timer (1 hour) +// 113 - set Orcon to AutoCO2 mode + // List of States: // 1 - Itho ventilation unit to lowest speed @@ -56,6 +71,17 @@ // 23 -Itho to high speed with hardware timer (20 min) // 33 -Itho to high speed with hardware timer (30 min) +// 100 - Orcon ventilation unit to standby +// 101 - Orcon ventilation unit to low speed +// 102 - Orcon ventilation unit to medium speed +// 103 - Orcon ventilation unit to high speed +// 104 - Orcon ventilation unit to Auto +// 111 - Orcon to standby with hardware timer (12 hours) +// 111 - Orcon to low speed with hardware timer (1 hour) +// 112 - Orcon to medium speed with hardware timer (13 hours) +// 113 - Orcon to high speed with hardware timer (1 hour) +// 113 - Orcon to AutoCO2 mode + // Usage for http (not case sensitive): // http://ip/control?cmd=STATE,1111 // http://ip/control?cmd=STATE,1 @@ -242,6 +268,9 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) LoadCustomTaskSettings(event->TaskIndex, reinterpret_cast(&PLUGIN_118_ExtraSettings), sizeof(PLUGIN_118_ExtraSettings)); addFormSubHeader(F("Remote RF Controls")); addFormTextBox(F("Unit ID remote 1"), F("pID1"), PLUGIN_118_ExtraSettings.ID1, 8); + # if P118_FEATURE_ORCON + addFormNote(F("For Orcon: The addres of remote 1 will be used as source/sender address")); + # endif // if P118_FEATURE_ORCON addFormTextBox(F("Unit ID remote 2"), F("pID2"), PLUGIN_118_ExtraSettings.ID2, 8); addFormTextBox(F("Unit ID remote 3"), F("pID3"), PLUGIN_118_ExtraSettings.ID3, 8); @@ -254,6 +283,11 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) addFormNumericBox(F("Device ID byte 3"), F("pdevid3"), P118_CONFIG_DEVID3, 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 library")); + # if P118_FEATURE_ORCON + addFormNote(F("For Orcon: This is the destination ID a.k.a. the ID of the Ventilation unit.")); + + addFormCheckBox(F("Enable Orcon support"), F("orcon"), P118_CONFIG_ORCON); + # endif // if P118_FEATURE_ORCON success = true; break; } @@ -272,7 +306,10 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) P118_CONFIG_DEVID1 = getFormItemInt(F("pdevid1"), 10); P118_CONFIG_DEVID2 = getFormItemInt(F("pdevid2"), 87); P118_CONFIG_DEVID3 = getFormItemInt(F("pdevid3"), 81); - success = true; + # if P118_FEATURE_ORCON + P118_CONFIG_ORCON = isFormItemChecked(F("orcon")) ? 1 : 0; + # endif // if P118_FEATURE_ORCON + success = true; break; } } diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index 6ea10a725c..75e2d9157e 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -39,6 +39,9 @@ bool P118_data_struct::plugin_init(struct EventStruct *event) { if (nullptr != _rf) { success = true; + # if P118_FEATURE_ORCON + _rf->enableOrcon(P118_CONFIG_ORCON == 1); // Enabled? + # endif // if P118_FEATURE_ORCON // DeviceID used to send commands, can also be changed on the fly for multi itho control, 10,87,81 corresponds with old library _rf->setDeviceID(P118_CONFIG_DEVID1, P118_CONFIG_DEVID2, P118_CONFIG_DEVID3); @@ -81,13 +84,21 @@ bool P118_data_struct::plugin_exit(struct EventStruct *event) { bool P118_data_struct::plugin_once_a_second(struct EventStruct *event) { // decrement timer when timermode is running - if (_State >= 10) { _Timer--; } + if ((_State >= 10) && (_Timer > 0)) { + _Timer--; + + if (_Timer == 0) { _Timer--; } + } // if timer has elapsed set Fan state to low - if ((_State >= 10) && (_Timer <= 0)) + if ((_State >= 10) && (_Timer < 0)) { - _State = 1; - _Timer = 0; + if (_State < 100) { + _State = 1; // Itho low + } else { + _State = 101; // Orcon low + } + _Timer = 0; // Avoid doing this again } // Publish new data when vars are changed or init has runned or timer is running (update every 2 sec) @@ -231,6 +242,151 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str PluginWriteLog(F("timer 3")); break; } + # if P118_FEATURE_ORCON + case 100: // Fan StandBy + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconStandBy, srcID, destID); + _State = 100; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon standBy")); + success = true; + break; + } + case 101: // Fan low + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconLow, srcID, destID); + _State = 101; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon low speed")); + success = true; + break; + } + case 102: // Fan medium + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconMedium, srcID, destID); + _State = 102; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon medium speed")); + success = true; + break; + } + case 103: // Fan high + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconHigh, srcID, destID); + _State = 103; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon high speed")); + success = true; + break; + } + case 104: // Fan auto + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconAuto, srcID, destID); + _State = 104; + _Timer = 0; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon auto speed")); + success = true; + break; + } + case 110: // Timer 12*60 minutes @ speed 0 + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconTimer0, srcID, destID); + _State = 110; + _Timer = PLUGIN_118_OrconTime0; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon Timer 0")); + success = true; + break; + } + case 111: // Timer 60 minutes @ speed 1 + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconTimer1, srcID, destID); + _State = 111; + _Timer = PLUGIN_118_OrconTime1; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon Timer 1")); + success = true; + break; + } + case 112: // Timer 13*60 minutes @ speed 2 + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconTimer2, srcID, destID); + _State = 112; + _Timer = PLUGIN_118_OrconTime2; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon Timer 2")); + success = true; + break; + } + case 113: // Timer 60 minutes @ speed 3 + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconTimer3, srcID, destID); + _State = 113; + _Timer = PLUGIN_118_OrconTime3; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon Timer 3")); + success = true; + break; + } + case 114: + { + uint8_t srcID[3], destID[3]; + SetDestIDSrcID(event, srcID, destID, _ExtraSettings.ID1); + _rf->sendCommand(OrconAutoCO2, srcID, destID); + _State = 114; + _Timer = PLUGIN_118_OrconTime3; + _LastIDindex = 0; + _rf->initReceive(); + PluginWriteLog(F("Orcon Auto CO2")); + success = true; + break; + } + # else // if P118_FEATURE_ORCON + case 100: + case 101: + case 102: + case 103: + case 104: + case 110: + case 111: + case 112: + case 113: + case 114: + PluginWriteLog(F("Orcon support not included!")); + break; + # endif // if P118_FEATURE_ORCON default: { PluginWriteLog(F("INVALID")); @@ -356,6 +512,90 @@ void P118_data_struct::ITHOcheck() { if (_dbgLog) { log += F("leave"); } break; + # if P118_FEATURE_ORCON + case OrconStandBy: + + if (_dbgLog) { log += F("Orcon standby"); } + _State = 100; + _Timer = 0; + _LastIDindex = index; + break; + case OrconLow: + + if (_dbgLog) { log += F("Orcon low"); } + _State = 101; + _Timer = 0; + _LastIDindex = index; + break; + case OrconMedium: + + if (_dbgLog) { log += F("Orcon medium"); } + _State = 102; + _Timer = 0; + _LastIDindex = index; + break; + case OrconHigh: + + if (_dbgLog) { log += F("Orcon high"); } + _State = 103; + _Timer = 0; + _LastIDindex = index; + break; + case OrconAuto: + + if (_dbgLog) { log += F("Orcon auto"); } + _State = 104; + _Timer = 0; + _LastIDindex = index; + break; + case OrconTimer0: + + if (_dbgLog) { log += +F("Orcon Timer0"); } + _State = 110; + _Timer = PLUGIN_118_OrconTime0; + _LastIDindex = index; + break; + case OrconTimer1: + + if (_dbgLog) { log += +F("Orcon Timer1"); } + _State = 111; + _Timer = PLUGIN_118_OrconTime1; + _LastIDindex = index; + break; + case OrconTimer2: + + if (_dbgLog) { log += +F("Orcon Timer2"); } + _State = 112; + _Timer = PLUGIN_118_OrconTime2; + _LastIDindex = index; + break; + case OrconTimer3: + + if (_dbgLog) { log += +F("Orcon Timer3"); } + _State = 113; + _Timer = PLUGIN_118_OrconTime3; + _LastIDindex = index; + break; + case OrconAutoCO2: + + if (_dbgLog) { log += +F("Orcon AutoCO2"); } + _State = 114; + _Timer = 0; + _LastIDindex = index; + break; + # else // if P118_FEATURE_ORCON + case OrconStandBy: + case OrconLow: + case OrconMedium: + case OrconHigh: + case OrconAuto: + case OrconTimer0: + case OrconTimer1: + case OrconTimer2: + case OrconTimer3: + case OrconAutoCO2: + break; + # endif // if P118_FEATURE_ORCON } } else { if (_dbgLog) { @@ -413,4 +653,66 @@ void P118_data_struct::ISR_ithoCheck(P118_data_struct *self) { self->_Int = true; } +# if P118_FEATURE_ORCON + +/** + * Orcon specific: Set Destination ID + */ +void P118_data_struct::SetDestIDSrcID(struct EventStruct *event, uint8_t (& srcID)[3], uint8_t (& destID)[3], char (& tmpTmpID)[9]) +{ + destID[0] = PCONFIG(1) - 0; + destID[1] = PCONFIG(2) - 0; + destID[2] = PCONFIG(3) - 0; + + const char *delimiter = ","; + char *token; + + // char tmpID[9] = PLUGIN_118_ExtraSettings.ID1; // copy needed otherwise we modify PLUGIN_118_ExtraSettings.ID1 itself + char tmpID[9]; + + memcpy(tmpID, tmpTmpID, 9); + token = strtok(tmpID, delimiter); // select the first part + + if (token) { + srcID[0] = strtol(token, NULL, 16); // convert first string part (hex) to int + } else { + srcID[0] = 0; + } + token = strtok(NULL, delimiter); + + if (token) { + srcID[1] = strtol(token, NULL, 16); // convert first string part (hex) to int + } else { + srcID[1] = 0; + } + token = strtok(NULL, delimiter); + + if (token) { + srcID[2] = strtol(token, NULL, 16); // convert first string part (hex) to int + } else { + srcID[2] = 0; + } + + # ifndef BUILD_NO_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = (F("srcID: ")); + log += String(srcID[0]); + log += (F(",")); + log += String(srcID[1]); + log += (F(",")); + log += String(srcID[2]); + log += (F(" destID: ")); + log += String(destID[0]); + log += (F(",")); + log += String(destID[1]); + log += (F(",")); + log += String(destID[2]); + addLogMove(LOG_LEVEL_DEBUG, log); + } + # endif // ifndef BUILD_NO_DEBUG +} + +# endif // if P118_FEATURE_ORCON + #endif // ifdef USES_P118 diff --git a/src/src/PluginStructs/P118_data_struct.h b/src/src/PluginStructs/P118_data_struct.h index f7c0577139..37f4a83678 100644 --- a/src/src/PluginStructs/P118_data_struct.h +++ b/src/src/PluginStructs/P118_data_struct.h @@ -10,11 +10,18 @@ # include "IthoCC1101.h" # include "IthoPacket.h" -# define P118_DEBUG_LOG // Enable for some (extra) logging +# define P118_DEBUG_LOG // Enable for some (extra) logging +# define P118_FEATURE_ORCON 1 // Enable use of Orcon commands # if defined(LIMIT_BUILD_SIZE) && defined(P118_DEBUG_LOG) # undef P118_DEBUG_LOG # endif // if defined(LIMIT_BUILD_SIZE) && defined(P118_DEBUG_LOG) +# ifdef LIMIT_BUILD_SIZE +// # if P118_FEATURE_ORCON +// # undef P118_FEATURE_ORCON +// # define P118_FEATURE_ORCON 0 +// # endif // if P118_FEATURE_ORCON +# endif // ifdef LIMIT_BUILD_SIZE # define P118_TIMEOUT_LIMIT 5000 // If initialization takes > 5 seconds, most likely the hardware is not correctly connected @@ -25,11 +32,21 @@ # define P118_CONFIG_DEVID2 PCONFIG(2) # define P118_CONFIG_DEVID3 PCONFIG(3) # define P118_CONFIG_RF_LOG PCONFIG(4) +# define P118_CONFIG_ORCON PCONFIG(5) // 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 +# define PLUGIN_118_Time1 (10 * 60) +# define PLUGIN_118_Time2 (20 * 60) +# define PLUGIN_118_Time3 (30 * 60) + +// For reference only as the Orcon ventilation unit runs the timer inside +# if P118_FEATURE_ORCON +# define PLUGIN_118_OrconTime0 (12 * 60 * 60) +# define PLUGIN_118_OrconTime1 (60 * 60) +# define PLUGIN_118_OrconTime2 (13 * 60 * 60) +# define PLUGIN_118_OrconTime3 (60 * 60) +# endif // if P118_FEATURE_ORCON + // This extra settings struct is needed because the default settingsstruct doesn't support strings struct PLUGIN_118_ExtraSettingsStruct { @@ -67,6 +84,13 @@ struct P118_data_struct : public PluginTaskData_base { return _rf != nullptr; } + # if P118_FEATURE_ORCON + void SetDestIDSrcID(struct EventStruct *event, + uint8_t (&srcID)[3], + uint8_t (&destID)[3], + char(&tmpTmpID)[9]); + # endif // if P118_FEATURE_ORCON + IthoCC1101 *_rf = nullptr; // extra for interrupt handling From 84886d05429303b4ecb9608394bb13a87162b5de Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 18 Aug 2022 19:40:28 +0200 Subject: [PATCH 26/33] [P118] Update documentation with Orcon commands --- docs/source/Plugin/P118.rst | 4 ++++ .../Plugin/P118_DeviceConfiguration.png | Bin 49289 -> 55853 bytes docs/source/Plugin/P118_commands.repl | 14 +++++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/source/Plugin/P118.rst b/docs/source/Plugin/P118.rst index f10ee12d66..ae143a4319 100644 --- a/docs/source/Plugin/P118.rst +++ b/docs/source/Plugin/P118.rst @@ -78,6 +78,7 @@ Remote RF Controls * **Device ID byte 1..3**: Device ID of the ESPEasy plugin, defaults to 10,87,81. The plugin acts as a remote and can have a unique ID. Can be changed from default if you have interference with neighbours. After a change you should again join the ESP with the fan. +* **Enable Orcon support**: Enables the extra protocol for the Orcon brand of ventilation units, that use the same protocol, but with a different command set. To avoid possible interference, it is disabled by default. Data Acquisition ^^^^^^^^^^^^^^^^ @@ -142,6 +143,9 @@ Change log .. versionchanged:: 2.0 . + |added| + 2022-08-18: Add support for Orcon devices. + |added| 2022-08-10: Made multi-instance compatible, CS pin configurable and interrupt pin usage optional. diff --git a/docs/source/Plugin/P118_DeviceConfiguration.png b/docs/source/Plugin/P118_DeviceConfiguration.png index f7d06c54d867f2f152f30659a6a516ef21d67409..9fea9fa538b3138e89d58beeee1a0f78317580a3 100644 GIT binary patch literal 55853 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%0DG=*;d#0Ln-pliqna!U>NHiMqo75F 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 GFaHl1*&Hwc literal 49289 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 diff --git a/docs/source/Plugin/P118_commands.repl b/docs/source/Plugin/P118_commands.repl index 799426dbf6..19a6277603 100644 --- a/docs/source/Plugin/P118_commands.repl +++ b/docs/source/Plugin/P118_commands.repl @@ -35,5 +35,17 @@ Fan states "33", "Timer3", "Itho to high speed with hardware timer (30 min)" "1111", "", "Join with an ITHO ventilation unit that is waiting for join requests (first minutes after power-on)" "9999", "", "Leave connection with an ITHO ventilation unit (maybe not supported on all models). Needs a new Join (1111) to be able to control a unit." + "100", "Orcon standBy", "Orcon ventilation unit on standby" + "101", "Orcon low speed", "Orcon ventilation to lowest speed" + "102", "Orcon medium speed", "Orcon ventilation to medium speed" + "103", "Orcon high speed", "Itho ventilation to high speed" + "104", "Orcon auto speed", "Itho ventilation to auto speed (based on Humidity and/or CO2)" + "110", "Orcon Timer 0", "Orcon ventilation unit on standby for 12*60 minutes" + "111", "Orcon Timer 1", "Orcon ventilation to lowest speed for 60 minutes" + "112", "Orcon Timer 2", "Orcon ventilation unit to medium speed for 13*60 minutes" + "113", "Orcon Timer 3", "Orcon ventilation unit to high speed for 60 minutes" + "114", "Orcon Auto CO2", "Itho ventilation to auto speed for 60 minutes (based on Humidity and/or CO2)" -Two special states exist: 1111 for a join command and 9999 for a leave command, check the Itho manual on how to put your fain into pairing mode. \ No newline at end of file +Two special states exist: 1111 for a join command and 9999 for a leave command, check the Itho manual on how to put your fain into pairing mode. + +For Orcon the Unit ID remote 1 will be used as the spoofed sender ID From 432c9eb0c981c396fb5ae221dcffa525e2d1ad5f Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 18 Aug 2022 21:01:43 +0200 Subject: [PATCH 27/33] [P118] Move plugin to `Collection E` to make the build fit in again --- docs/source/Plugin/_plugin_substitutions_p11x.repl | 4 ++-- src/src/CustomBuild/define_plugin_sets.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/Plugin/_plugin_substitutions_p11x.repl b/docs/source/Plugin/_plugin_substitutions_p11x.repl index 72e079fd1a..5651047f3d 100644 --- a/docs/source/Plugin/_plugin_substitutions_p11x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p11x.repl @@ -93,14 +93,14 @@ .. |P118_type| replace:: :cyan:`Communication` .. |P118_typename| replace:: :cyan:`Communication - Itho` .. |P118_porttype| replace:: `.` -.. |P118_status| replace:: :yellow:`COLLECTION D` +.. |P118_status| replace:: :yellow:`COLLECTION E` .. |P118_github| replace:: _P118_Itho.ino .. _P118_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P118_Itho.ino .. |P118_usedby| replace:: `.` .. |P118_shortinfo| replace:: `Itho RFT fan control using a CC1101 transceiver` .. |P118_maintainer| replace:: `svollebregt` .. |P118_compileinfo| replace:: `.` -.. |P118_usedlibraries| replace:: `https://github.com/arjenhiemstra/IthoEcoFanRFT/tree/master/Master (currently using v2.0.0 due to v2.1.0 recieve issues i.c.w. plugin)` +.. |P118_usedlibraries| replace:: `https://github.com/arjenhiemstra/IthoEcoFanRFT/tree/master/Master (currently using a local adapted version of v2.0.0, due to v2.1.0 recieve issues i.c.w. plugin)` .. |P119_name| replace:: :cyan:`ITG3205` .. |P119_type| replace:: :cyan:`Gyro` diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 5a708393be..042b12a58f 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1276,15 +1276,15 @@ To create/register a plugin, you have to : #define USES_P114 // VEML6075 UVA/UVB sensor #define USES_P115 // Fuel Gauge MAX1704x #define USES_P117 // SCD30 - // Disable Itho when using second heap as it no longer fits. - #if !defined(USE_SECOND_HEAP) - #define USES_P118 // Itho ventilation control - #endif #define USES_P124 // I2C MultiRelay #define USES_P127 // CDM7160 #endif #ifdef PLUGIN_SET_COLLECTION_E + // Disable Itho when using second heap as it no longer fits. + #if !defined(USE_SECOND_HEAP) + #define USES_P118 // Itho ventilation control + #endif #define USES_P119 // ITG3205 Gyro #define USES_P120 // ADXL345 I2C #define USES_P121 // HMC5883L From 71fd31cd1ed8524e35fbc756ff75332d79d00c89 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 19 Aug 2022 13:28:25 +0200 Subject: [PATCH 28/33] [Itho Lib] Code improvements as suggested (mostly) --- lib/Itho/CC1101.h | 2 +- lib/Itho/IthoCC1101.cpp | 354 +++++++++++++++++++--------------------- lib/Itho/IthoCC1101.h | 97 +++++------ 3 files changed, 217 insertions(+), 236 deletions(-) diff --git a/lib/Itho/CC1101.h b/lib/Itho/CC1101.h index 44ce434e63..3fa7afe604 100644 --- a/lib/Itho/CC1101.h +++ b/lib/Itho/CC1101.h @@ -17,7 +17,7 @@ #endif // ifndef PIN_SPI_SS #ifndef ITHO_MAX_WAIT -# define ITHO_MAX_WAIT 1000 // Wait no longer than this nr of milliseconds +# define ITHO_MAX_WAIT 1000 // Wait no longer than this nr of milliseconds #endif // ifndef ITHO_MAX_WAIT /* Type of transfers */ diff --git a/lib/Itho/IthoCC1101.cpp b/lib/Itho/IthoCC1101.cpp index e2461e18bb..c2baba48c4 100644 --- a/lib/Itho/IthoCC1101.cpp +++ b/lib/Itho/IthoCC1101.cpp @@ -311,6 +311,14 @@ bool IthoCC1101::checkForNewPacket() { } bool IthoCC1101::parseMessageCommand() { + // TODO nl0pvm: make this orcon proof? + #if defined(CRC_FILTER) + uint8_t mLen = 0; + # define SET_MLEN(n) mLen = n; + #else // if defined(CRC_FILTER) + # define SET_MLEN(n) + #endif // if defined(CRC_FILTER) + messageDecode(&inMessage, &inIthoPacket); // deviceType of message type? @@ -324,108 +332,78 @@ bool IthoCC1101::parseMessageCommand() { // counter1 inIthoPacket.counter = inIthoPacket.dataDecoded[4]; - const bool isHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageHighCommandBytes); - const bool isRVHighCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVHighCommandBytes); - const bool isMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageMediumCommandBytes); - const bool isRVMediumCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVMediumCommandBytes); - const bool isLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageLowCommandBytes); - const bool isRVLowCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVLowCommandBytes); - const bool isRVAutoCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVAutoCommandBytes); - const bool isStandByCommand = checkIthoCommand(&inIthoPacket, ithoMessageStandByCommandBytes); - const bool isTimer1Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer1CommandBytes); - const bool isTimer2Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer2CommandBytes); - const bool isTimer3Command = checkIthoCommand(&inIthoPacket, ithoMessageTimer3CommandBytes); - const bool isJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageJoinCommandBytes); - const bool isJoin2Command = checkIthoCommand(&inIthoPacket, ithoMessageJoin2CommandBytes); - const bool isRVJoinCommand = checkIthoCommand(&inIthoPacket, ithoMessageRVJoinCommandBytes); - const bool isLeaveCommand = checkIthoCommand(&inIthoPacket, ithoMessageLeaveCommandBytes); - - const bool isOrconStandByCommand = checkIthoCommand(&inIthoPacket, orconMessageStandByCommandBytes); - const bool isOrconLowCommand = checkIthoCommand(&inIthoPacket, orconMessageLowCommandBytes); - const bool isOrconMediumCommand = checkIthoCommand(&inIthoPacket, orconMessageMediumCommandBytes); - const bool isOrconFullCommand = checkIthoCommand(&inIthoPacket, orconMessageFullCommandBytes); - const bool isOrconAutoCommand = checkIthoCommand(&inIthoPacket, orconMessageAutoCommandBytes); - const bool isOrconTimer0Command = checkIthoCommand(&inIthoPacket, orconMessageTimer0CommandBytes); - const bool isOrconTimer1Command = checkIthoCommand(&inIthoPacket, orconMessageTimer1CommandBytes); - const bool isOrconTimer2Command = checkIthoCommand(&inIthoPacket, orconMessageTimer2CommandBytes); - const bool isOrconTimer3Command = checkIthoCommand(&inIthoPacket, orconMessageTimer3CommandBytes); - const bool isOrconAutoCO2Command = checkIthoCommand(&inIthoPacket, orconMessageAutoCO2CommandBytes); - // 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 (_enableOrcon) { - if (isOrconStandByCommand) { inIthoPacket.command = OrconStandBy; } - - if (isOrconLowCommand) { inIthoPacket.command = OrconLow; } - - if (isOrconMediumCommand) { inIthoPacket.command = OrconMedium; } - - if (isOrconFullCommand) { inIthoPacket.command = OrconHigh; } - - if (isOrconAutoCommand) { inIthoPacket.command = OrconAuto; } - - if (isOrconTimer0Command) { inIthoPacket.command = OrconTimer0; } - - if (isOrconTimer1Command) { inIthoPacket.command = OrconTimer1; } - - if (isOrconTimer2Command) { inIthoPacket.command = OrconTimer2; } - - if (isOrconTimer3Command) { inIthoPacket.command = OrconTimer3; } - - if (isOrconAutoCO2Command) { inIthoPacket.command = OrconAutoCO2; } + // TODO: When enabling CRC_FILTER, most likely commands without SET_MLEN() need that check too + if (checkIthoCommand(&inIthoPacket, ithoMessageHighCommandBytes)) { + inIthoPacket.command = IthoHigh; + SET_MLEN(11) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageRVHighCommandBytes)) { + inIthoPacket.command = IthoHigh; + } else if (checkIthoCommand(&inIthoPacket, ithoMessageMediumCommandBytes)) { + inIthoPacket.command = IthoMedium; + SET_MLEN(11) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageRVMediumCommandBytes)) { + inIthoPacket.command = IthoMedium; + } else if (checkIthoCommand(&inIthoPacket, ithoMessageLowCommandBytes)) { + inIthoPacket.command = IthoLow; + SET_MLEN(11) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageRVLowCommandBytes)) { + inIthoPacket.command = IthoLow; + } else if (checkIthoCommand(&inIthoPacket, ithoMessageRVAutoCommandBytes)) { + inIthoPacket.command = IthoStandby; + } else if (checkIthoCommand(&inIthoPacket, ithoMessageStandByCommandBytes)) { + inIthoPacket.command = IthoStandby; + SET_MLEN(11) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageTimer1CommandBytes)) { + inIthoPacket.command = IthoTimer1; + SET_MLEN(11) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageTimer2CommandBytes)) { + inIthoPacket.command = IthoTimer2; + SET_MLEN(11) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageTimer3CommandBytes)) { + inIthoPacket.command = IthoTimer3; + SET_MLEN(11) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageJoinCommandBytes)) { + inIthoPacket.command = IthoJoin; + SET_MLEN(20) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageJoin2CommandBytes)) { + inIthoPacket.command = IthoJoin; + SET_MLEN(20) + } else if (checkIthoCommand(&inIthoPacket, ithoMessageRVJoinCommandBytes)) { + inIthoPacket.command = IthoJoin; + } else if (checkIthoCommand(&inIthoPacket, ithoMessageLeaveCommandBytes)) { + inIthoPacket.command = IthoLeave; + SET_MLEN(14) + } else if (_enableOrcon) { + if (checkIthoCommand(&inIthoPacket, orconMessageStandByCommandBytes)) { + inIthoPacket.command = OrconStandBy; + } else if (checkIthoCommand(&inIthoPacket, orconMessageLowCommandBytes)) { + inIthoPacket.command = OrconLow; + } else if (checkIthoCommand(&inIthoPacket, orconMessageMediumCommandBytes)) { + inIthoPacket.command = OrconMedium; + } else if (checkIthoCommand(&inIthoPacket, orconMessageFullCommandBytes)) { + inIthoPacket.command = OrconHigh; + } else if (checkIthoCommand(&inIthoPacket, orconMessageAutoCommandBytes)) { + inIthoPacket.command = OrconAuto; + } else if (checkIthoCommand(&inIthoPacket, orconMessageTimer0CommandBytes)) { + inIthoPacket.command = OrconTimer0; + } else if (checkIthoCommand(&inIthoPacket, orconMessageTimer1CommandBytes)) { + inIthoPacket.command = OrconTimer1; + } else if (checkIthoCommand(&inIthoPacket, orconMessageTimer2CommandBytes)) { + inIthoPacket.command = OrconTimer2; + } else if (checkIthoCommand(&inIthoPacket, orconMessageTimer3CommandBytes)) { + inIthoPacket.command = OrconTimer3; + } else if (checkIthoCommand(&inIthoPacket, orconMessageAutoCO2CommandBytes)) { + inIthoPacket.command = OrconAutoCO2; + } } + #undef SET_MLEN #if defined(CRC_FILTER) - // TODO nl0pvm: make this orcon proof - 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]) { + if ((mLen != 0) && (getCounter2(&inIthoPacket, mLen) != inIthoPacket.dataDecoded[mLen])) { inIthoPacket.command = IthoUnknown; return false; } @@ -448,20 +426,21 @@ bool IthoCC1101::checkIthoCommand(IthoPacket *itho, const uint8_t commandBytes[] // AA - Present DeviceID fields // PP - Present Params - if ((itho->deviceType == 28) || (itho->deviceType == 24)) { offset = 2; } + if ((itho->deviceType == 28) || (itho->deviceType == 24)) { + offset = 2; + } // for (int i = 4; i < 6; i++) // for Orcon: the code above makes that only 3 bytes (byte 4, 5 and 6) are checked. That gives false positves - for (int i = 0; i < 6; i++) - - { + for (int i = 0; i < 6; i++) { // this is required for differentiating between Orcon and Itho commands. However I don't know what the reason was to comment this out. // thus this needs to be verified by Itho users - 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 ((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])) { + if ((itho->dataDecoded[i + 5 + offset] != pgm_read_byte(&(commandBytes[i]))) && + (itho->dataDecodedChk[i + 5 + offset] != pgm_read_byte(&(commandBytes[i])))) { return false; } } @@ -570,13 +549,14 @@ void IthoCC1101::createOrconMessageCommand(IthoPacket *itho, CC1101Packet *packe itho->dataDecoded[5] = destId[1]; itho->dataDecoded[6] = destId[2]; - const uint8_t *commandBytes = getMessageCommandBytes(itho->command); + const uint8_t *commandBytes = getMessageCommandBytes(itho->command); + const uint8_t commandLength = getMessageCommandLength(itho->command); - for (uint8_t i = 0; i < getMessageCommandLength(itho->command); i++) { - itho->dataDecoded[i + 7] = commandBytes[i]; + for (uint8_t i = 0; i < commandLength; i++) { + itho->dataDecoded[i + 7] = pgm_read_byte(&(commandBytes[i])); } - itho->length = 7 + 1 + getMessageCommandLength(itho->command); + itho->length = 7 + 1 + commandLength; itho->dataDecoded[itho->length - 1] = getCRC(itho, itho->length - 1); itho->length += 1; @@ -625,7 +605,7 @@ void IthoCC1101::createMessageCommand(IthoPacket *itho, CC1101Packet *packet) const uint8_t *commandBytes = getMessageCommandBytes(itho->command); for (uint8_t i = 0; i < 6; i++) { - itho->dataDecoded[i + 5] = commandBytes[i]; + itho->dataDecoded[i + 5] = pgm_read_byte(&(commandBytes[i])); } // set counter2 @@ -667,7 +647,7 @@ void IthoCC1101::createMessageJoin(IthoPacket *itho, CC1101Packet *packet) const uint8_t *commandBytes = getMessageCommandBytes(itho->command); for (uint8_t i = 0; i < 6; i++) { - itho->dataDecoded[i + 5] = commandBytes[i]; + itho->dataDecoded[i + 5] = pgm_read_byte(&(commandBytes[i])); } // set deviceID @@ -723,7 +703,7 @@ void IthoCC1101::createMessageLeave(IthoPacket *itho, CC1101Packet *packet) const uint8_t *commandBytes = getMessageCommandBytes(itho->command); for (uint8_t i = 0; i < 6; i++) { - itho->dataDecoded[i + 5] = commandBytes[i]; + itho->dataDecoded[i + 5] = pgm_read_byte(&(commandBytes[i])); } // set deviceID @@ -1024,36 +1004,36 @@ void IthoCC1101::messageDecode(CC1101Packet *packet, IthoPacket *itho) { } } -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; -} +// 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) const +// { +// 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; @@ -1067,51 +1047,51 @@ String IthoCC1101::getLastIDstr(bool ashex) { 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 = F("Length="); - - str += inMessage.length; - str += '.'; - - 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 += F("Device type?: "); - str += String(inIthoPacket.deviceType); - str += F(" - 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; -} +// 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 = F("Length="); + +// str += inMessage.length; +// str += '.'; + +// 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() const { +// String str; + +// if (inIthoPacket.length > 11) { +// str += F("Device type?: "); +// str += String(inIthoPacket.deviceType); +// str += F(" - 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 index 0cd9f111e4..28f8c04c47 100644 --- a/lib/Itho/IthoCC1101.h +++ b/lib/Itho/IthoCC1101.h @@ -2,6 +2,7 @@ * Author: Klusjesman, supersjimmie, modified and reworked by arjenhiemstra * 2022-08-10 tonhuisman: Fixed perpetual blocking while loops by limiting these to 3000 msec. * 2022-08-11 tonhuisman: Change 3000 to #define ITHO_MAX_WAIT for easy adjustment + * 2022-08-19 tonhuisman: Fix excessive memory use, comment unused methods to reduce bin size */ #ifndef __ITHOCC1101_H__ @@ -13,39 +14,39 @@ // 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 }; +static const uint8_t ithoPaTableSend[8] PROGMEM = { 0x6F, 0x26, 0x2E, 0x8C, 0x87, 0xCD, 0xC7, 0xC0 }; +static const uint8_t ithoPaTableReceive[8] PROGMEM = { 0x6F, 0x26, 0x2E, 0x7F, 0x8A, 0x84, 0xCA, 0xC4 }; // message command bytes -const uint8_t orconMessageStandByCommandBytes[] = { 34, 241, 3, 0, 0, 4 }; -const uint8_t orconMessageLowCommandBytes[] = { 34, 241, 3, 0, 1, 4 }; -const uint8_t orconMessageMediumCommandBytes[] = { 34, 241, 3, 0, 2, 4 }; -const uint8_t orconMessageFullCommandBytes[] = { 34, 241, 3, 0, 3, 4 }; -const uint8_t orconMessageAutoCommandBytes[] = { 34, 241, 3, 0, 4, 4 }; - -const uint8_t orconMessageTimer0CommandBytes[] = { 34, 243, 7, 0, 82, 12, 0, 4, 4, 4 }; // Timer 12*60 minuten @ speed 0 -const uint8_t orconMessageTimer1CommandBytes[] = { 34, 243, 7, 0, 18, 60, 1, 4, 4, 4 }; // Timer 60 minuten @ speed 1 -const uint8_t orconMessageTimer2CommandBytes[] = { 34, 243, 7, 0, 82, 13, 2, 4, 4, 4 }; // Timer 13*60 minuten @ speed 2 -const uint8_t orconMessageTimer3CommandBytes[] = { 34, 243, 7, 0, 18, 60, 3, 4, 4, 4 }; // Timer 60 minuten @ speed 3 -const uint8_t orconMessageAutoCO2CommandBytes[] = { 34, 243, 7, 0, 18, 60, 4, 4, 4, 4 }; // Timer 60 minuten @ speed auto - -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 }; +static const uint8_t orconMessageStandByCommandBytes[] PROGMEM = { 34, 241, 3, 0, 0, 4 }; +static const uint8_t orconMessageLowCommandBytes[] PROGMEM = { 34, 241, 3, 0, 1, 4 }; +static const uint8_t orconMessageMediumCommandBytes[] PROGMEM = { 34, 241, 3, 0, 2, 4 }; +static const uint8_t orconMessageFullCommandBytes[] PROGMEM = { 34, 241, 3, 0, 3, 4 }; +static const uint8_t orconMessageAutoCommandBytes[] PROGMEM = { 34, 241, 3, 0, 4, 4 }; + +static const uint8_t orconMessageTimer0CommandBytes[] PROGMEM = { 34, 243, 7, 0, 82, 12, 0, 4, 4, 4 }; // Timer 12*60 minutes @ speed 0 +static const uint8_t orconMessageTimer1CommandBytes[] PROGMEM = { 34, 243, 7, 0, 18, 60, 1, 4, 4, 4 }; // Timer 60 minutes @ speed 1 +static const uint8_t orconMessageTimer2CommandBytes[] PROGMEM = { 34, 243, 7, 0, 82, 13, 2, 4, 4, 4 }; // Timer 13*60 minutes @ speed 2 +static const uint8_t orconMessageTimer3CommandBytes[] PROGMEM = { 34, 243, 7, 0, 18, 60, 3, 4, 4, 4 }; // Timer 60 minutes @ speed 3 +static const uint8_t orconMessageAutoCO2CommandBytes[] PROGMEM = { 34, 243, 7, 0, 18, 60, 4, 4, 4, 4 }; // Timer 60 minutes @ speed auto + +static const uint8_t ithoMessageRVHighCommandBytes[] PROGMEM = { 49, 224, 4, 0, 0, 200 }; +static const uint8_t ithoMessageHighCommandBytes[] PROGMEM = { 34, 241, 3, 0, 4, 4 }; +static const uint8_t ithoMessageFullCommandBytes[] PROGMEM = { 34, 241, 3, 0, 4, 4 }; +static const uint8_t ithoMessageMediumCommandBytes[] PROGMEM = { 34, 241, 3, 0, 3, 4 }; +static const uint8_t ithoMessageRVMediumCommandBytes[] PROGMEM = { 34, 241, 3, 0, 3, 7 }; +static const uint8_t ithoMessageLowCommandBytes[] PROGMEM = { 34, 241, 3, 0, 2, 4 }; +static const uint8_t ithoMessageRVLowCommandBytes[] PROGMEM = { 49, 224, 4, 0, 0, 1 }; +static const uint8_t ithoMessageRVAutoCommandBytes[] PROGMEM = { 34, 241, 3, 0, 5, 7 }; +static const uint8_t ithoMessageStandByCommandBytes[] PROGMEM = { 0, 0, 0, 0, 0, 0 }; // unkown, tbd +static const uint8_t ithoMessageTimer1CommandBytes[] PROGMEM = { 34, 243, 3, 0, 0, 10 }; // 10 minutes full speed +static const uint8_t ithoMessageTimer2CommandBytes[] PROGMEM = { 34, 243, 3, 0, 0, 20 }; // 20 minutes full speed +static const uint8_t ithoMessageTimer3CommandBytes[] PROGMEM = { 34, 243, 3, 0, 0, 30 }; // 30 minutes full speed +static const uint8_t ithoMessageJoinCommandBytes[] PROGMEM = { 31, 201, 12, 0, 34, 241 }; +static const uint8_t ithoMessageJoin2CommandBytes[] PROGMEM = { 31, 201, 12, 99, 34, 248 }; // join command of RFT AUTO Co2 remote +static const uint8_t ithoMessageRVJoinCommandBytes[] PROGMEM = { 31, 201, 24, 0, 49, 224 }; // join command of RFT-RV +static const uint8_t ithoMessageLeaveCommandBytes[] PROGMEM = { 31, 201, 6, 0, 31, 201 }; // itho rft-rv // unknown, high @@ -90,13 +91,13 @@ class IthoCC1101 : protected CC1101 { } // init,reset CC1101 void initReceive(); - uint8_t getLastCounter() { - return outIthoPacket.counter; - } // counter is increased before sending a command + // uint8_t getLastCounter() const { + // return outIthoPacket.counter; + // } // counter is increased before sending a command - void setSendTries(uint8_t sendTries) { - this->sendTries = sendTries; - } + // 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; @@ -104,24 +105,24 @@ class IthoCC1101 : protected CC1101 { // receive bool checkForNewPacket(); // check RX fifo for new data - IthoPacket getLastPacket() { - return inIthoPacket; - } // retrieve last received/parsed packet from remote + // IthoPacket getLastPacket() const { + // return inIthoPacket; + // } // retrieve last received/parsed packet from remote - IthoCommand getLastCommand() { + IthoCommand getLastCommand() const { 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 getLastInCounter() const { + // return inIthoPacket.counter; + // } // retrieve last received/parsed command from remote - uint8_t ReadRSSI(); - bool checkID(const uint8_t *id); - int* getLastID(); + // uint8_t ReadRSSI(); + // bool checkID(const uint8_t *id) const; + // int* getLastID(); String getLastIDstr(bool ashex = true); - String getLastMessagestr(bool ashex = true); - String LastMessageDecoded(); + // String getLastMessagestr(bool ashex = true); + // String LastMessageDecoded() const; // send void sendCommand(IthoCommand command, From 5dfdb2fcaf4980bd0054d9f4c5cf3e27ddcd08d6 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 14 Sep 2022 21:47:28 +0200 Subject: [PATCH 29/33] [P118] Fix LOG_LEVEL_DEBUG compilation --- src/src/PluginStructs/P118_data_struct.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index 75e2d9157e..ab323ac4e9 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -399,11 +399,18 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str } void P118_data_struct::ITHOcheck() { - bool _dbgLog = _log && loglevelActiveFor(LOG_LEVEL_DEBUG); + bool _dbgLog = _log + # ifndef BUILD_NO_DEBUG + && loglevelActiveFor(LOG_LEVEL_DEBUG) + # endif // ifndef BUILD_NO_DEBUG + ; + + # ifndef BUILD_NO_DEBUG if (_dbgLog) { addLog(LOG_LEVEL_DEBUG, "ITHO: RF signal received"); // All logs statements contain if-statement to disable logging to } // reduce log clutter when many RF sources are present + # endif // ifndef BUILD_NO_DEBUG if (_rf->checkForNewPacket()) { IthoCommand cmd = _rf->getLastCommand(); @@ -605,9 +612,12 @@ void P118_data_struct::ITHOcheck() { } } + # ifndef BUILD_NO_DEBUG + if (_dbgLog) { addLogMove(LOG_LEVEL_DEBUG, log); } + # endif // ifndef BUILD_NO_DEBUG } } From 1929bf977a10dc8eecbe876c09edf60fbc3c301b Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 18 Sep 2022 22:38:20 +0200 Subject: [PATCH 30/33] [P118] Hide Debug log option from UI when not available, small improvements --- docs/source/Plugin/P118.rst | 2 +- src/_P118_Itho.ino | 7 ++++++- src/src/PluginStructs/P118_data_struct.cpp | 19 +++++++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/source/Plugin/P118.rst b/docs/source/Plugin/P118.rst index ae143a4319..6e80b4d546 100644 --- a/docs/source/Plugin/P118.rst +++ b/docs/source/Plugin/P118.rst @@ -72,7 +72,7 @@ Remote RF Controls * **Unit ID remote 1..3**: ID of another remote that controls your Itho fan. When set it allows the plugin to monitor for commands and act on them. The ID consist out of 3 numbers separated by ','. Up to 3 remotes are supported. After the controller is joined with the ventilation unit, and enabling the **Enable minimal RF INFO log** option below, these ID's can be extracted from the logs at the INFO level by pressing a button on the remote. Most remotes will repeat a command ca. 3 times, in case there is disturbance in communication, so that should be easily recognizable. -* **Enable RF DEBUG log**: When enabled all recieved RF packages will be shown in the ESPEasy log at DEBUG level. Useful to determine the ID of other remotes and the messages they send. Disable when not necessary to reduce writing to the log. +* **Enable RF DEBUG log**: When enabled all recieved RF packages will be shown in the ESPEasy log at DEBUG level. Useful to determine the ID of other remotes and the messages they send. Disable when not necessary to reduce writing to the log. (Only available when Debug logging is available in the build.) * **Enable minimal RF INFO lgo**: When enabled, this will write the ID and raw command sent from any device using the same frequency and protocol. Can be used to determine entries for **Unit ID remote 1..3**. Disable when not necessary to reduce writing to the log. diff --git a/src/_P118_Itho.ino b/src/_P118_Itho.ino index e99e51daaf..1f32e352a9 100644 --- a/src/_P118_Itho.ino +++ b/src/_P118_Itho.ino @@ -35,6 +35,7 @@ // Orcon code can be partially disabled by setting P118_FEATURE_ORCON 0 in P118_data_struc.h // Support for orcon must be enabled in settings, to avoid possible interference with Itho. // Re-enabled timer support for Orcon, as it is only a status update, NOT a ventilator update +// tonhuisman, 18-09-2022 - Hide Debug log option in device configuration when Debug log is not available. // Recommended to disable RF receive logging to minimize code execution within interrupts @@ -274,8 +275,10 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) addFormTextBox(F("Unit ID remote 2"), F("pID2"), PLUGIN_118_ExtraSettings.ID2, 8); addFormTextBox(F("Unit ID remote 3"), F("pID3"), PLUGIN_118_ExtraSettings.ID3, 8); + # ifndef BUILD_NO_DEBUG addFormCheckBox(F("Enable RF DEBUG log"), F("plog"), P118_CONFIG_LOG); // Makes RF logging optional to reduce clutter in // the log + # endif // ifndef BUILD_NO_DEBUG addFormCheckBox(F("Enable minimal RF INFO log"), F("prflog"), P118_CONFIG_RF_LOG); // Log only the received Device ID's at INFO level addFormNumericBox(F("Device ID byte 1"), F("pdevid1"), P118_CONFIG_DEVID1, 0, 255); @@ -300,7 +303,9 @@ boolean Plugin_118(uint8_t function, struct EventStruct *event, String& string) strcpy(PLUGIN_118_ExtraSettings.ID3, web_server.arg(F("pID3")).c_str()); SaveCustomTaskSettings(event->TaskIndex, reinterpret_cast(&PLUGIN_118_ExtraSettings), sizeof(PLUGIN_118_ExtraSettings)); - P118_CONFIG_LOG = isFormItemChecked(F("plog")); + # ifndef BUILD_NO_DEBUG + P118_CONFIG_LOG = isFormItemChecked(F("plog")); + # endif // ifndef BUILD_NO_DEBUG P118_CONFIG_RF_LOG = isFormItemChecked(F("prflog")); P118_CONFIG_DEVID1 = getFormItemInt(F("pdevid1"), 10); diff --git a/src/src/PluginStructs/P118_data_struct.cpp b/src/src/PluginStructs/P118_data_struct.cpp index ab323ac4e9..1e86331af1 100644 --- a/src/src/PluginStructs/P118_data_struct.cpp +++ b/src/src/PluginStructs/P118_data_struct.cpp @@ -253,7 +253,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon standBy")); - success = true; break; } case 101: // Fan low @@ -266,7 +265,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon low speed")); - success = true; break; } case 102: // Fan medium @@ -279,7 +277,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon medium speed")); - success = true; break; } case 103: // Fan high @@ -292,7 +289,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon high speed")); - success = true; break; } case 104: // Fan auto @@ -305,7 +301,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon auto speed")); - success = true; break; } case 110: // Timer 12*60 minutes @ speed 0 @@ -318,7 +313,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon Timer 0")); - success = true; break; } case 111: // Timer 60 minutes @ speed 1 @@ -331,7 +325,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon Timer 1")); - success = true; break; } case 112: // Timer 13*60 minutes @ speed 2 @@ -344,7 +337,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon Timer 2")); - success = true; break; } case 113: // Timer 60 minutes @ speed 3 @@ -357,7 +349,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon Timer 3")); - success = true; break; } case 114: @@ -370,7 +361,6 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str _LastIDindex = 0; _rf->initReceive(); PluginWriteLog(F("Orcon Auto CO2")); - success = true; break; } # else // if P118_FEATURE_ORCON @@ -385,12 +375,13 @@ bool P118_data_struct::plugin_write(struct EventStruct *event, const String& str case 113: case 114: PluginWriteLog(F("Orcon support not included!")); + success = false; break; # endif // if P118_FEATURE_ORCON default: { PluginWriteLog(F("INVALID")); - success = true; + success = false; break; } } @@ -646,7 +637,11 @@ void P118_data_struct::PublishData(struct EventStruct *event) { } void P118_data_struct::PluginWriteLog(const String& command) { - String log = F("Send Itho command for: "); + String log = F("Send Itho" + # if P118_FEATURE_ORCON + "/Orcon" + # endif // if P118_FEATURE_ORCON + " command for: "); log += command; From d29d6aef1ca9c39705049a4d357d6285fa9a683d Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 23 Sep 2022 20:43:00 +0200 Subject: [PATCH 31/33] [Build] Add CLIMATE configuration and move P118 there with all climate sensors --- platformio_esp32_envs.ini | 9 ++ platformio_esp32s2_envs.ini | 8 ++ platformio_esp82xx_envs.ini | 10 ++ src/src/CustomBuild/define_plugin_sets.h | 139 ++++++++++++++++++++++- 4 files changed, 162 insertions(+), 4 deletions(-) diff --git a/platformio_esp32_envs.ini b/platformio_esp32_envs.ini index 25b71c9fd5..085a21b8c8 100644 --- a/platformio_esp32_envs.ini +++ b/platformio_esp32_envs.ini @@ -261,6 +261,15 @@ build_flags = ${esp32_common.build_flags} -DFEATURE_ARDUINO_OTA=1 -DPLUGIN_DISPLAY_COLLECTION +[env:climate_ESP32_4M316k] +extends = esp32_common +board = esp32_4M +lib_deps = ${esp32_common.lib_deps} + ServoESP32 +build_flags = ${esp32_common.build_flags} + -DFEATURE_ARDUINO_OTA=1 + -DPLUGIN_CLIMATE_COLLECTION + [env:neopixel_ESP32_4M316k] extends = esp32_common board = esp32_4M diff --git a/platformio_esp32s2_envs.ini b/platformio_esp32s2_envs.ini index 9c90e9cd82..1f84dbe1b7 100644 --- a/platformio_esp32s2_envs.ini +++ b/platformio_esp32s2_envs.ini @@ -115,3 +115,11 @@ lib_deps = ${esp32s2_common.lib_deps} ServoESP32 build_flags = ${esp32s2_common.build_flags} -D PLUGIN_DISPLAY_COLLECTION + +[env:climate_ESP32s2_4M316k] +extends = esp32s2_common +board = esp32s2 +lib_deps = ${esp32s2_common.lib_deps} + ServoESP32 +build_flags = ${esp32s2_common.build_flags} + -D PLUGIN_CLIMATE_COLLECTION diff --git a/platformio_esp82xx_envs.ini b/platformio_esp82xx_envs.ini index 68698bef7e..9fd633ecfb 100644 --- a/platformio_esp82xx_envs.ini +++ b/platformio_esp82xx_envs.ini @@ -745,6 +745,16 @@ build_flags = ${regular_platform.build_flags} -D PLUGIN_DISPLAY_COLLECTION -D WEBSERVER_USE_CDN_JS_CSS +; climate : 4096k version ---------------------------- +[env:climate_ESP8266_4M1M] +extends = esp8266_4M1M +platform = ${regular_platform.platform} +platform_packages = ${regular_platform.platform_packages} +build_flags = ${regular_platform.build_flags} + ${esp8266_4M1M.build_flags} + -D PLUGIN_CLIMATE_COLLECTION + -D WEBSERVER_USE_CDN_JS_CSS + ; neopixel : 4096k version ---------------------------- [env:neopixel_ESP8266_4M1M] extends = esp8266_4M1M diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 49738c86c3..bf378d98f1 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -310,6 +310,35 @@ To create/register a plugin, you have to : #endif #endif +#ifdef PLUGIN_CLIMATE_COLLECTION + #ifdef PLUGIN_BUILD_NORMAL + #undef PLUGIN_BUILD_NORMAL + #endif + #define PLUGIN_SET_NONE // Specifically configured below + #define CONTROLLER_SET_STABLE + #define NOTIFIER_SET_STABLE + #ifndef FEATURE_ESPEASY_P2P + #define FEATURE_ESPEASY_P2P 1 + #endif + + #ifndef FEATURE_I2CMULTIPLEXER + #define FEATURE_I2CMULTIPLEXER 1 + #endif + #ifndef FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES + #define FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES 1 + #endif + #define KEEP_TRIGONOMETRIC_FUNCTIONS_RULES + #ifndef FEATURE_PLUGIN_STATS + #define FEATURE_PLUGIN_STATS 1 + #endif + #ifndef FEATURE_CHART_JS + #define FEATURE_CHART_JS 1 + #endif + #ifndef FEATURE_RULES_EASY_COLOR_CODE + #define FEATURE_RULES_EASY_COLOR_CODE 1 + #endif +#endif + #ifdef PLUGIN_BUILD_NORMAL #define PLUGIN_SET_STABLE #define CONTROLLER_SET_STABLE @@ -896,6 +925,9 @@ To create/register a plugin, you have to : #ifndef PLUGIN_DISPLAY_COLLECTION #define PLUGIN_DISPLAY_COLLECTION #endif + #ifndef PLUGIN_CLIMATE_COLLECTION + #define PLUGIN_CLIMATE_COLLECTION + #endif #ifndef PLUGIN_NEOPIXEL_COLLECTION #define PLUGIN_NEOPIXEL_COLLECTION #endif @@ -1339,10 +1371,6 @@ To create/register a plugin, you have to : #endif #ifdef PLUGIN_SET_COLLECTION_E - // Disable Itho when using second heap as it no longer fits. - #if !defined(USE_SECOND_HEAP) - #define USES_P118 // Itho ventilation control - #endif #define USES_P119 // ITG3205 Gyro #define USES_P120 // ADXL345 I2C #define USES_P121 // HMC5883L @@ -1464,6 +1492,109 @@ To create/register a plugin, you have to : #endif #endif +// Collection of all climate plugins. +#ifdef PLUGIN_CLIMATE_COLLECTION + #ifndef PLUGIN_DESCR + #define PLUGIN_DESCR "Climate" + #endif + + // Features and plugins cherry picked from stable set + #ifndef FEATURE_SERVO + #define FEATURE_SERVO 1 + #endif + #define FEATURE_RTTTL 1 + + #define USES_P001 // Switch + #define USES_P002 // ADC + #define USES_P003 // Pulse + #define USES_P004 // Dallas + #define USES_P005 // DHT + #define USES_P006 // BMP085 + + #define USES_P011 // PME + #define USES_P012 // LCD + #define USES_P014 // SI7021 + #define USES_P018 // Dust + + #define USES_P021 // Level + #define USES_P023 // OLED + #define USES_P024 // MLX90614 + #define USES_P026 // SysInfo + #define USES_P028 // BME280 + #define USES_P029 // Output + + #define USES_P031 // SHT1X + #define USES_P032 // MS5611 + #define USES_P033 // Dummy + #define USES_P034 // DHT12 + #define USES_P036 // FrameOLED + #define USES_P037 // MQTTImport + #define USES_P038 // NeoPixel + #define USES_P039 // Environment - Thermocouple + + #define USES_P043 // ClkOutput + #define USES_P044 // P1WifiGateway + #define USES_P049 // MHZ19 + + #define USES_P052 // SenseAir + #define USES_P053 // PMSx003 + #define USES_P056 // SDS011-Dust + #define USES_P059 // Encoder + + #define USES_P073 // 7DGT + + // Enable extra climate-related plugins (CO2/Temp/Hum) + #ifndef USES_P047 + #define USES_P047 // Soil Moisture + #endif + #ifndef USES_P049 + #define USES_P049 // MH-Z19 + #endif + #ifndef USES_P051 + #define USES_P051 // AM2320 + #endif + #ifndef USES_P068 + #define USES_P068 // SHT3x + #endif + #ifndef USES_P069 + #define USES_P069 // LM75 + #endif + #ifndef USES_P072 + #define USES_P072 // HCD1080 + #endif + #ifndef USES_P081 + #define USES_P081 // Cron + #endif + #ifndef USES_P083 + #define USES_P083 // SGP30 + #endif + #ifndef USES_P090 + #define USES_P090 // CCS811 + #endif + #ifndef USES_P103 + #define USES_P103 // Atlas EZO + #endif + #ifndef USES_P105 + #define USES_P105 // AHT10/20/21 + #endif + #ifndef USES_P106 + #define USES_P106 // BME680 + #endif + #ifndef USES_P117 + #define USES_P117 // SCD30 + #endif + // Disable Itho when using second heap as it no longer fits. + #if !defined(USES_P118) && !defined(USE_SECOND_HEAP) + #define USES_P118 // Itho ventilation control + #endif + #ifndef USES_P127 + #define USES_P127 // CDM7160 + #endif + #ifndef USES_P135 + #define USES_P135 // SCD4x + #endif +#endif + // Collection of all NeoPixel plugins #ifdef PLUGIN_NEOPIXEL_COLLECTION #ifndef PLUGIN_DESCR From f3fcf9258265855b0f356ce8b315de2e059a962b Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 23 Sep 2022 20:44:54 +0200 Subject: [PATCH 32/33] [Docs] Update documentation for CLIMATE builds, fix some errors in documentation --- docs/source/Plugin/P038.rst | 4 ++-- docs/source/Plugin/P128.rst | 6 +++--- docs/source/Plugin/_Plugin.rst | 2 +- docs/source/Plugin/_plugin_categories.repl | 2 +- .../Plugin/_plugin_substitutions_p00x.repl | 14 +++++++------- .../Plugin/_plugin_substitutions_p01x.repl | 8 ++++---- .../Plugin/_plugin_substitutions_p02x.repl | 12 ++++++------ .../Plugin/_plugin_substitutions_p03x.repl | 16 ++++++++-------- .../Plugin/_plugin_substitutions_p04x.repl | 8 ++++---- .../Plugin/_plugin_substitutions_p05x.repl | 10 +++++----- .../Plugin/_plugin_substitutions_p06x.repl | 4 ++-- .../Plugin/_plugin_substitutions_p07x.repl | 4 ++-- .../Plugin/_plugin_substitutions_p08x.repl | 8 ++++---- .../Plugin/_plugin_substitutions_p09x.repl | 2 +- .../Plugin/_plugin_substitutions_p10x.repl | 6 +++--- .../Plugin/_plugin_substitutions_p11x.repl | 4 ++-- .../Plugin/_plugin_substitutions_p12x.repl | 2 +- .../Plugin/_plugin_substitutions_p13x.repl | 2 +- 18 files changed, 57 insertions(+), 57 deletions(-) diff --git a/docs/source/Plugin/P038.rst b/docs/source/Plugin/P038.rst index 93e2e42efc..6f534fd895 100644 --- a/docs/source/Plugin/P038.rst +++ b/docs/source/Plugin/P038.rst @@ -47,7 +47,7 @@ Configuration * **Enabled** The device can be disabled or enabled. When not enabled the device should not use any resources. Actuator -^^^^^^^^ +~~~~~~~~ * **GPIO -> DIN**: Select the GPIO pin that the LED stripe is connected to. @@ -64,7 +64,7 @@ Actuator Commands available -^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~ .. include:: P038_commands.repl diff --git a/docs/source/Plugin/P128.rst b/docs/source/Plugin/P128.rst index 3c564603fc..b887e191e2 100644 --- a/docs/source/Plugin/P128.rst +++ b/docs/source/Plugin/P128.rst @@ -49,7 +49,7 @@ Configuration * **Enabled** The device can be disabled or enabled. When not enabled the device should not use any resources. Actuator -^^^^^^^^ +~~~~~~~~ * **GPIO -> Stripe data**: Select the GPIO pin the stripe is connected to (Only available on ESP32!) @@ -63,14 +63,14 @@ Due to the design of the used library and the methods used, on ESP8266 only GPIO * **Max brightness**: The maximum brightness allowed for the stripe. Range: 1..255. This is also the initial brightness set during initialization. Can *not* be overridden by the ``dim`` subcommand, and also the maximum value for the ``rainbow`` subcommand. Data Aquisition -^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~ 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. **Interval** By default, Interval will be set to 0 sec. This is optional. When set, the **Values** will be sent to any configured controllers, and events generated to be handled in rules. Values -^^^^^^ +~~~~~~ The value names are fixed and not configurable. These names are: diff --git a/docs/source/Plugin/_Plugin.rst b/docs/source/Plugin/_Plugin.rst index 5f09930555..02da1aa4c3 100644 --- a/docs/source/Plugin/_Plugin.rst +++ b/docs/source/Plugin/_Plugin.rst @@ -218,7 +218,7 @@ There are different released versions of ESP Easy: :red:`DEVELOPMENT` is used for plugins that are still being developed and are not considered stable at all. Currently there are no DEVELOPMENT builds available. -:yellow:`ENERGY` :yellow:`DISPLAY` :yellow:`IR` :yellow:`IRext` :yellow:`NEOPIXEL` are specialized builds holding all Energy-, Display-, Infra Red- (extended) and NeoPixel related plugins. +:yellow:`ENERGY` :yellow:`DISPLAY` :yellow:`IR` :yellow:`IRext` :yellow:`NEOPIXEL` :yellow:`CLIMATE` are specialized builds holding all Energy-, Display-, Infra Red- (extended), NeoPixel- and Climate- related plugins. :yellow:`MAX` is the build that has all plugins that are available in the ESPEasy repository. Only available for ESP32 16MB Flash units. diff --git a/docs/source/Plugin/_plugin_categories.repl b/docs/source/Plugin/_plugin_categories.repl index e6c5355ad6..0ffb7696e8 100644 --- a/docs/source/Plugin/_plugin_categories.repl +++ b/docs/source/Plugin/_plugin_categories.repl @@ -7,7 +7,7 @@ .. |Plugin_Energy_AC| replace:: :ref:`P076_page`, :ref:`P077_page`, :ref:`P078_page`, :ref:`P102_page`, :ref:`P108_page` .. |Plugin_Energy_DC| replace:: :ref:`P027_page`, :ref:`P085_page`, :ref:`P115_page`, :ref:`P132_page` .. |Plugin_Energy_Heat| replace:: :ref:`P088_page`, :ref:`P093_page` -.. |Plugin_Environment| replace:: :ref:`P004_page`, :ref:`P005_page`, :ref:`P006_page`, :ref:`P014_page`, :ref:`P024_page`, :ref:`P028_page`, :ref:`P030_page`, :ref:`P031_page`, :ref:`P032_page`, :ref:`P034_page`, :ref:`P039_page`, :ref:`P047_page`, :ref:`P051_page`, :ref:`P068_page`, :ref:`P069_page`, :ref:`P072_page`, :ref:`P103_page`, :ref:`P104_page`, :ref:`P105_page`, :ref:`P106_page` +.. |Plugin_Environment| replace:: :ref:`P004_page`, :ref:`P005_page`, :ref:`P006_page`, :ref:`P014_page`, :ref:`P024_page`, :ref:`P028_page`, :ref:`P030_page`, :ref:`P031_page`, :ref:`P032_page`, :ref:`P034_page`, :ref:`P039_page`, :ref:`P047_page`, :ref:`P051_page`, :ref:`P068_page`, :ref:`P069_page`, :ref:`P072_page`, :ref:`P103_page`, :ref:`P105_page`, :ref:`P106_page` .. |Plugin_Extra_IO| replace:: :ref:`P011_page`, :ref:`P022_page` .. |Plugin_Gases| replace:: :ref:`P049_page`, :ref:`P052_page`, :ref:`P083_page`, :ref:`P090_page`, :ref:`P117_page`, :ref:`P127_page`, :ref:`P135_page` .. |Plugin_Generic| replace:: :ref:`P003_page`, :ref:`P026_page`, :ref:`P033_page`, :ref:`P037_page`, :ref:`P081_page`, :ref:`P100_page` diff --git a/docs/source/Plugin/_plugin_substitutions_p00x.repl b/docs/source/Plugin/_plugin_substitutions_p00x.repl index 8f030c65f4..827993f48b 100644 --- a/docs/source/Plugin/_plugin_substitutions_p00x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p00x.repl @@ -4,7 +4,7 @@ .. |P000_type| replace:: :cyan:`GPIO handling` .. |P000_typename| replace:: :cyan:`Internal GPIO handling` .. |P000_porttype| replace:: `.` -.. |P000_status| replace:: :green:`NORMAL` +.. |P000_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P000_github| replace:: P001_Switch.ino .. _P000_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P001_Switch.ino .. |P000_usedby_GPIO| replace:: :ref:`P000_Relay_page`, :ref:`P000_Servo_motor_page` @@ -21,7 +21,7 @@ .. |P001_type| replace:: :cyan:`Switch Input` .. |P001_porttype| replace:: `.` .. |P001_typename| replace:: :cyan:`Switch Input - Switch` -.. |P001_status| replace:: :green:`NORMAL` +.. |P001_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P001_github| replace:: P001_Switch.ino .. _P001_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P001_Switch.ino .. |P001_usedby| replace:: :ref:`P001_Switch_page`, :ref:`P001_Door_switch_page`, :ref:`P001_PIR_sensor_page` @@ -34,7 +34,7 @@ .. |P002_type| replace:: :cyan:`Analog Input` .. |P002_typename| replace:: :cyan:`Analog Input - Internal` .. |P002_porttype| replace:: `.` -.. |P002_status| replace:: :green:`NORMAL` +.. |P002_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P002_github| replace:: P002_ADC.ino .. _P002_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P002_ADC.ino .. |P002_usedby| replace:: `.` @@ -47,7 +47,7 @@ .. |P003_type| replace:: :cyan:`Generic` .. |P003_typename| replace:: :cyan:`Generic - Pulse counter` .. |P003_porttype| replace:: `.` -.. |P003_status| replace:: :green:`NORMAL` +.. |P003_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P003_github| replace:: P003_Pulse.ino .. _P003_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P003_Pulse.ino .. |P003_usedby| replace:: :ref:`P003_LJ12A3_page`, :ref:`P003_TCR5000_page`, :ref:`P003_YFS401_page` @@ -60,7 +60,7 @@ .. |P004_type| replace:: :cyan:`Environment` .. |P004_typename| replace:: :cyan:`Environment - DS18b20` .. |P004_porttype| replace:: `.` -.. |P004_status| replace:: :green:`NORMAL` +.. |P004_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P004_github| replace:: P004_Dallas.ino .. _P004_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P004_Dallas.ino .. |P004_usedby| replace:: :ref:`P004_page` @@ -73,7 +73,7 @@ .. |P005_type| replace:: :cyan:`Environment` .. |P005_typename| replace:: :cyan:`Environment - DHT11/12/22 SONOFF2301/7021` .. |P005_porttype| replace:: `.` -.. |P005_status| replace:: :green:`NORMAL` +.. |P005_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P005_github| replace:: P005_DHT.ino .. _P005_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P005_DHT.ino .. |P005_usedby| replace:: :ref:`P005_DHT11_DHT22_page` @@ -86,7 +86,7 @@ .. |P006_type| replace:: :cyan:`Environment` .. |P006_typename| replace:: :cyan:`Environment - BMP085/180` .. |P006_porttype| replace:: `.` -.. |P006_status| replace:: :green:`NORMAL` +.. |P006_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P006_github| replace:: P006_BMP085.ino .. _P006_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P006_BMP085.ino .. |P006_usedby| replace:: :ref:`P006_BMP085_page`, :ref:`P006_BMP180_page` diff --git a/docs/source/Plugin/_plugin_substitutions_p01x.repl b/docs/source/Plugin/_plugin_substitutions_p01x.repl index cc1d6004e4..4cb9bd25f5 100644 --- a/docs/source/Plugin/_plugin_substitutions_p01x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p01x.repl @@ -28,7 +28,7 @@ .. |P012_type| replace:: :cyan:`Display` .. |P012_typename| replace:: :cyan:`Display - LCD2004` .. |P012_porttype| replace:: `.` -.. |P012_status| replace:: :green:`NORMAL` :yellow:`DISPLAY` +.. |P012_status| replace:: :green:`NORMAL` :yellow:`DISPLAY` :yellow:`CLIMATE` .. |P012_github| replace:: P012_LCD.ino .. _P012_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P012_LCD.ino .. |P012_usedby| replace:: `.` @@ -54,7 +54,7 @@ .. |P014_type| replace:: :cyan:`Environment` .. |P014_typename| replace:: :cyan:`Environment - SI7021/HTU21D` .. |P014_porttype| replace:: `.` -.. |P014_status| replace:: :green:`NORMAL` +.. |P014_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P014_github| replace:: P014_SI7021.ino .. _P014_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P014_SI7021.ino .. |P014_usedby| replace:: `.` @@ -106,7 +106,7 @@ .. |P018_type| replace:: :cyan:`Dust` .. |P018_typename| replace:: :cyan:`Dust - Sharp GP2Y10` .. |P018_porttype| replace:: `.` -.. |P018_status| replace:: :green:`NORMAL` +.. |P018_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P018_github| replace:: P018_Dust.ino .. _P018_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P018_Dust.ino .. |P018_usedby| replace:: `.` @@ -119,7 +119,7 @@ .. |P019_type| replace:: :cyan:`Switch input` .. |P019_typename| replace:: :cyan:`Switch input - PCF8574` .. |P019_porttype| replace:: `.` -.. |P019_status| replace:: :green:`NORMAL` +.. |P019_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P019_github| replace:: P019_PCF8574.ino .. _P019_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P019_PCF8574.ino .. |P019_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p02x.repl b/docs/source/Plugin/_plugin_substitutions_p02x.repl index ca44f0b4b1..418d589c8d 100644 --- a/docs/source/Plugin/_plugin_substitutions_p02x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p02x.repl @@ -15,7 +15,7 @@ .. |P021_type| replace:: :cyan:`Regulator` .. |P021_typename| replace:: :cyan:`Regulator - Level Control` .. |P021_porttype| replace:: `.` -.. |P021_status| replace:: :green:`NORMAL` +.. |P021_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P021_github| replace:: P021_Level.ino .. _P021_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P021_Level.ino .. |P021_usedby| replace:: `.` @@ -41,7 +41,7 @@ .. |P023_type| replace:: :cyan:`Display` .. |P023_typename| replace:: :cyan:`Display - OLED SSD1306` .. |P023_porttype| replace:: `.` -.. |P023_status| replace:: :green:`NORMAL` :yellow:`DISPLAY` +.. |P023_status| replace:: :green:`NORMAL` :yellow:`DISPLAY` :yellow:`CLIMATE` .. |P023_github| replace:: P023_OLED.ino .. _P023_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P023_OLED.ino .. |P023_usedby| replace:: `.` @@ -54,7 +54,7 @@ .. |P024_type| replace:: :cyan:`Environment` .. |P024_typename| replace:: :cyan:`Environment - MLX90614` .. |P024_porttype| replace:: `.` -.. |P024_status| replace:: :green:`NORMAL` +.. |P024_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P024_github| replace:: P024_MLX90614.ino .. _P024_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P024_MLX90614.ino .. |P024_usedby| replace:: :ref:`P024_MLX90614_page` @@ -80,7 +80,7 @@ .. |P026_type| replace:: :cyan:`Generic` .. |P026_typename| replace:: :cyan:`Generic - System Info` .. |P026_porttype| replace:: `.` -.. |P026_status| replace:: :green:`NORMAL` +.. |P026_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P026_github| replace:: P026_Sysinfo.ino .. _P026_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P026_Sysinfo.ino .. |P026_usedby| replace:: `.` @@ -106,7 +106,7 @@ .. |P028_type| replace:: :cyan:`Environment` .. |P028_typename| replace:: :cyan:`Environment - BMx280` .. |P028_porttype| replace:: `.` -.. |P028_status| replace:: :green:`NORMAL` +.. |P028_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P028_github| replace:: P028_BME280.ino .. _P028_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P028_BME280.ino .. |P028_usedby| replace:: :ref:`P028_page` @@ -119,7 +119,7 @@ .. |P029_type| replace:: :cyan:`Output` .. |P029_typename| replace:: :cyan:`Output - Domoticz MQTT Helper` .. |P029_porttype| replace:: `.` -.. |P029_status| replace:: :green:`NORMAL` +.. |P029_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P029_github| replace:: P029_Output.ino .. _P029_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P029_Output.ino .. |P029_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p03x.repl b/docs/source/Plugin/_plugin_substitutions_p03x.repl index fd4f86f70c..a3409d3733 100644 --- a/docs/source/Plugin/_plugin_substitutions_p03x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p03x.repl @@ -15,7 +15,7 @@ .. |P031_type| replace:: :cyan:`Environment` .. |P031_typename| replace:: :cyan:`Environment - SHT1X` .. |P031_porttype| replace:: `.` -.. |P031_status| replace:: :green:`NORMAL` +.. |P031_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P031_github| replace:: P031_SHT1X.ino .. _P031_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P031_SHT1X.ino .. |P031_usedby| replace:: `.` @@ -28,7 +28,7 @@ .. |P032_type| replace:: :cyan:`Environment` .. |P032_typename| replace:: :cyan:`Environment - MS5611 (GY-63)` .. |P032_porttype| replace:: `.` -.. |P032_status| replace:: :green:`NORMAL` +.. |P032_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P032_github| replace:: P032_MS5611.ino .. _P032_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P032_MS5611.ino .. |P032_usedby| replace:: `.` @@ -41,7 +41,7 @@ .. |P033_type| replace:: :cyan:`Generic` .. |P033_typename| replace:: :cyan:`Generic - Dummy Device` .. |P033_porttype| replace:: `.` -.. |P033_status| replace:: :green:`NORMAL` +.. |P033_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P033_github| replace:: P033_Dummy.ino .. _P033_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P033_Dummy.ino .. |P033_usedby| replace:: `.` @@ -54,7 +54,7 @@ .. |P034_type| replace:: :cyan:`Environment` .. |P034_typename| replace:: :cyan:`Environment - DHT12 (I2C)` .. |P034_porttype| replace:: `.` -.. |P034_status| replace:: :green:`NORMAL` +.. |P034_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P034_github| replace:: P034_DHT12.ino .. _P034_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P034_DHT12.ino .. |P034_usedby| replace:: `.` @@ -80,7 +80,7 @@ .. |P036_type| replace:: :cyan:`Display` .. |P036_typename| replace:: :cyan:`Display - OLED SSD1306/SH1106 Framed` .. |P036_porttype| replace:: `.` -.. |P036_status| replace:: :green:`NORMAL` :yellow:`DISPLAY` +.. |P036_status| replace:: :green:`NORMAL` :yellow:`DISPLAY` :yellow:`CLIMATE` .. |P036_github| replace:: P036_FrameOLED.ino .. _P036_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P036_FrameOLED.ino .. |P036_usedby| replace:: `.` @@ -93,7 +93,7 @@ .. |P037_type| replace:: :cyan:`Generic` .. |P037_typename| replace:: :cyan:`Generic - MQTT Import` .. |P037_porttype| replace:: `.` -.. |P037_status| replace:: :green:`NORMAL` +.. |P037_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P037_github| replace:: P037_MQTTImport.ino .. _P037_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P037_MQTTImport.ino .. |P037_usedby| replace:: `.` @@ -106,7 +106,7 @@ .. |P038_type| replace:: :cyan:`Output` .. |P038_typename| replace:: :cyan:`Output - NeoPixel (Basic)` .. |P038_porttype| replace:: `.` -.. |P038_status| replace:: :green:`NORMAL` :yellow:`NEOPIXEL` +.. |P038_status| replace:: :green:`NORMAL` :yellow:`NEOPIXEL` :yellow:`CLIMATE` .. |P038_github| replace:: P038_NeoPixel.ino .. _P038_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P038_NeoPixel.ino .. |P038_usedby| replace:: `NeoPixel RGB and RGBW LEDs and LED Strips` @@ -119,7 +119,7 @@ .. |P039_type| replace:: :cyan:`Environment` .. |P039_typename| replace:: :cyan:`Environment - Thermosensors` .. |P039_porttype| replace:: `.` -.. |P039_status| replace:: :green:`NORMAL` +.. |P039_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P039_github| replace:: P039_Thermosensors.ino .. _P039_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P039_Thermosensors.ino .. |P039_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p04x.repl b/docs/source/Plugin/_plugin_substitutions_p04x.repl index 9812376013..41e6858bfb 100644 --- a/docs/source/Plugin/_plugin_substitutions_p04x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p04x.repl @@ -41,7 +41,7 @@ .. |P043_type| replace:: :cyan:`Output` .. |P043_typename| replace:: :cyan:`Output - Clock` .. |P043_porttype| replace:: `.` -.. |P043_status| replace:: :green:`NORMAL` +.. |P043_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P043_github| replace:: P043_ClkOutput.ino .. _P043_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P043_ClkOutput.ino .. |P043_usedby| replace:: `.` @@ -54,7 +54,7 @@ .. |P044_type| replace:: :cyan:`Communication` .. |P044_typename| replace:: :cyan:`Communication - P1 Wifi Gateway` .. |P044_porttype| replace:: `.` -.. |P044_status| replace:: :green:`NORMAL` +.. |P044_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P044_github| replace:: P044_P1WifiGateway.ino .. _P044_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P044_P1WifiGateway.ino .. |P044_usedby| replace:: `.` @@ -93,7 +93,7 @@ .. |P047_type| replace:: :cyan:`Environment` .. |P047_typename| replace:: :cyan:`Environment - Soil moisture sensor` .. |P047_porttype| replace:: `.` -.. |P047_status| replace:: :yellow:`COLLECTION` +.. |P047_status| replace:: :yellow:`COLLECTION` :yellow:`CLIMATE` .. |P047_github| replace:: P047_i2c-soil-moisture-sensor.ino .. _P047_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P047_i2c-soil-moisture-sensor.ino .. |P047_usedby| replace:: `.` @@ -119,7 +119,7 @@ .. |P049_type| replace:: :cyan:`Gases` .. |P049_typename| replace:: :cyan:`Gases - CO2 MH-Z19` .. |P049_porttype| replace:: `Serial` -.. |P049_status| replace:: :green:`NORMAL` +.. |P049_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P049_github| replace:: P049_MHZ19.ino .. _P049_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P049_MHZ19.ino .. |P049_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p05x.repl b/docs/source/Plugin/_plugin_substitutions_p05x.repl index e3ba62afdc..cf78e7fdb4 100644 --- a/docs/source/Plugin/_plugin_substitutions_p05x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p05x.repl @@ -15,7 +15,7 @@ .. |P051_type| replace:: :cyan:`Environment` .. |P051_typename| replace:: :cyan:`Environment - AM2320` .. |P051_porttype| replace:: `.` -.. |P051_status| replace:: :yellow:`COLLECTION` +.. |P051_status| replace:: :yellow:`COLLECTION` :yellow:`CLIMATE` .. |P051_github| replace:: P051_AM2320.ino .. _P051_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P051_AM2320.ino .. |P051_usedby| replace:: `.` @@ -28,7 +28,7 @@ .. |P052_type| replace:: :cyan:`Gases` .. |P052_typename| replace:: :cyan:`Gases - CO2 Senseair` .. |P052_porttype| replace:: `Serial` -.. |P052_status| replace:: :green:`NORMAL` +.. |P052_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P052_github| replace:: P052_SenseAir.ino .. _P052_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P052_SenseAir.ino .. |P052_usedby| replace:: :ref:`P052_S8_page`, :ref:`P052_S11_page`, :ref:`P052_tSense_K70_page` @@ -41,7 +41,7 @@ .. |P053_type| replace:: :cyan:`Dust` .. |P053_typename| replace:: :cyan:`Dust - PMSx003 / PMSx003ST` .. |P053_porttype| replace:: `Serial` -.. |P053_status| replace:: :green:`NORMAL` +.. |P053_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P053_github| replace:: P053_PMSx003.ino .. _P053_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P053_PMSx003.ino .. |P053_usedby| replace:: `.` @@ -80,7 +80,7 @@ .. |P056_type| replace:: :cyan:`Dust` .. |P056_typename| replace:: :cyan:`Dust - SDS011/018/198` .. |P056_porttype| replace:: `Serial` -.. |P056_status| replace:: :green:`NORMAL` +.. |P056_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P056_github| replace:: P056_SDS011-Dust.ino .. _P056_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P056_SDS011-Dust.ino .. |P056_usedby| replace:: `.` @@ -119,7 +119,7 @@ .. |P059_type| replace:: :cyan:`Switch input` .. |P059_typename| replace:: :cyan:`Switch input - Rotary Encoder` .. |P059_porttype| replace:: `.` -.. |P059_status| replace:: :green:`NORMAL` +.. |P059_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P059_github| replace:: P059_Encoder.ino .. _P059_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P059_Encoder.ino .. |P059_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p06x.repl b/docs/source/Plugin/_plugin_substitutions_p06x.repl index c2fa42465a..19382367c4 100644 --- a/docs/source/Plugin/_plugin_substitutions_p06x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p06x.repl @@ -106,7 +106,7 @@ .. |P068_type| replace:: :cyan:`Environment` .. |P068_typename| replace:: :cyan:`Environment - SHT30/31/35` .. |P068_porttype| replace:: `.` -.. |P068_status| replace:: :yellow:`COLLECTION A` +.. |P068_status| replace:: :yellow:`COLLECTION A` :yellow:`CLIMATE` .. |P068_github| replace:: P068_SHT3x.ino .. _P068_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P068_SHT3x.ino .. |P068_usedby| replace:: `.` @@ -119,7 +119,7 @@ .. |P069_type| replace:: :cyan:`Environment` .. |P069_typename| replace:: :cyan:`Environment - LM75A` .. |P069_porttype| replace:: `.` -.. |P069_status| replace:: :yellow:`COLLECTION B` +.. |P069_status| replace:: :yellow:`COLLECTION B` :yellow:`CLIMATE` .. |P069_github| replace:: P069_LM75A.ino .. _P069_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P069_LM75A.ino .. |P069_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p07x.repl b/docs/source/Plugin/_plugin_substitutions_p07x.repl index 05c92940af..21fbe88b13 100644 --- a/docs/source/Plugin/_plugin_substitutions_p07x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p07x.repl @@ -29,7 +29,7 @@ .. |P072_type| replace:: :cyan:`Environment` .. |P072_typename| replace:: :cyan:`Environment - HDC1080 (I2C)` .. |P072_porttype| replace:: `.` -.. |P072_status| replace:: :yellow:`COLLECTION A` +.. |P072_status| replace:: :yellow:`COLLECTION A` :yellow:`CLIMATE` .. |P072_github| replace:: P072_HDC1080.ino .. _P072_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P072_HDC1080.ino .. |P072_usedby| replace:: `.` @@ -42,7 +42,7 @@ .. |P073_type| replace:: :cyan:`Display` .. |P073_typename| replace:: :cyan:`Display - 7-segment display` .. |P073_porttype| replace:: `.` -.. |P073_status| replace:: :green:`NORMAL` +.. |P073_status| replace:: :green:`NORMAL` :yellow:`CLIMATE` .. |P073_github| replace:: P073_7DGT.ino .. _P073_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P073_7DGT.ino .. |P073_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p08x.repl b/docs/source/Plugin/_plugin_substitutions_p08x.repl index 1ca69d54dc..3ce6940103 100644 --- a/docs/source/Plugin/_plugin_substitutions_p08x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p08x.repl @@ -15,7 +15,7 @@ .. |P081_type| replace:: :cyan:`Generic` .. |P081_typename| replace:: :cyan:`Generic - CRON` .. |P081_porttype| replace:: `.` -.. |P081_status| replace:: :yellow:`COLLECTION` +.. |P081_status| replace:: :yellow:`COLLECTION` :yellow:`CLIMATE` .. |P081_github| replace:: P081_Cron.ino .. _P081_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P081_Cron.ino .. |P081_usedby| replace:: `.` @@ -38,10 +38,10 @@ .. |P082_usedlibraries| replace:: https://github.com/mikalhart/TinyGPSPlus/tree/v1.0.2 .. |P083_name| replace:: :cyan:`SGP30` -.. |P083_type| replace:: :cyan:`Gasses` -.. |P083_typename| replace:: :cyan:`Gasses - SGP30` +.. |P083_type| replace:: :cyan:`Gases` +.. |P083_typename| replace:: :cyan:`Gases - SGP30` .. |P083_porttype| replace:: `.` -.. |P083_status| replace:: :yellow:`COLLECTION A` +.. |P083_status| replace:: :yellow:`COLLECTION A` :yellow:`CLIMATE` .. |P083_github| replace:: P083_SGP30.ino .. _P083_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P083_SGP30.ino .. |P083_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p09x.repl b/docs/source/Plugin/_plugin_substitutions_p09x.repl index 71708fe217..c445d1f0f4 100644 --- a/docs/source/Plugin/_plugin_substitutions_p09x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p09x.repl @@ -2,7 +2,7 @@ .. |P090_type| replace:: :cyan:`Gases` .. |P090_typename| replace:: :cyan:`Gases - CCS811 TVOC/eCO2` .. |P090_porttype| replace:: `.` -.. |P090_status| replace:: :yellow:`COLLECTION A` +.. |P090_status| replace:: :yellow:`COLLECTION A` :yellow:`CLIMATE` .. |P090_github| replace:: P090_CCS811.ino .. _P090_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P090_CCS811.ino .. |P090_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p10x.repl b/docs/source/Plugin/_plugin_substitutions_p10x.repl index 58c29dd175..0c766b7947 100644 --- a/docs/source/Plugin/_plugin_substitutions_p10x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p10x.repl @@ -41,7 +41,7 @@ .. |P103_type| replace:: :cyan:`Environment` .. |P103_typename| replace:: :cyan:`Environment - Atlas Scientific EZO pH` .. |P103_porttype| replace:: `.` -.. |P103_status| replace:: :yellow:`MAX` +.. |P103_status| replace:: :yellow:`MAX` :yellow:`CLIMATE` .. |P103_github| replace:: P103_Atlas_EZO_pH.ino .. _P103_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P103_Atlas_EZO_pH.ino .. |P103_usedby| replace:: `.` @@ -79,7 +79,7 @@ .. |P105_type| replace:: :cyan:`Environment` .. |P105_typename| replace:: :cyan:`Environment - AHT10/20/21` .. |P105_porttype| replace:: `.` -.. |P105_status| replace:: :yellow:`COLLECTION A` +.. |P105_status| replace:: :yellow:`COLLECTION A` :yellow:`CLIMATE` .. |P105_github| replace:: P105_AHT.ino .. _P105_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P105_AHT.ino .. |P105_usedby| replace:: `.` @@ -96,7 +96,7 @@ .. |P106_type| replace:: :cyan:`Environment` .. |P106_typename| replace:: :cyan:`Environment - BME680` .. |P106_porttype| replace:: `.` -.. |P106_status| replace:: :yellow:`COLLECTION B` +.. |P106_status| replace:: :yellow:`COLLECTION B` :yellow:`CLIMATE` .. |P106_github| replace:: P106_BME680.ino .. _P106_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P106_BME680.ino .. |P106_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p11x.repl b/docs/source/Plugin/_plugin_substitutions_p11x.repl index 5651047f3d..56bddbb591 100644 --- a/docs/source/Plugin/_plugin_substitutions_p11x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p11x.repl @@ -80,7 +80,7 @@ .. |P117_type| replace:: :cyan:`Gases` .. |P117_typename| replace:: :cyan:`Gases - SCD30 CO2, Humidity, Temperature` .. |P117_porttype| replace:: `.` -.. |P117_status| replace:: :yellow:`COLLECTION D` +.. |P117_status| replace:: :yellow:`COLLECTION D` :yellow:`CLIMATE` .. |P117_github| replace:: _P117_SCD30.ino .. _P117_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P117_SCD30.ino .. |P117_usedby| replace:: `.` @@ -93,7 +93,7 @@ .. |P118_type| replace:: :cyan:`Communication` .. |P118_typename| replace:: :cyan:`Communication - Itho` .. |P118_porttype| replace:: `.` -.. |P118_status| replace:: :yellow:`COLLECTION E` +.. |P118_status| replace:: :yellow:`CLIMATE` .. |P118_github| replace:: _P118_Itho.ino .. _P118_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P118_Itho.ino .. |P118_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p12x.repl b/docs/source/Plugin/_plugin_substitutions_p12x.repl index 069df90f5e..2be4533650 100644 --- a/docs/source/Plugin/_plugin_substitutions_p12x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p12x.repl @@ -67,7 +67,7 @@ .. |P127_type| replace:: :cyan:`Gases` .. |P127_typename| replace:: :cyan:`Gases - CO2 CDM7160` .. |P127_porttype| replace:: `.` -.. |P127_status| replace:: :yellow:`COLLECTION D` +.. |P127_status| replace:: :yellow:`COLLECTION D` :yellow:`CLIMATE` .. |P127_github| replace:: P127_CDM7160.ino .. _P127_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P127_CDM7160.ino .. |P127_usedby| replace:: `.` diff --git a/docs/source/Plugin/_plugin_substitutions_p13x.repl b/docs/source/Plugin/_plugin_substitutions_p13x.repl index 7b1a4fd16c..bdf9ca8ec8 100644 --- a/docs/source/Plugin/_plugin_substitutions_p13x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p13x.repl @@ -54,7 +54,7 @@ .. |P135_type| replace:: :cyan:`Gases` .. |P135_typename| replace:: :cyan:`Gases - CO2 SCD4x` .. |P135_porttype| replace:: `.` -.. |P135_status| replace:: :yellow:`COLLECTION E` +.. |P135_status| replace:: :yellow:`COLLECTION E` :yellow:`CLIMATE` .. |P135_github| replace:: P135_SCD4x.ino .. _P135_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P135_SCD4x.ino .. |P135_usedby| replace:: `.` From 7d1eafa1b5e66d16005e422dbdd45da8f56ba5e6 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 23 Sep 2022 20:45:52 +0200 Subject: [PATCH 33/33] [Build] Fix compiler warnings for signed/unsigned int compare --- src/src/WebServer/CustomPage.cpp | 8 ++++---- src/src/WebServer/LoadFromFS.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/src/WebServer/CustomPage.cpp b/src/src/WebServer/CustomPage.cpp index 16c0bb13f8..212632ee45 100644 --- a/src/src/WebServer/CustomPage.cpp +++ b/src/src/WebServer/CustomPage.cpp @@ -148,18 +148,18 @@ bool handle_custom(const String& path) { if (dataFile) { // Read the file per line and serve per line to reduce amount of memory needed. - uint32_t available = dataFile.available(); + size_t available = dataFile.available(); String line; line.reserve(128); while (available > 0) { - int32_t chunksize = 64; + size_t chunksize = 64; if (available < chunksize) { chunksize = available; } uint8_t buf[64] = {0}; - const int read = dataFile.read(buf, chunksize); + const size_t read = dataFile.read(buf, chunksize); if (read == chunksize) { - for (int32_t i = 0; i < chunksize; ++i) { + for (size_t i = 0; i < chunksize; ++i) { const char c = (char)buf[i]; line += c; if (c == '\n') { diff --git a/src/src/WebServer/LoadFromFS.cpp b/src/src/WebServer/LoadFromFS.cpp index 68caf62986..ebc3ed71a4 100644 --- a/src/src/WebServer/LoadFromFS.cpp +++ b/src/src/WebServer/LoadFromFS.cpp @@ -285,20 +285,20 @@ size_t streamFromFS(String path, bool htmlEscape) { return bytesStreamed; } - uint32_t available = f.available(); + size_t available = f.available(); String escaped; while (available > 0) { - int32_t chunksize = 64; + size_t chunksize = 64; if (available < chunksize) { chunksize = available; } uint8_t buf[64] = { 0 }; - const int read = f.read(buf, chunksize); + const size_t read = f.read(buf, chunksize); if (read == chunksize) { - for (int32_t i = 0; i < chunksize; ++i) { + for (size_t i = 0; i < chunksize; ++i) { const char c = (char)buf[i]; if (htmlEscape && htmlEscapeChar(c, escaped)) {