baed4b3079
Changed Tx headers to [OEM] and [BTC] for the OEM controller and this Bluetooth controller. Added #ifdefs to accomodate Mega and ESP32. Built and tested on ESP32, Serial1 talks to heater OK using standard pin numbering :-)
452 lines
No EOL
13 KiB
C++
452 lines
No EOL
13 KiB
C++
/*
|
|
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.
|
|
|
|
Terminology: Tx is to the heater unit, Rx is from the heater unit.
|
|
|
|
Typical data frame timing on the blue wire is:
|
|
__Tx_Rx____________________________Tx_Rx____________________________Tx_Rx___________
|
|
|
|
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
|
|
no control from the Arduino will be allowed.
|
|
This allows sniffing of the blue wire in a normal system.
|
|
|
|
The binary data is received from the line.
|
|
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.
|
|
|
|
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.
|
|
|
|
If no activity is sensed in a second, it is assumed no controller is attached and we
|
|
have full control over the heater.
|
|
|
|
Either way we can now inject a message onto the blue wire allowing our custom
|
|
on/off control.
|
|
We must remain synchronous with the OEM controller if it exists otherwise E-07
|
|
faults will be caused.
|
|
|
|
Typical data frame timing on the blue wire is then:
|
|
__OEMTx_HtrRx__OurTx_HtrRx____________OEMTx_HtrRx__OurTx_HtrRx____________OEMTx_HtrRx__OurTx_HtrRx_________
|
|
|
|
The second HtrRx to the next OEMTx delay is always > 100ms and is paced by the OEM controller.
|
|
The delay before seeing Heater Rx data after any Tx is usually much less than 10ms.
|
|
But this does rise if new max/min or voltage settings are sent.
|
|
**The heater only ever sends Rx data in response to a data frame from a controller**
|
|
|
|
A HC-05 Bluetooth module is attached to Serial2:
|
|
TXD -> Rx2 (pin 17)
|
|
RXD -> Tx2 (pin 16)
|
|
EN(key) -> pin 15
|
|
|
|
|
|
This code only works with boards that have more than one hardware serial port like Arduino
|
|
Mega, Due, Zero etc.
|
|
|
|
|
|
The circuit:
|
|
- a Tx Rx multiplexer is required to combine the Arduino's Tx1 And Rx1 pins onto the blue wire.
|
|
- a Tx Enable signal from pin 20 controls the multiplexer
|
|
- Serial logging software on Serial0 via USB link
|
|
|
|
created 23 Sep 2018 by Ray Jones
|
|
|
|
This example code is in the public domain.
|
|
*/
|
|
|
|
#include "Protocol.h"
|
|
#include "TxManage.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();
|
|
|
|
class CommStates {
|
|
public:
|
|
// comms states
|
|
enum eCS {
|
|
Idle, ControllerRx, ControllerReport, HeaterRx1, HeaterReport1, SelfTx, HeaterRx2, HeaterReport2
|
|
};
|
|
CommStates() {
|
|
set(Idle);
|
|
}
|
|
void set(eCS eState) {
|
|
m_State = eState;
|
|
m_Count = 0;
|
|
}
|
|
bool is(eCS eState) {
|
|
return m_State == eState;
|
|
}
|
|
bool saveData(unsigned char* pData, unsigned char val, int limit = 24) { // returns true when buffer filled
|
|
pData[m_Count++] = val;
|
|
return m_Count == limit;
|
|
}
|
|
private:
|
|
int m_State;
|
|
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
|
|
long lastRxTime; // used to observe inter character delays
|
|
bool bBlueToothAvailable = false;
|
|
String BluetoothRxData;
|
|
|
|
|
|
|
|
void setup()
|
|
{
|
|
// initialize listening serial port
|
|
// 25000 baud, Tx and Rx channels of Chinese heater comms interface:
|
|
// Tx/Rx data to/from heater, 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__)
|
|
// ESP32
|
|
#define RXD2 16
|
|
#define TXD2 17
|
|
BlueWire.begin(25000, SERIAL_8N1, Rx1Pin, Tx1Pin);
|
|
#endif
|
|
|
|
// initialise serial monitor on serial port 0
|
|
USB.begin(115200);
|
|
|
|
// prepare for first long delay detection
|
|
lastRxTime = millis();
|
|
|
|
TxManage.begin(); // ensure Tx enable pin 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);
|
|
|
|
BluetoothDetect();
|
|
}
|
|
|
|
void loop()
|
|
{
|
|
unsigned long timenow = millis();
|
|
|
|
// check for test commands received from PC Over USB
|
|
if(USB.available()) {
|
|
char rxval = USB.read();
|
|
if(rxval == '+') {
|
|
TxManage.RequestOn();
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
|
|
// Handle time interval where we send data to the blue wire
|
|
if(CommState.is(CommStates::SelfTx)) {
|
|
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
|
|
}
|
|
}
|
|
|
|
|
|
// calc elapsed time since last rxd byte to detect no other controller, or start of frame sequence
|
|
unsigned long RxTimeElapsed = timenow - lastRxTime;
|
|
|
|
// 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 :-)
|
|
}
|
|
|
|
// 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::HeaterRx2) ) {
|
|
|
|
CommState.set(CommStates::Idle);
|
|
}
|
|
}
|
|
|
|
// read from port 1, the "blue wire" (to/from heater), store according to CommState
|
|
if (BlueWire.available()) {
|
|
|
|
lastRxTime = timenow;
|
|
|
|
// detect start of a new frame sequence from OEM controller
|
|
if( CommState.is(CommStates::Idle) && (RxTimeElapsed > 100)) {
|
|
CommState.set(CommStates::ControllerRx);
|
|
}
|
|
|
|
int inByte = BlueWire.read(); // read hex byte
|
|
|
|
if( CommState.is(CommStates::ControllerRx) ) {
|
|
if(CommState.saveData(Controller.Data, inByte) ) {
|
|
CommState.set(CommStates::ControllerReport);
|
|
}
|
|
}
|
|
|
|
else if( CommState.is(CommStates::HeaterRx1) ) {
|
|
if( CommState.saveData(Heater1.Data, inByte) ) {
|
|
CommState.set(CommStates::HeaterReport1);
|
|
}
|
|
}
|
|
|
|
else if( CommState.is(CommStates::HeaterRx2) ) {
|
|
if( CommState.saveData(Heater2.Data, inByte) ) {
|
|
CommState.set(CommStates::HeaterReport2);
|
|
}
|
|
}
|
|
|
|
} // BlueWire.available
|
|
|
|
|
|
if( CommState.is(CommStates::ControllerReport) ) {
|
|
// filled controller frame, report
|
|
BlueToothReport("[OEM]", Controller.Data);
|
|
SerialReport("Ctrl ", Controller.Data, " ");
|
|
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);
|
|
|
|
if(digitalRead(ListenOnlyPin)) {
|
|
bool bOurParams = false;
|
|
TxManage.Start(Controller, timenow, bOurParams);
|
|
CommState.set(CommStates::SelfTx);
|
|
}
|
|
else {
|
|
CommState.set(CommStates::Idle); // "Listen Only" input 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");
|
|
// if(!digitalRead(ListenOnlyPin)) {
|
|
BlueToothReport("[HTR]", Heater2.Data); // pin not grounded, suppress duplicate to BT
|
|
// }
|
|
CommState.set(CommStates::Idle);
|
|
}
|
|
|
|
} // loop
|
|
|
|
void SerialReport(const char* hdr, const unsigned char* pData, const char* ftr)
|
|
{
|
|
USB.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
|
|
}
|
|
USB.print(ftr); // footer
|
|
}
|
|
|
|
void BluetoothDetect()
|
|
{
|
|
#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);
|
|
|
|
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: ");
|
|
// incoming command from BT app!
|
|
BluetoothRxData.remove(0, 5); // strip away "[CMD]" header
|
|
if(BluetoothRxData.startsWith("ON")) {
|
|
USB.write("ON\n");
|
|
TxManage.RequestOn();
|
|
}
|
|
else if(BluetoothRxData.startsWith("OFF")) {
|
|
USB.write("OFF\n");
|
|
TxManage.RequestOff();
|
|
}
|
|
else if(BluetoothRxData.startsWith("Pmin")) {
|
|
USB.write("Pmin\n");
|
|
}
|
|
else if(BluetoothRxData.startsWith("Pmax")) {
|
|
USB.write("Pmax\n");
|
|
}
|
|
else if(BluetoothRxData.startsWith("Fmin")) {
|
|
USB.write("Fmin\n");
|
|
}
|
|
else if(BluetoothRxData.startsWith("Fmax")) {
|
|
USB.write("Fmax\n");
|
|
}
|
|
}
|
|
BluetoothRxData = ""; //flush string, ready for new data
|
|
} |