diff --git a/app/commonVars.ts b/app/commonVars.ts new file mode 100644 index 0000000..75f3dd2 --- /dev/null +++ b/app/commonVars.ts @@ -0,0 +1,10 @@ +import { IfcPVWSRequest, PVWSRequestType } from "@/app/types"; + +export const instListPV = "CS:INSTLIST"; +export const socketURL = + process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8080/pvws/pv"; + +export const instListSubscription: IfcPVWSRequest = { + type: PVWSRequestType.subscribe, + pvs: [instListPV], +}; diff --git a/app/components/InstList.ts b/app/components/InstList.ts deleted file mode 100644 index e85c58f..0000000 --- a/app/components/InstList.ts +++ /dev/null @@ -1,38 +0,0 @@ -import useWebSocket from "react-use-websocket"; -import { dehex_and_decompress } from "./dehex_and_decompress"; - -import { IfcPVWSMessage, IfcPVWSRequest } from "@/app/types"; - -const INSTLIST_PV = "CS:INSTLIST"; - -export default function InstList() { - const socketURL = process.env.NEXT_PUBLIC_WS_URL ?? ""; - - const { - sendJsonMessage, - lastJsonMessage, - }: { - sendJsonMessage: (a: IfcPVWSRequest) => void; - lastJsonMessage: IfcPVWSMessage; - } = useWebSocket(socketURL, { - shouldReconnect: (closeEvent) => true, - }); - - sendJsonMessage({ type: "subscribe", pvs: [INSTLIST_PV] }); - - let instList = null; - - if (lastJsonMessage) { - if (lastJsonMessage.b64byt) { - const response = dehex_and_decompress(atob(lastJsonMessage.b64byt)); - if (typeof response == "string") { - instList = JSON.parse(response); - } - } - } - - if (!instList) { - return; - } - return instList; -} diff --git a/app/components/InstrumentPage.test.ts b/app/components/InstrumentPage.test.ts index e7630ee..e741b84 100644 --- a/app/components/InstrumentPage.test.ts +++ b/app/components/InstrumentPage.test.ts @@ -1,4 +1,9 @@ -import { ConfigOutput, IfcBlock, IfcPVWSRequest } from "@/app/types"; +import { + ConfigOutput, + IfcBlock, + IfcPVWSRequest, + PVWSRequestType, +} from "@/app/types"; import { getGroupsWithBlocksFromConfigOutput, RC_ENABLE, @@ -14,7 +19,7 @@ test("subscribeToBlockPVs subscribes to all run control PVs", () => { subscribeToBlockPVs(mockSendJsonMessage, aBlock); expect(mockSendJsonMessage.mock.calls.length).toBe(1); const expectedCall: IfcPVWSRequest = { - type: "subscribe", + type: PVWSRequestType.subscribe, pvs: [aBlock, aBlock + RC_ENABLE, aBlock + RC_INRANGE, aBlock + SP_RBV], }; expect(JSON.stringify(mockSendJsonMessage.mock.calls[0][0])).toBe( diff --git a/app/components/InstrumentPage.tsx b/app/components/InstrumentPage.tsx index 24455aa..9e9f7ae 100644 --- a/app/components/InstrumentPage.tsx +++ b/app/components/InstrumentPage.tsx @@ -3,7 +3,10 @@ import React, { useEffect, useState } from "react"; import TopBar from "./TopBar"; import Groups from "./Groups"; import useWebSocket from "react-use-websocket"; -import { dehex_and_decompress } from "./dehex_and_decompress"; +import { + dehex_and_decompress, + instListFromBytes, +} from "./dehex_and_decompress"; import { findPVInDashboard, Instrument } from "./Instrument"; import { useSearchParams } from "next/navigation"; import { @@ -11,15 +14,17 @@ import { ConfigOutputBlock, IfcBlock, IfcGroup, - IfcPV, IfcPVWSMessage, IfcPVWSRequest, + instList, + PVWSRequestType, } from "@/app/types"; import { - findPVByAddress, ExponentialOnThresholdFormat, + findPVByAddress, } from "@/app/components/PVutils"; import CheckToggle from "@/app/components/CheckToggle"; +import { instListPV, instListSubscription, socketURL } from "@/app/commonVars"; let lastUpdate: string = ""; @@ -46,7 +51,7 @@ export function subscribeToBlockPVs( * Subscribes to a block and its associated run control PVs */ sendJsonMessage({ - type: "subscribe", + type: PVWSRequestType.subscribe, pvs: [ block_address, block_address + RC_ENABLE, @@ -101,15 +106,12 @@ export function toPrecision( function InstrumentData({ instrumentName }: { instrumentName: string }) { const [showHiddenBlocks, setShowHiddenBlocks] = useState(false); - const [showSetpoints, setShowSetpoints] = useState(false); - const [showTimestamps, setShowTimestamps] = useState(false); const CONFIG_DETAILS = "CS:BLOCKSERVER:GET_CURR_CONFIG_DETAILS"; - const [instlist, setInstlist] = useState | null>(null); + const [instlist, setInstlist] = useState(null); const [currentInstrument, setCurrentInstrument] = useState( null, ); - const socketURL = - process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8080/pvws/pv"; + const instName = instrumentName; useEffect(() => { @@ -130,10 +132,7 @@ function InstrumentData({ instrumentName }: { instrumentName: string }) { useEffect(() => { // This is an initial useEffect to subscribe to lots of PVs including the instlist. - sendJsonMessage({ - type: "subscribe", - pvs: ["CS:INSTLIST"], - }); + sendJsonMessage(instListSubscription); if (instName == "" || instName == null || instlist == null) { return; @@ -142,8 +141,8 @@ function InstrumentData({ instrumentName }: { instrumentName: string }) { let prefix = ""; for (const item of instlist) { - if (item["name"] == instName.toUpperCase()) { - prefix = item["pvPrefix"]; + if (item.name == instName.toUpperCase()) { + prefix = item.pvPrefix; } } if (!prefix) { @@ -156,7 +155,7 @@ function InstrumentData({ instrumentName }: { instrumentName: string }) { setCurrentInstrument(instrument); sendJsonMessage({ - type: "subscribe", + type: PVWSRequestType.subscribe, pvs: [`${prefix}${CONFIG_DETAILS}`], }); @@ -164,7 +163,10 @@ function InstrumentData({ instrumentName }: { instrumentName: string }) { for (const pv of instrument.runInfoPVs.concat( instrument.dashboard.flat(3), )) { - sendJsonMessage({ type: "subscribe", pvs: [pv.pvaddress] }); + sendJsonMessage({ + type: PVWSRequestType.subscribe, + pvs: [pv.pvaddress], + }); } } }, [instlist, instName, sendJsonMessage, currentInstrument]); @@ -178,11 +180,8 @@ function InstrumentData({ instrumentName }: { instrumentName: string }) { const updatedPVName: string = updatedPV.pv; const updatedPVbytes: string | null | undefined = updatedPV.b64byt; - if (updatedPVName == "CS:INSTLIST" && updatedPVbytes != null) { - const dehexedInstList = dehex_and_decompress(atob(updatedPVbytes)); - if (dehexedInstList != null && typeof dehexedInstList == "string") { - setInstlist(JSON.parse(dehexedInstList)); - } + if (updatedPVName == instListPV && updatedPVbytes != null) { + setInstlist(instListFromBytes(updatedPVbytes)); } if (!currentInstrument) { @@ -200,9 +199,6 @@ function InstrumentData({ instrumentName }: { instrumentName: string }) { } lastUpdate = updatedPVbytes; const res = dehex_and_decompress(atob(updatedPVbytes)); - if (res == null || typeof res != "string") { - return; - } currentInstrument.groups = getGroupsWithBlocksFromConfigOutput( JSON.parse(res), sendJsonMessage, diff --git a/app/components/dehex_and_decompress.test.ts b/app/components/dehex_and_decompress.test.ts index 42768e5..8c7c275 100644 --- a/app/components/dehex_and_decompress.test.ts +++ b/app/components/dehex_and_decompress.test.ts @@ -1,8 +1,443 @@ -import { dehex_and_decompress } from "./dehex_and_decompress"; +import { + dehex_and_decompress, + instListFromBytes, +} from "./dehex_and_decompress"; +const instListHexed = + "Nzg5Y2I1OTg2ZjRmZGIzMDEwYzZiZjRhOTVkNzIwMGRiNjY5YTNlZjRjZTJhNGQ2ZTIzOGQ4MGUyYjQzYTg0MjUwMDYxMjFiODhjMjM0NjlkYTc3YzdjZWZmNWUyZmFkMjNjY2NiM2ViZjJhZjdjNDM5ZGY5ZDdkZmUyZmY4N2RmOTZiMTk0YzI3MDE5MTQ5YTE4MmJkNDk3MGZiYjA3YWNlNmEzMThiZTZhZGZlZjgyNzdmNWFkZWRjZmRiNTNhY2JhNmE1M2ViNWUwNmVhNWFlNmU5N2Q3MmZmN2NiNmJjMzllOWY1ZTk2NDZmY2Y5ZjRmMGYyYjgzMmJmY2YyZmNjYWZkNWYyZWFhZTY2ZmZmNzI2NWRkMDcwMjY0NTg2ODZlZDExMTBiODI2MmVhMTAzNWU4ODRjMDUzZDA3Mzc5N2Y3YWI3NTBiMzMxNjMzMjQ3ZTIzODNlMDU2N2VmYjRiYjM5YzQ5ZjQ5ZDFiYjBmMWNhMTZiYzM5YWUzYzEzMjkzOTVlYzhhZjFmYjBlMGViMTQzYTI4ZTkxNzhhNzgyODk3NzQ3ZGQ5NTU5MWU3NDJlYWVkMGIxZjg1MjYxYzYyYTRkNTgxODU1MmY3MTgzZjI1OTIwYmVjMzM3NDAwMzhhODgwNTNlMjI5YjIyYmVmNDgxYTYzYmJhZDUyZTE1ZTRiNjNhN2E4NzQxZTMyNGQzNGRiOTlmNDExZTUwMjViZmE1YTg2MmI2ZjY0YTc4NWRmMWE5MzcxYTI5MTk4OGQwYzYyNWFkOWVkOWRiMzg0NjU5NDRhOTYyNWRiZGZkOWQ2ODI5ODIyMGUzYTAwM2M1NGMwMmRlNTFjMmFjZDBmMjEzODEyYmU5MTQxNzAyYjdiNGEzNTI2MTk1NjYyMWIxOWFlYmM5MWRkOGFhYjQ4NTU0ZWMzZGRiMTE3OGFlYTIyMWY3MGQwNDFjNGM3NDIxZDFlMzlhZWJmOGI5OTMyNTdlNjgzNzYyMDA2ODYyYWZjYzlkMTkwNzM1ZWNlNjQxZTBkNWE1YTg3YjAxOTk1ZjA2M2U0NjZjODk0NzBhNTQ5OWE4YTQ0OTI3Yzc2YjZkYmQ0ODA5ZDAxMjM0ZTI5MzNjYTQzNmUzYTBlOWViOTNkMDI5Y2Q0YzRiZjk5NTNhYThhNTM4NjE1Y2QxZTAxNjY2YWUyNmYzMzU1MTkzODk4Yjk4MzM5M2JiYTkwZWU0ZGM2YWQwZWE3MzI2NTE5NTY1YjViMDA2YjZiMDlmYzc2MzVjOWJlNTFiY2M0ZjcwOGYwNTExMzZmNDU1ZTUxN2NhYTY5NzUxMGJmZDQzZDRlMzU5NDczYjI0ZmIwMTQ2OTAxNGM5MTEyNzhiNDYwMWE1MjQ0NTI2Y2RmZjYwODVjODY4YTM4ZTU0M2M0OTQ5MDExOTUzNGRhZWUyM2ExNThiYmFkNTUxMGRmYTg2MzYzOGZkZTIzMjRjN2Q2YTQ5MWUxZmUzMGIyZGZkZDIxZDRjMDFjZDAwMWUwYTEwMjFlNjc4MThjYTI3MzQ4MmRjM2ZlNmY2NGZmNzU1Y2NkYjFiZTU2YWIzMDI5ZTdlZmQwZDViNDUwMjFiNjNmNWIxZDk4Mjg3NTdmZGYyMDE1MjdkODkxYTc1MmUxNzk0NzljNzg5YTQwNTM4YThkZmViNTBhYTM1MmM3YzFkZjM1ZjFiOTk5MDhkMDhkMjdiMTEzYmU5NWZkYzYwZmNkMjZjMjY2YmI1NmRmMzg2YjFiZGRhZDFlNGIxYWE3MzRkNDgyNTMyZDc3MjViZTg4MzU3ZTAyZWIxMWI4MDEyYWUyYjMzNzE0MTIzYmY2MzYzMjhjNmY2NGE3NmYzMTYyMWQzMmM2YTVjMDZhNDAwNzgwOGIwYTc4NmQ0ZDExZDU1YzIwZjMxMmU5NDBkZjg0YTZkMzhhYzUwNzVlNDc4NTAyOWQxMzBhNzQ0ODI4N2M1ZGI4YjE0YzUzZWNkYWE1ZDUzN2ZhODBkMTdkNjc4MTM5Mjc5ODdmZTM0NzhiMWE2YzllMmMwY2YwZWRjMzk2Y2U0M2I0MjNiNTYwNzM1MjMyYzA1MzVkZTY4NWMyM2U0NTIzNmZkYzg3MjhiNzBmZTE5MDA1ZGY5OTlhMjE5MTFiMTk0NGI2YjJmZjU2NmM5ZjNhNzhmMjVlODc4ODlmODUzYTBhZGZlMWU0OWQ1MzIyNTMyYzMxMWIxZGU2YTVkNWRmZTFmMDZkMWYzYmI4Mzg4MDYyOTYxNjlmMWQ4ZjM1ZTNlZTI1NThjY2Y2ZWQ2NTM0MjcwOTc2M2YwMTMwNzI3YmJmMWYxZTM4MTY1MTg3MmMxNjcxNWM4ZTVjYzg1MGRkMTEzODU1NTdjNDc3MjVlMWMyOGUyMmQ4NjY2ZTAxZGNjZTI1ZjBkOWRjNDMzNjcwYjFkYzIzYjA5YzU1YzRhMzBiNzY0Y2U3NDljMTM0NTVlODc1ZjMzYTg1YmRhNmE0YzRmMTQ2NmRkMGNlYzUyYmNhNGYxMTE5AA=="; +const instListArray = [ + { + name: "ARGUS", + hostName: "NDXARGUS", + pvPrefix: "IN:ARGUS:", + isScheduled: true, + groups: [], + seci: true, + }, + { + name: "CHRONUS", + hostName: "NDXCHRONUS", + pvPrefix: "IN:CHRONUS:", + isScheduled: true, + groups: ["MUONS"], + seci: false, + }, + { + name: "HIFI", + hostName: "NDXHIFI", + pvPrefix: "IN:HIFI:", + isScheduled: true, + groups: [], + seci: true, + }, + { + name: "CHIPIR", + hostName: "NDXCHIPIR", + pvPrefix: "IN:CHIPIR:", + isScheduled: true, + groups: [], + seci: true, + }, + { + name: "CRYOLAB_R80", + hostName: "NDXCRYOLAB_R80", + pvPrefix: "IN:CRYOLA7E:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, + { + name: "DCLAB", + hostName: "NDXDCLAB", + pvPrefix: "IN:DCLAB:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, + { + name: "LARMOR", + hostName: "NDXLARMOR", + pvPrefix: "IN:LARMOR:", + isScheduled: true, + groups: ["SANS"], + seci: false, + }, + { + name: "ALF", + hostName: "NDXALF", + pvPrefix: "IN:ALF:", + isScheduled: true, + groups: ["EXCITATIONS"], + seci: false, + }, + { + name: "DEMO", + hostName: "NDXDEMO", + pvPrefix: "IN:DEMO:", + isScheduled: false, + groups: [], + seci: false, + }, + { + name: "IMAT", + hostName: "NDXIMAT", + pvPrefix: "IN:IMAT:", + isScheduled: true, + groups: ["ENGINEERING"], + seci: false, + }, + { + name: "MUONFE", + hostName: "NDXMUONFE", + pvPrefix: "IN:MUONFE:", + isScheduled: false, + groups: ["MUONS"], + seci: false, + }, + { + name: "ZOOM", + hostName: "NDXZOOM", + pvPrefix: "IN:ZOOM:", + isScheduled: true, + groups: ["SANS"], + seci: false, + }, + { + name: "IRIS", + hostName: "NDXIRIS", + pvPrefix: "IN:IRIS:", + isScheduled: true, + groups: ["MOLSPEC"], + seci: false, + }, + { + name: "IRIS_SETUP", + hostName: "NDXIRIS_SETUP", + pvPrefix: "IN:IRIS_S29:", + isScheduled: false, + groups: ["MOLSPEC"], + seci: false, + }, + { + name: "ENGINX_SETUP", + hostName: "NDXENGINX_SETUP", + pvPrefix: "IN:ENGINX49:", + isScheduled: false, + groups: ["ENGINEERING"], + seci: false, + }, + { + name: "HRPD_SETUP", + hostName: "NDXHRPD_SETUP", + pvPrefix: "IN:HRPD_S3D:", + isScheduled: false, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "HRPD", + hostName: "NDXHRPD", + pvPrefix: "IN:HRPD:", + isScheduled: true, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "POLARIS", + hostName: "NDXPOLARIS", + pvPrefix: "IN:POLARIS:", + isScheduled: true, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "VESUVIO", + hostName: "NDXVESUVIO", + pvPrefix: "IN:VESUVIO:", + isScheduled: true, + groups: ["MOLSPEC"], + seci: false, + }, + { + name: "ENGINX", + hostName: "NDXENGINX", + pvPrefix: "IN:ENGINX:", + isScheduled: true, + groups: ["ENGINEERING", "CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "MERLIN", + hostName: "NDXMERLIN", + pvPrefix: "IN:MERLIN:", + isScheduled: true, + groups: ["EXCITATIONS"], + seci: false, + }, + { + name: "RIKENFE", + hostName: "NDXRIKENFE", + pvPrefix: "IN:RIKENFE:", + isScheduled: false, + groups: ["MUONS"], + seci: false, + }, + { + name: "SELAB", + hostName: "NDXSELAB", + pvPrefix: "IN:SELAB:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, + { + name: "EMMA-A", + hostName: "NDXEMMA-A", + pvPrefix: "IN:EMMA-A:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, + { + name: "SANDALS", + hostName: "NDXSANDALS", + pvPrefix: "IN:SANDALS:", + isScheduled: true, + groups: ["DISORDERED"], + seci: false, + }, + { + name: "GEM", + hostName: "NDXGEM", + pvPrefix: "IN:GEM:", + isScheduled: true, + groups: ["DISORDERED", "CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "MAPS", + hostName: "NDXMAPS", + pvPrefix: "IN:MAPS:", + isScheduled: true, + groups: ["EXCITATIONS"], + seci: false, + }, + { + name: "OSIRIS", + hostName: "NDXOSIRIS", + pvPrefix: "IN:OSIRIS:", + isScheduled: true, + groups: ["MOLSPEC"], + seci: false, + }, + { + name: "INES", + hostName: "NDXINES", + pvPrefix: "IN:INES:", + isScheduled: true, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "SXD", + hostName: "NDXSXD", + pvPrefix: "IN:SXD:", + isScheduled: true, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "TOSCA", + hostName: "NDXTOSCA", + pvPrefix: "IN:TOSCA:", + isScheduled: true, + groups: ["MOLSPEC"], + seci: false, + }, + { + name: "LOQ", + hostName: "NDXLOQ", + pvPrefix: "IN:LOQ:", + isScheduled: true, + groups: ["SANS"], + seci: false, + }, + { + name: "LET", + hostName: "NDXLET", + pvPrefix: "IN:LET:", + isScheduled: true, + groups: ["EXCITATIONS"], + seci: false, + }, + { + name: "MARI", + hostName: "NDXMARI", + pvPrefix: "IN:MARI:", + isScheduled: true, + groups: ["EXCITATIONS"], + seci: false, + }, + { + name: "CRISP", + hostName: "NDXCRISP", + pvPrefix: "IN:CRISP:", + isScheduled: false, + groups: ["REFLECTOMETRY"], + seci: false, + }, + { + name: "SOFTMAT", + hostName: "NDXSOFTMAT", + pvPrefix: "IN:SOFTMAT:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, + { + name: "SURF", + hostName: "NDXSURF", + pvPrefix: "IN:SURF:", + isScheduled: true, + groups: ["REFLECTOMETRY"], + seci: false, + }, + { + name: "NIMROD", + hostName: "NDXNIMROD", + pvPrefix: "IN:NIMROD:", + isScheduled: true, + groups: ["DISORDERED"], + seci: false, + }, + { + name: "DETMON", + hostName: "NDADETMON", + pvPrefix: "TE:NDADETF1:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, + { + name: "EMU", + hostName: "NDXEMU", + pvPrefix: "IN:EMU:", + isScheduled: true, + groups: ["MUONS"], + seci: false, + }, + { + name: "INTER", + hostName: "NDXINTER", + pvPrefix: "IN:INTER:", + isScheduled: true, + groups: ["REFLECTOMETRY"], + seci: false, + }, + { + name: "POLREF", + hostName: "NDXPOLREF", + pvPrefix: "IN:POLREF:", + isScheduled: true, + groups: ["REFLECTOMETRY"], + seci: false, + }, + { + name: "SANS2D", + hostName: "NDXSANS2D", + pvPrefix: "IN:SANS2D:", + isScheduled: true, + groups: ["SANS"], + seci: false, + }, + { + name: "MUSR", + hostName: "NDXMUSR", + pvPrefix: "IN:MUSR:", + isScheduled: true, + groups: ["MUONS"], + seci: false, + }, + { + name: "WISH", + hostName: "NDXWISH", + pvPrefix: "IN:WISH:", + isScheduled: true, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "WISH_SETUP", + hostName: "NDXWISH_SETUP", + pvPrefix: "IN:WISH_S9C:", + isScheduled: false, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "PEARL", + hostName: "NDXPEARL", + pvPrefix: "IN:PEARL:", + isScheduled: true, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "PEARL_SETUP", + hostName: "NDXPEARL_SETUP", + pvPrefix: "IN:PEARL_5B:", + isScheduled: false, + groups: ["CRYSTALLOGRAPHY"], + seci: false, + }, + { + name: "HIFI-CRYOMAG", + hostName: "NDXHIFI-CRYOMAG", + pvPrefix: "IN:HIFI-C11:", + isScheduled: false, + groups: ["MUONS"], + seci: false, + }, + { + name: "OFFSPEC", + hostName: "NDXOFFSPEC", + pvPrefix: "IN:OFFSPEC:", + isScheduled: true, + groups: ["REFLECTOMETRY"], + seci: false, + }, + { + name: "MOTION", + hostName: "NDXMOTION", + pvPrefix: "IN:MOTION:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, + { + name: "SCIDEMO", + hostName: "NDXSCIDEMO", + pvPrefix: "IN:SCIDEMO:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, + { + name: "IBEXGUITEST", + hostName: "NDXIBEXGUITEST", + pvPrefix: "IN:IBEXGUAD:", + isScheduled: false, + groups: ["SUPPORT"], + seci: false, + }, +]; test("dehexes and decompresses a string that is hexed and compressed", () => { const expected = "test123"; const raw = "789c2b492d2e31343206000aca0257"; const result = dehex_and_decompress(raw); expect(result).toBe(expected); }); + +test("instListFromBytes returns an instlist with an instrument in", () => { + expect(instListFromBytes(instListHexed)).toEqual(instListArray); +}); diff --git a/app/components/dehex_and_decompress.ts b/app/components/dehex_and_decompress.ts index 1d4a894..5a7005e 100644 --- a/app/components/dehex_and_decompress.ts +++ b/app/components/dehex_and_decompress.ts @@ -1,9 +1,10 @@ import pako from "pako"; +import { instList } from "@/app/types"; function unhexlify(str: string): string { let result = ""; for (let i = 0, l = str.length; i < l; i += 2) { - result += String.fromCharCode(parseInt(str.substr(i, 2), 16)); + result += String.fromCharCode(parseInt(str.slice(i, i + 2), 16)); } return result; } @@ -14,9 +15,7 @@ function unhexlify(str: string): string { * @param {*} input raw data * @returns dehexed and decompressed data (you can choose to JSON parse it or not afterwards) */ -export function dehex_and_decompress( - input: string, -): string | Uint8Array | null { +export function dehex_and_decompress(input: string): string { // DEHEX const unhexed = unhexlify(input); const charData = unhexed.split("").map(function (x) { @@ -26,3 +25,14 @@ export function dehex_and_decompress( const binData = new Uint8Array(charData); return pako.inflate(binData, { to: "string" }); } + +/** + * instListFromBytes + * this function is a thin wrapper around dehex_and_decompress that takes bytes and returns an instlist object. + * if the instlist is empty or not a string it will return an empty array. + * @param input raw unconverted bytes from the CS:INSTLIST PV. + */ +export function instListFromBytes(input: string): instList { + const dehexedInstList = dehex_and_decompress(atob(input)); + return JSON.parse(dehexedInstList); +} diff --git a/app/instruments/page.tsx b/app/instruments/page.tsx index f3ececf..a08832c 100644 --- a/app/instruments/page.tsx +++ b/app/instruments/page.tsx @@ -1,40 +1,61 @@ "use client"; -import { Inter } from "next/font/google"; import Link from "next/link"; -import InstList from "@/app/components/InstList"; -const inter = Inter({ subsets: ["latin"] }); +import { useEffect, useState } from "react"; +import { IfcPVWSMessage, IfcPVWSRequest } from "@/app/types"; +import { instListFromBytes } from "@/app/components/dehex_and_decompress"; +import useWebSocket from "react-use-websocket"; +import { instListPV, instListSubscription, socketURL } from "@/app/commonVars"; +import createInstrumentGroupsFromInstlist from "@/app/instruments/utils"; -export default function Home() { - let instList = InstList(); +export default function Instruments() { + const [instrumentGroups, setInstrumentGroups] = useState< + Map> + >(new Map()); - if (!instList) { - return

Loading...

; - } + const { + sendJsonMessage, + lastJsonMessage, + }: { + sendJsonMessage: (a: IfcPVWSRequest) => void; + lastJsonMessage: IfcPVWSMessage; + } = useWebSocket(socketURL, { + shouldReconnect: (closeEvent) => true, + }); - instList = Array.from(instList); + useEffect(() => { + // On page load, subscribe to the instrument list as it's required to get each instrument. + sendJsonMessage(instListSubscription); + }, [sendJsonMessage]); - let instruments = new Map(); + useEffect(() => { + // Instlist has changed + if (!lastJsonMessage) { + return; + } - for (let inst of instList) { - let groups = inst["groups"]; - let name = inst["name"]; + const updatedPV: IfcPVWSMessage = lastJsonMessage; + const updatedPVbytes: string | null | undefined = updatedPV.b64byt; - for (let group of groups) { - if (!instruments.has(group)) { - instruments.set(group, []); - } - instruments.get(group).push(name); + if (updatedPV.pv == instListPV && updatedPVbytes != null) { + const newInstrumentGroups = createInstrumentGroupsFromInstlist( + instListFromBytes(updatedPVbytes), + ); + setInstrumentGroups(newInstrumentGroups); } + }, [lastJsonMessage]); + + if (!instrumentGroups.size) { + return

Loading...

; } return (
- {[...instruments].sort().map(([group, insts]) => { + {Array.from(instrumentGroups.entries()).map(([group, insts]) => { return (
{ + const instName = "ANINST"; + const groups = ["GROUP1"]; + const instList: instList = [ + { + name: instName, + hostName: "blah", + groups: groups, + isScheduled: true, + seci: false, + pvPrefix: "SOME:PREFIX", + }, + ]; + const result = createInstrumentGroupsFromInstlist(instList); + expect(result.get(groups[0])).toEqual([instName]); +}); + +test("createInstrumentGroupsFromInstlist ignores instrument if it has no group", () => { + const instName = "ANINST"; + const groups: Array = []; + const instList: instList = [ + { + name: instName, + hostName: "blah", + groups: groups, + isScheduled: true, + seci: false, + pvPrefix: "SOME:PREFIX", + }, + ]; + const result = createInstrumentGroupsFromInstlist(instList); + expect(result.size).toEqual(0); +}); + +test("createInstrumentGroupsFromInstlist adds to a group if it already exists", () => { + const instName1 = "BOB"; + const instName2 = "ALICE"; + const groups: Array = ["GROUP1"]; + const instList: instList = [ + { + name: instName1, + hostName: "blah", + groups: groups, + isScheduled: true, + seci: false, + pvPrefix: "SOME:PREFIX", + }, + { + name: instName2, + hostName: "blah", + groups: groups, + isScheduled: true, + seci: false, + pvPrefix: "SOME:PREFIX", + }, + ]; + const result = createInstrumentGroupsFromInstlist(instList); + expect(result.get(groups[0])).toEqual([instName1, instName2]); +}); diff --git a/app/instruments/utils.ts b/app/instruments/utils.ts new file mode 100644 index 0000000..c796c93 --- /dev/null +++ b/app/instruments/utils.ts @@ -0,0 +1,16 @@ +import { instList } from "@/app/types"; + +export default function createInstrumentGroupsFromInstlist( + jsonInstList: instList, +): Map> { + let newInstrumentGroups: Map> = new Map(); + for (let inst of jsonInstList) { + for (let group of inst.groups) { + if (!newInstrumentGroups.has(group)) { + newInstrumentGroups.set(group, []); + } + newInstrumentGroups.get(group)!.push(inst.name); + } + } + return newInstrumentGroups; +} diff --git a/app/types.ts b/app/types.ts index 8ac765f..e952287 100644 --- a/app/types.ts +++ b/app/types.ts @@ -63,21 +63,25 @@ export interface IfcPVWSMessage { nanos?: number | null; } +export enum PVWSRequestType { + subscribe = "subscribe", + clear = "clear", +} export interface IfcPVWSRequest { /** * A request to send to PVWS. */ - type: string; + type: PVWSRequestType; pvs: Array; } export interface IfcInstrumentStatus { /** - * Instrument status used for the wall display. + * Instrument status used for the wall display. Contains runstate PV and current runstate. */ - name: string; - status?: string; - pv?: string; + name: string; // Name of the instrument + runstate?: string; // Runstate + runstatePV?: string; // Runstate PV address } // Column[Row[labelPV, valuePV]] @@ -141,3 +145,19 @@ export interface ConfigOutputIocMacro { name: string; value: string; } + +export interface targetStation { + targetStation: string; + instruments: Array; +} + +export interface instListEntry { + name: string; + hostName: string; + isScheduled: boolean; + pvPrefix: string; + seci: boolean; + groups: Array; +} + +export type instList = Array; diff --git a/app/wall/components/InstrumentWallCard.tsx b/app/wall/components/InstrumentWallCard.tsx index 6ece175..f7de5e4 100644 --- a/app/wall/components/InstrumentWallCard.tsx +++ b/app/wall/components/InstrumentWallCard.tsx @@ -14,12 +14,12 @@ export default function WallCard({ return (
@@ -28,7 +28,7 @@ export default function WallCard({ {instrument.name} - {instrument.status ? instrument.status : "UNKNOWN"} + {instrument.runstate ? instrument.runstate : "UNKNOWN"}
diff --git a/app/wall/components/ShowHideBeamInfo.tsx b/app/wall/components/ShowHideBeamInfo.tsx index 97d7744..c728de2 100644 --- a/app/wall/components/ShowHideBeamInfo.tsx +++ b/app/wall/components/ShowHideBeamInfo.tsx @@ -1,13 +1,15 @@ import Image from "next/image"; -import { useState } from "react"; +import { useEffect, useState } from "react"; export default function ShowHideBeamInfo() { - const [date, setDate] = useState(Date.now()); - - setInterval(() => { - // Update the date, used by the beam image to get a fresh image, every 5 seconds so we're not constantly reloading the image on every render. - setDate(Date.now()); - }, 5000); + const [date, setDate] = useState(0); + useEffect(() => { + const interval = setInterval(() => { + // Update the date, used by the beam image to get a fresh image, every 15 seconds so we're not constantly reloading the image on every render. + setDate(Date.now()); + }, 15000); + return () => clearInterval(interval); + }, [date]); return (
@@ -21,9 +23,9 @@ export default function ShowHideBeamInfo() { beam info diff --git a/app/wall/page.tsx b/app/wall/page.tsx index 92ad0dd..cd2875d 100644 --- a/app/wall/page.tsx +++ b/app/wall/page.tsx @@ -1,106 +1,106 @@ "use client"; -import { Inter } from "next/font/google"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import useWebSocket from "react-use-websocket"; -import { dehex_and_decompress } from "../components/dehex_and_decompress"; +import { instListFromBytes } from "../components/dehex_and_decompress"; import InstrumentGroup from "./components/InstrumentGroup"; import ShowHideBeamInfo from "./components/ShowHideBeamInfo"; import JenkinsJobIframe from "./components/JenkinsJobsIframe"; +import { IfcPVWSMessage, IfcPVWSRequest, targetStation } from "@/app/types"; +import { instListPV, instListSubscription, socketURL } from "@/app/commonVars"; import { - IfcInstrumentStatus, - IfcPVWSMessage, - IfcPVWSRequest, -} from "@/app/types"; - -const inter = Inter({ subsets: ["latin"] }); + updateInstrumentRunstate, + updateInstrumentRunstatePV, +} from "@/app/wall/utils"; export default function WallDisplay() { const runstatePV = "DAE:RUNSTATE_STR"; - const instListPV = "CS:INSTLIST"; - const [TS1Data] = useState>( - [ - { name: "ALF" }, - { name: "CRISP" }, - { name: "EMMA-A" }, - { name: "EMU" }, - { name: "ENGINX" }, - { name: "GEM" }, - { - name: "HIFI-CRYOMAG", - }, - { name: "HRPD" }, - { name: "INES" }, - { name: "IRIS" }, - { name: "LOQ" }, - { name: "MAPS" }, - { name: "MARI" }, - { name: "MERLIN" }, - { name: "MUONFE" }, - { name: "MUSR" }, - { name: "OSIRIS" }, - { name: "PEARL" }, - { name: "POLARIS" }, - { name: "RIKENFE" }, - { name: "SANDALS" }, - { name: "SCIDEMO" }, - { name: "SURF" }, - { name: "SXD" }, - { name: "TOSCA" }, - { name: "VESUVIO" }, - ].sort((a, b) => a.name.localeCompare(b.name)), - ); - const [TS2Data] = useState>( - [ - { name: "CHIPIR" }, - { name: "IMAT" }, - { name: "INTER" }, - { name: "LARMOR" }, - { name: "LET" }, - { name: "NIMROD" }, - { name: "OFFSPEC" }, - { name: "POLREF" }, - { name: "SANS2D" }, - { name: "WISH" }, - { name: "ZOOM" }, - ].sort((a, b) => a.name.localeCompare(b.name)), - ); - const [miscData] = useState>( - [ - { name: "ARGUS" }, - { name: "CHRONUS" }, - { - name: "CRYOLAB_R80", - }, - { name: "DCLAB" }, - { name: "DEMO" }, - { name: "DETMON" }, - { - name: "ENGINX_SETUP", - }, - { name: "HIFI" }, - { - name: "HRPD_SETUP", - }, - { - name: "IBEXGUITEST", - }, - { - name: "IRIS_SETUP", - }, - { name: "MOTION" }, - { - name: "PEARL_SETUP", - }, - { name: "SELAB" }, - { name: "SOFTMAT" }, - { - name: "WISH_SETUP", - }, - ].sort((a, b) => a.name.localeCompare(b.name)), - ); - - const socketURL = process.env.NEXT_PUBLIC_WS_URL!; + const [data, setData] = useState>([ + { + targetStation: "Target Station 1", + instruments: [ + { name: "ALF" }, + { name: "CRISP" }, + { name: "EMMA-A" }, + { name: "EMU" }, + { name: "ENGINX" }, + { name: "GEM" }, + { + name: "HIFI-CRYOMAG", + }, + { name: "HRPD" }, + { name: "INES" }, + { name: "IRIS" }, + { name: "LOQ" }, + { name: "MAPS" }, + { name: "MARI" }, + { name: "MERLIN" }, + { name: "MUONFE" }, + { name: "MUSR" }, + { name: "OSIRIS" }, + { name: "PEARL" }, + { name: "POLARIS" }, + { name: "RIKENFE" }, + { name: "SANDALS" }, + { name: "SCIDEMO" }, + { name: "SURF" }, + { name: "SXD" }, + { name: "TOSCA" }, + { name: "VESUVIO" }, + ], + }, + { + targetStation: "Target Station 2", + instruments: [ + { name: "CHIPIR" }, + { name: "IMAT" }, + { name: "INTER" }, + { name: "LARMOR" }, + { name: "LET" }, + { name: "NIMROD" }, + { name: "OFFSPEC" }, + { name: "POLREF" }, + { name: "SANS2D" }, + { name: "WISH" }, + { name: "ZOOM" }, + ], + }, + { + targetStation: "Miscellaneous", + instruments: [ + { name: "ARGUS" }, + { name: "CHRONUS" }, + { + name: "CRYOLAB_R80", + }, + { name: "DCLAB" }, + { name: "DEMO" }, + { name: "DETMON" }, + { + name: "ENGINX_SETUP", + }, + { name: "HIFI" }, + { + name: "HRPD_SETUP", + }, + { + name: "IBEXGUITEST", + }, + { + name: "IRIS_SETUP", + }, + { name: "MOTION" }, + { + name: "PEARL_SETUP", + }, + { name: "SELAB" }, + { name: "SOFTMAT" }, + { + name: "WISH_SETUP", + }, + ], + }, + ]); const { sendJsonMessage, @@ -114,10 +114,7 @@ export default function WallDisplay() { useEffect(() => { // On page load, subscribe to the instrument list as it's required to get each instrument's PV prefix. - sendJsonMessage({ - type: "subscribe", - pvs: [instListPV], - }); + sendJsonMessage(instListSubscription); }, [sendJsonMessage]); useEffect(() => { @@ -132,40 +129,29 @@ export default function WallDisplay() { let updatedPVvalue: string | null | undefined = updatedPV.text; if (updatedPVName == instListPV && updatedPVbytes != null) { - // Act on an instlist change - subscribe to each instrument's runstate PV. - const dehexedInstList = dehex_and_decompress(atob(updatedPVbytes)); - if (dehexedInstList != null && typeof dehexedInstList == "string") { - const instListDict = JSON.parse(dehexedInstList); - for (const item of instListDict) { - // Iterate through the instlist, find their associated object in the ts1data, ts2data or miscData arrays, get the runstate PV and subscribe - const instName = item["name"]; - const instPrefix = item["pvPrefix"]; - const foundInstrument = [...TS1Data, ...TS2Data, ...miscData].find( - (instrument) => instrument.name === instName, + const instListDict = instListFromBytes(updatedPVbytes); + for (const instrument of instListDict) { + setData((prev) => { + return updateInstrumentRunstatePV( + prev, + instrument, + runstatePV, + sendJsonMessage, ); - if (foundInstrument) { - // Subscribe to the instrument's runstate PV - foundInstrument.pv = instPrefix + runstatePV; - sendJsonMessage({ type: "subscribe", pvs: [foundInstrument.pv] }); - } - } - } - } else { - // An instrument's runstate has changed. Find the instrument and update its status (if there is one!). - const foundInstrument = [...TS1Data, ...TS2Data, ...miscData].find( - (instrument) => instrument.pv === updatedPVName, - ); - if (updatedPVvalue && foundInstrument) { - foundInstrument.status = updatedPVvalue; + }); } + } else if (updatedPVvalue) { + setData((prev) => { + return updateInstrumentRunstate(prev, updatedPVName, updatedPVvalue); + }); } - }, [lastJsonMessage, TS1Data, TS2Data, miscData, sendJsonMessage]); + }, [lastJsonMessage, sendJsonMessage]); return (
-
+
@@ -176,18 +162,16 @@ export default function WallDisplay() {

Instrument Status:

- - - + + {data.map((targetStation) => { + return ( + + ); + })}

diff --git a/app/wall/utils.test.ts b/app/wall/utils.test.ts new file mode 100644 index 0000000..a389946 --- /dev/null +++ b/app/wall/utils.test.ts @@ -0,0 +1,96 @@ +import { + updateInstrumentRunstate, + updateInstrumentRunstatePV, +} from "@/app/wall/utils"; +import { IfcInstrumentStatus, instListEntry, targetStation } from "@/app/types"; + +test("updateInstrumentRunstate returns new array with runstate of instrument changed", () => { + const instrumentName = "anInstrumentName"; + const runStatePV = "AN:INST:DAE:RUNSTATE"; + const instrument: IfcInstrumentStatus = { + name: instrumentName, + runstatePV: runStatePV, + }; + const original: targetStation = { + targetStation: "Target station -1", + instruments: [instrument], + }; + const expectedValue = "RUNNING"; + expect( + updateInstrumentRunstate([original], runStatePV, expectedValue)[0] + .instruments[0].runstate, + ).toBe(expectedValue); +}); +test("updateInstrumentRunstate returns untouched array if runstate PV is not found", () => { + const runStatePV = "AN:INST:DAE:RUNSTATE"; + const instrument: IfcInstrumentStatus = { + name: "notThatInstrument", + runstatePV: "BLAH", + }; + const original: targetStation = { + targetStation: "Target station 42", + instruments: [instrument], + }; + const expectedValue = "RUNNING"; + expect( + updateInstrumentRunstate([original], runStatePV, expectedValue)[0] + .instruments[0].runstate, + ).toBe(undefined); +}); + +test("updateInstrumentRunstatePV returns untouched array if instrument is not found", () => { + const runStatePvThatShouldNeverGetUsed = "INV:RUNSTATE"; + const mockSendJsonMessage = jest.fn(); + const invalidInstrument: instListEntry = { + name: "invalidInstrument", + groups: [], + pvPrefix: "invalidPrefix", + isScheduled: true, + seci: false, + hostName: "invalidHostname", + }; + const original: targetStation = { + targetStation: "Target station 3", + instruments: [], + }; + const returned = updateInstrumentRunstatePV( + [original], + invalidInstrument, + runStatePvThatShouldNeverGetUsed, + mockSendJsonMessage, + ); + expect(returned[0].instruments.length).toBe(0); + expect(mockSendJsonMessage).toHaveBeenCalledTimes(0); +}); + +test("updateInstrumentRunstatePV returns new array with runstate PV updated and subscribed to", () => { + const runStatePvThatShouldGetUsed = "NEW:RUNSTATE"; + const mockSendJsonMessage = jest.fn(); + const inst: instListEntry = { + name: "instrument", + groups: [], + pvPrefix: "prefix", + isScheduled: true, + seci: false, + hostName: "hostname", + }; + const original: targetStation = { + targetStation: "Target station 4", + instruments: [inst], + }; + const returned = updateInstrumentRunstatePV( + [original], + inst, + runStatePvThatShouldGetUsed, + mockSendJsonMessage, + ); + expect(returned[0].instruments.length).toBe(1); + expect(returned[0].instruments[0].runstatePV).toBe( + inst.pvPrefix + runStatePvThatShouldGetUsed, + ); + expect(mockSendJsonMessage).toHaveBeenCalledTimes(1); + expect(mockSendJsonMessage).toHaveBeenCalledWith({ + type: "subscribe", + pvs: [inst.pvPrefix + runStatePvThatShouldGetUsed], + }); +}); diff --git a/app/wall/utils.ts b/app/wall/utils.ts new file mode 100644 index 0000000..b6baa73 --- /dev/null +++ b/app/wall/utils.ts @@ -0,0 +1,58 @@ +import { + IfcPVWSRequest, + instListEntry, + PVWSRequestType, + targetStation, +} from "@/app/types"; + +/** + * Copy the original array, update the given runstate PV's runstate value, then return the copied array. + * @param prev the previous array of target stations, containing instruments. + * @param updatedPVName the runstate PV address + * @param updatedPVvalue the runstate + */ +export function updateInstrumentRunstate( + prev: Array, + updatedPVName: string, + updatedPVvalue: string, +) { + const newData: Array = [...prev]; + newData.forEach((targetStation) => { + const foundInstrument = targetStation.instruments.find( + (instrument) => instrument.runstatePV === updatedPVName, + ); + if (foundInstrument) foundInstrument.runstate = updatedPVvalue; + }); + return newData; +} + +/** + * Copy an original array then update an instrument's runstate PV, then subscribe to it. return the copied array. + * @param prev the original array of target stations containing instrument runstate information + * @param instListEntry the instrument to change + * @param runstatePV the new runstate PV to update the array with and subscribe to + * @param sendJsonMessage a callback to subscribe to the runstate PV + */ +export function updateInstrumentRunstatePV( + prev: Array, + instListEntry: instListEntry, + runstatePV: string, + sendJsonMessage: (a: IfcPVWSRequest) => void, +) { + const newData: Array = [...prev]; + // Iterate through instruments in the instlist, get the runstate PV and subscribe + newData.forEach((targetStation) => { + const foundInstrument = targetStation.instruments.find( + (instrument) => instrument.name === instListEntry.name, + ); + if (foundInstrument) { + foundInstrument.runstatePV = instListEntry.pvPrefix + runstatePV; + // Subscribe to the instrument's runstate PV + sendJsonMessage({ + type: PVWSRequestType.subscribe, + pvs: [foundInstrument.runstatePV], + }); + } + }); + return newData; +} diff --git a/jest.config.ts b/jest.config.ts index 76b0d8e..7577895 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -12,7 +12,12 @@ const config: Config = { // Add more setup options before each test is run // setupFilesAfterEnv: ['/jest.setup.ts'], collectCoverage: true, - collectCoverageFrom: ["app/**/*.{ts,tsx}", "!**/*types.ts"], + collectCoverageFrom: [ + "app/**/*.{ts,tsx}", + "!**/*types.ts", + "!**/*commonVars.ts", + "!**/*layout.tsx", + ], }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async