diff --git a/AppInventor/BluetoothHeater.aia b/AppInventor/BluetoothHeater.aia new file mode 100644 index 0000000..e66d51a Binary files /dev/null and b/AppInventor/BluetoothHeater.aia differ diff --git a/Arduino/SenderTrial2/.vscode/c_cpp_properties.json b/Arduino/SenderTrial2/.vscode/c_cpp_properties.json index d7283d2..67e1940 100644 --- a/Arduino/SenderTrial2/.vscode/c_cpp_properties.json +++ b/Arduino/SenderTrial2/.vscode/c_cpp_properties.json @@ -3,6 +3,8 @@ { "name": "Win32", "includePath": [ + "C:\\Program Files (x86)\\Arduino\\tools\\**", + "C:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\**", "C:\\Users\\ray\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\**", "C:\\Users\\ray\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\1.0.0\\**", "C:\\Users\\ray\\AppData\\Local\\Arduino15\\packages\\arduino\\tools\\**", diff --git a/Arduino/SenderTrial2/.vscode/settings.json b/Arduino/SenderTrial2/.vscode/settings.json index 7127d0b..be44c5e 100644 --- a/Arduino/SenderTrial2/.vscode/settings.json +++ b/Arduino/SenderTrial2/.vscode/settings.json @@ -2,5 +2,15 @@ "sketch": "SenderTrial2.ino", "port": "COM4", "board": "arduino:sam:arduino_due_x_dbg", - "output": "c:\\Users\\ray\\AppData\\Local\\Arduino\\" + "output": "c:\\Users\\ray\\AppData\\Local\\Arduino\\", + "files.associations": { + "list": "cpp", + "vector": "cpp", + "xhash": "cpp", + "xstring": "cpp", + "xtree": "cpp", + "algorithm": "cpp", + "initializer_list": "cpp", + "xutility": "cpp" + } } \ No newline at end of file diff --git a/Arduino/SenderTrial2/Bluetooth.h b/Arduino/SenderTrial2/Bluetooth.h new file mode 100644 index 0000000..2dc6ea3 --- /dev/null +++ b/Arduino/SenderTrial2/Bluetooth.h @@ -0,0 +1,12 @@ +#include + +class CProtocol; + +void Bluetooth_Init(); +void Bluetooth_SendFrame(const char* pHdr, const CProtocol& Frame); +void Bluetooth_Check(); + +extern void Command_Interpret(String line); // decodes received command lines, implemented in main .ino file! + + + diff --git a/Arduino/SenderTrial2/BluetoothESP32.cpp b/Arduino/SenderTrial2/BluetoothESP32.cpp new file mode 100644 index 0000000..bf6bc0e --- /dev/null +++ b/Arduino/SenderTrial2/BluetoothESP32.cpp @@ -0,0 +1,218 @@ +#include "Bluetooth.h" +#include "pins.h" +#include "Protocol.h" +#include "debugport.h" + +#ifdef ESP32 + +const int LED = 2; + +// ESP32 + +String BluetoothRxLine; + +#ifndef ESP32_USE_BLE_RLJ + +///////////////////////////////////////////////////////////////////////////////////////// +// CLASSIC BLUETOOTH +// | +// V + + +#include "BluetoothSerial.h" + +BluetoothSerial SerialBT; + +void Bluetooth_Init() +{ + pinMode(LED, OUTPUT); + + if(!SerialBT.begin("ESPHEATER")) { + DebugPort.println("An error occurred initialising Bluetooth"); + } +} + +void Bluetooth_Check() +{ + if(SerialBT.available()) { + char rxVal = SerialBT.read(); + if(isControl(rxVal)) { // "End of Line" + BluetoothRxLine += '\0'; + Command_Interpret(BluetoothRxLine); + BluetoothRxLine = ""; + } + else { + BluetoothRxLine += rxVal; // append new char to our Rx buffer + } + } +} + +void Bluetooth_SendFrame(const char* pHdr, const CProtocol& Frame) +{ + if(SerialBT.hasClient()) { + + if(Frame.verifyCRC()) { + digitalWrite(LED, !digitalRead(LED)); // toggle LED + SerialBT.print(pHdr); + SerialBT.write(Frame.Data, 24); + } + else { + DebugPort.print("Bluetooth data not sent, CRC error "); + DebugPort.println(pHdr); + } + } + else { + digitalWrite(LED, 0); + } +} + +// ^ +// | +// CLASSIC BLUETOOTH +///////////////////////////////////////////////////////////////////////////////////////// + +#else // ESP32_USE_BLE_RLJ + +///////////////////////////////////////////////////////////////////////////////////////// +// BLE +// | +// V + + +#include +#include +#include +#include + +#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID +#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +void BLE_Send(std::string Data); + +BLEServer *pServer = NULL; +BLECharacteristic* pTxCharacteristic = NULL; +volatile bool deviceConnected = false; +bool oldDeviceConnected = false; + +class MyServerCallbacks : public BLEServerCallbacks { + + void onConnect(BLEServer* pServer) { + deviceConnected = true; + } + + void onDisconnect(BLEServer* pServer) { + deviceConnected = false; + } + +}; + + +class MyCallbacks : public BLECharacteristicCallbacks { + + // this callback is called when the ESP WRITE characteristic has been written to by a client + // We need to *read* the new information! + void onWrite(BLECharacteristic* pCharacteristic) { + + std::string rxValue = pCharacteristic->getValue(); + + while(rxValue.length() > 0) { + char rxVal = rxValue[0]; + if(isControl(rxVal)) { // "End of Line" + Command_Interpret(BluetoothRxLine); + BluetoothRxLine = ""; + } + else { + BluetoothRxLine += rxVal; // append new char to our Rx buffer + } + rxValue.erase(0, 1); + } + } + +}; + +void Bluetooth_Init() +{ + // create the BLE device + BLEDevice::init("DieselHeater"); + + // create the BLE server + pServer = BLEDevice::createServer(); + pServer->setCallbacks(new MyServerCallbacks); + + // create the BLE service + BLEService *pService = pServer->createService(SERVICE_UUID); + + // create a BLE characteristic + pTxCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID_TX, + BLECharacteristic::PROPERTY_NOTIFY + ); + pTxCharacteristic->addDescriptor(new BLE2902()); + + + BLECharacteristic* pRxCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID_RX, + BLECharacteristic::PROPERTY_WRITE + ); + pRxCharacteristic->setCallbacks(new MyCallbacks/*()*/); + + // start the service + pService->start(); + // start advertising + pServer->getAdvertising()->start(); + DebugPort.println("Awaiting a client to notify..."); +} + +void Bluetooth_Report(const char* pHdr, const CProtocol& Frame) +{ + if(deviceConnected) { + if(Frame.verifyCRC()) { + // BLE can only squirt 20 bytes per packet. + // build the entire message then divide and conquer + std::string txData = pHdr; + txData.append((char*)Frame.Data, 24); + + BLE_Send(txData); + } + } +} + +void Bluetooth_Check() +{ + // disconnecting + if (!deviceConnected && oldDeviceConnected) { + delay(500); // give the bluetooth stack the chance to get things ready + pServer->startAdvertising(); // restart advertising + DebugPort.println("start advertising"); + oldDeviceConnected = deviceConnected; + } + // connecting + if (deviceConnected && !oldDeviceConnected) { + // do stuff here on connecting + oldDeviceConnected = deviceConnected; + } + +} + +// break down supplied string into 20 byte chunks (or less) +// BLE can only handle 20 bytes per packet! +void BLE_Send(std::string Data) +{ + while(!Data.empty()) { + std::string substr = Data.substr(0, 20); + int len = substr.length(); + pTxCharacteristic->setValue((uint8_t*)Data.data(), len); + pTxCharacteristic->notify(); + Data.erase(0, len); + } +} + +// ^ +// | +// BLE +///////////////////////////////////////////////////////////////////////////////////////// + +#endif // ESP32_USE_BLE_RLJ + +#endif // __ESP32__ \ No newline at end of file diff --git a/Arduino/SenderTrial2/BluetoothHC05.cpp b/Arduino/SenderTrial2/BluetoothHC05.cpp new file mode 100644 index 0000000..1d9d090 --- /dev/null +++ b/Arduino/SenderTrial2/BluetoothHC05.cpp @@ -0,0 +1,155 @@ +#include "Bluetooth.h" +#include "pins.h" +#include "Protocol.h" +#include "debugport.h" + +// Bluetooth access via HC-05 Module, using a UART + +#ifndef ESP32 +// NOTE: ESP32 uses an entirely different mechanism, please refer to BluetoothESP32.cpp/.h + +#ifdef __arm__ +// for Arduino Due +static UARTClass& Bluetooth(Serial2); +#else +// for Mega +static HardwareSerial& Bluetooth(Serial2); // TODO: make proper ESP32 BT client +#endif + +bool Bluetooth_ATCommand(const char* cmd); + +String BluetoothRxData; + +const int BTRates[] = { + 9600, 38400, 115200, 19200, 57600, 2400, 4800 +}; + +bool bHC05Available = false; + +void Bluetooth_Init() +{ + + // search for BlueTooth adapter, trying the common baud rates, then less common + // as the device cannot be guaranteed to power up with the key pin high + // we are at the mercy of the baud rate stored in the module. + Bluetooth.begin(9600); + digitalWrite(KeyPin, HIGH); + delay(500); + + DebugPort.println("\r\n\r\nAttempting to detect HC-05 Bluetooth module..."); + + int BTidx = 0; + int maxTries = sizeof(BTRates)/sizeof(int); + for(BTidx = 0; BTidx < maxTries; BTidx++) { + DebugPort.print(" @ "); + DebugPort.print(BTRates[BTidx]); + DebugPort.print(" baud... "); + Bluetooth.begin(BTRates[BTidx]); // open serial port at a certain baud rate + Bluetooth.print("\r\n"); + Bluetooth.setTimeout(50); + + if(Bluetooth_ATCommand("AT\r\n")) { + DebugPort.println(" OK."); + break; + } + // failed, try another baud rate + DebugPort.println(""); + Bluetooth.flush(); + } + + DebugPort.println(""); + if(BTidx == maxTries) { + DebugPort.println("FAILED to detect HC-05 Bluetooth module :-("); + } + else { + if(BTRates[BTidx] == 115200) { + DebugPort.println("HC-05 found and already set to 115200 baud, skipping Init."); + bHC05Available = true; + } + else { + do { + DebugPort.println("HC-05 found"); + + DebugPort.print(" Setting Name to \"DieselHeater\"... "); + if(!Bluetooth_ATCommand("AT+NAME=\"DieselHeater\"\r\n")) { + DebugPort.println("FAILED"); + break; + } + DebugPort.println("OK"); + + DebugPort.print(" Setting baud rate to 115200N81..."); + if(!Bluetooth_ATCommand("AT+UART=115200,1,0\r\n")) { + DebugPort.println("FAILED"); + break; + }; + DebugPort.println("OK"); + + Bluetooth.begin(115200); + bHC05Available = true; + + } while(0); + + } + } + digitalWrite(KeyPin, LOW); // leave HC-05 command mode + + delay(500); + + if(!bHC05Available) + Bluetooth.end(); // close serial port if no module found + + DebugPort.println(""); +} + +void Bluetooth_Check() +{ + // check for data coming back over Bluetooth + if(bHC05Available) { + if(Bluetooth.available()) { + char rxVal = Bluetooth.read(); + if(isControl(rxVal)) { // "End of Line" + BluetoothRxData += '\0'; + Command_Interpret(BluetoothRxData); + BluetoothRxData = ""; + } + else { + BluetoothRxData += rxVal; // append new char to our Rx buffer + } + } + } +} + + +void Bluetooth_SendFrame(const char* pHdr, const CProtocol& Frame) +{ + if(bHC05Available) { + if(Frame.verifyCRC()) { + Bluetooth.print(pHdr); + Bluetooth.write(Frame.Data, 24); + } + else { + DebugPort.print("Bluetooth data not sent, CRC error "); + DebugPort.println(pHdr); + } + } +} + +// local function, typically to perform Hayes commands with HC-05 +bool Bluetooth_ATCommand(const char* cmd) +{ + if(bHC05Available) { + Bluetooth.print(cmd); + char RxBuffer[16]; + memset(RxBuffer, 0, 16); + int read = Bluetooth.readBytesUntil('\n', RxBuffer, 16); // \n is not included in returned string! + if((read == 3) && (0 == strcmp(RxBuffer, "OK\r")) ) { + return true; + } + return false; + } + return false; +} + +#endif + + diff --git a/Arduino/SenderTrial2/NVStorage.cpp b/Arduino/SenderTrial2/NVStorage.cpp new file mode 100644 index 0000000..86e2697 --- /dev/null +++ b/Arduino/SenderTrial2/NVStorage.cpp @@ -0,0 +1,118 @@ +#include "Arduino.h" +#include "NVStorage.h" + + +CHeaterStorage::CHeaterStorage() +{ + calValues.Pmin = 14; + calValues.Pmax = 40; + calValues.Fmin = 1500; + calValues.Fmax = 4500; + calValues.ThermostatMode = 1; + calValues.setTemperature = 22; +} + +unsigned char +CHeaterStorage::getPmin() +{ + return calValues.Pmin; +} + +unsigned char +CHeaterStorage::getPmax() +{ + return calValues.Pmax; +} + +unsigned short +CHeaterStorage::getFmin() +{ + return calValues.Fmin; +} + +unsigned short +CHeaterStorage::getFmax() +{ + return calValues.Fmax; +} + +unsigned char +CHeaterStorage::getTemperature() +{ + return calValues.setTemperature; +} + +unsigned char +CHeaterStorage::getThermostatMode() +{ + return calValues.ThermostatMode; +} + +void +CHeaterStorage::setPmin(unsigned char val) +{ + calValues.Pmin = val; +} + +void +CHeaterStorage::setPmax(unsigned char val) +{ + calValues.Pmax = val; +} + +void +CHeaterStorage::setFmin(unsigned short val) +{ + calValues.Fmin = val; +} + +void +CHeaterStorage::setFmax(unsigned short val) +{ + calValues.Fmax = val; +} + +void +CHeaterStorage::setTemperature(unsigned char val) +{ + calValues.setTemperature = val; +} + +void +CHeaterStorage::setThermostatMode(unsigned char val) +{ + calValues.ThermostatMode = val; +} + + +/////////////////////////////////////////////////////////////////////////////////////// +// ESP32 +// +#ifdef ESP32 + +CESP32HeaterStorage::CESP32HeaterStorage() +{ +} + +CESP32HeaterStorage::~CESP32HeaterStorage() +{ + preferences.end(); +} + +void +CESP32HeaterStorage::init() +{ + preferences.begin("dieselheater", false); +} + +void CESP32HeaterStorage::load() +{ + preferences.getBytes("calValues", &calValues, sizeof(sNVStore)); +} + +void CESP32HeaterStorage::save() +{ + preferences.putBytes("calValues", &calValues, sizeof(sNVStore)); +} + +#endif // ESP32 \ No newline at end of file diff --git a/Arduino/SenderTrial2/NVStorage.h b/Arduino/SenderTrial2/NVStorage.h new file mode 100644 index 0000000..a3dac97 --- /dev/null +++ b/Arduino/SenderTrial2/NVStorage.h @@ -0,0 +1,69 @@ + +struct sNVStore { + unsigned char Pmin; + unsigned char Pmax; + unsigned short Fmin; + unsigned short Fmax; + unsigned char ThermostatMode; + unsigned char setTemperature; +}; + +class CNVStorage { + public: + CNVStorage() {}; + virtual ~CNVStorage() {}; + virtual void init() = 0; + virtual void load() = 0; + virtual void save() = 0; +}; + + +class CHeaterStorage : public CNVStorage { +protected: + sNVStore calValues; +public: + CHeaterStorage(); + virtual ~CHeaterStorage() {}; + + // TODO: These are only here to allow building without fully + // fleshing out NV storage for Due, Mega etc + // these should be removed once complete (pure virtual) + void init() {}; + void load() {}; + void save() {}; + + + unsigned char getPmin(); + unsigned char getPmax(); + unsigned short getFmin(); + unsigned short getFmax(); + unsigned char getTemperature(); + unsigned char getThermostatMode(); + + void setPmin(unsigned char val); + void setPmax(unsigned char val); + void setFmin(unsigned short val); + void setFmax(unsigned short val); + void setTemperature(unsigned char val); + void setThermostatMode(unsigned char val); +}; + + +#ifdef ESP32 + +#include + +class CESP32HeaterStorage : public CHeaterStorage { + Preferences preferences; +public: + CESP32HeaterStorage(); + virtual ~CESP32HeaterStorage(); + void init(); + void load(); + void save(); +}; + +#endif + +extern CHeaterStorage* pNVStorage; + diff --git a/Arduino/SenderTrial2/Protocol.cpp b/Arduino/SenderTrial2/Protocol.cpp index b19e941..34ef4d7 100644 --- a/Arduino/SenderTrial2/Protocol.cpp +++ b/Arduino/SenderTrial2/Protocol.cpp @@ -1,14 +1,15 @@ #include #include "Protocol.h" - +#include "debugport.h" + unsigned short -CProtocol::CalcCRC(int len) +CProtocol::CalcCRC(int len) const { // calculate a CRC-16/MODBUS checksum using the first 22 bytes of the data array unsigned short wCRCWord = 0xFFFF; int wLength = len; - unsigned char* pData = Data; + const unsigned char* pData = Data; while (wLength--) { unsigned char nTemp = *pData++ ^ wCRCWord; @@ -34,20 +35,29 @@ CProtocol::setCRC(unsigned short CRC) unsigned short -CProtocol::getCRC() +CProtocol::getCRC() const { unsigned short CRC; CRC = Data[22]; // MSB of CRC in Data[22] CRC <<= 8; CRC |= Data[23]; // LSB of CRC in Data[23] + return CRC; } // return true for CRC match bool -CProtocol::verifyCRC() +CProtocol::verifyCRC() const { unsigned short CRC = CalcCRC(22); // calculate CRC based on first 22 bytes - return (getCRC() == CRC); // does it match the stored values? + unsigned short FrameCRC = getCRC(); + bool bOK = (FrameCRC == CRC); + if(!bOK) { + DebugPort.print("verifyCRC FAILED: calc:"); + DebugPort.print(CRC, HEX); + DebugPort.print(" data:"); + DebugPort.println(FrameCRC, HEX); + } + return bOK; // does it match the stored values? } CProtocol& @@ -57,21 +67,6 @@ CProtocol::operator=(const CProtocol& rhs) return *this; } -int -CProtocol::getCommand() -{ - return Controller.Command; -} - -void -CProtocol::setCommand(int cmd) -{ - Controller.Command = cmd; -} - -/*unsigned char getCommand(); - void setCommand(int mode);*/ - void CProtocol::setFan_Min(short Speed) diff --git a/Arduino/SenderTrial2/Protocol.h b/Arduino/SenderTrial2/Protocol.h index 2da41e4..ccb8c3c 100644 --- a/Arduino/SenderTrial2/Protocol.h +++ b/Arduino/SenderTrial2/Protocol.h @@ -98,16 +98,24 @@ public: public: CProtocol() { Init(0); }; CProtocol(int TxMode) { Init(TxMode); }; + void Init(int Txmode); // CRC handlers - unsigned short CalcCRC(int len); // calculate and set the CRC upon len bytes - void setCRC(); // calculate and set the CRC in the buffer + unsigned short CalcCRC(int len) const; // calculate the CRC upon len bytes + void setCRC(); // calculate and set the CRC in the buffer void setCRC(unsigned short CRC); // set the CRC in the buffer - unsigned short getCRC(); // extract CRC value from buffer - bool verifyCRC(); // return true for CRC match - // command - int getCommand(); - void setCommand(int mode); + unsigned short getCRC() const; // extract CRC value from buffer + bool verifyCRC() const; // return true for CRC match + + void setActiveMode() { Controller.Byte0 = 0x76; }; // this allows heater to save tuning params to EEPROM + void setPassiveMode() { Controller.Byte0 = 0x78; }; // this prevents heater saving tuning params to EEPROM + // command helpers + void resetCommand() { setRawCommand(0x00); }; + void onCommand() { setRawCommand(0xA0); }; + void offCommand() { setRawCommand(0x05); }; + // raw command + int getRawCommand() { return Controller.Command; }; + void setRawCommand(int mode) { Controller.Command = mode; }; // Run state unsigned char getRunState() { return Heater.RunState; }; void setRunState(unsigned char state) { Heater.RunState = state; }; @@ -144,10 +152,13 @@ public: unsigned char getTemperature_Desired() { return Controller.DesiredTemperature; }; unsigned char getTemperature_Min() { return Controller.MinTemperature; }; unsigned char getTemperature_Max() { return Controller.MaxTemperature; }; + void setThermostatMode(unsigned on) { Controller.OperatingMode = on ? 0x32 : 0xCD; }; + // glow plug short getGlowPlug_Current(); // glow plug current short getGlowPlug_Voltage(); // glow plug voltage void setGlowPlug_Current(short ampsx100); // glow plug current void setGlowPlug_Voltage(short voltsx10); // glow plug voltage + // heat exchanger short getTemperature_HeatExchg(); // temperature of heat exchanger void setTemperature_HeatExchg(short degC); // temperature of heat exchanger diff --git a/Arduino/SenderTrial2/SenderTrial2.ino b/Arduino/SenderTrial2/SenderTrial2.ino index 43c10cb..b52ccb5 100644 --- a/Arduino/SenderTrial2/SenderTrial2.ino +++ b/Arduino/SenderTrial2/SenderTrial2.ino @@ -2,7 +2,7 @@ Chinese Heater Half Duplex Serial Data Sending Tool Connects to the blue wire of a Chinese heater, which is the half duplex serial link. - Sends and receives data from serial port 1. + Sends and receives data from hardware serial port 1. Terminology: Tx is to the heater unit, Rx is from the heater unit. @@ -12,7 +12,7 @@ This software can connect to the blue wire in a normal OEM system, detecting the OEM controller and allowing extraction of the data or injecting on/off commands. - If Pin 21 is grounded on the Due, this simple stream will be reported over USB and + If Pin 21 is grounded on the Due, this simple stream will be reported over Serial and no control from the Arduino will be allowed. This allows sniffing of the blue wire in a normal system. @@ -20,11 +20,11 @@ If it has been > 100ms since the last blue wire activity this indicates a new frame sequence is starting from the OEM controller. Synchronise as such then count off the next 24 bytes storing them in the Controller's - data array. These bytes are then reported over USB to the PC in ASCII. + data array. These bytes are then reported over Serial to the PC in ASCII. It is then expected the heater will respond with it's 24 bytes. Capture those bytes and store them in the Heater1 data array. - Once again these bytes are then reported over USB to the PC in ASCII. + Once again these bytes are then reported over Serial to the PC in ASCII. If no activity is sensed in a second, it is assumed no controller is attached and we have full control over the heater. @@ -64,18 +64,33 @@ #include "Protocol.h" #include "TxManage.h" +#include "pins.h" +#include "NVStorage.h" +#include "debugport.h" -void SerialReport(const char* hdr, const unsigned char* pData, const char* ftr); -void BluetoothDetect(); -bool BlueToothCommand(const char* cmd); -void BlueToothReport(const char* pHdr, const unsigned char Data[24]); -void BluetoothInterpret(); +#define BLUETOOTH +#define DEBUG_BTRX + +#ifdef BLUETOOTH +#include "Bluetooth.h" +#endif + +#if defined(__arm__) +// Required for Arduino Due, UARTclass is derived from HardwareSerial +static UARTClass& BlueWire(Serial1); +#else +// for ESP32, Mega +// HardwareSerial is it for these boards +static HardwareSerial& BlueWire(Serial1); +#endif + +void DebugReportFrame(const char* hdr, const CProtocol& Frame, const char* ftr); class CommStates { public: // comms states enum eCS { - Idle, ControllerRx, ControllerReport, HeaterRx1, HeaterReport1, SelfTx, HeaterRx2, HeaterReport2 + Idle, ControllerRx, ControllerReport, HeaterRx1, HeaterReport1, BTC_Tx, HeaterRx2, HeaterReport2 }; CommStates() { set(Idle); @@ -96,123 +111,100 @@ private: int m_Count; }; -#if defined(__arm__) -// for Arduino Due -UARTClass& USB(Serial); -UARTClass& BlueWire(Serial1); -UARTClass& BlueTooth(Serial2); -#else -// for ESP32, Mega -HardwareSerial& USB(Serial); -HardwareSerial& BlueWire(Serial1); -#if defined(__ESP32__) -// ESP32 -HardwareSerial& BlueTooth(Serial2); // TODO: make proper ESP32 BT client -#else -// Mega -HardwareSerial& BlueTooth(Serial2); -#endif -#endif - -#if defined(__ESP32__) -const int TxEnbPin = 22; -#else -const int TxEnbPin = 20; -#endif -const int ListenOnlyPin = 21; -const int KeyPin = 15; -const int Tx1Pin = 18; -const int Rx1Pin = 19; -const int Tx2Pin = 16; -const int Rx2Pin = 17; - -const int BTRates[] = { - 9600, 38400, 115200, 19200, 57600, 2400, 4800 -}; CommStates CommState; -CTxManage TxManage(TxEnbPin, Serial1); -CProtocol Controller; // most recent data packet received from OEM controller found on blue wire -CProtocol Heater1; // data packet received from heater in response to OEM controller packet -CProtocol Heater2; // data packet received from heater in response to our packet -CProtocol SelfParams(CProtocol::CtrlMode); // holds our local parameters, used in case of no OEM controller +CTxManage TxManage(TxEnbPin, BlueWire); +CProtocol OEMControllerFrame; // data packet received from heater in response to OEM controller packet +CProtocol HeaterFrame1; // data packet received from heater in response to OEM controller packet +CProtocol HeaterFrame2; // data packet received from heater in response to our packet +CProtocol DefaultBTCParams(CProtocol::CtrlMode); // defines the default parameters, used in case of no OEM controller long lastRxTime; // used to observe inter character delays -bool bBlueToothAvailable = false; -String BluetoothRxData; +// setup Non Volatile storage +// this is very much hardware dependent, we can use the ESP32's FLASH +#ifdef ESP32 +CESP32HeaterStorage NVStorage; +#else +CHeaterStorage NVStorage; // dummy, for now +#endif +CHeaterStorage* pNVStorage = NULL; void setup() { - // initialize listening serial port + // 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, special baud rate for Chinese heater controllers + // Tx/Rx data to/from heater, + // Note special baud rate for Chinese heater controllers pinMode(ListenOnlyPin, INPUT_PULLUP); pinMode(KeyPin, OUTPUT); -// pinMode(Tx2Pin, OUTPUT); digitalWrite(KeyPin, LOW); -// digitalWrite(Tx2Pin, HIGH); #if defined(__arm__) || defined(__AVR__) BlueWire.begin(25000); - pinMode(19, INPUT_PULLUP); // required for MUX to work properly -#else if defined(__ESP32__) + pinMode(Rx1Pin, INPUT_PULLUP); // required for MUX to work properly +#elif ESP32 // ESP32 -#define RXD2 16 -#define TXD2 17 - BlueWire.begin(25000, SERIAL_8N1, Rx1Pin, Tx1Pin); + BlueWire.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 // initialise serial monitor on serial port 0 - USB.begin(115200); + // this is the usual USB connection to a PC + DebugPort.begin(115200); // prepare for first long delay detection lastRxTime = millis(); - TxManage.begin(); // ensure Tx enable pin setup + TxManage.begin(); // ensure Tx enable pin is setup // define defaults should heater controller be missing - SelfParams.setTemperature_Desired(23); - SelfParams.setTemperature_Actual(22); - SelfParams.Controller.OperatingVoltage = 120; - SelfParams.setPump_Min(16); - SelfParams.setPump_Max(55); - SelfParams.setFan_Min(1680); - SelfParams.setFan_Max(4500); + DefaultBTCParams.setTemperature_Desired(23); + DefaultBTCParams.setTemperature_Actual(22); + DefaultBTCParams.Controller.OperatingVoltage = 120; + DefaultBTCParams.setPump_Min(16); + DefaultBTCParams.setPump_Max(55); + DefaultBTCParams.setFan_Min(1680); + DefaultBTCParams.setFan_Max(4500); - BluetoothDetect(); +#ifdef BLUETOOTH + Bluetooth_Init(); +#endif + + // create pointer to CHeaterStorage + // via the magic of polymorphism we can use this to access whatever + // storage is required for a specifc platform in a uniform way + pNVStorage = &NVStorage; + pNVStorage->init(); + pNVStorage->load(); } +// main functional loop is based about a state machine approach, waiting for data +// to appear upon the blue wire, and marshalling into an appropriate receive buffer +// according to the state. + + void loop() { unsigned long timenow = millis(); // check for test commands received from PC Over USB - if(USB.available()) { - char rxval = USB.read(); + if(DebugPort.available()) { + char rxval = DebugPort.read(); if(rxval == '+') { - TxManage.RequestOn(); + TxManage.queueOnRequest(); } if(rxval == '-') { - TxManage.RequestOff(); - } - } - - // check for data coming back over Bluetooth - if(BlueTooth.available()) { - char rxVal = BlueTooth.read(); - if(isControl(rxVal)) { // "End of Line" - BluetoothRxData += '\0'; - BluetoothInterpret(); - } - else { - BluetoothRxData += rxVal; // append new char to our Rx buffer + TxManage.queueOffRequest(); } } +#ifdef BLUETOOTH + Bluetooth_Check(); // check for Bluetooth activity +#endif // Handle time interval where we send data to the blue wire - if(CommState.is(CommStates::SelfTx)) { + if(CommState.is(CommStates::BTC_Tx)) { lastRxTime = timenow; // we are pumping onto blue wire, track this activity! if(TxManage.CheckTx(timenow) ) { // monitor our data delivery CommState.set(CommStates::HeaterRx2); // then await heater repsonse @@ -226,25 +218,28 @@ void loop() // check for no rx traffic => no OEM controller if(CommState.is(CommStates::Idle) && (RxTimeElapsed >= 970)) { // have not seen any receive data for a second. - // OEM controller probably not connected. - // Skip to SelfTx, sending our own settings. - CommState.set(CommStates::SelfTx); - bool bOurParams = true; - TxManage.Start(SelfParams, timenow, bOurParams); - BlueToothReport("[BTC]", SelfParams.Data); // BTC => Bluetooth Controller :-) + // OEM controller is probably not connected. + // Skip state machine immediately to BTC_Tx, sending our own settings. + CommState.set(CommStates::BTC_Tx); + bool isBTCmaster = true; + TxManage.PrepareFrame(DefaultBTCParams, isBTCmaster); // use our parameters, and mix in NV storage values + TxManage.Start(timenow); +#ifdef BLUETOOTH + Bluetooth_SendFrame("[BTC]", TxManage.getFrame()); // BTC => Bluetooth Controller :-) +#endif } // precautionary action if all 24 bytes were not received whilst expecting them if(RxTimeElapsed > 50) { if( CommState.is(CommStates::ControllerRx) || - CommState.is(CommStates::HeaterRx1) || + CommState.is(CommStates::HeaterRx1) || CommState.is(CommStates::HeaterRx2) ) { CommState.set(CommStates::Idle); } } - // read from port 1, the "blue wire" (to/from heater), store according to CommState + // read data from Serial port 1, the "blue wire" (to/from heater), store according to CommState if (BlueWire.available()) { lastRxTime = timenow; @@ -257,19 +252,19 @@ void loop() int inByte = BlueWire.read(); // read hex byte if( CommState.is(CommStates::ControllerRx) ) { - if(CommState.saveData(Controller.Data, inByte) ) { + if(CommState.saveData(OEMControllerFrame.Data, inByte) ) { CommState.set(CommStates::ControllerReport); } } else if( CommState.is(CommStates::HeaterRx1) ) { - if( CommState.saveData(Heater1.Data, inByte) ) { + if( CommState.saveData(HeaterFrame1.Data, inByte) ) { CommState.set(CommStates::HeaterReport1); } } else if( CommState.is(CommStates::HeaterRx2) ) { - if( CommState.saveData(Heater2.Data, inByte) ) { + if( CommState.saveData(HeaterFrame2.Data, inByte) ) { CommState.set(CommStates::HeaterReport2); } } @@ -279,174 +274,127 @@ void loop() if( CommState.is(CommStates::ControllerReport) ) { // filled controller frame, report - BlueToothReport("[OEM]", Controller.Data); - SerialReport("Ctrl ", Controller.Data, " "); +#ifdef BLUETOOTH + // echo received OEM controller frame over Bluetooth, using [OEM] header + Bluetooth_SendFrame("[OEM]", OEMControllerFrame); +#endif + DebugReportFrame("OEM ", OEMControllerFrame, " "); CommState.set(CommStates::HeaterRx1); } else if(CommState.is(CommStates::HeaterReport1) ) { // received heater frame (after controller message), report - SerialReport("Htr1 ", Heater1.Data, "\r\n"); - BlueToothReport("[HTR]", Heater1.Data); + DebugReportFrame("Htr1 ", HeaterFrame1, "\r\n"); +#ifdef BLUETOOTH + // echo heater reponse data to Bluetooth client + Bluetooth_SendFrame("[HTR]", HeaterFrame1); +#endif if(digitalRead(ListenOnlyPin)) { - bool bOurParams = false; - TxManage.Start(Controller, timenow, bOurParams); - CommState.set(CommStates::SelfTx); + bool isBTCmaster = false; + TxManage.PrepareFrame(OEMControllerFrame, isBTCmaster); // parrot OEM parameters, but block NV modes + TxManage.Start(timenow); + CommState.set(CommStates::BTC_Tx); } else { - CommState.set(CommStates::Idle); // "Listen Only" input held low, don't send out Tx + CommState.set(CommStates::Idle); // "Listen Only" input is held low, don't send out Tx } } else if( CommState.is(CommStates::HeaterReport2) ) { // received heater frame (after our control message), report - SerialReport("Htr2 ", Heater2.Data, "\r\n"); + DebugReportFrame("Htr2 ", HeaterFrame2, "\r\n"); // if(!digitalRead(ListenOnlyPin)) { - BlueToothReport("[HTR]", Heater2.Data); // pin not grounded, suppress duplicate to BT +#ifdef BLUETOOTH + Bluetooth_SendFrame("[HTR]", HeaterFrame2); // pin not grounded, suppress duplicate to BT +#endif // } CommState.set(CommStates::Idle); } } // loop -void SerialReport(const char* hdr, const unsigned char* pData, const char* ftr) +void DebugReportFrame(const char* hdr, const CProtocol& Frame, const char* ftr) { - USB.print(hdr); // header + DebugPort.print(hdr); // header for(int i=0; i<24; i++) { char str[16]; - sprintf(str, "%02X ", pData[i]); // build 2 dig hex values - USB.print(str); // and print + sprintf(str, "%02X ", Frame.Data[i]); // build 2 dig hex values + DebugPort.print(str); // and print } - USB.print(ftr); // footer + DebugPort.print(ftr); // footer } -void BluetoothDetect() +void Command_Interpret(String line) { - #if defined(__ESP32__) - #else - // search for BlueTooth adapter, trying the common baud rates, then less common - // as the device cannot be guaranteed to power up with the key pin high - // we are at the mercy of the baud rate stored in the module. - BlueTooth.begin(9600); - digitalWrite(KeyPin, HIGH); - delay(500); + unsigned char cVal; + unsigned short sVal; + + #ifdef DEBUG_BTRX + DebugPort.println(line); + DebugPort.println(); + #endif - USB.println("\r\n\r\nAttempting to detect HC-05 Bluetooth module..."); - - int BTidx = 0; - int maxTries = sizeof(BTRates)/sizeof(int); - for(BTidx = 0; BTidx < maxTries; BTidx++) { - USB.print(" @ "); - USB.print(BTRates[BTidx]); - USB.print(" baud... "); - BlueTooth.begin(BTRates[BTidx]); // open serial port at a certain baud rate - BlueTooth.print("\r\n"); - BlueTooth.setTimeout(50); - - if(BlueToothCommand("AT\r\n")) { - USB.println(" OK."); - break; - } - // failed, try another baud rate - USB.println(""); - BlueTooth.flush(); - } - - USB.println(""); - if(BTidx == maxTries) { - USB.println("FAILED to detect HC-05 Bluetooth module :-("); - } - else { - if(BTRates[BTidx] == 115200) { - USB.println("HC-05 found and already set to 115200 baud, skipping Init."); - bBlueToothAvailable = true; - } - else { - do { - USB.println("HC-05 found"); - - USB.print(" Setting Name to \"DieselHeater\"... "); - if(!BlueToothCommand("AT+NAME=\"DieselHeater\"\r\n")) { - USB.println("FAILED"); - break; - } - USB.println("OK"); - - USB.print(" Setting baud rate to 115200N81..."); - if(!BlueToothCommand("AT+UART=115200,1,0\r\n")) { - USB.println("FAILED"); - break; - }; - USB.println("OK"); - - BlueTooth.begin(115200); - bBlueToothAvailable = true; - - } while(0); - - } - } - digitalWrite(KeyPin, LOW); // leave HC-05 command mode - - delay(500); - - if(!bBlueToothAvailable) - BlueTooth.end(); // close serial port if no module found - - USB.println(""); -#endif -} - -bool BlueToothCommand(const char* cmd) -{ - if(bBlueToothAvailable) { - BlueTooth.print(cmd); - char RxBuffer[16]; - memset(RxBuffer, 0, 16); - int read = BlueTooth.readBytesUntil('\n', RxBuffer, 16); // \n is not included in returned string! - if((read == 3) && (0 == strcmp(RxBuffer, "OK\r")) ) { - return true; - } - return false; - } - return false; -} - -void BlueToothReport(const char* pHdr, const unsigned char Data[24]) -{ - if(bBlueToothAvailable) { - BlueTooth.print(pHdr); - BlueTooth.write(Data, 24); - } -} - -void BluetoothInterpret() -{ - if(BluetoothRxData.startsWith("[CMD]") ) { - USB.write("BT command Rx'd: "); + if(line.startsWith("[CMD]") ) { + DebugPort.write("BT command Rx'd: "); // incoming command from BT app! - BluetoothRxData.remove(0, 5); // strip away "[CMD]" header - if(BluetoothRxData.startsWith("ON")) { - USB.write("ON\n"); - TxManage.RequestOn(); + line.remove(0, 5); // strip away "[CMD]" header + if(line.startsWith("ON") ) { + DebugPort.write("ON\n"); + TxManage.queueOnRequest(); } - else if(BluetoothRxData.startsWith("OFF")) { - USB.write("OFF\n"); - TxManage.RequestOff(); + else if(line.startsWith("OFF")) { + DebugPort.write("OFF\n"); + TxManage.queueOffRequest(); } - else if(BluetoothRxData.startsWith("Pmin")) { - USB.write("Pmin\n"); + else if(line.startsWith("Pmin")) { + line.remove(0, 4); + DebugPort.write("Pmin="); + cVal = (line.toFloat() * 10) + 0.5; + DebugPort.println(cVal); + pNVStorage->setPmin(cVal); } - else if(BluetoothRxData.startsWith("Pmax")) { - USB.write("Pmax\n"); + else if(line.startsWith("Pmax")) { + line.remove(0, 4); + DebugPort.write("Pmax="); + cVal = (line.toFloat() * 10) + 0.5; + DebugPort.println(cVal); + pNVStorage->setPmax(cVal); } - else if(BluetoothRxData.startsWith("Fmin")) { - USB.write("Fmin\n"); + else if(line.startsWith("Fmin")) { + line.remove(0, 4); + DebugPort.print("Fmin="); + sVal = line.toInt(); + DebugPort.println(sVal); + pNVStorage->setFmin(sVal); } - else if(BluetoothRxData.startsWith("Fmax")) { - USB.write("Fmax\n"); + else if(line.startsWith("Fmax")) { + line.remove(0, 4); + DebugPort.print("Fmax="); + sVal = line.toInt(); + DebugPort.println(sVal); + pNVStorage->setFmax(sVal); } + else if(line.startsWith("save")) { + line.remove(0, 4); + DebugPort.write("save\n"); + pNVStorage->save(); + } + else if(line.startsWith("degC")) { + line.remove(0, 4); + DebugPort.write("degC="); + cVal = line.toInt(); + DebugPort.println(cVal); + pNVStorage->setTemperature(cVal); + } + else if(line.startsWith("Mode")) { + line.remove(0, 4); + DebugPort.write("Mode="); + cVal = !pNVStorage->getThermostatMode(); + pNVStorage->setThermostatMode(cVal); + DebugPort.println(cVal); + } + } - BluetoothRxData = ""; //flush string, ready for new data -} \ No newline at end of file +} + diff --git a/Arduino/SenderTrial2/TxManage.cpp b/Arduino/SenderTrial2/TxManage.cpp index 19c70b7..e3c95ef 100644 --- a/Arduino/SenderTrial2/TxManage.cpp +++ b/Arduino/SenderTrial2/TxManage.cpp @@ -1,52 +1,90 @@ -#include #include "TxManage.h" +#include "NVStorage.h" -extern void SerialReport(const char* hdr, const unsigned char* pData, const char* ftr); +extern void DebugReportFrame(const char* hdr, const CProtocol&, const char* ftr); -CTxManage::CTxManage(int TxEnbPin, HardwareSerial& serial) : - m_Serial(serial), - m_Frame(CProtocol::CtrlMode) +CTxManage::CTxManage(int TxGatePin, HardwareSerial& serial) : + m_BlueWireSerial(serial), + m_TxFrame(CProtocol::CtrlMode) { m_bOnReq = false; m_bOffReq = false; m_bTxPending = false; m_nStartTime = 0; - m_nTxEnbPin = TxEnbPin; + m_nTxGatePin = TxGatePin; } void CTxManage::begin() { - pinMode(m_nTxEnbPin, OUTPUT); - digitalWrite(m_nTxEnbPin, LOW); + pinMode(m_nTxGatePin, OUTPUT); + digitalWrite(m_nTxGatePin, LOW); } void -CTxManage::RequestOn() +CTxManage::queueOnRequest() { m_bOnReq = true; } void -CTxManage::RequestOff() +CTxManage::queueOffRequest() { m_bOffReq = true; } -void -CTxManage::Start(const CProtocol& ref, unsigned long timenow, bool self) +void +CTxManage::PrepareFrame(const CProtocol& basisFrame, bool isBTCmaster) { - m_Frame = ref; + // copy supplied frame, typically this will be the values an OEM controller delivered + // which means we parrot that data by default. + // When parroting, we must especially avoid ping ponging "set temperature"! + // Otherwise we are supplied with the default params for standalone mode, which we + // then instil the NV parameters + m_TxFrame = basisFrame; + + // ALWAYS install on/off commands if required + m_TxFrame.resetCommand(); // no command upon blue wire initially, unless a request is pending + if(m_bOnReq) { + m_bOnReq = false; + m_TxFrame.onCommand(); + } + if(m_bOffReq) { + m_bOffReq = false; + m_TxFrame.offCommand(); + } + // 0x78 prevents the controller showing bum information when we parrot the OEM controller // heater is happy either way, the OEM controller has set the max/min stuff already - m_Frame.Data[0] = self ? 0x76 : 0x78; + if(isBTCmaster) { + m_TxFrame.setActiveMode(); // this allows heater to svae the tuning params to EEPROM + m_TxFrame.setFan_Min(pNVStorage->getFmin()); + m_TxFrame.setFan_Max(pNVStorage->getFmax()); + m_TxFrame.setPump_Min(pNVStorage->getPmin()); + m_TxFrame.setPump_Max(pNVStorage->getPmax()); + m_TxFrame.setThermostatMode(pNVStorage->getThermostatMode()); + m_TxFrame.setTemperature_Desired(pNVStorage->getTemperature()); + } + else { + m_TxFrame.setPassiveMode(); // this prevents the tuning parameters being saved by heater + } - if(timenow == 0) + // ensure CRC valid + m_TxFrame.setCRC(); +} + +void +CTxManage::Start(unsigned long timenow) +{ + if(timenow == 0) // avoid a black hole if millis() wraps timenow++; m_nStartTime = timenow; m_bTxPending = true; } +// generate a Tx Gate, then send the TxFrame to the Blue wire +// Note the serial data is ISR driven, we need to hold off +// for a while to let teh buffewred dat clear before closing the Tx Gate. bool CTxManage::CheckTx(unsigned long timenow) { @@ -56,51 +94,20 @@ CTxManage::CheckTx(unsigned long timenow) if(diff > m_nStartDelay) { // begin front porch of Tx gating pulse - digitalWrite(m_nTxEnbPin, HIGH); + digitalWrite(m_nTxGatePin, HIGH); } if(m_bTxPending && (diff > (m_nStartDelay + m_nFrontPorch))) { // begin serial transmission m_bTxPending = false; - _send(); + m_BlueWireSerial.write(m_TxFrame.Data, 24); // write native binary values + DebugReportFrame("BTC ", m_TxFrame, " "); // report frame to debug port } if(diff > (m_nStartDelay + m_nFrameTime)) { // conclude Tx gating m_nStartTime = 0; - digitalWrite(m_nTxEnbPin, LOW); + digitalWrite(m_nTxGatePin, LOW); } } return m_nStartTime == 0; } -void -CTxManage::_send() -{ - // install on/off commands if required - if(m_bOnReq) { - m_bOnReq = false; - m_Frame.Controller.Command = 0xa0; - } - else if(m_bOffReq) { - m_bOffReq = false; - m_Frame.Controller.Command = 0x05; - } - else { - m_Frame.Controller.Command = 0x00; - } - - // ensure CRC valid - m_Frame.setCRC(); - - // send to heater - using binary - for(int i=0; i<24; i++) { - m_Serial.write(m_Frame.Data[i]); // write native binary values - } - - Report(); -} - -void -CTxManage::Report() -{ - SerialReport("Self ", m_Frame.Data, " "); -} diff --git a/Arduino/SenderTrial2/TxManage.h b/Arduino/SenderTrial2/TxManage.h index fd7f9cb..b33b105 100644 --- a/Arduino/SenderTrial2/TxManage.h +++ b/Arduino/SenderTrial2/TxManage.h @@ -8,23 +8,25 @@ class CTxManage const int m_nFrontPorch = 2; public: - CTxManage(int TxEnbPin, HardwareSerial& serial); - void RequestOn(); - void RequestOff(); - void Start(const CProtocol& ref, unsigned long timenow, bool self); + CTxManage(int TxGatePin, HardwareSerial& serial); + void queueOnRequest(); + void queueOffRequest(); + void PrepareFrame(const CProtocol& Frame, bool isBTCmaster); + void Start(unsigned long timenow); bool CheckTx(unsigned long timenow); - void Report(); void begin(); + const CProtocol& getFrame() const { return m_TxFrame; }; private: - CProtocol m_Frame; + CProtocol m_TxFrame; bool m_bOnReq; bool m_bOffReq; bool m_bTxPending; - int m_nTxEnbPin; + int m_nTxGatePin; unsigned long m_nStartTime; - HardwareSerial& m_Serial; + HardwareSerial& m_BlueWireSerial; - void _send(); }; +extern CTxManage TxManage; + diff --git a/Arduino/SenderTrial2/debugport.h b/Arduino/SenderTrial2/debugport.h new file mode 100644 index 0000000..2def60c --- /dev/null +++ b/Arduino/SenderTrial2/debugport.h @@ -0,0 +1,8 @@ + + +#if defined(__arm__) +// Typically Arduino Due +static UARTClass& DebugPort(Serial); +#else +static HardwareSerial& DebugPort(Serial); // reference Serial as DebugPort +#endif \ No newline at end of file diff --git a/Arduino/SenderTrial2/pins.h b/Arduino/SenderTrial2/pins.h new file mode 100644 index 0000000..fce6f39 --- /dev/null +++ b/Arduino/SenderTrial2/pins.h @@ -0,0 +1,8 @@ +const int TxEnbPin = 22; +const int ListenOnlyPin = 21; +const int KeyPin = 15; +const int Tx1Pin = 18; +const int Rx1Pin = 19; +const int Tx2Pin = 16; +const int Rx2Pin = 17; +