7e1a4940ac
Can start heater if high humidity Altitude reported to heater per BMP180 in OEM controllers. lowered min Hz limit to 500
295 lines
12 KiB
C++
295 lines
12 KiB
C++
/*
|
|
* This file is part of the "bluetoothheater" distribution
|
|
* (https://gitlab.com/mrjones.id.au/bluetoothheater)
|
|
*
|
|
* Copyright (C) 2018 Ray Jones <ray@mrjones.id.au>
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "TxManage.h"
|
|
#include "../Utility/NVStorage.h"
|
|
#include "../Utility/helpers.h"
|
|
|
|
//#define DEBUG_THERMOSTAT
|
|
|
|
|
|
extern void DebugReportFrame(const char* hdr, const CProtocol&, const char* ftr);
|
|
|
|
// CTxManage is used to send a data frame to the blue wire
|
|
//
|
|
// As the blue wire is bidirectional, we need to only allow our transmit data
|
|
// to reach the blue wire when we actually want to send data.
|
|
// At all other times we are listening to the blue wire, receiving any async data
|
|
//
|
|
// This requires external circuitry to toggle the Tx/Rx modes.
|
|
// A "Tx Gating" signal is used.
|
|
// when high, transmit data is sent to the blue wire
|
|
// when low, transmit data is blocked (Hi-Z)
|
|
//
|
|
// Ideally the circuit also prevents feeding back our own Tx data into the Rx pin
|
|
// but the main software loop handles this situation by only accepting Rx data when expected.
|
|
//
|
|
// Timing diagram
|
|
// ____________________
|
|
// Tx Gate ____________________| |___________________________
|
|
// _____________________________________________________________________
|
|
// Tx Data |||||||||||||||
|
|
|
|
unsigned long CTxManage::m_nStartTime = 0;
|
|
int CTxManage::m_nTxGatePin = 0;
|
|
|
|
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_nTxGatePin = TxGatePin;
|
|
_rawCommand = 0;
|
|
m_HWTimer = NULL;
|
|
}
|
|
|
|
// static function used for the tx gate termination
|
|
void CTxManage::GateTerminate()
|
|
{
|
|
digitalWrite(m_nTxGatePin, LOW); // default to receive mode
|
|
m_nStartTime = 0; // cancel, we are DONE
|
|
}
|
|
|
|
|
|
void CTxManage::begin()
|
|
{
|
|
pinMode(m_nTxGatePin, OUTPUT);
|
|
digitalWrite(m_nTxGatePin, LOW); // default to receive mode
|
|
|
|
// use a hardware timer to terminate the Tx gate shortly after the completion of the 24 byte transmit packet
|
|
m_HWTimer = timerBegin(2, 80, true);
|
|
//set time in uS of Tx gate from when actual tx data bytes are loaded
|
|
// 240 bits @ 25000bps is 9.6ms, we'll use 9.7ms for a bit of tolerance
|
|
timerAlarmWrite(m_HWTimer, 10000-300, false);
|
|
timerAttachInterrupt(m_HWTimer, &GateTerminate, true);
|
|
timerAlarmDisable(m_HWTimer); //disable interrupt for now
|
|
timerSetAutoReload(m_HWTimer, false);
|
|
}
|
|
|
|
void
|
|
CTxManage::queueOnRequest(bool set)
|
|
{
|
|
m_bOnReq = set; // allow cancellation via heater response frame decode
|
|
m_bOffReq = false;
|
|
}
|
|
|
|
void
|
|
CTxManage::queueOffRequest(bool set)
|
|
{
|
|
m_bOffReq = set; // allow cancellation via heater response frame decode
|
|
m_bOnReq = false;
|
|
}
|
|
|
|
void
|
|
CTxManage::queueRawCommand(uint8_t val)
|
|
{
|
|
_rawCommand = val;
|
|
}
|
|
|
|
void
|
|
CTxManage::PrepareFrame(const CProtocol& basisFrame, bool isBTCmaster)
|
|
{
|
|
// 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
|
|
#ifndef PROTOCOL_INVESTIGATION
|
|
m_TxFrame.resetCommand(); // no command upon blue wire initially, unless a request is pending
|
|
if(_rawCommand) {
|
|
m_TxFrame.setRawCommand(_rawCommand);
|
|
_rawCommand = 0;
|
|
}
|
|
else {
|
|
if(m_bOnReq) {
|
|
// m_bOnReq = false; // requires cancel via queueOnRequest(false)
|
|
m_TxFrame.onCommand();
|
|
}
|
|
if(m_bOffReq) {
|
|
// m_bOffReq = false; // requires cancel via queueOffRequest(false)
|
|
m_TxFrame.offCommand();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// 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
|
|
if(isBTCmaster) {
|
|
m_TxFrame.setActiveMode(); // this allows heater to save the tuning params to EEPROM
|
|
m_TxFrame.setFan_Min(NVstore.getHeaterTuning().Fmin);
|
|
m_TxFrame.setFan_Max(NVstore.getHeaterTuning().Fmax);
|
|
m_TxFrame.setPump_Min(NVstore.getHeaterTuning().getPmin());
|
|
m_TxFrame.setPump_Max(NVstore.getHeaterTuning().getPmax());
|
|
|
|
float altitude;
|
|
if(getTempSensor().getAltitude(altitude)) { // if a BME280 is fitted
|
|
m_TxFrame.setAltitude(altitude);
|
|
}
|
|
else {
|
|
m_TxFrame.setAltitude(3500); // default height - yes it is weird, but that's what the simple controllers send!
|
|
}
|
|
|
|
float tActual = getTemperatureSensor();
|
|
int8_t s8Temp = (int8_t)(tActual + 0.5);
|
|
m_TxFrame.setTemperature_Actual(s8Temp); // use current temp, for now
|
|
m_TxFrame.setHeaterDemand(getDemandDegC());
|
|
m_TxFrame.setThermostatModeProtocol(1); // assume using thermostat control for now
|
|
|
|
if(!getThermostatModeActive()) {
|
|
m_TxFrame.setThermostatModeProtocol(0); // not using any form of thermostat control
|
|
m_TxFrame.setHeaterDemand(getDemandPump()); // set fixed Hz demand instead
|
|
m_TxFrame.setTemperature_Actual(0); // must force actual to 0 for Hz mode
|
|
}
|
|
else if(NVstore.getUserSettings().ThermostatMethod) {
|
|
uint8_t ThermoMode = NVstore.getUserSettings().ThermostatMethod; // get the METHOD of thermostat control
|
|
float Window = NVstore.getUserSettings().ThermostatWindow;
|
|
float tCurrent = getTemperatureSensor();
|
|
float tDesired = float(getDemandDegC());
|
|
float tDelta = tCurrent - tDesired;
|
|
float fTemp;
|
|
#ifdef DEBUG_THERMOSTAT
|
|
DebugPort.printf("Window=%.1f tCurrent=%.1f tDesired=%.1f tDelta=%.1f\r\n", Window, tCurrent, tDesired, tDelta);
|
|
#endif
|
|
Window /= 2;
|
|
switch(ThermoMode) {
|
|
|
|
case 3: // GPIO controlled thermostat mode
|
|
if(getExternalThermostatModeActive()) {
|
|
if(getExternalThermostatOn()) {
|
|
s8Temp = m_TxFrame.getTemperature_Max(); // input active (contact closure) - max burn
|
|
}
|
|
else {
|
|
s8Temp = m_TxFrame.getTemperature_Min(); // input not active (contact open) - min burn
|
|
}
|
|
m_TxFrame.setHeaterDemand(s8Temp);
|
|
m_TxFrame.setThermostatModeProtocol(0); // direct heater to use Hz Mode
|
|
m_TxFrame.setTemperature_Actual(0); // must force actual to 0 for Hz mode
|
|
break;
|
|
}
|
|
// deliberately fall through if not enabled for GPIO control to standard thermostat
|
|
// |
|
|
// V
|
|
case 0: // conventional heater controlled thermostat mode
|
|
m_TxFrame.setThermostatModeProtocol(1); // using heater thermostat control
|
|
s8Temp = (int8_t)(tActual + 0.5);
|
|
m_TxFrame.setTemperature_Actual(s8Temp);
|
|
#ifdef DEBUG_THERMOSTAT
|
|
DebugPort.printf("Conventional thermostat mode: tActual = %d\r\n", u8Temp);
|
|
#endif
|
|
break;
|
|
|
|
case 1: // heater controlled thermostat mode - BUT actual temp is tweaked via a changed window
|
|
m_TxFrame.setThermostatModeProtocol(1); // using heater thermostat control
|
|
s8Temp = (int8_t)(tActual + 0.5); // use rounded actual unless within window
|
|
if(fabs(tDelta) < Window) {
|
|
// hold at desired if inside window
|
|
s8Temp = getDemandDegC();
|
|
}
|
|
else if(fabs(tDelta) <= 1.0) {
|
|
// force outside if delta is <= 1 but greater than window
|
|
s8Temp = getDemandDegC() + ((tDelta > 0) ? 1 : -1);
|
|
}
|
|
m_TxFrame.setTemperature_Actual(s8Temp);
|
|
#ifdef DEBUG_THERMOSTAT
|
|
DebugPort.printf("Heater controlled windowed thermostat mode: tActual=%d\r\n", u8Temp);
|
|
#endif
|
|
break;
|
|
|
|
case 2: // BTC controlled thermostat mode
|
|
// map linear deviation within thermostat window to a Hz value,
|
|
// Hz mode however uses the desired temperature field, somewhere between 8 - 35 for min/max
|
|
// so create a desired "temp" according the the current hystersis
|
|
tDelta /= Window; // convert tDelta to fraction of window (CAUTION - may be > +-1 !)
|
|
#ifdef DEBUG_THERMOSTAT
|
|
DebugPort.printf("Linear window thermostat mode: Fraction=%f", tDelta);
|
|
#endif
|
|
fTemp = (m_TxFrame.getTemperature_Max() + m_TxFrame.getTemperature_Min()) * 0.5; // midpoint - tDelta = 0 hinges here
|
|
tDelta *= (m_TxFrame.getTemperature_Max() - fTemp); // linear offset from setpoint
|
|
fTemp -= tDelta; // lower Hz when over temp, higher Hz when under!
|
|
// bounds limit - recall original tDelta was NOT managed prior!
|
|
LOWERLIMIT(fTemp, m_TxFrame.getTemperature_Min());
|
|
UPPERLIMIT(fTemp, m_TxFrame.getTemperature_Max());
|
|
// apply modifed desired temperature (works in conjunction with thermostatmode = 0!)
|
|
s8Temp = (int8_t)(fTemp + 0.5);
|
|
m_TxFrame.setHeaterDemand(s8Temp);
|
|
m_TxFrame.setThermostatModeProtocol(0); // direct heater to use Hz Mode
|
|
m_TxFrame.setTemperature_Actual(0); // must force actual to 0 for Hz mode
|
|
#ifdef DEBUG_THERMOSTAT
|
|
DebugPort.printf(" tDesired (pseudo Hz demand) = %d\r\n", u8Temp);
|
|
#endif
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_TxFrame.Controller.OperatingVoltage = NVstore.getHeaterTuning().sysVoltage;
|
|
m_TxFrame.Controller.FanSensor = NVstore.getHeaterTuning().fanSensor;
|
|
m_TxFrame.Controller.GlowDrive = NVstore.getHeaterTuning().glowDrive;
|
|
}
|
|
else {
|
|
m_TxFrame.setPassiveMode(); // this prevents the tuning parameters being saved by heater
|
|
}
|
|
|
|
// ensure CRC valid
|
|
m_TxFrame.setCRC();
|
|
}
|
|
|
|
void
|
|
CTxManage::Start(unsigned long timenow)
|
|
{
|
|
m_nStartTime = timenow + m_nStartDelay; // create a dwell period if an OEM controller is present after it's data exchange
|
|
m_nStartTime |= 1; // avoid a black hole if millis() has wrapped to zero
|
|
m_bTxPending = true;
|
|
}
|
|
|
|
// generate a Tx Gate, then send the TxFrame to the Blue wire
|
|
// Note the serial data is ISR driven, we supply all 24 bytes to the Tx buffer which is then drained automatically
|
|
// the Tx Gate is closed shortly after the last byte is completed.
|
|
bool
|
|
CTxManage::CheckTx(unsigned long timenow)
|
|
{
|
|
if(m_nStartTime && m_bTxPending) {
|
|
|
|
long diff = timenow - m_nStartTime;
|
|
|
|
// dwell since OEM exchange has expired ?
|
|
if(diff >= 0) {
|
|
// begin front porch of Tx gating pulse
|
|
digitalWrite(m_nTxGatePin, HIGH);
|
|
}
|
|
if(diff >= m_nFrontPorch) {
|
|
// front porch expired, start actual serial transmission
|
|
// Tx gate remains held high
|
|
// it is then brought low by the timer alarm callback, which also cancels m_nStartTime
|
|
m_bTxPending = false;
|
|
m_BlueWireSerial.write(m_TxFrame.Data, 24); // write native binary values
|
|
timerWrite(m_HWTimer, 0); //reset tx gate timeout
|
|
timerAlarmEnable(m_HWTimer); // timeout will cause cessation of the Tx gate
|
|
}
|
|
}
|
|
return m_nStartTime == 0; // returns true when done
|
|
}
|
|
|