mirror of https://github.com/arendst/Tasmota.git
Refactor and fix PID sensor (PID_USE_LOCAL_SENSOR) read race condition (#22162)
* Refactor and fix PID sensor (PID_USE_LOCAL_SENSOR) read race condition Refactor PID since it was calling pid.tick willy-nilly upon demand from MQTT and the web instead of on a periodic basis (and was being called with time interval of 0 when those times lined up!). Refactor web/mqtt display because there was shared code (that code turned out to be misguided and belonged in Every_Second loop, but now we are also similar to 39 thermostat) Logging revealed that the vast majority of the time the sensor JSON was parsed to obtain current sensor data when using PID local sensor, it was failing to parse (and it would typically only work for a second around TELE_PERIOD, but even then, not reliably). This bug almost certainly affects xdrv_39_thermostat too, but using xsns_75_prometheus.ino as a template, we are able to update PV once per second, which allows us to be a lot more responsive. There is no danger of being "too responsive" because that's the point of PID, and the PID loop already scales feedback by interval between ticks. * Reduce logging of PID now that query side-effects removed * Comment out all new logging, but leave present for next debugger
This commit is contained in:
parent
83eb8aeaa7
commit
694691e34a
|
@ -94,10 +94,12 @@
|
|||
|
||||
#define PID_UPDATE_SECS 0 // How often to run the pid algorithm (integer secs) or 0 to run the algorithm
|
||||
// each time a new pv value is received, for most applictions specify 0.
|
||||
// Otherwise set this to a time
|
||||
// that is short compared to the response of the process. For example,
|
||||
// something like 15 seconds may well be appropriate for a domestic room
|
||||
// heating application.
|
||||
// Otherwise set this to a time that is short compared to the response of
|
||||
// the process. For example, something like 15 seconds may well be appropriate
|
||||
// for a domestic room heating application. Keep in mind that the PID loop is
|
||||
// "tick"ed once per PV update if 0 is specified, so if PV are received at
|
||||
// non-constant intervals, you may be better off specifying a value here that
|
||||
// is larger than your typical PV update times.
|
||||
// May be adjusted via MQTT using cmnd PidUpdateSecs
|
||||
|
||||
#define PID_USE_TIMPROP 1 // To use an internal relay for a time proportioned output to drive the
|
||||
|
@ -109,18 +111,22 @@
|
|||
// not explicitly defined, then it will not be added by default.
|
||||
|
||||
#define PID_USE_LOCAL_SENSOR // If defined then the local sensor will be used for pv. Leave undefined if
|
||||
// this is not required. The rate that the sensor is read is defined by TELE_PERIOD
|
||||
// this is not required. The sensor is read every second, and you can slow
|
||||
// down updates for slow systems by explictly setting UPDATE_SECS, or more
|
||||
// prefably, appropriately adjusting P,I,D values.
|
||||
// If not using the sensor then you can supply process values via MQTT using
|
||||
// cmnd PidPv
|
||||
|
||||
#define PID_LOCAL_SENSOR_NAME "DS18B20" // local sensor name when PID_USE_LOCAL_SENSOR is defined above
|
||||
// the JSON payload is parsed for this sensor to find the present value
|
||||
// eg "ESP32":{"Temperature":31.4},"DS18B20":{"Temperature":12.6}
|
||||
// eg "ESP32", "DS18B20" and "ANALOG" in the following data:
|
||||
// "ESP32":{"Temperature":31.4},"DS18B20":{"Temperature":12.6},"ANALOG":{"Temperature1":19.6}
|
||||
|
||||
#define PID_LOCAL_SENSOR_TYPE D_JSON_TEMPERATURE // Choose one of D_JSON_TEMPERATURE D_JSON_HUMIDITY D_JSON_PRESSURE
|
||||
// or any string as the sensor type. The JSON payload is parsed for the
|
||||
// value in this field
|
||||
// eg "HDC1080":{"Temperature":24.8,"Humidity":79.2}
|
||||
// eg "Temperature", "Temperature1" in the following data:
|
||||
// "HDC1080":{"Temperature":24.8,"Humidity":79.2},"ANALOG":{"Temperature1":19.6}
|
||||
|
||||
#define PID_SHUTTER 1 // if using the PID to control a 3-way valve, create Tasmota Shutter and define the
|
||||
// number of the shutter here. Otherwise leave this commented out
|
||||
|
@ -237,6 +243,8 @@ struct {
|
|||
bool run_pid_now = false; // tells PID_Every_Second to run the pid algorithm
|
||||
long current_time_secs = 0; // a counter that counts seconds since initialisation
|
||||
bool shutdown = false; // power commands will be ignored when true
|
||||
|
||||
double power = 0;
|
||||
} Pid;
|
||||
|
||||
void PIDInit()
|
||||
|
@ -250,23 +258,31 @@ void PIDEverySecond() {
|
|||
Pid.current_time_secs++; // increment time
|
||||
// run the pid algorithm if Pid.run_pid_now is true or if the right number of seconds has passed or if too long has
|
||||
// elapsed since last pv update. If too long has elapsed the the algorithm will deal with that.
|
||||
if (Pid.run_pid_now || Pid.current_time_secs - Pid.last_pv_update_secs > Pid.max_interval || (Pid.update_secs != 0 && sec_counter++ % Pid.update_secs == 0)) {
|
||||
if (!Pid.run_pid_now) {
|
||||
PIDShowSensor(); // set actual process value
|
||||
}
|
||||
PIDProcessSensor(); // set actual process value, needs to have mqtt data already populated
|
||||
if (Pid.run_pid_now ||
|
||||
Pid.current_time_secs - Pid.last_pv_update_secs > Pid.max_interval ||
|
||||
(Pid.update_secs != 0 && sec_counter++ % Pid.update_secs == 0)) {
|
||||
PIDRun();
|
||||
Pid.run_pid_now = false;
|
||||
}
|
||||
}
|
||||
|
||||
void PIDShowSensor() {
|
||||
// Called each time new sensor data available, data in mqtt data in same format
|
||||
// as published in tele/SENSOR
|
||||
// Update period is specified in TELE_PERIOD
|
||||
void PIDProcessSensor() {
|
||||
// Called periodically (every second) to feed current sensor value
|
||||
// (if enabled; data in mqtt data in same format as published in
|
||||
// tele/SENSOR) and to log whether either sensor or input PV data is
|
||||
// stale
|
||||
|
||||
float sensor_reading = NAN;
|
||||
|
||||
#if defined PID_USE_LOCAL_SENSOR
|
||||
// Taking https://github.com/arendst/Tasmota/discussions/18328 as a
|
||||
// template of how to reliably read sensor values and not be subject
|
||||
// to race conditions affecting the completeness and parsability of
|
||||
// that data:
|
||||
ResponseClear();
|
||||
MqttShowSensor(true); //Pull sensor data
|
||||
|
||||
// copy the string into a new buffer that will be modified and
|
||||
// parsed to find the local sensor reading if it's there
|
||||
String buf = ResponseData();
|
||||
|
@ -276,12 +292,14 @@ void PIDShowSensor() {
|
|||
JsonParserToken value_token = root[PID_LOCAL_SENSOR_NAME].getObject()[PSTR(PID_LOCAL_SENSOR_TYPE)];
|
||||
if (value_token.isNum()) {
|
||||
sensor_reading = value_token.getFloat();
|
||||
//AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("PID: PIDProcessSensor: Got isNum: %s"), buf.c_str());
|
||||
}
|
||||
//} else {
|
||||
//AddLog(LOG_LEVEL_DEBUG, PSTR("PID: PIDProcessSensor: not valid JSON: %s"), buf.c_str());
|
||||
}
|
||||
#endif // PID_USE_LOCAL_SENSOR
|
||||
|
||||
if (!isnan(sensor_reading)) {
|
||||
|
||||
// pass the value to the pid alogorithm to use as current pv
|
||||
Pid.last_pv_update_secs = Pid.current_time_secs;
|
||||
Pid.pid.setPv(sensor_reading, Pid.last_pv_update_secs);
|
||||
|
@ -292,8 +310,9 @@ void PIDShowSensor() {
|
|||
}
|
||||
} else {
|
||||
// limit sensor not seen message to every 60 seconds to avoid flooding the logs
|
||||
if ((Pid.current_time_secs - Pid.last_pv_update_secs) > Pid.max_interval && ((Pid.current_time_secs - Pid.last_pv_update_secs)%60)==0) {
|
||||
AddLog(LOG_LEVEL_ERROR, PSTR("PID: Local temperature sensor missing for longer than PID_MAX_INTERVAL"));
|
||||
if ((Pid.current_time_secs - Pid.last_pv_update_secs) > Pid.max_interval &&
|
||||
((Pid.current_time_secs - Pid.last_pv_update_secs)%60)==0) {
|
||||
AddLog(LOG_LEVEL_ERROR, PSTR("PID: Local temperature sensor missing for longer than PID_MAX_INTERVAL - PID have fallen back to manual"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -401,11 +420,31 @@ void CmndSetShutdown(void) {
|
|||
ResponseCmndNumber(Pid.shutdown);
|
||||
}
|
||||
|
||||
void PIDShowValues(void) {
|
||||
#ifdef USE_WEBSERVER
|
||||
#define D_PID_DISPLAY_NAME "PID Controller"
|
||||
#define D_PID_SET_POINT "Set Point"
|
||||
#define D_PID_PRESENT_VALUE "Current Value"
|
||||
#define D_PID_POWER "Power"
|
||||
#define D_PID_MODE "Controller Mode"
|
||||
#define D_PID_MODE_AUTO "Auto"
|
||||
#define D_PID_MODE_MANUAL "Manual"
|
||||
#define D_PID_MODE_OFF "Off"
|
||||
|
||||
const char HTTP_PID_HL[] PROGMEM = "{s}<hr>{m}<hr>{e}";
|
||||
const char HTTP_PID_INFO[] PROGMEM = "{s}" D_PID_DISPLAY_NAME "{m}%s{e}";
|
||||
const char HTTP_PID_SP_FORMAT[] PROGMEM = "{s}%s " "{m}%*_f ";
|
||||
const char HTTP_PID_PV_FORMAT[] PROGMEM = "{s}%s " "{m}%*_f ";
|
||||
const char HTTP_PID_POWER_FORMAT[] PROGMEM = "{s}%s " "{m}%*_f " D_UNIT_PERCENT;
|
||||
#endif // USE_WEBSERVER
|
||||
|
||||
void PIDShowValues(bool json) {
|
||||
char str_buf[FLOATSZ];
|
||||
char chr_buf;
|
||||
int i_buf;
|
||||
double d_buf;
|
||||
float f_buf;
|
||||
|
||||
if (json) {
|
||||
ResponseAppend_P(PSTR(",\"PID\":{"));
|
||||
|
||||
d_buf = Pid.pid.getPv();
|
||||
|
@ -446,67 +485,56 @@ void PIDShowValues(void) {
|
|||
|
||||
i_buf = (Pid.current_time_secs - Pid.last_pv_update_secs) > Pid.pid.getMaxInterval();
|
||||
ResponseAppend_P(PSTR("\"PidSensorLost\":%d,"), i_buf);
|
||||
// The actual power value
|
||||
d_buf = Pid.pid.tick(Pid.current_time_secs);
|
||||
|
||||
d_buf = Pid.power;
|
||||
dtostrfd(d_buf, 2, str_buf);
|
||||
ResponseAppend_P(PSTR("\"PidPower\":%s"), str_buf);
|
||||
|
||||
ResponseAppend_P(PSTR("}"));
|
||||
}
|
||||
ResponseJsonEnd();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSERVER
|
||||
#define D_PID_DISPLAY_NAME "PID Controller"
|
||||
#define D_PID_SET_POINT "Set Point"
|
||||
#define D_PID_PRESENT_VALUE "Current Value"
|
||||
#define D_PID_POWER "Power"
|
||||
#define D_PID_MODE "Controller Mode"
|
||||
#define D_PID_MODE_AUTO "Auto"
|
||||
#define D_PID_MODE_MANUAL "Manual"
|
||||
#define D_PID_MODE_OFF "Off"
|
||||
|
||||
const char HTTP_PID_HL[] PROGMEM = "{s}<hr>{m}<hr>{e}";
|
||||
const char HTTP_PID_INFO[] PROGMEM = "{s}" D_PID_DISPLAY_NAME "{m}%s{e}";
|
||||
const char HTTP_PID_SP_FORMAT[] PROGMEM = "{s}%s " "{m}%*_f ";
|
||||
const char HTTP_PID_PV_FORMAT[] PROGMEM = "{s}%s " "{m}%*_f ";
|
||||
const char HTTP_PID_POWER_FORMAT[] PROGMEM = "{s}%s " "{m}%*_f " D_UNIT_PERCENT;
|
||||
|
||||
void PIDShowValuesWeb(void) {
|
||||
float f_buf;
|
||||
|
||||
WSContentSend_P(HTTP_PID_HL);
|
||||
WSContentSend_P(HTTP_PID_INFO, (Pid.pid.getAuto()==1) ? D_PID_MODE_AUTO : Pid.pid.tick(Pid.current_time_secs)>0.0f ? D_PID_MODE_MANUAL : D_PID_MODE_OFF);
|
||||
WSContentSend_P(HTTP_PID_INFO, (Pid.pid.getAuto()==1) ?
|
||||
D_PID_MODE_AUTO :
|
||||
Pid.power>0.0f ? D_PID_MODE_MANUAL : D_PID_MODE_OFF);
|
||||
|
||||
if (Pid.pid.getAuto()==1 || Pid.pid.tick(Pid.current_time_secs)>0.0f) {
|
||||
f_buf = (float)Pid.pid.getSp();
|
||||
if (Pid.pid.getAuto()==1 || Pid.power > 0.0f) {
|
||||
f_buf = Pid.pid.getSp();
|
||||
WSContentSend_PD(HTTP_PID_SP_FORMAT, D_PID_SET_POINT, 1, &f_buf);
|
||||
f_buf = (float)Pid.pid.getPv();
|
||||
f_buf = Pid.pid.getPv();
|
||||
WSContentSend_PD(HTTP_PID_PV_FORMAT, D_PID_PRESENT_VALUE, 1, &f_buf);
|
||||
f_buf = Pid.pid.tick(Pid.current_time_secs)*100.0f;
|
||||
f_buf = Pid.power*100.0f;
|
||||
WSContentSend_PD(HTTP_PID_POWER_FORMAT, D_PID_POWER, 0, &f_buf);
|
||||
}
|
||||
}
|
||||
#endif // USE_WEBSERVER
|
||||
}
|
||||
|
||||
void PIDRun(void) {
|
||||
double power = Pid.pid.tick(Pid.current_time_secs);
|
||||
//AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("PIDRUN(): tick"));
|
||||
Pid.power = Pid.pid.tick(Pid.current_time_secs);
|
||||
#ifdef PID_DONT_USE_PID_TOPIC
|
||||
// This part is left inside to regularly publish the PID Power via
|
||||
// `%topic%/PID {"power":"0.000"}`
|
||||
char str_buf[FLOATSZ];
|
||||
dtostrfd(power, 3, str_buf);
|
||||
dtostrfd(Pid.power, 3, str_buf);
|
||||
Response_P(PSTR("{\"%s\":\"%s\"}"), "power", str_buf);
|
||||
MqttPublishPrefixTopicRulesProcess_P(TELE, "PID");
|
||||
#endif // PID_DONT_USE_PID_TOPIC
|
||||
|
||||
#if defined PID_SHUTTER
|
||||
// send output as a position from 0-100 to defined shutter
|
||||
int pos = power * 100;
|
||||
int pos = Pid.power * 100;
|
||||
//AddLog(LOG_LEVEL_DEBUG, PSTR("PIDRun: Setting Shutter to %i"), pos );
|
||||
ShutterSetPosition(PID_SHUTTER, pos);
|
||||
#endif //PID_SHUTTER
|
||||
|
||||
#if defined(PID_USE_TIMPROP) && (PID_USE_TIMPROP > 0)
|
||||
// send power to appropriate timeprop output
|
||||
TimepropSetPower( PID_USE_TIMPROP-1, power );
|
||||
//AddLog(LOG_LEVEL_DEBUG, PSTR("PIDRun: Setting TimeProp to %d"), Pid.power );
|
||||
TimepropSetPower( PID_USE_TIMPROP-1, Pid.power );
|
||||
#endif // PID_USE_TIMPROP
|
||||
}
|
||||
|
||||
|
@ -520,6 +548,7 @@ bool Xdrv49(uint32_t function) {
|
|||
bool result = false;
|
||||
|
||||
switch (function) {
|
||||
// Call sequence documented https://tasmota.github.io/docs/API
|
||||
case FUNC_INIT:
|
||||
PIDInit();
|
||||
break;
|
||||
|
@ -527,22 +556,18 @@ bool Xdrv49(uint32_t function) {
|
|||
PIDEverySecond();
|
||||
break;
|
||||
case FUNC_SHOW_SENSOR:
|
||||
// only use this if the pid loop is to use the local sensor for pv
|
||||
#if defined PID_USE_LOCAL_SENSOR
|
||||
PIDShowSensor();
|
||||
#endif // PID_USE_LOCAL_SENSOR
|
||||
break;
|
||||
case FUNC_COMMAND:
|
||||
result = DecodeCommand(kPIDCommands, PIDCommand);
|
||||
break;
|
||||
case FUNC_JSON_APPEND:
|
||||
PIDShowValues();
|
||||
PIDShowValues(true);
|
||||
break;
|
||||
#ifdef USE_WEBSERVER
|
||||
case FUNC_WEB_SENSOR:
|
||||
PIDShowValuesWeb();
|
||||
PIDShowValues(false);
|
||||
break;
|
||||
#endif // USE_WEBSERVER
|
||||
case FUNC_COMMAND:
|
||||
result = DecodeCommand(kPIDCommands, PIDCommand);
|
||||
break;
|
||||
case FUNC_ACTIVE:
|
||||
result = true;
|
||||
break;
|
||||
|
|
Loading…
Reference in New Issue