diff --git a/.env b/.env index 2851113..f0fd15a 100644 --- a/.env +++ b/.env @@ -105,6 +105,11 @@ REACT_APP_PVWS_HTTP_URL=http://localhost:8081/pvws/ REACT_APP_LIVE_MONITOR_WARN=50 REACT_APP_LIVE_MONITOR_MAX=100 +# By default, PV Info treats byte arrays as strings. Change this to false to treat them as byte arrays. +# PVWS was updated in 2024 to send "b64byt" instead of using the "text" field for these waveforms of chars +# https://github.com/ornl-epics/pvws/pull/23 +PVWS_TREAT_BYTE_ARRAY_AS_STRING=true + # By default, PV Info does not allow the user to subscribe to waveform PVs. Switch this to true # to allow viewing of waveform PVs. Viewing large waveforms isn't useful in PV Info but # for small waveforms it can be nice to see. PVWS uses EPICS_CA_MAX_ARRAY_BYTES and that diff --git a/src/api.js b/src/api.js index 60f02b5..4b402ce 100644 --- a/src/api.js +++ b/src/api.js @@ -1,4 +1,5 @@ import { ApiProxyConnector } from "@elastic/search-ui-elasticsearch-connector"; +import { toByteArray } from "base64-js"; import CustomElasticSearchAPIConnector from "./components/caputlog/CustomElasticSearchAPIConnector"; const channelFinderURL = import.meta.env.PROD ? import.meta.env.REACT_APP_CF_URL : import.meta.env.REACT_APP_CF_URL_DEV; @@ -239,6 +240,105 @@ async function standardQuery(requestURI, options = null, handleData = null) { }) } +function parseWebSocketMessage(jsonMessage, fixedPrecision = null) { + if (jsonMessage === null) { + return null; + } + else if (jsonMessage.type === "update") { + if (jsonMessage.pv === undefined) { + console.log("Websocket message without a PV name"); + return null; + } + let pvData = jsonMessage; + if ("alarm_low" in pvData) { + if (pvData.alarm_low === "NaN" || pvData.alarm_low === "Infinity" || pvData.alarm_low === "-Infinity") { + pvData.alarm_low = "n/a"; + } + } + if ("alarm_high" in pvData) { + if (pvData.alarm_high === "NaN" || pvData.alarm_high === "Infinity" || pvData.alarm_high === "-Infinity") { + pvData.alarm_high = "n/a"; + } + } + if ("warn_low" in pvData) { + if (pvData.warn_low === "NaN" || pvData.warn_low === "Infinity" || pvData.warn_low === "-Infinity") { + pvData.warn_low = "n/a"; + } + } + if ("warn_high" in pvData) { + if (pvData.warn_high === "NaN" || pvData.warn_high === "Infinity" || pvData.warn_high === "-Infinity") { + pvData.warn_high = "n/a"; + } + } + if ("seconds" in pvData) { + if ("nanos" in pvData) { + pvData.timestamp = new Date(pvData.seconds * 1000 + (pvData.nanos * 1e-6)).toLocaleString(); + } else { + pvData.timestamp = new Date(pvData.seconds * 1000).toLocaleString(); + } + } + // determine the "pv_value" which will be displayed in the UI + // see "handleMessage" in https://github.com/ornl-epics/pvws/blob/main/src/main/webapp/js/pvws.js + if ("text" in pvData) { + pvData.pv_value = pvData.text; + } else if ("b64dbl" in pvData) { + let bytes = toByteArray(pvData.b64dbl); + let value_array = new Float64Array(bytes.buffer); + pvData.pv_value = Array.prototype.slice.call(value_array); + } else if ("b64int" in pvData) { + let bytes = toByteArray(pvData.b64int); + let value_array = new Int32Array(bytes.buffer); + pvData.pv_value = Array.prototype.slice.call(value_array); + } else if ("b64byt" in pvData) { + let bytes = toByteArray(pvData.b64byt); + if (import.meta.env.PVWS_TREAT_BYTE_ARRAY_AS_STRING === "false") { + let value_array = new Uint8Array(bytes.buffer); + pvData.pv_value = Array.prototype.slice.call(value_array); + } else { + try { + const decoder = new TextDecoder('utf-8'); + pvData.pv_value = decoder.decode(bytes); + } catch (error) { + console.log("Error decoding byte array: ", error.message); + } + } + } else if ("b64flt" in pvData) { + let bytes = toByteArray(pvData.b64flt); + let value_array = new Float32Array(bytes.buffer); + pvData.pv_value = Array.prototype.slice.call(value_array); + } else if ("b64srt" in pvData) { + let bytes = toByteArray(pvData.b64srt); + let value_array = new Int16Array(bytes.buffer); + pvData.pv_value = Array.prototype.slice.call(value_array); + } else if ("value" in pvData) { + if (fixedPrecision) { + if ((Number(pvData.value) >= 0.01 && Number(pvData.value) < 1000000000) || (Number(pvData.value) <= -0.01 && Number(pvData.value) > -1000000000) || Number(pvData.value) === 0) { + pvData.pv_value = Number(pvData.value.toFixed(Number(fixedPrecision))); + } else { + pvData.pv_value = Number(pvData.value).toExponential(Number(fixedPrecision)); + } + } + else { + // if precision was explicitly set (and badly assume 0 is not explicit) then use that + if (pvData.precision !== null && pvData.precision !== "" && !isNaN(pvData.precision) && pvData.precision !== 0) { + pvData.pv_value = (Number(pvData.value) >= 0.01 || Number(pvData.value) === 0) ? Number(pvData.value.toFixed(Number(pvData.precision))) : Number(pvData.value).toExponential(Number(pvData.precision)); + } + // otherwise show full value + else { + pvData.pv_value = (Number(pvData.value) >= 0.01 || Number(pvData.value) === 0) ? Number(pvData.value) : Number(pvData.value).toExponential(); + } + } + } else { + pvData.pv_value = null; + } + return pvData; + } + else { + console.log("Unknown message type"); + return null; + } +} + const logEnum = { ONLINE_LOG: "online_log", ALARM_LOG: "alarm_log" @@ -269,6 +369,7 @@ const api = { HELPERS_ENUM: queryHelperEnum, CAPUTLOG_URL: caputlogURL, CAPUTLOG_CONNECTOR: caputLogConnector, + PARSE_WEBSOCKET_MSG: parseWebSocketMessage, } export default api; diff --git a/src/components/home/queryresults/value/Value.jsx b/src/components/home/queryresults/value/Value.jsx index cea5a54..c00ee19 100644 --- a/src/components/home/queryresults/value/Value.jsx +++ b/src/components/home/queryresults/value/Value.jsx @@ -3,7 +3,6 @@ import useWebSocket from 'react-use-websocket'; import api from '../../../../api'; import colors from '../../../../colors'; import PropTypes from "prop-types"; -import { toByteArray } from 'base64-js'; const propTypes = { pvName: PropTypes.string, @@ -27,54 +26,18 @@ function Value(props) { // parse web socket message. filter on useWebSocket above means we only parse messages for this PV useEffect(() => { - if (lastJsonMessage === null) { - return; + const jsonMessage = api.PARSE_WEBSOCKET_MSG(lastJsonMessage, 2); // fix precision to 2 on the PV table + if (jsonMessage === null) { + return; // unable to parse, could be invalid message type, no PV name, null lastJsonMessage } - const jsonMessage = lastJsonMessage; - if (jsonMessage.type === "update") { - const severity = jsonMessage.severity; - const units = jsonMessage.units; - const text = jsonMessage.text; - const b64dbl = jsonMessage.b64dbl; - const b64int = jsonMessage.b64int; - const value = jsonMessage.value; - const pv = jsonMessage.pv; - if (pv === undefined) { - console.log("Websocket message without an PV name"); - return; - } - if (severity !== undefined) { - setPVSeverity(severity); - } - if (units !== undefined) { - setPVUnit(units); - } - if (text !== undefined) { - setPVValue(text); - } - else if (b64dbl !== undefined) { - let bytes = toByteArray(b64dbl); - let value_array = new Float64Array(bytes.buffer); - value_array = Array.prototype.slice.call(value_array); - setPVValue(value_array); - } - else if (b64int !== undefined) { - let bytes = toByteArray(b64int); - let value_array = new Int32Array(bytes.buffer); - value_array = Array.prototype.slice.call(value_array); - setPVValue(value_array); - } - else if (value !== undefined) { - if ((Number(value) >= 0.01 && Number(value) < 1000000000) || (Number(value) <= -0.01 && Number(value) > -1000000000) || Number(value) === 0) { - setPVValue(Number(value.toFixed(2))); - } - else { - setPVValue(Number(value).toExponential(2)); - } - } + if ("severity" in jsonMessage) { + setPVSeverity(jsonMessage.severity); } - else { - console.log("Unexpected message type: ", jsonMessage); + if ("units" in jsonMessage) { + setPVUnit(jsonMessage.units); + } + if ("pv_value" in jsonMessage) { + setPVValue(jsonMessage.pv_value); } }, [lastJsonMessage]); diff --git a/src/components/pv/valuetable/ValueTable.jsx b/src/components/pv/valuetable/ValueTable.jsx index 8cb1463..e603d7f 100644 --- a/src/components/pv/valuetable/ValueTable.jsx +++ b/src/components/pv/valuetable/ValueTable.jsx @@ -1,7 +1,6 @@ import React, { Fragment, useState, useEffect } from "react"; import { Typography } from "@mui/material"; import useWebSocket from 'react-use-websocket'; -import { toByteArray } from 'base64-js'; import api from "../../../api"; import colors from "../../../colors"; import PropTypes from "prop-types"; @@ -34,8 +33,7 @@ function ValueTable(props) { const [snapshot, setSnapshot] = useState(true); const [subscribed, setSubscribed] = useState(false); - const socketUrl = api.PVWS_URL; - const { sendJsonMessage, lastJsonMessage } = useWebSocket(socketUrl, { + const { sendJsonMessage, lastJsonMessage } = useWebSocket(api.PVWS_URL, { onClose: () => { if (props.snapshot && !props.pvMonitoring) return; setPVValue(null); @@ -85,140 +83,51 @@ function ValueTable(props) { }, [props.pvMonitoring, snapshot, props.isLoading, props.pvData, props.pvName, handleErrorMessage, handleOpenErrorAlert, subscribed, sendJsonMessage, handleSeverity]); useEffect(() => { - if (lastJsonMessage !== null) { - if (!props.pvMonitoring && !snapshot) return; - const message = lastJsonMessage; - if (message.type === "update") { - // pv, severity, value, text, units, precision, labels - if ("units" in message) { - setPVUnits(message.units); - } - if ("min" in message) { - setPVMin(message.min); - } - if ("max" in message) { - setPVMax(message.max); - } - if ("alarm_low" in message) { - const alarm_low = message.alarm_low; - if (alarm_low === "NaN" || alarm_low === "Infinity" || alarm_low === "-Infinity") { - setPVAlarmLow("n/a"); - } - else { - setPVAlarmLow(alarm_low); - } - } - if ("alarm_high" in message) { - const alarm_high = message.alarm_high; - if (alarm_high === "NaN" || alarm_high === "Infinity" || alarm_high === "-Infinity") { - setPVAlarmHigh("n/a"); - } - else { - setPVAlarmHigh(alarm_high); - } - } - if ("warn_low" in message) { - const warn_low = message.warn_low; - if (warn_low === "NaN" || warn_low === "Infinity" || warn_low === "-Infinity") { - setPVWarnLow("n/a"); - } - else { - setPVWarnLow(warn_low); - } - } - if ("warn_high" in message) { - const warn_high = message.warn_high; - if (warn_high === "NaN" || warn_high === "Infinity" || warn_high === "-Infinity") { - setPVWarnHigh("n/a"); - } - else { - setPVWarnHigh(warn_high); - } - } - if ("precision" in message) { - setPVPrecision(message.precision); - } - if ("seconds" in message) { - let timestamp = ""; - if ("nanos" in message) { - timestamp = new Date(message.seconds * 1000 + (message.nanos * 1e-6)).toLocaleString(); - } - else { - timestamp = new Date(message.seconds * 1000).toLocaleString(); - } - if (!props.snapshot) { - if (pvSeverity === "INVALID" || message.severity === "INVALID") { - setPVTimestamp(timestamp); - } else if (message.seconds !== 631152000) { - setPVTimestamp(timestamp); - } - } - } - else { - setPVTimestamp(null); - } - if ("severity" in message && props.pvMonitoring) { - if (message.severity === "NONE") { - setAlarmColor(colors.SEV_COLORS["OK"]); - } else if (message.severity !== "") { - message.severity in colors.SEV_COLORS ? setAlarmColor(colors.SEV_COLORS[message.severity]) : setAlarmColor("#000"); - } - if (!props.snapshot) { - setPVSeverity(message.severity); - } - } - if ("text" in message) { - if (!props.snapshot) { - setPVValue(message.text); - } - if (snapshot) { - setSnapshot(false); - } - - } - // see "handleMessage" in https://github.com/ornl-epics/pvws/blob/main/src/main/webapp/js/pvws.js - else if ("b64dbl" in message) { - if (!props.snapshot) { - let bytes = toByteArray(message.b64dbl); - let value_array = new Float64Array(bytes.buffer); - value_array = Array.prototype.slice.call(value_array); - setPVValue(value_array); - } - if (snapshot) { - setSnapshot(false); - } - } - else if ("b64int" in message) { - if (!props.snapshot) { - let bytes = toByteArray(message.b64int); - let value_array = new Int32Array(bytes.buffer); - value_array = Array.prototype.slice.call(value_array); - setPVValue(value_array); - } - if (snapshot) { - setSnapshot(false); - } - } - else if ("value" in message) { - if (!props.snapshot) { - // if precision was explicitly set (and badly assume 0 is not explicit) then use that - if (pvPrecision !== null && pvPrecision !== "" && !isNaN(pvPrecision) && pvPrecision !== 0) { - setPVValue((Number(message.value) >= 0.01 || Number(message.value) === 0) ? Number(message.value.toFixed(Number(pvPrecision))) : Number(message.value).toExponential(Number(pvPrecision))); - } - // otherwise show full value - else { - setPVValue((Number(message.value) >= 0.01 || Number(message.value) === 0) ? Number(message.value) : Number(message.value).toExponential()); - } - } - if (snapshot) { - setSnapshot(false); - } + if (!props.pvMonitoring && !snapshot) { + return; + } + const message = api.PARSE_WEBSOCKET_MSG(lastJsonMessage); + if (message === null) { + return; // unable to parse, could be invalid message type, no PV name, null lastJsonMessage + } + if ("units" in message) setPVUnits(message.units); + if ("min" in message) setPVMin(message.min); + if ("max" in message) setPVMax(message.max); + if ("alarm_low" in message) setPVAlarmLow(message.alarm_low); + if ("alarm_high" in message) setPVAlarmHigh(message.alarm_high); + if ("warn_low" in message) setPVWarnLow(message.warn_low); + if ("warn_high" in message) setPVWarnHigh(message.warn_high); + if ("precision" in message) setPVPrecision(message.precision); + if ("seconds" in message) { + if (!props.snapshot) { + if (pvSeverity === "INVALID" || message.severity === "INVALID") { + setPVTimestamp(message.timestamp); + } else if (message.seconds !== 631152000) { + setPVTimestamp(message.timestamp); } } - else { - console.log("Unexpected message type: ", message); + } else { + setPVTimestamp(null); + } + if ("severity" in message && props.pvMonitoring) { + if (message.severity === "NONE") { + setAlarmColor(colors.SEV_COLORS["OK"]); + } else if (message.severity !== "") { + message.severity in colors.SEV_COLORS ? setAlarmColor(colors.SEV_COLORS[message.severity]) : setAlarmColor("#000"); + } + if (!props.snapshot) { + setPVSeverity(message.severity); } } + if (message.pv_value === null) { + return; // only if no text, b64dbl, b64int, ..., value property found + } + if (!props.snapshot) { + setPVValue(message.pv_value); + } + if (snapshot) { + setSnapshot(false); + } }, [lastJsonMessage, props.pvMonitoring, props.snapshot, snapshot, pvPrecision, pvSeverity]); if (props.isLoading) {