From d0d82ee8fb67607dc5e905d1e0271fc3b73020e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Thu, 9 Jan 2020 00:32:53 +0100 Subject: [PATCH] add firehazard.c --- .gitignore | 1 + components/fileserver/src/token_subs.c | 16 +++- main/CMakeLists.txt | 2 + main/analog.c | 55 +++++++---- main/analog.h | 4 +- main/app_main.c | 33 ++++++- main/arduinopid.c | 106 ++++++++++++++++++++++ main/arduinopid.h | 52 +++++++++++ main/files/embed/index.html | 59 +++++++++--- main/firehazard.c | 121 +++++++++++++++++++++++++ main/firehazard.h | 25 +++++ main/utils.h | 5 + main/web/websrv.c | 103 ++++++++++++++++++--- pt100_lookup.ods | Bin 0 -> 14751 bytes 14 files changed, 533 insertions(+), 49 deletions(-) create mode 100644 main/arduinopid.c create mode 100644 main/arduinopid.h create mode 100644 main/firehazard.c create mode 100644 main/firehazard.h create mode 100644 pt100_lookup.ods diff --git a/.gitignore b/.gitignore index d614d13..8f740ea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ cmake-build-debug build/ .idea/ sdkconfig.old +.~lock* diff --git a/components/fileserver/src/token_subs.c b/components/fileserver/src/token_subs.c index 76a8d5e..d0bd8b5 100644 --- a/components/fileserver/src/token_subs.c +++ b/components/fileserver/src/token_subs.c @@ -166,11 +166,17 @@ esp_err_t tpl_kv_replacer(httpd_req_t *r, void *context, const char *token, tpl_ struct tpl_kv_list *head = context; SLIST_FOREACH(entry, head, link) { if (0==strcmp(entry->key, token)) { - return httpd_resp_send_chunk_escaped(r, - entry->subst_heap ? - entry->subst_heap : - entry->subst, - -1, escape); + if (entry->subst_heap) { + if (entry->subst_heap[0]) { + return httpd_resp_send_chunk_escaped(r, entry->subst_heap, -1, escape); + } + } else { + if (entry->subst[0]) { + return httpd_resp_send_chunk_escaped(r, entry->subst, -1, escape); + } + return ESP_OK; + } + return ESP_OK; } } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f6d21f1..a17bd32 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -19,6 +19,8 @@ set(COMPONENT_SRCS "web/websrv.c" "files/files_enum.c" "utils.c" + "arduinopid.c" + "firehazard.c" ) set(COMPONENT_ADD_INCLUDEDIRS "." "liquid" "graphics") diff --git a/main/analog.c b/main/analog.c index 43aedd6..a167383 100644 --- a/main/analog.c +++ b/main/analog.c @@ -6,6 +6,7 @@ #include "driver/gpio.h" #include "driver/adc.h" #include "esp_adc_cal.h" +#include "firehazard.h" static esp_adc_cal_characteristics_t *adc_chars; @@ -20,15 +21,12 @@ float reg_meas_history[REG_HISTORY_LEN] = {}; float reg_tset_history[REG_HISTORY_LEN] = {}; uint32_t history_counter = 0; -// TODO move to regulator module (make extern) -float reg_setpoint = 125; - static void analog_service(void *arg); static TaskHandle_t hAnalog; -#define DEFAULT_VREF 1100 -#define NO_OF_SAMPLES 64 +#define DEFAULT_VREF 1100 // TODO try to find the exact value +#define NO_OF_SAMPLES 128 void analog_init() { printf("Analog init\n"); @@ -42,6 +40,32 @@ void analog_init() { assert (rv == pdPASS); } +#define TSENSE_LOOKUP_LEN 81 +#define TSENSE_T_STEP 5 +#define TSENSE_T_MIN 0 +#define TSENSE_T_MAX 400 +static const float TSENSE_LOOKUP[TSENSE_LOOKUP_LEN] = { + // 4k7 + //0.067859346082665f, 0.069156572911158f, 0.070450833857595f, 0.07174213479836f, 0.073030481589859f, 0.074315880068592f, 0.075598336051229f, 0.076877855334685f, 0.078154443696192f, 0.079428106893372f, 0.080698850664312f, 0.081966680727637f, 0.083231602782579f, 0.084493622509052f, 0.085752745567722f, 0.087008977600079f, 0.088262324228509f, 0.089512791056363f, 0.090760383668026f, 0.092005107628991f, 0.093246968485926f, 0.094485971766743f, 0.095722122980667f, 0.096955427618307f, 0.098185891151722f, 0.099413519034488f, 0.10063831670177f, 0.101860289570385f, 0.10307944303887f, 0.10429578248755f, 0.105509313278605f, 0.106720040756132f, 0.107927970246218f, 0.109133107056997f, 0.110335456478721f, 0.111535023783824f, 0.112731814226983f, 0.113925833045189f, 0.115117085457804f, 0.116305576666627f, 0.117491311855962f, 0.118674296192672f, 0.119854534826251f, 0.12103203288888f, 0.122206795495492f, 0.123378827743833f, 0.124548134714525f, 0.125714721471126f, 0.12687859306019f, 0.128039754511331f, 0.129198210837281f, 0.13035396703395f, 0.131507028080486f, 0.132657398939339f, 0.133805084556313f, 0.134950089860629f, 0.136092419764986f, 0.137232079165615f, 0.138369072942339f, 0.139503405958634f, 0.14063508306168f, 0.141764109082427f, 0.142890488835645f, 0.144014227119983f, 0.145135328718029f, 0.146253798396361f, 0.147369640905609f, 0.148482860980504f, 0.14959346333994f, 0.150701452687026f, 0.151806833709142f, 0.152909611077994f, 0.154009789449667f, 0.155107373464683f, 0.156202367748052f, 0.157294776909324f, 0.15838460554265f, 0.159471858226827f, 0.160556539525357f, 0.161638653986497f, 0.162718206143312f + // 2k7 + //0.118961788031723f,0.121199278149888f,0.123430305551104f,0.125654890818048f,0.12787305443543f,0.130084816790549f,0.132290198173841f,0.134489218779432f,0.13668189870568f,0.138868257955715f,0.141048316437981f,0.143222093966762f,0.145389610262718f,0.147550884953408f,0.149705937573814f,0.151854787566858f,0.153997454283918f,0.156133956985341f,0.158264314840951f,0.160388546930552f,0.162506672244433f,0.164618709683859f,0.166724678061576f,0.168824596102292f,0.170918482443171f,0.173006355634316f,0.17508823413925f,0.177164136335394f,0.179234080514542f,0.181298084883333f,0.183356167563718f,0.185408346593427f,0.187454639926429f,0.189495065433395f,0.191529640902147f,0.19355838403812f,0.195581312464802f,0.197598443724189f,0.199609795277224f,0.20161538450424f,0.203615228705396f,0.205609345101115f,0.207597750832512f,0.209580462961826f,0.211557498472848f,0.213528874271338f,0.215494607185455f,0.217454713966165f,0.219409211287665f,0.221358115747788f,0.223301443868418f,0.225239212095895f,0.227171436801418f,0.229098134281449f,0.231019320758111f,0.232935012379582f,0.234845225220494f,0.236749975282318f,0.238649278493758f,0.240543150711133f,0.242431607718764f,0.24431466522935f,0.246192338884353f,0.248064644254368f,0.249931596839502f,0.25179321206974f,0.253649505305317f,0.255500491837083f,0.257346186886869f,0.259186605607846f,0.261021763084885f,0.262851674334914f,0.264676354307276f,0.266495817884073f,0.268310079880525f,0.270119155045314f,0.271923058060928f,0.273721803544007f,0.275515406045682f,0.277303880051916f,0.27908723998384f + 0.118709444844989f,0.120942188771995f,0.123168483690844f,0.125388350140555f,0.127601808562385f,0.129808879300387f,0.132009582601957f,0.134203938618385f,0.136391967405395f,0.138573688923688f,0.140749123039476f,0.142918289525014f,0.145081208059131f,0.14723789822775f,0.149388379524415f,0.151532671350807f,0.153670793017255f,0.155802763743251f,0.157928602657955f,0.1600483288007f,0.16216196112149f,0.1642695184815f,0.166371019653566f,0.168466483322681f,0.170555928086473f,0.172639372455698f,0.174716834854712f,0.176788333621955f,0.178853887010421f,0.180913513188126f,0.182967230238583f,0.185015056161259f,0.18705700887204f,0.189093106203687f,0.191123365906294f,0.193147805647736f,0.195166443014119f,0.197179295510229f,0.19918638055997f,0.201187715506807f,0.203183317614203f,0.205173204066052f,0.207157391967109f,0.209135898343422f,0.211108740142754f,0.213075934235005f,0.215037497412637f,0.216993446391085f,0.218943797809176f,0.220888568229535f,0.222827774139f,0.224761431949025f,0.226689557996082f,0.228612168542064f,0.230529279774684f,0.232440907807868f,0.234347068682147f,0.236247778365053f,0.238143052751499f,0.24003290766417f,0.241917358853906f,0.243796422000076f,0.245670112710962f,0.247538446524132f,0.249401438906812f,0.251259105256259f,0.253111460900123f,0.254958521096822f,0.256800301035897f,0.258636815838374f,0.260468080557129f,0.262294110177234f,0.264114919616321f,0.265930523724925f,0.267740937286839f,0.269546175019461f,0.271346251574132f,0.273141181536489f,0.274930979426797f,0.276715659700291f,0.278495236747511f +}; + +static float v_to_c(float v){ + // TODO use binary search.. lol + for (int i = 1; i < TSENSE_LOOKUP_LEN; i++) { + float cur = TSENSE_LOOKUP[i]; + if (cur >= v) { + float prev = TSENSE_LOOKUP[i-1]; + + float ratio = (v - prev) / (cur - prev); + return TSENSE_T_MIN + ((float) i + ratio) * TSENSE_T_STEP; + } + } + + return TSENSE_T_MAX; +} static void __attribute__((noreturn)) analog_service(void *arg) { while (1) { @@ -53,20 +77,17 @@ static void __attribute__((noreturn)) analog_service(void *arg) { adc_reading /= NO_OF_SAMPLES; //Convert adc_reading to voltage in mV - uint32_t voltage = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars); + uint32_t mv = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars); #define CORRECT -10; - voltage += CORRECT; + mv += CORRECT; -// printf("Raw: %d ... Voltage: %dmV ...", adc_reading, voltage); + printf("Raw: %d ... Voltage: %dmV ...", adc_reading, mv); - float volts = voltage * 0.001f; + float volts = mv * 0.001f; -#define R1 4750 -#define V1 3.3f - float r_pt100 = (volts * R1)/(V1 - volts); - float celsius = (r_pt100/100.0f - 1.0f) / 3.9083E-3f; -// printf("Rpt %.3f, Celsius: %.1f\n", r_pt100, celsius); + float celsius = v_to_c(volts); + printf("Celsius: %.1f°C\n", celsius); measurement_celsius = celsius; @@ -75,10 +96,12 @@ static void __attribute__((noreturn)) analog_service(void *arg) { reg_tset_history[i] = reg_tset_history[i+1]; } reg_meas_history[REG_HISTORY_LEN-1] = celsius; - reg_tset_history[REG_HISTORY_LEN-1] = reg_setpoint; + reg_tset_history[REG_HISTORY_LEN-1] = fire_get_setpoint(true); history_counter = (history_counter + 1) % 20; - vTaskDelay(pdMS_TO_TICKS(500)); + fire_regulate(celsius); + + vTaskDelay(pdMS_TO_TICKS(ANALOG_SAMPLE_TIME_MS)); } } diff --git a/main/analog.h b/main/analog.h index b4b746e..d8b1133 100644 --- a/main/analog.h +++ b/main/analog.h @@ -7,7 +7,9 @@ #ifndef REFLOWER_ANALOG_H #define REFLOWER_ANALOG_H -#define REG_HISTORY_LEN 121 +#define ANALOG_SAMPLE_TIME_MS 1000 + +#define REG_HISTORY_LEN 241 extern float reg_meas_history[REG_HISTORY_LEN]; extern float reg_tset_history[REG_HISTORY_LEN]; extern uint32_t history_counter; diff --git a/main/app_main.c b/main/app_main.c index 055f2e7..893ddf3 100644 --- a/main/app_main.c +++ b/main/app_main.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" @@ -20,6 +21,7 @@ #include "utils.h" #include "esp_event_loop.h" #include "nvs_flash.h" +#include "firehazard.h" /** * Application event handler @@ -75,7 +77,17 @@ static void initialise_wifi(void) void __attribute__((noreturn)) app_main() { - ESP_ERROR_CHECK(nvs_flash_init()); + // Initialize NVS + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + // NVS partition was truncated and needs to be erased + // Retry nvs_flash_init + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK( err ); + + //ESP_ERROR_CHECK(esp_register_shutdown_handler(cspemu_run_shutdown_handlers)); ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL)); @@ -108,6 +120,25 @@ void __attribute__((noreturn)) app_main() try_reconn_if_have_wifi_creds(); websrv_init(); + fire_init(); + + nvs_handle nvs; + + ESP_ERROR_CHECK(nvs_open("config", NVS_READWRITE, &nvs)); // must use RW to allow opening nonexistent + + union uf32 kp, ki, kd; + + // TODO set good defaults + kp.f = 0.3f; + ki.f = 0.5f; + kd.f = 0.0f; + + nvs_get_u32(nvs, "kp", &kp.u); + nvs_get_u32(nvs, "ki", &ki.u); + nvs_get_u32(nvs, "kd", &kd.u); + + fire_set_tuning(kp.f, ki.f, kd.f); + nvs_close(nvs); bool level = 0; while (1) { diff --git a/main/arduinopid.c b/main/arduinopid.c new file mode 100644 index 0000000..0468b07 --- /dev/null +++ b/main/arduinopid.c @@ -0,0 +1,106 @@ +#include "arduinopid.h" +#include +#include +#include +#include + +static void clampOutput(struct PID *self) { + if (self->Output > self->outMax) self->Output = self->outMax; + else if (self->Output < self->outMin) self->Output = self->outMin; +} + +static void clampIterm(struct PID *self) { + if (self->ITerm > self->outMax) self->ITerm = self->outMax; + else if (self->ITerm < self->outMin) self->ITerm = self->outMin; +} + +void PID_Compute(struct PID *self, float Input) +{ + if (!self->ctlMode) return; + self->Input = Input; + + uint32_t now = xTaskGetTickCount(); + int32_t timeChange = (now - self->lastTime); + if (timeChange >= self->SampleTime) { + /*Compute all the working error variables*/ + float error = self->Setpoint - Input; + self->ITerm += (self->ki * error); + + clampIterm(self); + + float dInput = (Input - self->lastInput); + + /*Compute PID Output*/ + self->Output = self->kp * error + self->ITerm - self->kd * dInput; + + clampOutput(self); + + /*Remember some variables for next time*/ + self->lastInput = Input; + self->lastTime = now; + } +} + +void PID_SetSetpoint(struct PID *self, float Setpoint) +{ + self->Setpoint = Setpoint; +} + +void PID_SetTunings(struct PID *self, float Kp, float Ki, float Kd) +{ + if (Kp < 0 || Ki < 0 || Kd < 0) return; + + float SampleTimeInSec = ((float) self->SampleTime) / 1000; + self->kp = Kp; + self->ki = Ki * SampleTimeInSec; + self->kd = Kd / SampleTimeInSec; + + if (self->controllerDirection == PID_REVERSE) { + self->kp = -self->kp; + self->ki = -self->ki; + self->kd = -self->kd; + } +} + +void PID_SetSampleTime(struct PID *self, uint32_t NewSampleTime) +{ + if (NewSampleTime > 0) { + float ratio = (float) NewSampleTime + / (float) self->SampleTime; + self->ki *= ratio; + self->kd /= ratio; + self->SampleTime = (uint32_t) NewSampleTime; + } +} + +void PID_SetOutputLimits(struct PID *self, float Min, float Max) +{ + if (Min > Max) return; + self->outMin = Min; + self->outMax = Max; + + clampOutput(self); + clampIterm(self); +} + +void PID_SetCtlMode(struct PID *self, enum PIDCtlMode Mode) +{ + bool newAuto = (Mode == PID_AUTOMATIC); + if (newAuto == !self->ctlMode) { /*we just went from manual to auto*/ + PID_Initialize(self); + } + self->ctlMode = newAuto; +} + +void PID_Initialize(struct PID *self) +{ + self->lastInput = self->Input; + self->ITerm = self->Output; + + clampIterm(self); +} + +void PID_SetControllerDirection(struct PID *self, enum PIDDirection Direction) +{ + self->controllerDirection = Direction; +} diff --git a/main/arduinopid.h b/main/arduinopid.h new file mode 100644 index 0000000..bcac135 --- /dev/null +++ b/main/arduinopid.h @@ -0,0 +1,52 @@ +/** + * adapted from the Arduino PID library + * + * Created on 2020/01/08. + */ + +#ifndef ARDUINOPID_H +#define ARDUINOPID_H + +#include + +enum PIDCtlMode { + PID_MANUAL = 0, + PID_AUTOMATIC = 1, +}; + +enum PIDDirection { + PID_DIRECT = 0, + PID_REVERSE = 1, +}; + +struct PID { + /*working variables*/ + uint32_t lastTime; + float Input, Output, Setpoint; + float ITerm, lastInput; + float kp, ki, kd; + uint32_t SampleTime; //1 sec = 1000 + float outMin, outMax; + enum PIDCtlMode ctlMode; // false + enum PIDDirection controllerDirection; +}; + +#define PID_DEFAULT() { .SampleTime = 1000, .ctlMode=PID_MANUAL, .controllerDirection=PID_DIRECT } + +void PID_Compute(struct PID *self, float Input); + +void PID_SetTunings(struct PID *self, float Kp, float Ki, float Kd); + +void PID_SetSampleTime(struct PID *self, uint32_t NewSampleTime); + +void PID_SetOutputLimits(struct PID *self, float Min, float Max); + +void PID_SetCtlMode(struct PID *self, enum PIDCtlMode Mode); + +void PID_Initialize(struct PID *self); + +void PID_SetSetpoint(struct PID *self, float Setpoint); + +void PID_SetControllerDirection(struct PID *self, enum PIDDirection Direction); + +#endif //ARDUINOPID_H diff --git a/main/files/embed/index.html b/main/files/embed/index.html index 98f2a13..454b124 100644 --- a/main/files/embed/index.html +++ b/main/files/embed/index.html @@ -42,13 +42,9 @@ stroke: black; } - path.major { - stroke-width: 2px; - } - .ylabels text { font-size: 10px; - text-anchor: end; + text-anchor: start; font-family: Droid Sans, sans-serif; vertical-align: middle; } @@ -63,13 +59,13 @@ fill: none; } - + - - + - + 400 °C 300 °C 200 °C @@ -90,13 +86,42 @@ -Sidebar + tsens = {temp}°C
+ +
+

Oven Control

+ + + + + + + + + + +
Heater:
tset =
+
+ +
+

PID tuning

+ + + + + +
Kp =
Ki =
Kd =
+
diff --git a/main/firehazard.c b/main/firehazard.c new file mode 100644 index 0000000..b45f589 --- /dev/null +++ b/main/firehazard.c @@ -0,0 +1,121 @@ +#include "firehazard.h" +#include "arduinopid.h" +#include +#include + +#include +#include +#include +#include "driver/ledc.h" +#include "esp_err.h" + +static const char *TAG = "fire"; + +static struct PID pid = PID_DEFAULT(); + +static void pwm_init(void); +static void pwm_set(float duty); + +void fire_get_tuning(float *kp, float *ki, float *kd) { + *kp = pid.kp; + *ki = pid.ki; + *kd = pid.kd; +} + +void fire_init() { + printf("Regulator init"); + + PID_Initialize(&pid); + PID_SetOutputLimits(&pid, 0, 1); + PID_SetCtlMode(&pid, PID_MANUAL); + + PID_SetTunings(&pid, 0.3, 0.01, 0.1); // TODO load from nvs + pwm_init(); + + fire_setlevel(20); +// fire_enable(true); +} + +void fire_set_tuning(float kp, float ki, float kd) { + ESP_LOGI(TAG, "PID set tuning Kp=%.3f, Ki=%.3f, Kd=%.3f", kp, ki, kd); + PID_SetTunings(&pid, kp, ki, kd); +} + +float fire_get_setpoint(bool off_is_zero) { + if (off_is_zero) { + return pid.ctlMode == PID_MANUAL ? 0 : pid.Setpoint; + } else { + return pid.Setpoint; + } +} + +void fire_setlevel(float cels) { + ESP_LOGI(TAG, "PID set target %.3f°C", cels); + + if (cels < 0) cels = 0; + if (cels > MAX_SETPOINT) cels = MAX_SETPOINT; + PID_SetSetpoint(&pid, cels); +} + +void fire_enable(bool enable) { + ESP_LOGI(TAG, "Heater %s", enable ? "enable" : "disable"); + + PID_SetCtlMode(&pid, enable ? PID_AUTOMATIC : PID_MANUAL); +} + +bool fire_enabled() { + return pid.ctlMode == PID_AUTOMATIC; +} + +void fire_regulate(float cels) { + PID_Compute(&pid, cels); + + if (cels > MAX_TSENSE || cels < MIN_TSENSE) { + ESP_LOGE(TAG, "Tsense out of bounds! Stopping."); + fire_enable(false); + } + + if (pid.ctlMode == PID_MANUAL) { + pwm_set(0); + } else { + printf("PID in %.2f°C, out %.3f, I %.3f\n", cels, pid.Output, pid.ITerm); + pwm_set(pid.Output); + } +} + + +#define PWM_CHANNEL LEDC_CHANNEL_1 +#define PWM_TIMER LEDC_TIMER_1 +#define PWM_BIT_NUM LEDC_TIMER_12_BIT + +#define PWM_PIN GPIO_NUM_14 + +static void pwm_init(void) +{ + ledc_channel_config_t ledc_channel_left = {0}; + ledc_channel_left.gpio_num = PWM_PIN; + ledc_channel_left.speed_mode = LEDC_HIGH_SPEED_MODE; + ledc_channel_left.channel = PWM_CHANNEL; + ledc_channel_left.intr_type = LEDC_INTR_DISABLE; + ledc_channel_left.timer_sel = PWM_TIMER; + ledc_channel_left.duty = 0; + + ledc_timer_config_t ledc_timer = {0}; + ledc_timer.speed_mode = LEDC_HIGH_SPEED_MODE; + ledc_timer.duty_resolution = PWM_BIT_NUM; + ledc_timer.timer_num = PWM_TIMER; + ledc_timer.freq_hz = 1; // TODO ?? + + ESP_ERROR_CHECK( ledc_channel_config(&ledc_channel_left) ); + ESP_ERROR_CHECK( ledc_timer_config(&ledc_timer) ); +} + +static void pwm_set(float duty) +{ + uint32_t max_duty = (1 << PWM_BIT_NUM);// - 1 + uint32_t dutycycle = lroundf((duty) * (float)max_duty); + printf("Dutycycle %d\n", dutycycle); + + ESP_ERROR_CHECK( ledc_set_duty(LEDC_HIGH_SPEED_MODE, PWM_CHANNEL, dutycycle) ); + ESP_ERROR_CHECK( ledc_update_duty(LEDC_HIGH_SPEED_MODE, PWM_CHANNEL) ); +} diff --git a/main/firehazard.h b/main/firehazard.h new file mode 100644 index 0000000..bfe6bce --- /dev/null +++ b/main/firehazard.h @@ -0,0 +1,25 @@ +/** + * TODO file description + * + * Created on 2020/01/08. + */ + +#ifndef REFLOWER_FIREHAZARD_H +#define REFLOWER_FIREHAZARD_H +#include + +#define MAX_SETPOINT 350 +#define MAX_TSENSE 400 +#define MIN_TSENSE 0 + +void fire_regulate(float cels); +void fire_init(); +void fire_setlevel(float cels); +void fire_enable(bool enable); + +float fire_get_setpoint(bool off_is_zero); +void fire_get_tuning(float *kp, float *ki, float *kd); +bool fire_enabled(); +void fire_set_tuning(float kp, float ki, float kd); + +#endif //REFLOWER_FIREHAZARD_H diff --git a/main/utils.h b/main/utils.h index 0ce39ce..3b8d131 100644 --- a/main/utils.h +++ b/main/utils.h @@ -73,4 +73,9 @@ void feed_all_dogs(void); /** Get embedded file size (must be declared with efile() first) */ #define efsize(varname) (varname##_end - varname) +union uf32 { + uint32_t u; + float f; +}; + #endif //CSPEMU_UTILS_H diff --git a/main/web/websrv.c b/main/web/websrv.c index 2e506f6..f5d9e56 100644 --- a/main/web/websrv.c +++ b/main/web/websrv.c @@ -3,7 +3,9 @@ #include #include +#include +#include "firehazard.h" #include "websrv.h" #include "esp_http_server.h" #include "utils.h" @@ -40,14 +42,14 @@ static struct tpl_kv_list build_index_replacements_kv(void) bool last_empty = true; // first is move bool suc; for (int i = 0; i < REG_HISTORY_LEN; i++) { - int x = i*5; + float x = i*2.5f; if (reg_meas_history[i] == 0) { - snprintf(scratch1, SCRATCH_SIZE, "M%d,0", x); - snprintf(scratch2, SCRATCH_SIZE, "M%d,0", x); + snprintf(scratch1, SCRATCH_SIZE, "M%.1f,0", x); + snprintf(scratch2, SCRATCH_SIZE, "M%.1f,0", x); last_empty = true; } else { - snprintf(scratch1, SCRATCH_SIZE, "%c%d,%d", last_empty ? 'M' : 'L', x, (int)(400 - reg_meas_history[i])); - snprintf(scratch2, SCRATCH_SIZE, "%c%d,%d", last_empty ? 'M' : 'L', x, (int)(400 - reg_tset_history[i])); + snprintf(scratch1, SCRATCH_SIZE, "%c%.1f,%d", last_empty ? 'M' : 'L', x, (int)(400 - reg_meas_history[i])); + snprintf(scratch2, SCRATCH_SIZE, "%c%.1f,%d", last_empty ? 'M' : 'L', x, (int)(400 - reg_tset_history[i])); last_empty = false; } @@ -60,8 +62,30 @@ static struct tpl_kv_list build_index_replacements_kv(void) tpl_kv_add_heapstr(&kv, "ser-act", path1); tpl_kv_add_heapstr(&kv, "ser-set", path2); + bool ena = fire_enabled(); + tpl_kv_add(&kv, "fire_dis_ck", ena ? "" : "selected"); + tpl_kv_add(&kv, "fire_en_ck", ena ? "selected" : ""); + tpl_kv_add_int(&kv, "timeshift", history_counter); + snprintf(scratch1, SCRATCH_SIZE, "%.2f", analog_read()); + tpl_kv_add(&kv, "temp", scratch1); + + snprintf(scratch1, SCRATCH_SIZE, "%.0f", fire_get_setpoint(false)); + tpl_kv_add(&kv, "tset", scratch1); + + float kp, ki, kd; + fire_get_tuning(&kp, &ki, &kd); + + snprintf(scratch1, SCRATCH_SIZE, "%.3f", kp); + tpl_kv_add(&kv, "kp", scratch1); + + snprintf(scratch1, SCRATCH_SIZE, "%.3f", ki); + tpl_kv_add(&kv, "ki", scratch1); + + snprintf(scratch1, SCRATCH_SIZE, "%.3f", kd); + tpl_kv_add(&kv, "kd", scratch1); + #undef SCRATCH_SIZE return kv; @@ -90,7 +114,7 @@ static esp_err_t handler_update(httpd_req_t *req) /* Set a param */ static esp_err_t handler_set(httpd_req_t *req) { - char buf[64]; + char buf[200]; int n = httpd_req_recv(req, buf, 63); if (n < 0) { ESP_LOGW(TAG, "rx er"); @@ -98,13 +122,70 @@ static esp_err_t handler_set(httpd_req_t *req) } buf[n]=0; - char keybuf[20]; - char valbuf[20]; - if (ESP_OK != httpd_query_key_value(buf, "key", keybuf, 20)) goto err; - if (ESP_OK != httpd_query_key_value(buf, "value", valbuf, 20)) goto err; + char val[20]; - // TODO handle + // select box 0/1 + esp_err_t rv = httpd_query_key_value(buf, "fire", val, 20); + if (rv == ESP_OK) { + fire_enable(val[0] == '1'); + } + + rv = httpd_query_key_value(buf, "tset", val, 20); + if (rv == ESP_OK) { + errno = 0; + float f = atoff(val); + if (!errno) { + fire_setlevel(f); + } + } + + float kp, ki, kd; + + // sorry + + // Kp + rv = httpd_query_key_value(buf, "kp", val, 20); + if (rv == ESP_OK) { + errno = 0; + kp = atoff(val); + if (!errno) { + // Ki + rv = httpd_query_key_value(buf, "ki", val, 20); + if (rv == ESP_OK) { + errno = 0; + ki = atoff(val); + if (!errno) { + // Kd + rv = httpd_query_key_value(buf, "kd", val, 20); + if (rv == ESP_OK) { + errno = 0; + kd = atoff(val); + if (!errno) { + fire_set_tuning(kp, ki, kd); + + // save to NVS + nvs_handle nvs; + ESP_ERROR_CHECK(nvs_open("config", NVS_READWRITE, &nvs)); + union uf32 ukp, uki, ukd; + ukp.f = kp; + uki.f = ki; + ukd.f = kd; + nvs_set_u32(nvs, "kp", ukp.u); + nvs_set_u32(nvs, "ki", uki.u); + nvs_set_u32(nvs, "kd", ukd.u); + nvs_commit(nvs); + nvs_close(nvs); + } + } + + } + } + + } + } + httpd_resp_set_hdr(req, "Location", "/"); + httpd_resp_set_status(req, "302 Found"); return httpd_resp_send(req, NULL, 0); err: diff --git a/pt100_lookup.ods b/pt100_lookup.ods new file mode 100644 index 0000000000000000000000000000000000000000..c712502e411c25285c42d347665a34fa8364bfd7 GIT binary patch literal 14751 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 J1;^8W{|AKd_9_4X literal 0 HcmV?d00001