Wed, 17 Apr 2024 13:18:22 +0200
Websocket ping/pong uses sequence numbers.
/***************************************************************************** * Copyright (C) 2024 * * Michiel Broek <mbroek at mbse dot eu> * * 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', map: 'metric>profile>uuid' }, { name: 'profile_name', map: 'metric>profile>name' }, { name: 'profile_state', map: 'metric>profile>state', type: 'string' }, { name: 'profile_percent', map: 'metric>profile>percent', type: 'int' }, { name: 'profile_inittemp_high', map: 'metric>profile>inittemp>high', type: 'float' }, { name: 'profile_inittemp_low', map: 'metric>profile>inittemp>low', type: 'float' }, { name: 'profile_steps', map: 'metric>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('<div class="LEDyellow_on"></div>Door'); } else { $('#fermenter_doorled').html('<div class="LEDyellow_off"></div>Door'); } } if (record.light_address) { if (record.light_state != '0') { $('#fermenter_lightled').html('<div class="LEDyellow_on"></div>Light'); } else { $('#fermenter_lightled').html('<div class="LEDyellow_off"></div>Light'); } } if (record.mode != 'OFF') { $('#fermenter_powerled').html('<div class="LEDblue_on"></div>Power'); } else { $('#fermenter_powerled').html('<div class="LEDblue_off"></div>Power'); } if (record.alarm != '0') { $('#fermenter_alarmled').html('<div class="LEDred_on"></div>Alarm'); } else { $('#fermenter_alarmled').html('<div class="LEDred_off"></div>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('<div class="LEDgreen_on"></div>Heat'); } else { $('#fermenter_led1').html('<div class="LEDgreen_off"></div>Heat'); } if (record.cooler_state != '0') { $('#fermenter_led2').html('<div class="LEDgreen_on"></div>Cool'); } else { $('#fermenter_led2').html('<div class="LEDgreen_off"></div>Cool'); } if (record.fan_state != '0') { $('#fermenter_led3').html('<div class="LEDgreen_on"></div>Fan'); } else { $('#fermenter_led3').html('<div class="LEDgreen_off"></div>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 via websockets 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) { websocket.send('{"pong":' + obj.ping + '}'); } if (obj.type == 'global') { console.log('ws got global ' + msg); global.name = obj.name; global.node = obj.node; global.os = obj.os; global.os_version = obj.os_version; global.FW = obj.FW; global.room_temp = obj.THB.temperature; global.room_hum = obj.THB.humidity; updateScreen(); } else if (obj.type == 'fermenter' && obj.unit == record.unit) { 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.metric.profile) { record.profile_uuid = obj.metric.profile.uuid; record.profile_name = obj.metric.profile.name; record.profile_state = obj.metric.profile.state; record.profile_percent = obj.metric.profile.percent; record.profile_inittemp_high = obj.metric.profile.inittemp.high; record.profile_inittemp_low = obj.metric.profile.inittemp.low; } else { record.profile_uuid = ''; record.profile_name = ''; record.profile_state = ''; record.profile_percent = 0; } updateScreen(); } } });