Automatically detects if another controller exists on the blue wire.

If found, it parrots the settings, but allows on/off commands to be inserted by a user.
If no other controller detected, it falls back and delivers our settings each second.

Currently reports all packets to USB, but will move onto Bluetooth delivery of heater data soon.
This commit is contained in:
rljonesau 2018-09-23 16:31:09 +10:00
parent 99e1b9d6bc
commit 9818b33e47
5 changed files with 310 additions and 186 deletions

View file

@ -2,12 +2,12 @@
#include "CFrame.h"
unsigned short
CFrame::CalcCRC()
CFrame::CalcCRC(int len)
{
// calculate a CRC-16/MODBUS checksum using the first 22 bytes of the data array
unsigned short wCRCWord = 0xFFFF;
int wLength = 22;
int wLength = len;
unsigned char* pData = Data;
while (wLength--)
{
@ -22,7 +22,7 @@ CFrame::CalcCRC()
void
CFrame::setCRC()
{
setCRC(CalcCRC());
setCRC(CalcCRC(22));
}
void
@ -32,6 +32,14 @@ CFrame::setCRC(unsigned short CRC)
Data[23] = (CRC >> 0) & 0xff; // LSB of CRC in Data[23]
}
CFrame&
CFrame::operator=(CFrame& rhs)
{
memcpy(Data, rhs.Data, 24);
return *this;
}
unsigned short
CFrame::getCRC()
{
@ -45,7 +53,7 @@ CFrame::getCRC()
bool
CFrame::verifyCRC()
{
unsigned short CRC = CalcCRC(); // calculate CRC based on first 22 bytes
unsigned short CRC = CalcCRC(22); // calculate CRC based on first 22 bytes
return (getCRC() == CRC); // does it match the stored values?
}

View file

@ -1,33 +1,35 @@
#ifndef _CFRAME_H_
#define _CFRAME_H_
class CFrame {
public:
union {
unsigned char Data[24];
struct {
unsigned char Byte0; // always 0x76
unsigned char Len; // always 0x16 == 22
unsigned char Command; // transient commands: 00: NOP, 0xa0 START, 0x05: STOP
unsigned char ActualTemperature; // 1degC / digit
unsigned char DesiredTemperature; // 1degC / digit
unsigned char MinPumpFreq; // 0.1Hz/digit
unsigned char MaxPumpFreq; // 0.1Hz/digit
unsigned char MinFanRPM_MSB; // 16 bit - big endian MSB
unsigned char MinFanRPM_LSB; // 16 bit - big endian LSB : 1 RPM / digit
unsigned char MaxFanRPM_MSB; // 16 bit - big endian MSB
unsigned char MaxFanRPM_LSB; // 16 bit - big endian LSB : 1 RPM / digit
unsigned char OperatingVoltage; // 120, 240 : 0.1V/digit
unsigned char FanSensor; // SN-1 or SN-2
unsigned char OperatingMode; // 0x32:Thermostat, 0xCD:Fixed
unsigned char MinTemperature; // Minimum settable temperature
unsigned char MaxTemperature; // Maximum settable temperature
unsigned char MinTempRise; // temp rise to sense running OK
unsigned char Prime; // 00: normal, 0x5A: fuel prime
unsigned char Unknown1_MSB; // always 0x01
unsigned char Unknown1_LSB; // always 0x2c "300 secs = max run without burn detected"?
unsigned char Unknown2_MSB; // always 0x0d
unsigned char Unknown2_LSB; // always 0xac "3500 ?"
unsigned char CRC_MSB;
unsigned char CRC_LSB;
unsigned char Byte0; // [0] always 0x76
unsigned char Len; // [1] always 0x16 == 22
unsigned char Command; // [2] transient commands: 00: NOP, 0xa0 START, 0x05: STOP
unsigned char ActualTemperature; // [3] 1degC / digit
unsigned char DesiredTemperature; // [4] 1degC / digit
unsigned char MinPumpFreq; // [5] 0.1Hz/digit
unsigned char MaxPumpFreq; // [6] 0.1Hz/digit
unsigned char MinFanRPM_MSB; // [7] 16 bit - big endian MSB
unsigned char MinFanRPM_LSB; // [8] 16 bit - big endian LSB : 1 RPM / digit
unsigned char MaxFanRPM_MSB; // [9] 16 bit - big endian MSB
unsigned char MaxFanRPM_LSB; // [10] 16 bit - big endian LSB : 1 RPM / digit
unsigned char OperatingVoltage; // [11] 120, 240 : 0.1V/digit
unsigned char FanSensor; // [12] SN-1 or SN-2
unsigned char OperatingMode; // [13] 0x32:Thermostat, 0xCD:Fixed
unsigned char MinTemperature; // [14] Minimum settable temperature
unsigned char MaxTemperature; // [15] Maximum settable temperature
unsigned char MinTempRise; // [16] temp rise to sense running OK
unsigned char Prime; // [17] 00: normal, 0x5A: fuel prime
unsigned char Unknown1_MSB; // [18] always 0x01
unsigned char Unknown1_LSB; // [19] always 0x2c "300 secs = max run without burn detected"?
unsigned char Unknown2_MSB; // [20] always 0x0d
unsigned char Unknown2_LSB; // [21] always 0xac "3500 ?"
unsigned char CRC_MSB; // [22]
unsigned char CRC_LSB; // [23]
} Tx;
struct {
unsigned char Byte0; // always 0x76
@ -97,7 +99,7 @@ public:
CFrame(int TxMode) { Init(TxMode); };
void Init(int Txmode);
// CRC handlers
unsigned short CalcCRC(); // calculate and set the CRC upon first 22 bytes
unsigned short CalcCRC(int len); // calculate and set the CRC upon first 22 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
@ -124,5 +126,8 @@ public:
short getTemperature_GlowPin(); // temperature of glow pin
short getTemperature_HeatExchg(); // temperature of heat exchanger
short getTemperature_Inlet(); // temperature near inlet
CFrame& operator=(CFrame& rhs);
};
#endif

View file

@ -42,24 +42,49 @@
*/
#include "CFrame.h"
#include "TxManage.h"
void SerialReport(const char* hdr, const unsigned char* pData, const char* ftr);
class CommStates {
public:
enum eCS {
Idle, CtrlRx, CtrlRpt, HtrRx1, HtrRpt1, SelfTx, HtrRx2, HtrRpt2
};
CommStates() {
set(Idle);
}
void set(eCS eState) {
m_State = eState;
m_Count = 0;
}
bool is(eCS eState) {
return m_State == eState;
}
bool rxData(unsigned char* pData, unsigned char val, int limit = 24) { // return true if buffer filled
pData[m_Count++] = val;
return m_Count == limit;
}
private:
int m_State;
int m_Count;
};
const int TxEnbPin = 17;
CommStates CommState;
CFrame Controller(CFrame::TxMode);
CFrame TxFrame(CFrame::TxMode);
CTxManage TxManage(TxEnbPin, Serial1);
CFrame Heater1;
CFrame Heater2;
const int TxEnbPin = 17;
long lastRxTime; // used to calculate inter character delay
long TxEnbTime; // used to reset TxEnb low
bool bOnEvent = false;
bool bOffEvent = false;
void CheckTx();
void setup()
{
pinMode(TxEnbPin, OUTPUT);
digitalWrite(TxEnbPin, LOW);
// 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
@ -72,196 +97,122 @@ void setup()
// prepare for first long delay detection
lastRxTime = millis();
TxManage.begin();
}
void loop()
{
static int count = 0;
static unsigned long lastTx = 0;
static bool bAllowTxSlot = false;
static int Stage = -1;
char str[16];
unsigned long timenow = millis();
// check for test commands received from PC Over USB
if(Serial.available()) {
char rxval = Serial.read();
if(rxval == '+') {
bOnEvent = true;
TxManage.RequestOn();
}
if(rxval == '-') {
bOffEvent = true;
TxManage.RequestOff();
}
}
if((Stage == 4) && (timenow - lastTx) > 10) {
TxEnbTime = timenow;
if(TxEnbTime == 0)
TxEnbTime++;
digitalWrite(TxEnbPin, HIGH);
if(CommState.is(CommStates::SelfTx)) {
// Interval where we should send data to the blue wire
lastRxTime = timenow; // not expecting rx data, but we are pumping onto blue wire!
TxManage.Tick(timenow); // keep trying to send our data
if(!TxManage.isBusy()) { // until completed
CommState.set(CommStates::HtrRx2); // then await heater repsonse
}
}
CheckTx();
// check serial data has gone quite for a while so we can trample in...
// calc elapsed time since last rxd byte to detect start of frame sequence
/* unsigned long TxSlot = timenow - lastRxTime;
// if(Slot > 50 && TxEnbTime == 0 && (bOnEvent || bOffEvent)) {
if(bAllowTxSlot && (TxSlot > 50) && (TxEnbTime == 0)) {
TxEnbTime = timenow;
if(TxEnbTime == 0)
TxEnbTime++;
digitalWrite(TxEnbPin, HIGH);
// 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 bSelfParams = true;
TxManage.Send(timenow, bSelfParams);
}
*/
// read from port 1, the "Tx Data" (to heater), send to the serial monitor:
// precaution if 24 bytes were not received whilst expecting them
if(RxTimeElapsed > 50) {
if( CommState.is(CommStates::CtrlRx) ||
CommState.is(CommStates::HtrRx1) ||
CommState.is(CommStates::HtrRx2) ) {
CommState.set(CommStates::Idle);
}
}
// read from port 1, the "blue wire" (to/from heater), store according to CommState
if (Serial1.available()) {
// calc elapsed time since last rxd byte to detect start of frame sequence
unsigned long diff = timenow - lastRxTime;
lastRxTime = timenow;
if((Stage == -1) && (diff > 100)) { // this indicates the start of a new frame sequence from the controller
Stage = 0;
count = 0;
if( CommState.is(CommStates::Idle) && (RxTimeElapsed > 100)) { // this indicates the start of a new frame sequence from another controller
CommState.set(CommStates::CtrlRx);
}
int inByte = Serial1.read(); // read hex byte
if(Stage == 0) {
Controller.Data[count++] = inByte;
if(count == 24) {
Stage = 1;
if( CommState.is(CommStates::CtrlRx) ) {
if(CommState.rxData(Controller.Data, inByte) ) {
CommState.set(CommStates::CtrlRpt);
}
}
if(Stage == 2) {
Heater1.Data[count++] = inByte;
if(count == 24) {
Stage = 3;
else if( CommState.is(CommStates::HtrRx1) ) {
if( CommState.rxData(Heater1.Data, inByte) ) {
CommState.set(CommStates::HtrRpt1);
}
}
if(Stage == 6) {
Heater2.Data[count++] = inByte;
if(count == 24) {
Stage = 7;
else if( CommState.is(CommStates::HtrRx2) ) {
if( CommState.rxData(Heater2.Data, inByte) ) {
CommState.set(CommStates::HtrRpt2);
}
}
}
} // Serial1.available
if( CommState.is(CommStates::CtrlRpt) ) {
// filled controller frame, report
SerialReport("Ctrl ", Controller.Data, " ");
CommState.set(CommStates::HtrRx1);
}
else if(CommState.is(CommStates::HtrRpt1) ) {
// received heater frame (after controller message), report
SerialReport("Htr1 ", Heater1.Data, "\r\n");
// dump to PC after capturing all 24 Rx bytes in a frame session
if(Stage == 1) { // filled controller frame, dump
TxManage.Copy(Controller); // replicate last obtained controller data
TxManage.Send(timenow, false);
CommState.set(CommStates::SelfTx);
}
else if( CommState.is(CommStates::HtrRpt2) ) {
// received heater frame (after our control message), report
char str[16];
sprintf(str, "Ctrl ", lastRxTime);
Serial.print(str); // print timestamp
for(int i=0; i<24; i++) {
sprintf(str, "%02X ", Controller.Data[i]); // make 2 dig hex values
Serial.print(str); // and print
}
// Serial.println(); // newline and done
SerialReport("Htr2 ", Heater2.Data, "\r\n");
Stage = 2;
count = 0;
CommState.set(CommStates::Idle);
}
} // Stage == 1
if(Stage == 3) { // filled heater frame, dump
char str[16];
sprintf(str, " Htr1 ", lastRxTime);
Serial.print(str); // print timestamp
for(int i=0; i<24; i++) {
sprintf(str, "%02X ", Heater1.Data[i]); // make 2 dig hex values
Serial.print(str); // and print
}
Serial.println(); // newline and done
Stage = 4;
count = 0;
lastTx = timenow;
} // Stage == 1
if(Stage == 7) { // filled heater frame, dump
char str[16];
sprintf(str, " Htr2 ", lastRxTime);
Serial.print(str); // print timestamp
for(int i=0; i<24; i++) {
sprintf(str, "%02X ", Heater2.Data[i]); // make 2 dig hex values
Serial.print(str); // and print
}
Serial.println(); // newline and done
Stage = -1;
count = 0;
} // Stage == 1
} // loop
void CheckTx()
void SerialReport(const char* hdr, const unsigned char* pData, const char* ftr)
{
char str[16];
if(TxEnbTime) {
long diff = timenow - TxEnbTime;
if(diff >= 12) {
TxEnbTime = 0;
digitalWrite(TxEnbPin, LOW);
Stage = 6;
}
Serial.print(hdr); // header
for(int i=0; i<24; i++) {
char str[16];
sprintf(str, "%02X ", pData[i]); // build 2 dig hex values
Serial.print(str); // and print
}
if((Stage == 4) && (timenow - lastTx) > 10) {
Stage = 5;
TxFrame.Tx.Byte0 = 0x78;
TxFrame.setTemperature_Desired(35);
TxFrame.setTemperature_Actual(22);
TxFrame.Tx.OperatingVoltage = 240;
TxFrame.setPump_Min(16);
TxFrame.setPump_Max(55);
TxFrame.setFan_Min(1680);
TxFrame.setFan_Max(4500);
if(bOnEvent) {
bOnEvent = false;
TxFrame.Tx.Command = 0xa0;
}
else if(bOffEvent) {
bOffEvent = false;
TxFrame.Tx.Command = 0x05;
}
else {
TxFrame.Tx.Command = 0x00;
}
TxFrame.setCRC();
// send to serial monitor using ASCII
Serial.print("Us "); // and print ASCII data
for(int i=0; i<24; i++) {
sprintf(str, "%02X ", TxFrame.Data[i]); // make 2 dig hex ASCII values
Serial.print(str); // and print ASCII data
}
// send to heater - using binary
digitalWrite(TxEnbPin, HIGH);
for(int i=0; i<24; i++) {
Serial1.write(TxFrame.Data[i]); // write native binary values
}
}
Serial.print(ftr); // footer
}

View file

@ -0,0 +1,127 @@
#include <Arduino.h>
#include "TxManage.h"
extern void SerialReport(const char* hdr, const unsigned char* pData, const char* ftr);
CTxManage::CTxManage(int TxEnbPin, USARTClass& serial) :
m_Serial(serial),
m_Frame(CFrame::TxMode)
{
m_bOnReq = false;
m_bOffReq = false;
m_bTxPending = false;
m_bSelf = true;
m_nStartTime = 0;
m_nTxEnbPin = TxEnbPin;
}
void CTxManage::begin()
{
pinMode(m_nTxEnbPin, OUTPUT);
digitalWrite(m_nTxEnbPin, LOW);
}
void
CTxManage::RequestOn()
{
m_bOnReq = true;
}
void
CTxManage::RequestOff()
{
m_bOffReq = true;
}
void
CTxManage::Copy(CFrame& ref)
{
m_Frame = ref;
}
bool
CTxManage::isBusy()
{
return m_nStartTime != 0;
}
void
CTxManage::Send(unsigned long timenow, bool self)
{
if(timenow == 0)
timenow++;
m_nStartTime = timenow;
m_bSelf = self;
m_bTxPending = true;
}
void
CTxManage::Tick(unsigned long timenow)
{
if(m_nStartTime) {
long diff = timenow - m_nStartTime;
if(diff > m_nStartDelay) {
// begin front porch of Tx gating pulse
digitalWrite(m_nTxEnbPin, HIGH);
}
if(m_bTxPending && (diff > (m_nStartDelay + m_nFrontPorch))) {
// begin serial transmission
m_bTxPending = false;
_send();
}
if(diff > (m_nStartDelay + m_nFrameTime)) {
// conclude Tx gating
m_nStartTime = 0;
digitalWrite(m_nTxEnbPin, LOW);
}
}
}
void
CTxManage::_send()
{
if(m_bSelf) {
m_Frame.Data[0] = 0x76; // required for heater to use the max min information
m_Frame.Data[1] = 0x16;
m_Frame.setTemperature_Desired(35);
m_Frame.setTemperature_Actual(22);
m_Frame.Tx.OperatingVoltage = 120;
m_Frame.setPump_Min(16);
m_Frame.setPump_Max(55);
m_Frame.setFan_Min(1680);
m_Frame.setFan_Max(4500);
}
else {
m_Frame.Data[0] = 0x78; // this prevents the controller trying to show bum information, heater uses controller max/min settings
}
if(m_bOnReq) {
m_bOnReq = false;
m_Frame.Tx.Command = 0xa0;
}
else if(m_bOffReq) {
m_bOffReq = false;
m_Frame.Tx.Command = 0x05;
}
else {
m_Frame.Tx.Command = 0x00;
}
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, " ");
}

View file

@ -0,0 +1,33 @@
#include <Arduino.h>
#include "CFrame.h"
class CTxManage
{
public:
CTxManage(int TxEnbPin, USARTClass& serial);
void RequestOn();
void RequestOff();
void Copy(CFrame& ref);
bool isBusy();
void Tick(unsigned long timenow);
void Send(unsigned long timenow, bool self);
void Report();
void begin();
private:
CFrame m_Frame;
bool m_bOnReq;
bool m_bOffReq;
bool m_bTxPending;
bool m_bSelf;
int m_nTxEnbPin;
unsigned long m_nStartTime;
USARTClass& m_Serial;
const int m_nStartDelay = 20;
const int m_nFrameTime = 14;
const int m_nFrontPorch = 2;
void _send();
};