/* * This file is part of the "bluetoothheater" distribution * (https://gitlab.com/mrjones.id.au/bluetoothheater) * * Copyright (C) 2018 Ray Jones * Copyright (C) 2018 James Clark * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "BlueWireTask.h" #include "../cfg/BTCConfig.h" #include "../cfg/pins.h" #include "Protocol.h" #include "TxManage.h" // #include "SmartError.h" #include "../Utility/UtilClasses.h" #include "../Utility/DataFilter.h" #include "../Utility/FuelGauge.h" #include "../Utility/HourMeter.h" #include "../Utility/macros.h" // Setup Serial Port Definitions #if defined(__arm__) // Required for Arduino Due, UARTclass is derived from HardwareSerial static UARTClass& BlueWireSerial(Serial1); #else // for ESP32, Mega // HardwareSerial is it for these boards static HardwareSerial& BlueWireSerial(Serial1); #endif #define RX_DATA_TIMOUT 50 CommStates CommState; CTxManage TxManage(TxEnbPin, BlueWireSerial); CProtocol DefaultBTCParams(CProtocol::CtrlMode); // defines the default parameters, used in case of no OEM controller CModeratedFrame OEMCtrlFrame; // data packet received from heater in response to OEM controller packet CModeratedFrame HeaterFrame1; // data packet received from heater in response to OEM controller packet CProtocol HeaterFrame2; // data packet received from heater in response to our packet // CSmartError SmartError; CProtocolPackage reportHeaterData; CProtocolPackage primaryHeaterData; static bool bHasOEMController = false; static bool bHasOEMLCDController = false; static bool bHasHtrData = false; extern bool bReportRecyleEvents; extern bool bReportOEMresync; extern bool bReportBlueWireData; extern sFilteredData FilteredSamples; QueueHandle_t BlueWireMsgBuf = NULL; // cannot use general Serial.print etc from this task without causing conflicts QueueHandle_t BlueWireRxQueue = NULL; // queue to pass down heater receive data QueueHandle_t BlueWireTxQueue = NULL; // queue to pass down heater transmit data SemaphoreHandle_t BlueWireSemaphore = NULL; // flag to indicate completion of heater data exchange bool validateFrame(const CProtocol& frame, const char* name); void DebugReportFrame(const char* hdr, const CProtocol& Frame, const char* ftr, char* msg); // void updateFilteredData(); void initBlueWireSerial(); void pushDebugMsg(const char* msg) { if(BlueWireMsgBuf) xQueueSend(BlueWireMsgBuf, msg, 0); } void BlueWireTask(void*) { ////////////////////////////////////////////////////////////////////////////////////// // Blue wire data reception // Reads data from the "blue wire" Serial port, (to/from heater) // If an OEM controller exists we will also see it's data frames // Note that the data is read now, then held for later use in the state machine // static unsigned long lastRxTime = 0; // used to observe inter character delays static unsigned long moderator = 50; bool isBTCmaster = false; BlueWireMsgBuf = xQueueCreate(4, BLUEWIRE_MSGQUEUESIZE); BlueWireRxQueue = xQueueCreate(4, BLUEWIRE_DATAQUEUESIZE); BlueWireTxQueue = xQueueCreate(4, BLUEWIRE_DATAQUEUESIZE); BlueWireSemaphore = xSemaphoreCreateBinary(); TxManage.begin(); // ensure Tx enable pin is setup // define defaults should OEM controller be missing DefaultBTCParams.setHeaterDemand(23); DefaultBTCParams.setTemperature_Actual(22); DefaultBTCParams.setSystemVoltage(12.0); DefaultBTCParams.setPump_Min(1.6f); DefaultBTCParams.setPump_Max(5.5f); DefaultBTCParams.setFan_Min(1680); DefaultBTCParams.setFan_Max(4500); DefaultBTCParams.Controller.FanSensor = 1; initBlueWireSerial(); CommState.setCallback(pushDebugMsg); TxManage.setCallback(pushDebugMsg); for(;;) { sRxData BlueWireRxData; unsigned long timenow = millis(); // calc elapsed time since last rxd byte // used to detect no OEM controller, or the start of an OEM frame sequence unsigned long RxTimeElapsed = timenow - lastRxTime; if (BlueWireSerial.available()) { // Data is available, read and store it now, use it later // Note that if not in a recognised data receive frame state, the data // will be deliberately lost! BlueWireRxData.setValue(BlueWireSerial.read()); // read hex byte, store for later use lastRxTime = timenow; // tickle last rx time, for rx data timeout purposes } // precautionary state machine action if all 24 bytes were not received // whilst expecting a frame from the blue wire if(RxTimeElapsed > RX_DATA_TIMOUT) { if( CommState.is(CommStates::OEMCtrlRx) || CommState.is(CommStates::HeaterRx1) || CommState.is(CommStates::HeaterRx2) ) { if(RxTimeElapsed >= moderator) { moderator += 10; if(bReportRecyleEvents) { char msg[32]; sprintf(msg, "%ldms - ", RxTimeElapsed); pushDebugMsg(msg); } if(CommState.is(CommStates::OEMCtrlRx)) { bHasOEMController = false; bHasOEMLCDController = false; if(bReportRecyleEvents) { pushDebugMsg("Timeout collecting OEM controller data, returning to Idle State\r\n"); } } else if(CommState.is(CommStates::HeaterRx1)) { bHasHtrData = false; if(bReportRecyleEvents) { pushDebugMsg("Timeout collecting OEM heater response data, returning to Idle State\r\n"); } } else { bHasHtrData = false; if(bReportRecyleEvents) { pushDebugMsg("Timeout collecting BTC heater response data, returning to Idle State\r\n"); } } } if(bReportRecyleEvents) { pushDebugMsg("Recycling blue wire serial interface\r\n"); } #ifdef REBOOT_BLUEWIRE initBlueWireSerial(); #endif CommState.set(CommStates::ExchangeComplete); // revert to idle mode, after passing thru exchange complete mode } } /////////////////////////////////////////////////////////////////////////////////////////// // do our state machine to track the reception and delivery of blue wire data switch(CommState.get()) { case CommStates::Idle: moderator = 50; digitalWrite(LED_Pin, LOW); // Detect the possible start of a new frame sequence from an OEM controller // This will be the first activity for considerable period on the blue wire // The heater always responds to a controller frame, but otherwise never by itself if(RxTimeElapsed >= (NVstore.getUserSettings().FrameRate - 60)) { // compensate for the time spent just doing things in this state machine // have not seen any receive data for a second (typ.). // OEM controller is probably not connected. // Skip state machine immediately to BTC_Tx, sending our own settings. bHasHtrData = false; bHasOEMController = false; bHasOEMLCDController = false; isBTCmaster = true; TxManage.PrepareFrame(DefaultBTCParams, isBTCmaster); // use our parameters, and mix in NV storage values TxManage.Start(timenow); CommState.set(CommStates::TxStart); break; } if(BlueWireRxData.available() && (RxTimeElapsed > (RX_DATA_TIMOUT+10))) { if(bReportOEMresync) { char msg[64]; sprintf(msg, "Re-sync'd with OEM Controller. %ldms Idle time.\r\n", RxTimeElapsed); pushDebugMsg(msg); } bHasHtrData = false; bHasOEMController = true; CommState.set(CommStates::OEMCtrlRx); // we must add this new byte! // // ** IMPORTANT - we must drop through to OEMCtrlRx *NOW* (skipping break) ** // ** otherwise the first byte will be lost! ** // } else { break; // only break if we fail all Idle state tests } case CommStates::OEMCtrlRx: digitalWrite(LED_Pin, HIGH); // collect OEM controller frame if(BlueWireRxData.available()) { if(CommState.collectData(OEMCtrlFrame, BlueWireRxData.getValue()) ) { CommState.set(CommStates::OEMCtrlValidate); // collected 24 bytes, move on! } } break; case CommStates::OEMCtrlValidate: digitalWrite(LED_Pin, LOW); // test for valid CRC, abort and restarts Serial1 if invalid if(!validateFrame(OEMCtrlFrame, "OEM")) { break; } // filled OEM controller frame OEMCtrlFrame.setTime(); // LCD controllers use 0x76 as first byte, rotary knobs use 0x78 bHasOEMLCDController = (OEMCtrlFrame.Controller.Byte0 != 0x78); // xQueueSend(BlueWireTxQueue, OEMCtrlFrame.Data, 0); CommState.set(CommStates::HeaterRx1); break; case CommStates::HeaterRx1: digitalWrite(LED_Pin, HIGH); // collect heater frame, always in response to an OEM controller frame if(BlueWireRxData.available()) { if( CommState.collectData(HeaterFrame1, BlueWireRxData.getValue()) ) { CommState.set(CommStates::HeaterValidate1); } } break; case CommStates::HeaterValidate1: digitalWrite(LED_Pin, LOW); // test for valid CRC, abort and restarts Serial1 if invalid if(!validateFrame(HeaterFrame1, "RX1")) { bHasHtrData = false; break; } bHasHtrData = true; HeaterFrame1.setTime(); while(BlueWireSerial.available()) { pushDebugMsg("DUMPED ROGUE RX DATA\r\n"); BlueWireSerial.read(); } BlueWireSerial.flush(); // received heater frame (after controller message), report primaryHeaterData.set(HeaterFrame1, OEMCtrlFrame); // OEM is always *the* controller if(bReportBlueWireData) { primaryHeaterData.reportFrames(true, pushDebugMsg); } isBTCmaster = false; TxManage.PrepareFrame(OEMCtrlFrame, isBTCmaster); // parrot OEM parameters, but block NV modes CommState.set(CommStates::TxStart); break; case CommStates::TxStart: xQueueSend(BlueWireTxQueue, TxManage.getFrame().Data, 0); TxManage.Start(timenow); CommState.set(CommStates::TxInterval); break; case CommStates::TxInterval: // Handle time interval where we send data to the blue wire lastRxTime = timenow; // *we* are pumping onto blue wire, track this activity! if(TxManage.CheckTx(timenow) ) { // monitor progress of our data delivery CommState.set(CommStates::HeaterRx2); // then await heater repsonse } break; case CommStates::HeaterRx2: digitalWrite(LED_Pin, HIGH); // collect heater frame, in response to our control frame if(BlueWireRxData.available()) { #ifdef BADSTARTCHECK if(!CommState.checkValidStart(BlueWireData.getValue())) { DebugPort.println("***** Invalid start of frame - restarting Serial port *****"); initBlueWireSerial(); CommState.set(CommStates::Idle); } else { if( CommState.collectData(HeaterFrame2, BlueWireData.getValue()) ) { CommState.set(CommStates::HeaterValidate2); } } #else if( CommState.collectData(HeaterFrame2, BlueWireRxData.getValue()) ) { CommState.set(CommStates::HeaterValidate2); } #endif } break; case CommStates::HeaterValidate2: digitalWrite(LED_Pin, LOW); // test for valid CRC, abort and restart Serial1 if invalid if(!validateFrame(HeaterFrame2, "RX2")) { bHasHtrData = false; break; } bHasHtrData = true; // received heater frame (after our control message), report xQueueSend(BlueWireRxQueue, HeaterFrame2.Data, 0); // do some monitoring of the heater state variables // if abnormal transitions, introduce a smart error! // SmartError.monitor(HeaterFrame2); if(!bHasOEMController) // no OEM controller - BTC is *the* controller primaryHeaterData.set(HeaterFrame2, TxManage.getFrame()); if(bReportBlueWireData) { // debug or investigation purposes reportHeaterData.set(HeaterFrame2, TxManage.getFrame()); reportHeaterData.reportFrames(false, pushDebugMsg); } CommState.set(CommStates::ExchangeComplete); break; case CommStates::ExchangeComplete: xSemaphoreGive(BlueWireSemaphore); CommState.set(CommStates::Idle); break; } // switch(CommState) vTaskDelay(1); } } bool validateFrame(const CProtocol& frame, const char* name) { if(!frame.verifyCRC(pushDebugMsg)) { // Bad CRC - restart blue wire Serial port char msg[128]; sprintf(msg, "\007Bad CRC detected for %s frame - restarting blue wire's serial port\r\n", name); pushDebugMsg(msg); msg[0] = 0; // empty string DebugReportFrame("BAD CRC:", frame, "\r\n", msg); pushDebugMsg(msg); #ifdef REBOOT_BLUEWIRE initBlueWireSerial(); #endif CommState.set(CommStates::ExchangeComplete); return false; } return true; } void DebugReportFrame(const char* hdr, const CProtocol& Frame, const char* ftr, char* msg) { strcat(msg, hdr); // header for(int i=0; i<24; i++) { char str[16]; sprintf(str, " %02X", Frame.Data[i]); // build 2 dig hex values strcat(msg, str); // and print } strcat(msg, ftr); // footer } bool hasOEMcontroller() { return bHasOEMController; } bool hasOEMLCDcontroller() { return bHasOEMLCDController; } bool hasHtrData() { return bHasHtrData; } void initBlueWireSerial() { // initialize serial port to interact with the "blue wire" // 25000 baud, Tx and Rx channels of Chinese heater comms interface: // Tx/Rx data to/from heater, // Note special baud rate for Chinese heater controllers #if defined(__arm__) || defined(__AVR__) BlueWireSerial.begin(25000); pinMode(Rx1Pin, INPUT_PULLUP); // required for MUX to work properly #elif ESP32 // ESP32 BlueWireSerial.begin(25000, SERIAL_8N1, Rx1Pin, Tx1Pin); // need to explicitly specify pins for pin multiplexer! pinMode(Rx1Pin, INPUT_PULLUP); // required for MUX to work properly #endif } // 0x00 - Normal: BTC, with heater responding // 0x01 - Error: BTC, heater not responding // 0x02 - Special: OEM controller & heater responding // 0x03 - Error: OEM controller, heater not responding int getBlueWireStat() { int stat = 0; if(!bHasHtrData) { stat |= 0x01; } if(bHasOEMController) { stat |= 0x02; } return stat; } const char* getBlueWireStatStr() { static const char* BlueWireStates[] = { "BTC,Htr", "BTC", "OEM,Htr", "OEM" }; return BlueWireStates[getBlueWireStat()]; } void reqPumpPrime(bool on) { DefaultBTCParams.setPump_Prime(on); }