# HG changeset patch # User Michiel Broek # Date 1713193497 -7200 # Node ID cc49115e769e14f5d88a9e085931e8213ed8110e # Parent c867eb3f7fc1f7a484d045786ecc28c843e79faf Better websocket broadcast messages. Added GLOBAL JSON command to the server. Better logic to trigger websocket and mqtt data updates for the fermenter units. Websocket receive added fermenter mode, stage, setpoints, switches. Added more css styles for the fermenter screen. Added the fermenter screen php and javascript. diff -r c867eb3f7fc1 -r cc49115e769e thermferm/mqtt.c --- a/thermferm/mqtt.c Sun Apr 14 14:41:38 2024 +0200 +++ b/thermferm/mqtt.c Mon Apr 15 17:04:57 2024 +0200 @@ -1220,8 +1220,10 @@ /* * Build and send websocket message. */ - payload = xstrcpy((char *)"{\"fermenter\":"); - payloadu = unit_data(unit, true); + payload = xstrcpy((char *)"{\"type\":\"fermenter\",\"unit\":\""); + payload = xstrcat(payload, unit->alias); + payload = xstrcat(payload, (char *)"\",\"metric\":"); + payloadu = unit_data(unit, false); payload = xstrcat(payload, payloadu); payload = xstrcat(payload, (char *)"}"); ws_broadcast(payload); @@ -1229,6 +1231,8 @@ payload = NULL; free(payloadu); payloadu = NULL; + + unit->mqtt_flag &= ~MQTT_FLAG_DATA; } @@ -1410,20 +1414,19 @@ char *payload = NULL, buf[64]; struct utsname ubuf; - payload = xstrcpy((char *)"{\"thermferm\":{"); - + payload = xstrcpy((char *)"{\"type\":\"global\",\"name\":\""); + payload = xstrcat(payload, Config.name); + payload = xstrcat(payload, (char *)"\",\"node\":\""); if (uname(&ubuf) == 0) { - payload = xstrcat(payload, (char *)"\"node\":\""); payload = xstrcat(payload, ubuf.nodename); payload = xstrcat(payload, (char *)"\",\"os\":\""); payload = xstrcat(payload, ubuf.sysname); payload = xstrcat(payload, (char *)"\",\"os_version\":\""); payload = xstrcat(payload, ubuf.release); - payload = xstrcat(payload, (char *)"\""); } else { - payload = xstrcat(payload, (char *)"\"node\":\"Unknown\",\"os\":\"Unknown\",\"os_version\":\"Unknown\""); + payload = xstrcat(payload, (char *)"Unknown\",\"os\":\"Unknown\",\"os_version\":\"Unknown"); } - payload = xstrcat(payload, (char *)",\"FW\":\""); + payload = xstrcat(payload, (char *)"\",\"FW\":\""); payload = xstrcat(payload, (char *)VERSION); payload = xstrcat(payload, (char *)"\""); @@ -1444,7 +1447,7 @@ payload = xstrcat(payload, (char *)"}"); } - payload = xstrcat(payload, (char *)"}}"); + payload = xstrcat(payload, (char *)"}"); ws_broadcast(payload); free(payload); payload = NULL; diff -r c867eb3f7fc1 -r cc49115e769e thermferm/server.c --- a/thermferm/server.c Sun Apr 14 14:41:38 2024 +0200 +++ b/thermferm/server.c Mon Apr 15 17:04:57 2024 +0200 @@ -569,6 +569,7 @@ /* * GLOBAL GET * GLOBAL PUT + * GLOBAL JSON */ int cmd_global(int s, char *buf) { @@ -588,6 +589,7 @@ srv_send(s, (char *)"Recognized commands:"); srv_send(s, (char *)"GLOBAL GET Get global settings"); srv_send(s, (char *)"GLOBAL PUT Put global settings"); + srv_send(s, (char *)"GLOBAL JSON Get global json settings"); srv_send(s, (char *)"."); return 0; } @@ -617,6 +619,52 @@ return 0; } + if (strcmp(opt, (char *)"JSON") == 0) { + char *payload = NULL, tbuf[64]; + struct utsname ubuf; + + payload = xstrcpy((char *)"{\"type\":\"global\",\"name\":\""); + payload = xstrcat(payload, Config.name); + payload = xstrcat(payload, (char *)"\",\"node\":\""); + if (uname(&ubuf) == 0) { + payload = xstrcat(payload, ubuf.nodename); + payload = xstrcat(payload, (char *)"\",\"os\":\""); + payload = xstrcat(payload, ubuf.sysname); + payload = xstrcat(payload, (char *)"\",\"os_version\":\""); + payload = xstrcat(payload, ubuf.release); + } else { + payload = xstrcat(payload, (char *)"Unknown\",\"os\":\"Unknown\",\"os_version\":\"Unknown"); + } + payload = xstrcat(payload, (char *)"\",\"FW\":\""); + payload = xstrcat(payload, (char *)VERSION); + payload = xstrcat(payload, (char *)"\""); + + if (Config.temp_address || Config.hum_address) { + payload = xstrcat(payload, (char *)",\"THB\":{"); + if (Config.temp_address) { + payload = xstrcat(payload, (char *)"\"temperature\":"); + sprintf(tbuf, "%.1f", Config.temp_value / 1000.0); + payload = xstrcat(payload, tbuf); + } + if (Config.temp_address && Config.hum_address) + payload = xstrcat(payload, (char *)","); + if (Config.hum_address) { + payload = xstrcat(payload, (char *)"\"humidity\":"); + sprintf(tbuf, "%.1f", Config.hum_value / 1000.0); + payload = xstrcat(payload, tbuf); + } + payload = xstrcat(payload, (char *)"}"); + } + + payload = xstrcat(payload, (char *)"}"); + srv_send(s, (char *)"213 Global json data follows:"); + srv_send(s, payload); + srv_send(s, (char *)"."); + free(payload); + payload = NULL; + return 0; + } + if (strcmp(opt, (char *)"PUT") == 0) { int mqtt_reconnect = 0; while (1) { @@ -1535,13 +1583,17 @@ } if (strcmp(opt, (char *)"JSON") == 0) { + syslog(LOG_NOTICE, "UNIT JSON %s", param); for (unit = Config.units; unit; unit = unit->next) { if (strcmp(param, unit->uuid) == 0) { char *payload, *payloadu; srv_send(s, (char *)"213 Unit json data follows:"); - payload = xstrcpy((char *)"{\"fermenter\":"); - payloadu = unit_data(unit, true); + + payload = xstrcpy((char *)"{\"type\":\"fermenter\",\"unit\":\""); + payload = xstrcat(payload, unit->alias); + payload = xstrcat(payload, (char *)"\",\"metric\":"); + payloadu = unit_data(unit, false); payload = xstrcat(payload, payloadu); payload = xstrcat(payload, (char *)"}"); srv_send(s, payload); diff -r c867eb3f7fc1 -r cc49115e769e thermferm/thermferm.c --- a/thermferm/thermferm.c Sun Apr 14 14:41:38 2024 +0200 +++ b/thermferm/thermferm.c Mon Apr 15 17:04:57 2024 +0200 @@ -1029,7 +1029,7 @@ float LCDair, LCDbeer, LCDspL, LCDspH; unsigned char LCDstatC, LCDstatH; - unit->mqtt_flag &= ~MQTT_FLAG_DATA; +// unit->mqtt_flag &= ~MQTT_FLAG_DATA; unit->alarm_flag = 0; if (unit->air_address) { @@ -1623,7 +1623,7 @@ unit->mqtt_flag &= ~MQTT_FLAG_BIRTH; } else { publishDData(unit); - unit->mqtt_flag &= ~MQTT_FLAG_DATA; +// unit->mqtt_flag &= ~MQTT_FLAG_DATA; } if (unit->mqtt_flag & MQTT_FLAG_DEATH) { publishDDeath(unit); diff -r c867eb3f7fc1 -r cc49115e769e thermferm/websocket.c --- a/thermferm/websocket.c Sun Apr 14 14:41:38 2024 +0200 +++ b/thermferm/websocket.c Mon Apr 15 17:04:57 2024 +0200 @@ -26,12 +26,17 @@ #include "thermferm.h" #include "xutil.h" +#include "devices.h" #include "websocket.h" #include extern sys_config Config; extern int debug; +extern const char UNITMODE[5][8]; +extern const char UNITSTAGE[4][12]; +//extern const char PROFSTATE[5][6]; +//extern const char TEMPSTATE[3][8]; int my_ws_shutdown = 0; int my_ws_state = 0; @@ -63,6 +68,157 @@ +void fermenter_ws_receive(char *buf) +{ + struct json_object *val, *val2, *jobj; + units_list *unit; + bool changed; + + jobj = json_tokener_parse(buf); + json_object_object_get_ex(jobj, "unit", &val); + + for (unit = Config.units ; unit; unit = unit->next) { + if (strcmp((char *)json_object_get_string(val), unit->alias) == 0) { + /* + * Setpoints + * {"type":"fermenter","unit":"unit0","setpoint_low":20.3,"setpoint_high":20.7} + */ + if ((unit->mode == UNITMODE_FRIDGE) || (unit->mode == UNITMODE_BEER)) { + changed = false; + if (json_object_object_get_ex(jobj, "setpoint_low", &val)) { + if (unit->PID_heat->SetP != json_object_get_double(val)) { + changed = true; + syslog(LOG_NOTICE, "ws: unit %s setpoint low from %.1f to %.1f", unit->alias, unit->PID_heat->SetP, json_object_get_double(val)); + unit->PID_heat->SetP = json_object_get_double(val); + } + } + if (json_object_object_get_ex(jobj, "setpoint_high", &val)) { + if (unit->PID_cool->SetP != json_object_get_double(val)) { + changed = true; + syslog(LOG_NOTICE, "ws: unit %s setpoint high from %.1f to %.1f", unit->alias, unit->PID_cool->SetP, json_object_get_double(val)); + unit->PID_cool->SetP = json_object_get_double(val); + } + } + if (changed) { + if (unit->mode == UNITMODE_FRIDGE) { + unit->fridge_set_lo = unit->PID_heat->SetP; + unit->fridge_set_hi = unit->PID_cool->SetP; + } else { + unit->beer_set_lo = unit->PID_heat->SetP; + unit->beer_set_hi = unit->PID_cool->SetP; + } + unit->mqtt_flag |= MQTT_FLAG_DATA; + break; + } + } + + /* + * Unit mode + * {"type":"fermenter","unit":"unit0","mode":"NONE"} + */ + if (json_object_object_get_ex(jobj, "mode", &val)) { + for (int i = 0; i < 5; i++) { + if (strcmp((char *)json_object_get_string(val), UNITMODE[i]) == 0) { + if (unit->mode != i) { + unit->mqtt_flag |= MQTT_FLAG_DATA; + /* Initialize log if the unit is turned on */ + if ((unit->mode == UNITMODE_OFF) && (i != UNITMODE_OFF)) { + unit->mqtt_flag |= MQTT_FLAG_BIRTH; + } + if (i == UNITMODE_PROFILE) { + /* Do some checks and refuse profile mode cannot be set */ + if (unit->profile_uuid == NULL) { + syslog(LOG_NOTICE, "ws: unit %s refuse mode profile, not loaded", unit->alias); + break; + } + } + syslog(LOG_NOTICE, "ws: unit %s mode to %s", unit->alias, UNITMODE[i]); + unit->mode = i; + if ((unit->mode != UNITMODE_OFF) && ! unit->event_msg) + unit->event_msg = xstrcpy((char *)UNITMODE[i]); + /* Allways turn everything off after a mode change */ + unit->PID_cool->OutP = unit->PID_heat->OutP = 0.0; + unit->PID_cool->Mode = unit->PID_heat->Mode = PID_MODE_NONE; + unit->heater_state = unit->cooler_state = unit->fan_state = unit->light_state = unit->light_timer = 0; + unit->heater_wait = unit->cooler_wait = unit->fan_wait = unit->light_wait = 0; + device_out(unit->heater_address, unit->heater_state); + device_out(unit->cooler_address, unit->cooler_state); + device_out(unit->fan_address, unit->fan_state); + device_out(unit->light_address, unit->light_state); + if (unit->mode == UNITMODE_PROFILE) { + /* + * Set a sane default until it will be overruled by the + * main processing loop. + */ + unit->prof_target_lo = unit->profile_inittemp_lo; + unit->prof_target_hi = unit->profile_inittemp_hi;; + unit->prof_fridge_mode = 0; + unit->prof_state = PROFILE_OFF; + unit->prof_started = unit->prof_paused = unit->prof_primary_done = 0; + unit->prof_peak_abs = unit->prof_peak_rel = 0.0; + } + } + break; + } + } + } + + /* + * Unit stage + * {"type":"fermenter","unit":"unit0","stage":"SECONDARY"} + */ + if (json_object_object_get_ex(jobj, "stage", &val)) { + for (int i = 0; i < 4; i++) { + if (strcmp((char *)json_object_get_string(val), UNITSTAGE[i]) == 0) { + if (unit->stage != i) { + syslog(LOG_NOTICE, "DCMD change fermenter %s: stage to %s", unit->alias, UNITSTAGE[i]); + unit->mqtt_flag |= MQTT_FLAG_DATA; + unit->stage = i; + if ((unit->mode != UNITMODE_OFF) && ! unit->event_msg) + unit->event_msg = xstrcpy((char *)UNITSTAGE[i]); + } + break; + } + } + } + + /* + * Unit heater and cooler switch + * {"type":"fermenter","unit":"unit0","heater_state":100,"cooler_state":0} + */ + if ((json_object_object_get_ex(jobj, "heater_state", &val)) && + (json_object_object_get_ex(jobj, "cooler_state", &val2)) && + (unit->mode == UNITMODE_NONE)) { + if (json_object_get_int(val) != unit->heater_state) + unit->heater_state = json_object_get_int(val); + if (json_object_get_int(val2) != unit->cooler_state) + unit->cooler_state = json_object_get_int(val2); + if (unit->heater_state || unit->cooler_state) + unit->heater_state = unit->cooler_state = 0; // Safety + unit->mqtt_flag |= MQTT_FLAG_DATA; + syslog(LOG_NOTICE, "ws: unit %s heater_state to %d, cooler_state to %d", unit->alias, unit->heater_state, unit->cooler_state); + break; + } + + /* + * Unit fan switch + * {"type":"fermenter","unit":"unit0","fan_state":0} + */ + if ((json_object_object_get_ex(jobj, "fan_state", &val)) && (unit->mode == UNITMODE_NONE)) { + if (json_object_get_int(val) != unit->fan_state) + unit->fan_state = json_object_get_int(val); + unit->mqtt_flag |= MQTT_FLAG_DATA; + syslog(LOG_NOTICE, "ws: unit %s fan_state to %d", unit->alias, unit->fan_state); + break; + } + + return; + } + } + syslog(LOG_NOTICE, "fermenter_ws_receive(%s)", buf); +} + + static int callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { struct per_session_data__lws_mirror *pss = (struct per_session_data__lws_mirror *)user; @@ -123,15 +279,15 @@ * {"node":"rpi01","group_id":"fermenters","control":"reboot"} * {"node":"rpi01","group_id":"fermenters","control":"rebirth"} */ -// if (strncmp(buf, (char *)"{\"device\":\"fermenters\",", 23) == 0) { -// fermenter_ws_receive(buf); + if (strncmp(buf, (char *)"{\"type\":\"fermenter\",", 20) == 0) { + fermenter_ws_receive(buf); // } else if (strncmp(buf, (char *)"{\"device\":\"co2meters\",", 22) == 0) { // co2meter_ws_receive(buf); // } else if (strncmp(buf, (char *)"{\"device\":\"ispindels\",", 22) == 0) { // ispindel_ws_receive(buf); // } else if (strncmp(buf, (char *)"{\"node\":\"", 9) == 0) { // node_ws_receive(buf); -// } + } break; diff -r c867eb3f7fc1 -r cc49115e769e www/css/style.css --- a/www/css/style.css Sun Apr 14 14:41:38 2024 +0200 +++ b/www/css/style.css Mon Apr 15 17:04:57 2024 +0200 @@ -29,3 +29,227 @@ } +#fermenter_table { + width: 960px; + height: 210px; + background: #252526; + margin: 5px; + border: 2px solid; + border-color: #59b4d4; + border-radius: 5px 5px 5px 5px; +} + + +/* + * +----------------------------------------------------+ + * |+-------------------++--------++-------------------+| + * || || || || + * || || chiller|| || + * || || || || + * || Air temp |+--------+| Beer temp || + * || Temperature | | Pressure || + * || | | || + * || | | || + * || | | || + * |+-------------------+ +-------------------+| + * +----------------------------------------------------+ + */ + +#fermenter_thermometers { + width: 960px; + height: 390px; + float: left; + background-color: #252526; + margin: 5px; + margin-top: 3px; + border: 2px solid; + border-color: #59b4d4; + border-radius: 5px 5px 5px 5px; +} + + +/* + * +----------panel_top------------+ + * | door light alarm power LEDs | + * | | + * +-------------------------------+ + * +---------panel_display---------+ + * |+-------------++--------------+| + * || display 1 || display 2 || + * |+-------------++--------------+| + * +-------------------------------+ + * +---------panel_control---------+ + * |+--------++---------++--------+| + * || led 1 || led 2 || led 3 || + * |+--------++---------++--------+| + * |+--------++---------++--------+| + * || sw 1 || sw 2 || sw 3 || + * |+--------++---------++--------+| + * +-------------------------------+ + * +--------panel_buttons----------+ + * | | + * +-------------------------------+ + */ +#fermenter_panel_top { + width: 290px; + height: 100px; + float: right; + margin: 5px; + background-color: #252526; + border: 2px solid; + border-color: #59b4d4; + border-radius: 5px 5px 5px 5px; +} + +#fermenter_doorled, +#fermenter_lightled, +#fermenter_alarmled, +#fermenter_powerled { + width: 50px; + height: 30px; + float: left; + text-align: center; + margin-top: 15px; + margin-left: 20px; +} + +#fermenter_panel_display { + width: 290px; + height: 98px; + float: right; + margin: 5px; + margin-top: 3px; + background-color: #252526; + border: 2px solid; + border-color: #59b4d4; + border-radius: 5px 5px 5px 5px; +} + +#fermenter_display { + width: 145px; + height: 98px; + float: left; + text-align: center; +} + + +#fermenter_panel_control { + width: 290px; + height: 150px; + float: right; + margin: 5px; + margin-top: 3px; + background-color: #252526; + border: 2px solid; + border-color: #59b4d4; + border-radius: 5px 5px 5px 5px; +} + +#fermenter_led1, +#fermenter_led2, +#fermenter_led3 { + width: 96px; + height: 30px; + float: left; + text-align: center; + margin-top: 13px; +} + +#fermenter_toggle1 { + float: left; + margin-left: 29px; + margin-top: 20px; +} + +#fermenter_toggle2, +#fermenter_toggle3 { + float: left; + margin-left: 60px; + margin-top: 20px; +} + +#fermenter_panel_buttons { + width: 290px; + height: 227px; + float: right; + margin: 5px; + margin-top: 3px; + background-color: #252526; + border: 2px solid; + border-color: #59b4d4; + border-radius: 5px 5px 5px 5px; +} + +.LEDred_on { + margin: 5px auto; + width: 18px; + height: 18px; + background-color: #F40; + border-radius: 50%; + box-shadow: #000 0 0px 4px 1px, inset #C33 0 -1px 5px, #f44 0 2px 12px; +} + +.LEDred_off { + margin: 5px auto; + width: 18px; + height: 18px; + background-color: #820; + border-radius: 50%; + box-shadow: #400 0 0px 1px 1px; +} + +.LEDyellow_on { + margin: 5px auto; + width: 18px; + height: 18px; + background-color: #FF0; + border-radius: 50%; + box-shadow: #000 0 0px 4px 1px, inset #860 0 -1px 5px, #DD0 0 2px 12px; +} + +.LEDyellow_off { + margin: 5px auto; + width: 18px; + height: 18px; + background-color: #A90; + border-radius: 50%; + box-shadow: #440 0 0px 1px 1px; +} + +.LEDgreen_on { + margin: 5px auto; + width: 18px; + height: 18px; + background-color: #5E0; + border-radius: 50%; + box-shadow: #000 0 0px 4px 1px, inset #270 0 -1px 5px, #5D0 0 2px 12px; +} + +.LEDgreen_off { + margin: 5px auto; + width: 18px; + height: 18px; + background-color: #270; + border-radius: 50%; + box-shadow: #250 0 0px 1px 1px; +} + +.LEDblue_on { + margin: 5px auto; + width: 18px; + height: 18px; + background-color: #4AF; + border-radius: 50%; + box-shadow: #000 0 0px 4px 1px, inset #247 0 -1px 5px, #48F 0 2px 12px; +} + +.LEDblue_off { + margin: 5px auto; + width: 18px; + height: 18px; + background-color: #137; + border-radius: 50%; + box-shadow: #024 0 0px 1px 1px; +} + + diff -r c867eb3f7fc1 -r cc49115e769e www/favicon.ico Binary file www/favicon.ico has changed diff -r c867eb3f7fc1 -r cc49115e769e www/fermenter.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/www/fermenter.php Mon Apr 15 17:04:57 2024 +0200 @@ -0,0 +1,109 @@ + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fermenter overview
Room TH
Fermenter unit
Code and beer
Working mode
Fermentation fase
Fermentation profile
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
°C low
+
+
+
+
°C high
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+ Confirm abort profile +
+
+
+ Press "OK" to abort this profile. When started it will start from the beginning.
+ Press "Cancel" to close without aborting the profile. +
+
+
+ + +
+
+
+
+ + diff -r c867eb3f7fc1 -r cc49115e769e www/getfermenter.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/www/getfermenter.php Mon Apr 15 17:04:57 2024 +0200 @@ -0,0 +1,65 @@ + 15, 'usec' => 0)); + } else { + socket_close($sock); + } + } + return $sock; +} + + +/** + * @param string $command to send to the server. + * @return string with the complete reply from the + * server. This can be a multiline reply. + */ +function send_cmd($command) +{ + $sock = open_socket(); + if ($sock == false) { + return ""; + } + socket_write($sock, $command . "\r\n", 4096); + + $answer = ""; + while (1) { + $line = socket_read($sock, 4096); + if ($line === '') + break; + $answer .= $line; + } + socket_close($sock); + + return $answer; +} + + +function startsWith($haystack, $needle) +{ + return !strncmp($haystack, $needle, strlen($needle)); +} + + +if (isset($_GET["uuid"])) + $uuid = $_GET["uuid"]; +else + $uuid = "e53858c3-a97e-4b07-85e9-524257909f45"; + +$answer = send_cmd("UNIT JSON ".$uuid); +header("Content-type: application/json"); + +$arr = explode("\r\n", $answer); +if (startsWith($arr[0], "213")) { + echo $arr[1]; +} else { + echo '{}'; +} + diff -r c867eb3f7fc1 -r cc49115e769e www/images/computer.png Binary file www/images/computer.png has changed diff -r c867eb3f7fc1 -r cc49115e769e www/includes/global.inc.php --- a/www/includes/global.inc.php Sun Apr 14 14:41:38 2024 +0200 +++ b/www/includes/global.inc.php Mon Apr 15 17:04:57 2024 +0200 @@ -27,6 +27,11 @@ //$my_style = 'ui-redmond'; $my_style = 'ui-mbse'; +if (isset($_GET['uuid'])) + $my_uuid = $_GET['uuid']; +else + $my_uuid = ''; + require_once($_SERVER['DOCUMENT_ROOT'].'/version.php'); @@ -80,6 +85,7 @@ function page_header($title, $loadjs) { global $my_style; global $my_version; + global $my_uuid; ?> @@ -91,7 +97,7 @@ @@ -99,8 +105,25 @@ + + + + + + + + + + + + + + + + + @@ -132,8 +155,8 @@ if (strcmp($arr[$i], ".") == 0) break; $parts = explode(",", $arr[$i]); - echo '
  • '; - echo ''.$parts[1].'
  • '.PHP_EOL; + echo "
  • "; + echo "".$parts[1]."
  • ".PHP_EOL; $i++; } } diff -r c867eb3f7fc1 -r cc49115e769e www/js/fermenter.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/www/js/fermenter.js Mon Apr 15 17:04:57 2024 +0200 @@ -0,0 +1,494 @@ +/***************************************************************************** + * Copyright (C) 2024 + * + * Michiel Broek + * + * This file is part of mbsePi-apps thermferm + * + * This 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 2, or (at your option) any + * later version. + * + * Brewery Management System istributed 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 ThermFerm; see the file COPYING. If not, write to the Free + * Software Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + *****************************************************************************/ + +function createAbortElements() { + $('#eventWindow').jqxWindow({ + theme: theme, + position: { x: 440, y: 210 }, + width: 400, + height: 200, + resizable: false, + isModal: true, + modalOpacity: 0.4, + okButton: $('#delOk'), + cancelButton: $('#delCancel'), + initContent: function() { + $('#delOk').jqxButton({ template: 'danger', width: '65px', theme: theme }); + $('#delCancel').jqxButton({ template: 'success', width: '65px', theme: theme }); + $('#delCancel').focus(); + } + }); + $('#eventWindow').jqxWindow('hide'); +} + + +$(document).ready(function() { + + var record = {}, + global = {}, + blank = {}, + ppayload = '', + yl = 12, // Normal yeast temp range + yh = 24, + + gaugeoptions = { + min: 0, max: 45, width: 375, height: 375, + ranges: [{ startValue: 0, endValue: yl, style: { fill: '#3399FF', stroke: '#3399FF' }, endWidth: 10, startWidth: 10 }, + { startValue: yl, endValue: yh, style: { fill: '#00CC33', stroke: '#00CC33' }, endWidth: 10, startWidth: 10 }, + { startValue: yh, endValue: 45, style: { fill: '#FC6A6A', stroke: '#FC6A6A' }, endWidth: 10, startWidth: 10 }], + ticksMinor: { interval: 1, size: '5%' }, + ticksMajor: { interval: 5, size: '9%' }, + labels: { interval: 5 }, + style: { fill: '#eeeeee', stroke: '#666666' }, + value: 0, + colorScheme: 'scheme05' + }, + gaugeSmalloptions = { + min: -15, max: 25, width: 190, height: 190, + ranges: [{ startValue: -15, endValue: 0, startWidth: 5, endWidth: 5, style: { fill: '#3399FF', stroke: '#3399FF' }}, + { startValue: 0, endValue: 10, startWidth: 5, endWidth: 5, style: { fill: '#00CC33', stroke: '#00CC33' }}, + { startValue: 10, endValue: 25, startWidth: 5, endWidth: 5, style: { fill: '#FC6A6A', stroke: '#FC6A6A' }}], + ticksMinor: { interval: 1, size: '5%' }, + ticksMajor: { interval: 5, size: '9%' }, + labels: { interval: 5 }, + style: { fill: '#eeeeee', stroke: '#666666' }, + value: 0, + colorScheme: 'scheme05', + caption: { value: 'Chiller', position: 'bottom', offset: [0, 10] } + }, + switchoptions = { + height: 68, + width: 35, + onLabel: 'ON', + offLabel: 'OFF', + theme: theme, + thumbSize: '50%', + orientation: 'vertical' + }, + targetoptions = { inputMode: 'simple', theme: theme, width: 70, min: 0, max: 45, decimalDigits: 1, spinButtons: true }, + + globalSource = { + datatype: 'json', + cache: false, + datafields: [ + { name: 'type' }, + { name: 'name' }, + { name: 'node' }, + { name: 'os' }, + { name: 'os_version' }, + { name: 'FW' }, + { name: 'room_temp', map: 'THB>temperature', type: 'float' }, + { name: 'room_hum', map: 'THB>humidity', type: 'float' } + ], + id: 'name', + url: 'getglobal.php' + }, + globalData = new $.jqx.dataAdapter(globalSource, { + loadComplete: function(records) { + global = globalData.records[0]; + updateScreen(); + } + }), + url = 'getfermenter.php?uuid=' + my_uuid, + source = { + datatype: 'json', + datafields: [ + { name: 'type' }, + { name: 'unit' }, + { name: 'beercode', map: 'metric>product>code' }, + { name: 'beername', map: 'metric>product>name' }, + { name: 'yeast_lo', map: 'metric>product>yeast_lo' }, + { name: 'yeast_hi', map: 'metric>product>yeast_hi' }, + { name: 'air_state', map: 'metric>air>state' }, + { name: 'air_temperature', map: 'metric>air>temperature' }, + { name: 'beer_state', map: 'metric>beer>state' }, + { name: 'beer_temperature', map: 'metric>beer>temperature' }, + { name: 'chiller_state', map: 'metric>chiller>state' }, + { name: 'chiller_temperature', map: 'metric>chiller>temperature' }, + { name: 'heater_state', map: 'metric>heater>state' }, + { name: 'heater_usage', map: 'metric>heater>usage' }, + { name: 'cooler_state', map: 'metric>cooler>state' }, + { name: 'cooler_usage', map: 'metric>cooler>usage' }, + { name: 'fan_state', map: 'metric>fan>state' }, + { name: 'fan_usage', map: 'metric>fan>usage' }, + { name: 'light_address', map: 'metric>light>address' }, + { name: 'light_state', map: 'metric>light>state' }, + { name: 'light_usage', map: 'metric>light>usage' }, + { name: 'door_address', map: 'metric>door>address' }, + { name: 'door_state', map: 'metric>door>state' }, + { name: 'psu_address', map: 'metric>psu>address' }, + { name: 'psu_state', map: 'metric>psu>state' }, + { name: 'mode', map: 'metric>mode' }, + { name: 'alarm', map: 'metric>alarm', type: 'int' }, + { name: 'setpoint_high', map: 'metric>setpoint>high' }, + { name: 'setpoint_low', map: 'metric>setpoint>low' }, + { name: 'profile_uuid', type: 'string' }, + { name: 'profile_name', type: 'string' }, + { name: 'profile_state', type: 'string' }, + { name: 'profile_percent', type: 'int' }, + { name: 'profile_inittemp_high', type: 'float' }, + { name: 'profile_inittemp_low', type: 'float' }, + { name: 'profile_steps', type: 'string' }, + { name: 'stage', map: 'metric>stage', type: 'string' }, + { name: 'beeruuid', map: 'metric>product>uuid' } + ], + id: 'alias', + url: url + }, + dataAdapter = new $.jqx.dataAdapter(source, { + loadComplete: function(records) { + record = dataAdapter.records[0]; + updateScreen(); + } + }); + + function updateScreen() { + $('#room_thb').html(global.room_temp + '°C  ' + global.room_hum + '% humidity'); + $('#info_system').html(record.unit); + $('#info_beer').html(record.beercode + ' - ' + record.beername); + $('#info_mode').jqxDropDownList('selectItem', record.mode); + $('#info_stage').jqxDropDownList('selectItem', record.stage); + if (record.door_address) { + if (record.door_state != '0') { + $('#fermenter_doorled').html('
    Door'); + } else { + $('#fermenter_doorled').html('
    Door'); + } + } + if (record.light_address) { + if (record.light_state != '0') { + $('#fermenter_lightled').html('
    Light'); + } else { + $('#fermenter_lightled').html('
    Light'); + } + } + if (record.mode != 'OFF') { + $('#fermenter_powerled').html('
    Power'); + } else { + $('#fermenter_powerled').html('
    Power'); + } + if (record.alarm != '0') { + $('#fermenter_alarmled').html('
    Alarm'); + } else { + $('#fermenter_alarmled').html('
    Alarm'); + } + + $('#target_lo').val(record.setpoint_low); + $('#target_hi').val(record.setpoint_high); + if ((record.mode == 'FRIDGE') || (record.mode == 'BEER')) { + $('#target_lo').jqxNumberInput({ readOnly: false, Width: 70, spinButtons: true }); + $('#target_hi').jqxNumberInput({ readOnly: false, Width: 70, spinButtons: true }); + } else { + $('#target_lo').jqxNumberInput({ readOnly: true, Width: 50, spinButtons: false }); + $('#target_hi').jqxNumberInput({ readOnly: true, Width: 50, spinButtons: false }); + } + + $('.f_control_leds').show(); + if (record.heater_state != '0') { + $('#fermenter_led1').html('
    Heat'); + } else { + $('#fermenter_led1').html('
    Heat'); + } + if (record.cooler_state != '0') { + $('#fermenter_led2').html('
    Cool'); + } else { + $('#fermenter_led2').html('
    Cool'); + } + if (record.fan_state != '0') { + $('#fermenter_led3').html('
    Fan'); + } else { + $('#fermenter_led3').html('
    Fan'); + } + + if (record.mode == 'NONE') { + $('.f_control_switches').show(); + } else { + $('.f_control_switches').hide(); + } + if ((record.heater_state != '0') != $('#fermenter_toggle1').jqxSwitchButton('val')) + $('#fermenter_toggle1').val(record.heater_state != '0'); + if ((record.cooler_state != '0') != $('#fermenter_toggle2').jqxSwitchButton('val')) + $('#fermenter_toggle2').val(record.cooler_state != '0'); + if ((record.fan_state != '0') != $('#fermenter_toggle3').jqxSwitchButton('val')) + $('#fermenter_toggle3').val(record.fan_state != '0'); + + $('#info_profile').html(record.profile_name); + if (record.profile_name == '') + $('#info_mode').jqxDropDownList('disableItem', 'PROFILE'); + else + $('#info_mode').jqxDropDownList('enableItem', 'PROFILE'); + + if (record.mode == 'PROFILE') { + if (record.profile_state == 'OFF') { + $('#info_mode').jqxDropDownList({ disabled: false }); + $('#Profile1').jqxButton({ template: 'success', value: 'Starten' }); + $('#Profile1').show(); + $('#Profile2').hide(); + $('#status_profile').html(''); + } else if (record.profile_state == 'RUN') { + $('#info_mode').jqxDropDownList({ disabled: true }); + $('#Profile1').jqxButton({ template: 'danger', value: 'Afbreken' }); + $('#Profile2').jqxButton({ template: 'primary', value: 'Pauze' }); + $('#Profile1').show(); + $('#Profile2').show(); + $('#status_profile').html('Profiel actief, ' + record.profile_percent + '% gereed'); + } else if (record.profile_state == 'PAUSE') { + $('#info_mode').jqxDropDownList({ disabled: true }); + $('#Profile1').jqxButton({ template: 'danger', value: 'Afbreken' }); + $('#Profile2').jqxButton({ template: 'success', value: 'Doorgaan' }); + $('#Profile1').show(); + $('#Profile2').show(); + $('#status_profile').html('Profiel pauze, ' + record.profile_percent + '% gereed'); + } else if (record.profile_state == 'DONE') { + $('#info_mode').jqxDropDownList({ disabled: true }); + $('#Profile1').jqxButton({ template: 'primary', value: 'Profiel Ok' }); + $('#Profile1').show(); + $('#Profile2').hide(); + $('#status_profile').html('Profiel is gereed'); + } + } else { + $('#info_mode').jqxDropDownList({ disabled: false }); + $('#Profile1').hide(); + $('#Profile2').hide(); + $('#status_profile').html(''); + } + + var yl = record.yeast_lo; + var yh = record.yeast_hi; + var range = { ranges: [{ startValue: 0, endValue: yl, style: { fill: '#3399FF', stroke: '#3399FF' }, endWidth: 10, startWidth: 10 }, + { startValue: yl, endValue: yh, style: { fill: '#00CC33', stroke: '#00CC33' }, endWidth: 10, startWidth: 10 }, + { startValue: yh, endValue: 45, style: { fill: '#FC6A6A', stroke: '#FC6A6A' }, endWidth: 10, startWidth: 10 }]}; + $('#gaugeContainer_air').jqxGauge(range); + $('#gaugeContainer_beer').jqxGauge(range); + + if (record.air_temperature !== undefined) { + $('#gaugeContainer_air').jqxGauge({ caption: { value: 'Air: ' + record.air_temperature.toFixed(3) }}); + $('#gaugeContainer_air').jqxGauge({ value: record.air_temperature }); + } + if (record.air_state == 'OK') { + $('#gaugeContainer_air').jqxGauge({ disabled: false }); + } else { + $('#gaugeContainer_air').jqxGauge({ disabled: true }); + } + if (record.beer_temperature !== undefined) { + $('#gaugeContainer_beer').jqxGauge({ caption: { value: 'Beer: ' + record.beer_temperature.toFixed(3) }}); + $('#gaugeContainer_beer').jqxGauge({ value: record.beer_temperature }); + } + if (record.beer_state == 'OK') { + $('#gaugeContainer_beer').jqxGauge({ disabled: false }); + } else { + $('#gaugeContainer_beer').jqxGauge({ disabled: true }); + } + if (record.chiller_temperature !== undefined) { + $('#gaugeContainer_chiller').jqxGauge({ value: record.chiller_temperature }); + } + if (record.chiller_state == 'OK') { + $('#gaugeContainer_chiller').jqxGauge({ disabled: false }); + } else { + $('#gaugeContainer_chiller').jqxGauge({ disabled: true }); + } + } + + $('#gaugeContainer_air').jqxGauge(gaugeoptions); + $('#gaugeContainer_air').jqxGauge({ caption: { value: 'Air: 00.000' }}); + $('#gaugeContainer_beer').jqxGauge(gaugeoptions); + $('#gaugeContainer_beer').jqxGauge({ caption: { value: 'Beer: 00.000' }}); + $('#gaugeContainer_chiller').jqxGauge(gaugeSmalloptions); + + $('#fermenter_toggle1').jqxSwitchButton(switchoptions); + $('#fermenter_toggle2').jqxSwitchButton(switchoptions); + $('#fermenter_toggle3').jqxSwitchButton(switchoptions); + + srcMode = ['OFF', 'NONE', 'FRIDGE', 'BEER', 'PROFILE']; + srcStage = ['PRIMARY', 'SECONDARY', 'TERTIARY', 'CARBONATION']; + $('#info_mode').jqxDropDownList({ theme: theme, source: srcMode, width: 100, height: 24, dropDownHeight: 156 }); + $('#info_stage').jqxDropDownList({ theme: theme, source: srcStage, width: 150, height: 24, dropDownHeight: 125 }); + + $('#target_lo').jqxNumberInput(targetoptions); + $('#target_hi').jqxNumberInput(targetoptions); + + $('#Profile1').jqxButton({ template: 'info', width: '150px', height: 24, theme: theme }); + $('#Profile2').jqxButton({ template: 'info', width: '150px', height: 24, theme: theme }); + $('#Profile1').hide(); // Hide these until they are needed. + $('#Profile2').hide(); + + // Get the data immediatly and then at regular intervals to refresh. + dataAdapter.dataBind(); + globalData.dataBind(); + + $('#info_mode').on('select', function(event) { + if (event.args && event.args.item.value != record.mode) { + record.mode = event.args.item.value; + console.log('set mode ' + record.mode); + var msg = '{"type":"fermenter","unit":"' + record.unit + '","mode":"' + record.mode + '"}'; + websocket.send(msg); + } + }); + $('#info_stage').on('select', function(event) { + if (event.args && event.args.item.value != record.stage) { + record.stage = event.args.item.value; + console.log('set stage ' + record.stage); + var msg = '{"type":"fermenter","unit":"' + record.unit + '","stage":"' + record.stage + '"}'; + websocket.send(msg); + } + }); + + $('#target_lo').on('change', function(event) { + record.setpoint_low = parseFloat(event.args.value); + // Keep the high target above the low. + if (record.setpoint_low > record.setpoint_high) { + record.setpoint_high = record.setpoint_low; + $('#target_hi').val(record.setpoint_high); + } + console.log('set setpoints ' + record.setpoint_low + ' ' + record.setpoint_high); + websocket.send('{"type":"fermenter","unit":"' + record.unit + + '","setpoint_low":' + record.setpoint_low + ',"setpoint_high":' + record.setpoint_high + '}'); + }); + $('#target_hi').on('change', function(event) { + record.setpoint_high = parseFloat(event.args.value); + // Keep the low target below the high. + if (record.setpoint_high < record.setpoint_low) { + record.setpoint_low = record.setpoint_high; + $('#target_lo').val(record.setpoint_low); + } + console.log('set setpoints ' + record.setpoint_low + ' ' + record.setpoint_high); + websocket.send('{"type":"fermenter","unit":"' + record.unit + + '","setpoint_low":' + record.setpoint_low + ',"setpoint_high":' + record.setpoint_high + '}'); + }); + + $('#fermenter_toggle1').on('checked', function(event) { + if (record.mode == 'NONE' && record.heater_state != 0) { + console.log('set heater ' + $("#fermenter_toggle1").jqxSwitchButton('val')); + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","heater_state":0}'); + } + }); + $('#fermenter_toggle1').on('unchecked', function(event) { + if (record.mode == 'NONE' && record.heater_state == 0) { + console.log('set heater ' + $("#fermenter_toggle1").jqxSwitchButton('val')); + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","heater_state":100,"cooler_state":0}'); + } + }); + $('#fermenter_toggle2').on('checked', function(event) { + if (record.mode == 'NONE' && record.cooler_state != 0) { + console.log('set cooler ' + $("#fermenter_toggle2").jqxSwitchButton('val')); + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","cooler_state":0}'); + } + }); + $('#fermenter_toggle2').on('unchecked', function(event) { + if (record.mode == 'NONE' & record.cooler_state == 0) { + console.log('set cooler ' + $("#fermenter_toggle2").jqxSwitchButton('val')); + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","cooler_state":100,"heater_state":0}'); + } + }); + $('#fermenter_toggle3').on('checked', function(event) { + if (record.mode == 'NONE' && record.fan_state != 0) { + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","fan_state":0}'); + } + }); + $('#fermenter_toggle3').on('unchecked', function(event) { + if (record.mode == 'NONE' && record.fan_state == 0) { + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","fan_state":100}'); + } + }); + $('#Profile1').click(function() { + if (record.mode == 'PROFILE') { + if (record.profile_state == 'OFF') { + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","profile":{"command":"start"}}'); + } else if ((record.profile_state == 'RUN') || (record.profile_state == 'PAUSE')) { + // Open a popup to confirm this action. + $('#eventWindow').jqxWindow('open'); + $('#delOk').click(function() { + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","profile":{"command":"abort"}}'); + }); + } else if (record.profile_state == 'DONE') { + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","profile":{"command":"done"}}'); + } + } + }); + $('#Profile2').click(function() { + if (record.mode == 'PROFILE') { + if ((record.profile_state == 'RUN') || (record.profile_state == 'PAUSE')) { + websocket.send('{"type":"fermenter","unit":"' + record.unit + '","profile":{"command":"pause"}}'); + } + } + }); + + createAbortElements(); + + websocket.onmessage = function(evt) { + var msg = evt.data; + var obj = JSON.parse(msg); + + console.log('ws got ' + msg); + + if (obj.ping == 1) { + console.log('ws got ping'); + websocket.send('{"pong":1}'); + } + + if (obj.type == 'fermenter' && obj.unit == record.unit) { + console.log('ws got this device ' + msg); + record.beeruuid = obj.metric.product.uuid; + record.beercode = obj.metric.product.code; + record.beername = obj.metric.product.name; + record.yeast_lo = obj.metric.product.yeast_lo; + record.yeast_hi = obj.metric.product.yeast_hi; + record.air_state = obj.metric.air.state; + record.air_temperature = obj.metric.air.temperature; + record.beer_state = obj.metric.beer.state; + record.beer_temperature = obj.metric.beer.temperature; + record.chiller_state = obj.metric.chiller.state; + record.chiller_temperature = obj.metric.chiller.temperature; + if (obj.metric.heater.state !== undefined) + record.heater_state = obj.metric.heater.state; + if (obj.metric.cooler.state !== undefined) + record.cooler_state = obj.metric.cooler.state; + if (obj.metric.fan.state !== undefined) + record.fan_state = obj.metric.fan.state; + if (obj.metric.door) + record.door_state = obj.metric.door.state; + if (obj.metric.light) + record.light_state = obj.metric.light.state; + if (obj.metric.psu) + record.psu_state = obj.metric.psu.state; + record.mode = obj.metric.mode; + record.stage = obj.metric.stage; + record.alarm = obj.metric.alarm; + record.setpoint_low = obj.metric.setpoint.low; + record.setpoint_high = obj.metric.setpoint.high; + if (obj.profile) { + record.profile_uuid = obj.profile_uuid; + record.profile_name = obj.profile_name; + record.profile_state = obj.profile_state; + record.profile_percent = obj.profile_percent; + record.profile_inittemp_high = obj.profile_inittemp_high; + record.profile_inittemp_low = obj.profile_inittemp_low; + } else { + record.profile_uuid = ''; + record.profile_name = ''; + record.profile_state = ''; + record.profile_percent = 0; + } + updateScreen(); + } + } +});