From 1ed10b57f9275ebef676044f6bd7dfc038fee4ea Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Wed, 27 Nov 2024 23:26:06 +0000 Subject: [PATCH] tidy up some variables and refactor out some instlist parsing logic to try and keep the instruments page as small as possible --- app/commonVars.ts | 10 + app/components/InstList.ts | 38 -- app/components/InstrumentPage.test.ts | 9 +- app/components/InstrumentPage.tsx | 39 +- app/components/dehex_and_decompress.test.ts | 437 +++++++++++++++++++- app/components/dehex_and_decompress.ts | 18 +- app/instruments/page.test.tsx | 62 +++ app/instruments/page.tsx | 83 +++- app/types.ts | 25 +- app/wall/components/InstrumentWallCard.tsx | 8 +- app/wall/page.tsx | 77 ++-- jest.config.ts | 7 +- 12 files changed, 676 insertions(+), 137 deletions(-) create mode 100644 app/commonVars.ts delete mode 100644 app/components/InstList.ts create mode 100644 app/instruments/page.test.tsx 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..8e62865 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,16 @@ import { ConfigOutputBlock, IfcBlock, IfcGroup, - IfcPV, IfcPVWSMessage, IfcPVWSRequest, + 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 +50,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 +105,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 [currentInstrument, setCurrentInstrument] = useState( null, ); - const socketURL = - process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8080/pvws/pv"; + const instName = instrumentName; useEffect(() => { @@ -130,10 +131,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; @@ -156,7 +154,7 @@ function InstrumentData({ instrumentName }: { instrumentName: string }) { setCurrentInstrument(instrument); sendJsonMessage({ - type: "subscribe", + type: PVWSRequestType.subscribe, pvs: [`${prefix}${CONFIG_DETAILS}`], }); @@ -164,7 +162,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 +179,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 +198,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.test.tsx b/app/instruments/page.test.tsx new file mode 100644 index 0000000..f4f8a81 --- /dev/null +++ b/app/instruments/page.test.tsx @@ -0,0 +1,62 @@ +import { createInstrumentGroupsFromInstlist } from "@/app/instruments/page"; +import { instList } from "@/app/types"; + +test("createInstrumentGroupsFromInstlist adds an instrument to a group if it has one", () => { + 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/page.tsx b/app/instruments/page.tsx index f3ececf..858929e 100644 --- a/app/instruments/page.tsx +++ b/app/instruments/page.tsx @@ -1,40 +1,83 @@ "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, + instList, + PVWSRequestType, +} from "@/app/types"; +import { + dehex_and_decompress, + instListFromBytes, +} from "@/app/components/dehex_and_decompress"; +import useWebSocket from "react-use-websocket"; +import { instListPV, instListSubscription, socketURL } from "@/app/commonVars"; -export default function Home() { - let instList = InstList(); - - if (!instList) { - return

Loading...

; +export 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; +} - instList = Array.from(instList); +export default function Instruments() { + const [instrumentGroups, setInstrumentGroups] = useState< + Map> + >(new Map()); - let instruments = new Map(); + const { + sendJsonMessage, + lastJsonMessage, + }: { + sendJsonMessage: (a: IfcPVWSRequest) => void; + lastJsonMessage: IfcPVWSMessage; + } = useWebSocket(socketURL, { + shouldReconnect: (closeEvent) => true, + }); - for (let inst of instList) { - let groups = inst["groups"]; - let name = inst["name"]; + useEffect(() => { + // On page load, subscribe to the instrument list as it's required to get each instrument. + sendJsonMessage(instListSubscription); + }, [sendJsonMessage]); - for (let group of groups) { - if (!instruments.has(group)) { - instruments.set(group, []); - } - instruments.get(group).push(name); + useEffect(() => { + // Instlist has changed + if (!lastJsonMessage) { + return; } + + const updatedPV: IfcPVWSMessage = lastJsonMessage; + const updatedPVbytes: string | null | undefined = updatedPV.b64byt; + + 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 (
; } 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]] @@ -146,3 +150,14 @@ 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/page.tsx b/app/wall/page.tsx index 00cb552..194d6da 100644 --- a/app/wall/page.tsx +++ b/app/wall/page.tsx @@ -1,15 +1,20 @@ "use client"; 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 { + IfcPVWSMessage, + IfcPVWSRequest, + PVWSRequestType, + targetStation, +} from "@/app/types"; +import { instListPV, instListSubscription, socketURL } from "@/app/commonVars"; export default function WallDisplay() { const runstatePV = "DAE:RUNSTATE_STR"; - const instListPV = "CS:INSTLIST"; const [data, setData] = useState>([ { @@ -43,7 +48,7 @@ export default function WallDisplay() { { name: "SXD" }, { name: "TOSCA" }, { name: "VESUVIO" }, - ].sort((a, b) => a.name.localeCompare(b.name)), + ], }, { targetStation: "Target Station 2", @@ -59,7 +64,7 @@ export default function WallDisplay() { { name: "SANS2D" }, { name: "WISH" }, { name: "ZOOM" }, - ].sort((a, b) => a.name.localeCompare(b.name)), + ], }, { targetStation: "Miscellaneous", @@ -94,12 +99,10 @@ export default function WallDisplay() { { name: "WISH_SETUP", }, - ].sort((a, b) => a.name.localeCompare(b.name)), + ], }, ]); - const socketURL = process.env.NEXT_PUBLIC_WS_URL!; - const { sendJsonMessage, lastJsonMessage, @@ -112,10 +115,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(() => { @@ -130,42 +130,39 @@ 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"]; - setData((prev) => { - const newData: Array = [...prev]; - newData.map((targetStation) => { - const foundInstrument = targetStation.instruments.find( - (instrument) => instrument.name === instName, - ); - if (foundInstrument) { - // Subscribe to the instrument's runstate PV - foundInstrument.pv = instPrefix + runstatePV; - sendJsonMessage({ - type: "subscribe", - pvs: [foundInstrument.pv], - }); - } - }); - return newData; + const instListDict = instListFromBytes(updatedPVbytes); + for (const item of instListDict) { + // Iterate through instruments in the instlist, get the runstate PV and subscribe + const instName = item["name"]; + const instPrefix = item["pvPrefix"]; + setData((prev) => { + const newData: Array = [...prev]; + newData.map((targetStation) => { + const foundInstrument = targetStation.instruments.find( + (instrument) => instrument.name === instName, + ); + if (foundInstrument) { + foundInstrument.runstatePV = instPrefix + runstatePV; + // Subscribe to the instrument's runstate PV + sendJsonMessage({ + type: PVWSRequestType.subscribe, + pvs: [foundInstrument.runstatePV], + }); + } }); - } + return newData; + }); } } else if (updatedPVvalue) { setData((prev) => { const newData: Array = [...prev]; newData.map((targetStation) => { const foundInstrument = targetStation.instruments.findIndex( - (instrument) => instrument.pv === updatedPVName, + (instrument) => instrument.runstatePV === updatedPVName, ); - if (foundInstrument >= 0) - targetStation.instruments[foundInstrument].status = updatedPVvalue; + if (foundInstrument !== -1) + targetStation.instruments[foundInstrument].runstate = + updatedPVvalue; }); 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