From de12821d2fedb4e72725f1cf4580f79b2eb18c5f Mon Sep 17 00:00:00 2001 From: Sky French Date: Fri, 26 Jun 2020 10:41:44 +0100 Subject: [PATCH 1/9] json files for testing devices --- public/json/device.json | 123 +++++++++++++++++++++++++ public/json/drawer.json | 35 ++++++++ public/json/menu.json | 164 +++++++++++++++++++++++++++++++++ public/json/xpress.json | 194 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 516 insertions(+) create mode 100644 public/json/device.json create mode 100644 public/json/menu.json create mode 100644 public/json/xpress.json diff --git a/public/json/device.json b/public/json/device.json new file mode 100644 index 00000000..967ce615 --- /dev/null +++ b/public/json/device.json @@ -0,0 +1,123 @@ +{ + "type": "display", + "position": "relative", + "overflow": "auto", + "backgroundColor": "#012265", + "children": [ + { + "type": "shape", + "position": "relative", + "height": "5vh", + "width": "100%", + "backgroundColor": "#012265" + }, + { + "type": "slideshow", + "position": "relative", + "width": "90%", + "margin": "auto", + "overflow": "auto", + "backgroundColor": "white", + "border": { + "style": "line", + "width": 5, + "color": "#ffcf00", + "radius": 10 + }, + "children": [ + { + "type": "display", + "position": "relative", + "width": "100%", + "backgroundColor": "#cccccc", + "overflow": "auto", + "children": [ + { + "type": "label", + "position": "relative", + "height": "4rem", + "textAlign": "center", + "font": { + "style": "bold", + "size": 20 + }, + "backgroundColor": "#eeeeee", + "text": "Device Test" + }, + { + "type": "embeddedDisplay", + "position": "relative", + "file": "xpress.json", + "filetype": "json", + "defaultProtocol": "ca" + } + ] + }, + { + "type": "display", + "position": "relative", + "width": "100%", + "backgroundColor": "#cccccc", + "overflow": "auto", + "macros": { + "motor": "1" + }, + "children": [ + { + "type": "label", + "position": "relative", + "width": "100%", + "height": "4rem", + "textAlign": "center", + "font": { + "style": "bold", + "size": 30 + }, + "backgroundColor": "#eeeeee", + "text": "Embed" + }, + { + "type": "dynamicpage", + "position": "relative", + "routePath": "embed" + } + ] + }, + { + "type": "display", + "position": "relative", + "width": "100%", + "backgroundColor": "#cccccc", + "overflow": "auto", + "children": [ + { + "type": "label", + "position": "relative", + "width": "100%", + "height": "4rem", + "textAlign": "center", + "font": { + "style": "bold", + "size": 30 + }, + "backgroundColor": "#eeeeee", + "text": "Detail" + }, + { + "type": "dynamicpage", + "position": "relative", + "routePath": "detail" + } + ] + } + ] + }, + { + "type": "shape", + "position": "relative", + "height": "5vh", + "width": "100%", + "backgroundColor": "#012265" + } + ] +} diff --git a/public/json/drawer.json b/public/json/drawer.json index 9deb0734..f9741c66 100644 --- a/public/json/drawer.json +++ b/public/json/drawer.json @@ -103,6 +103,41 @@ ] } }, + + { + "type": "actionbutton", + "text": "Device Testing", + "position": "relative", + "width": "100%", + "height": "4rem", + "margin": "1% 5% 1% 5%", + "foregroundColor": "#012265", + "backgroundColor": "#ffcf00", + "font": { + "size": "25" + }, + "border": { + "style": "none", + "color": "#ffcf00", + "width": 1 + }, + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "app", + "pageDescription": { + "filename": "device", + "filetype": "json", + "macros": {} + } + } + } + ] + } + }, { "type": "actionbutton", "text": "Mobile Layout", diff --git a/public/json/menu.json b/public/json/menu.json new file mode 100644 index 00000000..62108574 --- /dev/null +++ b/public/json/menu.json @@ -0,0 +1,164 @@ +{ + "type": "flexcontainer", + "position": "relative", + "children": [ + { + "type": "actionbutton", + "text": "Beamline Synoptic", + "position": "relative", + "width": "300px", + "height": "200px", + "margin": "10px", + "backgroundColor": "#4da6ff", + "font": { + "size": 30 + }, + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "app", + "pageDescription": { + "filename": "beamline", + "filetype": "json", + "macros": {} + } + } + } + ] + } + }, + { + "type": "actionbutton", + "text": "Mobile Layout", + "position": "relative", + "width": "300px", + "height": "200px", + "margin": "10px", + "backgroundColor": "#ffa64d", + "font": { + "size": 30 + }, + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "app", + "pageDescription": { + "filename": "mobileDemo", + "filetype": "json", + "macros": {} + } + } + } + ] + } + }, + { + "type": "actionbutton", + "text": "Device Testing", + "position": "relative", + "width": "300px", + "height": "200px", + "margin": "10px", + "backgroundColor": "#ffa64d", + "font": { + "size": 30 + }, + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "app", + "pageDescription": { + "filename": "device", + "filetype": "json", + "macros": {} + } + } + } + ] + } + }, + { + "type": "actionbutton", + "text": "Performance Testing", + "position": "relative", + "width": "300px", + "height": "200px", + "margin": "10px", + "backgroundColor": "#4dffa6", + "font": { + "size": 30 + }, + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "app", + "pageDescription": { + "filename": "performance", + "filetype": "json", + "macros": {} + } + } + } + ] + } + }, + { + "type": "actionbutton", + "text": "Machine Status", + "position": "relative", + "width": "300px", + "height": "200px", + "margin": "10px", + "backgroundColor": "#e580ff", + "fontSize": "3rem", + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "location": "app", + "page": "machineStatus", + "macros": "{}" + } + } + ] + } + }, + { + "type": "actionbutton", + "text": "Modern Machine Status", + "position": "relative", + "width": "300px", + "height": "200px", + "margin": "10px", + "backgroundColor": "#ff7070", + "fontSize": "3rem", + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "location": "app", + "page": "modernMachineStatus", + "macros": "{}" + } + } + ] + } + } + ] +} diff --git a/public/json/xpress.json b/public/json/xpress.json new file mode 100644 index 00000000..5bbaca58 --- /dev/null +++ b/public/json/xpress.json @@ -0,0 +1,194 @@ +{ + "type": "display", + "position": "relative", + "width": "600px", + "height": "210px", + "margin": "10px", + "border": { + "style": "dotted", + "width": 3, + "color": "red" + }, + "children": [ + { + "type": "progressbar", + "position": "relative", + "width": "25%", + "pvName": "csim://sine(-10, 10, 100, 0.1)", + "precision": 2, + "min": -10, + "max": 10, + "border": { + "style": "line", + "width": 1, + "color": "#aaaaaa" + }, + "font": { + "size": 20, + "style": "bold" + } + }, + { + "type": "actionbutton", + "pvName": "sim://limit#1", + "text": "Motor 2", + "image": "DCM-base.png", + "position": "absolute", + "x": "150px", + "y": "0px", + "width": "100px", + "height": "100px", + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "embed", + "pageDescription": { + "filename": "motor", + "filetype": "json", + "description": "Motor 2 Info", + "macros": { "motor": "2" } + } + } + } + ] + } + }, + { + "type": "shape", + "position": "absolute", + "x": "250px", + "y": "50px", + "width": "100px", + "height": "100px", + "shapeWidth": "100px", + "shapeHeight": "5px", + "shapeRadius": "0", + "backgroundColor": "red" + }, + { + "type": "shape", + "position": "absolute", + "x": "100px", + "y": "0px", + "width": "50px", + "height": "100px", + "shapeWidth": "50px", + "shapeHeight": "100px", + "shapeRadius": "50%", + "backgroundColor": "green" + }, + { + "type": "actionbutton", + "pvName": "sim://limit#1", + "text": "Motor 3", + "image": "DCM-base.png", + "position": "absolute", + "x": "350px", + "y": "0px", + "width": "100px", + "height": "100px", + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "embed", + "pageDescription": { + "filename": "motor", + "filetype": "json", + "description": "Motor 3 Info", + "macros": { "motor": "3" } + } + } + } + ] + } + }, + { + "type": "actionbutton", + "pvName": "sim://limit#1", + "text": "RFFB OPI", + "position": "absolute", + "x": "450px", + "y": "0px", + "width": "100px", + "height": "100px", + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "embed", + "pageDescription": { + "filename": "rffb", + "filetype": "json", + "description": "Open RFFB", + "macros": {} + } + } + } + ] + } + }, + { + "type": "actionbutton", + "pvName": "sim://limit#1", + "text": "Digitel MPC Ionp", + "image": "ionp.svg", + "position": "absolute", + "x": "450px", + "y": "100px", + "width": "100px", + "height": "100px", + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "embed", + "pageDescription": { + "filename": "digitelmpcionp", + "filetype": "json", + "description": "Digitel MPC Ionp Sim", + "macros": {} + } + } + } + ] + } + }, + { + "type": "actionbutton", + "text": "Motor Summary Screen", + "position": "absolute", + "x": "20px", + "y": "125px", + "width": "200px", + "height": "40px", + "backgroundColor": "#aaaaaa", + "actions": { + "executeAsOne": false, + "actions": [ + { + "type": "OPEN_PAGE", + "openPageInfo": { + "page": "embed", + "pageDescription": { + "filename": "motorSummary", + "filetype": "json", + "macros": {}, + "description": "Motor Summary Screen" + } + } + } + ] + } + } + ] +} From d0fa490edb63046a558f5b05d832862f462d9a66 Mon Sep 17 00:00:00 2001 From: George Sheppard Date: Thu, 19 Nov 2020 16:44:34 +0000 Subject: [PATCH 2/9] Simplified middleware throttling --- src/redux/throttleMiddleware.test.ts | 35 ++++++++++++++++------------ src/redux/throttleMiddleware.ts | 33 ++++++++++---------------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/redux/throttleMiddleware.test.ts b/src/redux/throttleMiddleware.test.ts index 8adce6ce..d543aada 100644 --- a/src/redux/throttleMiddleware.test.ts +++ b/src/redux/throttleMiddleware.test.ts @@ -18,21 +18,26 @@ describe("UpdateThrottle", (): void => { }); it("collects updates", (): void => { const updater = new UpdateThrottle(100); - updater.queueUpdate({ - type: VALUE_CHANGED, - payload: { - pvName: "PV", - value: ddouble(0) - } - }); - updater.queueUpdate({ - type: VALUE_CHANGED, - payload: { - pvName: "PV", - value: ddouble(1) - } - }); - updater.clearQueue(mockStore); + updater.queueUpdate( + { + type: VALUE_CHANGED, + payload: { + pvName: "PV", + value: ddouble(0) + } + }, + mockStore + ); + updater.queueUpdate( + { + type: VALUE_CHANGED, + payload: { + pvName: "PV", + value: ddouble(1) + } + }, + mockStore + ); expect(mockStore.dispatch).toHaveBeenCalledTimes(1); }); }); diff --git a/src/redux/throttleMiddleware.ts b/src/redux/throttleMiddleware.ts index 10126a39..e2ed89db 100644 --- a/src/redux/throttleMiddleware.ts +++ b/src/redux/throttleMiddleware.ts @@ -1,36 +1,30 @@ import { VALUE_CHANGED, Action, VALUES_CHANGED } from "./actions"; import { MiddlewareAPI, Dispatch } from "redux"; +/** + * Throttling helper class, lets a queue build up and then sends it + * at set intervals + * @param updateMillis: the interval to flush the queue at (in milliseconds) + */ export class UpdateThrottle { private queue: Action[]; - private started: boolean; - private updateMillis: number; public ready: boolean; constructor(updateMillis: number) { this.queue = []; - this.started = false; - this.updateMillis = updateMillis; this.ready = true; - this.queueUpdate = this.queueUpdate.bind(this); - this.clearQueue = this.clearQueue.bind(this); - this.setReady = this.setReady.bind(this); + setInterval(() => (this.ready = true), updateMillis); } - public queueUpdate(action: Action): void { + public queueUpdate(action: Action, store: MiddlewareAPI): void { this.queue.push(action); - if (!this.started) { - setInterval(this.setReady, this.updateMillis); - this.started = true; + if (this.ready) { + this.sendQueue(store); } } - public setReady(): void { - this.ready = true; - } - - public clearQueue(store: MiddlewareAPI): void { + public sendQueue(store: MiddlewareAPI): void { store.dispatch({ type: VALUES_CHANGED, payload: [...this.queue] }); - this.queue.length = 0; + this.queue = []; this.ready = false; } } @@ -42,10 +36,7 @@ export const throttleMiddleware = (updater: UpdateThrottle) => ( // this makes the return value 'Action | undefined' ) => (next: Dispatch) => (action: Action): Action | undefined => { if (action.type === VALUE_CHANGED) { - updater.queueUpdate(action); - if (updater.ready) { - updater.clearQueue(store); - } + updater.queueUpdate(action, store); } else { return next(action); } From d446f39fcac3498da94556900bd05121e5bca7a1 Mon Sep 17 00:00:00 2001 From: George Sheppard Date: Tue, 24 Nov 2020 15:47:27 +0000 Subject: [PATCH 3/9] Initial draft at device functionality, collects device information from coniql successfully --- .env | 2 +- public/json/drawer.json | 15 +- public/json/opiTesting.json | 2 +- public/json/xpress.json | 191 +------------------- src/connection/coniql.ts | 99 +++++++++- src/connection/forwarder.ts | 18 +- src/connection/plugin.ts | 7 +- src/connection/sim.ts | 28 +++ src/redux/actions.ts | 51 +++++- src/redux/connectionMiddleware.ts | 54 +++++- src/redux/csState.ts | 79 +++++++- src/redux/store.ts | 4 +- src/redux/throttleMiddleware.ts | 28 ++- src/ui/hooks/useDevice.tsx | 13 ++ src/ui/hooks/useDeviceSubscription.tsx | 23 +++ src/ui/hooks/utils.ts | 11 ++ src/ui/widgets/Device/coniqlParser.ts | 79 ++++++++ src/ui/widgets/Device/device.tsx | 50 +++++ src/ui/widgets/EmbeddedDisplay/opiParser.ts | 3 +- src/ui/widgets/index.ts | 1 + 20 files changed, 535 insertions(+), 223 deletions(-) create mode 100644 src/ui/hooks/useDevice.tsx create mode 100644 src/ui/hooks/useDeviceSubscription.tsx create mode 100644 src/ui/widgets/Device/coniqlParser.ts create mode 100644 src/ui/widgets/Device/device.tsx diff --git a/.env b/.env index 3859776a..3ae173d7 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # React app settings. Restart the process for changes to take effect. # Set to connect to a Coniql server. -# REACT_APP_CONIQL_SOCKET=cs03r-sc-serv-40.diamond.ac.uk:8080 +REACT_APP_CONIQL_SOCKET=cs03r-sc-serv-18.diamond.ac.uk:8080 REACT_APP_BASE_URL=http://localhost:3000 REACT_APP_SIMULATION_TIME=100 diff --git a/public/json/drawer.json b/public/json/drawer.json index f9741c66..2a9d93e1 100644 --- a/public/json/drawer.json +++ b/public/json/drawer.json @@ -103,7 +103,6 @@ ] } }, - { "type": "actionbutton", "text": "Device Testing", @@ -126,12 +125,14 @@ "actions": [ { "type": "OPEN_PAGE", - "openPageInfo": { - "page": "app", - "pageDescription": { - "filename": "device", - "filetype": "json", - "macros": {} + "dynamicInfo": { + "name": "app", + "location": "app", + "description": "", + "file": { + "path": "xpress.json", + "macros": {}, + "defaultProtocol": "pva" } } } diff --git a/public/json/opiTesting.json b/public/json/opiTesting.json index a7a05dbb..cc4e673a 100644 --- a/public/json/opiTesting.json +++ b/public/json/opiTesting.json @@ -7,7 +7,7 @@ "position": "relative", "margin": "10px", "file": { - "path": "synoptic.opi", + "path": "motor_detail.opi", "macros": {}, "defaultProtocol": "ca" } diff --git a/public/json/xpress.json b/public/json/xpress.json index 5bbaca58..a8237223 100644 --- a/public/json/xpress.json +++ b/public/json/xpress.json @@ -1,194 +1,13 @@ { "type": "display", "position": "relative", - "width": "600px", - "height": "210px", - "margin": "10px", - "border": { - "style": "dotted", - "width": 3, - "color": "red" - }, + "width": "100%", "children": [ - { - "type": "progressbar", - "position": "relative", - "width": "25%", - "pvName": "csim://sine(-10, 10, 100, 0.1)", - "precision": 2, - "min": -10, - "max": 10, - "border": { - "style": "line", - "width": 1, - "color": "#aaaaaa" - }, - "font": { - "size": 20, - "style": "bold" - } - }, { - "type": "actionbutton", - "pvName": "sim://limit#1", - "text": "Motor 2", - "image": "DCM-base.png", - "position": "absolute", - "x": "150px", - "y": "0px", - "width": "100px", - "height": "100px", - "actions": { - "executeAsOne": false, - "actions": [ - { - "type": "OPEN_PAGE", - "openPageInfo": { - "page": "embed", - "pageDescription": { - "filename": "motor", - "filetype": "json", - "description": "Motor 2 Info", - "macros": { "motor": "2" } - } - } - } - ] - } - }, - { - "type": "shape", - "position": "absolute", - "x": "250px", - "y": "50px", - "width": "100px", - "height": "100px", - "shapeWidth": "100px", - "shapeHeight": "5px", - "shapeRadius": "0", - "backgroundColor": "red" - }, - { - "type": "shape", - "position": "absolute", - "x": "100px", - "y": "0px", - "width": "50px", - "height": "100px", - "shapeWidth": "50px", - "shapeHeight": "100px", - "shapeRadius": "50%", - "backgroundColor": "green" - }, - { - "type": "actionbutton", - "pvName": "sim://limit#1", - "text": "Motor 3", - "image": "DCM-base.png", - "position": "absolute", - "x": "350px", - "y": "0px", - "width": "100px", - "height": "100px", - "actions": { - "executeAsOne": false, - "actions": [ - { - "type": "OPEN_PAGE", - "openPageInfo": { - "page": "embed", - "pageDescription": { - "filename": "motor", - "filetype": "json", - "description": "Motor 3 Info", - "macros": { "motor": "3" } - } - } - } - ] - } - }, - { - "type": "actionbutton", - "pvName": "sim://limit#1", - "text": "RFFB OPI", - "position": "absolute", - "x": "450px", - "y": "0px", - "width": "100px", - "height": "100px", - "actions": { - "executeAsOne": false, - "actions": [ - { - "type": "OPEN_PAGE", - "openPageInfo": { - "page": "embed", - "pageDescription": { - "filename": "rffb", - "filetype": "json", - "description": "Open RFFB", - "macros": {} - } - } - } - ] - } - }, - { - "type": "actionbutton", - "pvName": "sim://limit#1", - "text": "Digitel MPC Ionp", - "image": "ionp.svg", - "position": "absolute", - "x": "450px", - "y": "100px", - "width": "100px", - "height": "100px", - "actions": { - "executeAsOne": false, - "actions": [ - { - "type": "OPEN_PAGE", - "openPageInfo": { - "page": "embed", - "pageDescription": { - "filename": "digitelmpcionp", - "filetype": "json", - "description": "Digitel MPC Ionp Sim", - "macros": {} - } - } - } - ] - } - }, - { - "type": "actionbutton", - "text": "Motor Summary Screen", - "position": "absolute", - "x": "20px", - "y": "125px", - "width": "200px", - "height": "40px", - "backgroundColor": "#aaaaaa", - "actions": { - "executeAsOne": false, - "actions": [ - { - "type": "OPEN_PAGE", - "openPageInfo": { - "page": "embed", - "pageDescription": { - "filename": "motorSummary", - "filetype": "json", - "macros": {}, - "description": "Motor Summary Screen" - } - } - } - ] - } + "type": "device", + "deviceName": "Xspress3", + "position": "relative", + "width": "100%" } ] } diff --git a/src/connection/coniql.ts b/src/connection/coniql.ts index bd084e21..3cfcba1a 100644 --- a/src/connection/coniql.ts +++ b/src/connection/coniql.ts @@ -22,7 +22,9 @@ import { ValueChangedCallback, nullConnCallback, nullValueCallback, - SubscriptionType + SubscriptionType, + DeviceCallback, + nullDeviceCallback } from "./plugin"; import { SubscriptionClient } from "subscriptions-transport-ws"; import { @@ -234,14 +236,45 @@ const PV_MUTATION = gql` } `; +// TODO: Turn id into a variable +const DEVICE_SUBSCRIPTION = gql` + query { + getDevice(id: "Xspress3") { + id + children(flatten: true) { + name + label + child { + __typename + ... on Channel { + id + } + ... on Device { + id + } + ... on Group { + layout + children { + name + } + } + } + } + } + } +`; + export class ConiqlPlugin implements Connection { private client: ApolloClient; private onConnectionUpdate: ConnectionChangedCallback; private onValueUpdate: ValueChangedCallback; + private onDeviceUpdate: DeviceCallback; private connected: boolean; private wsClient: SubscriptionClient; private disconnected: string[] = []; + private disconnectedDevices: string[] = []; private subscriptions: { [pvName: string]: Subscription }; + private deviceSubscriptions: { [device: string]: Subscription }; public constructor(socket: string) { const fragmentMatcher = new IntrospectionFragmentMatcher({ @@ -256,7 +289,11 @@ export class ConiqlPlugin implements Connection { for (const pvName of this.disconnected) { this._subscribe(pvName); } + for (const device of this.disconnectedDevices) { + this._subscribeDevice(device); + } this.disconnected = []; + this.disconnectedDevices = []; }); this.wsClient.onDisconnected((): void => { log.error("Websockect client disconnected."); @@ -275,13 +312,30 @@ export class ConiqlPlugin implements Connection { isReadonly: true }); } + for (const device of Object.keys(this.deviceSubscriptions)) { + if ( + this.deviceSubscriptions.hasOwnProperty(device) && + this.deviceSubscriptions[device] + ) { + this.deviceSubscriptions[device].unsubscribe(); + delete this.deviceSubscriptions[device]; + } else { + log.error(`Attempt to unsubscribe from ${device} failed`); + } + this.onConnectionUpdate(device, { + isConnected: false, + isReadonly: true + }); + } }); const link = this.createLink(socket); this.client = new ApolloClient({ link, cache }); this.onConnectionUpdate = nullConnCallback; this.onValueUpdate = nullValueCallback; + this.onDeviceUpdate = nullDeviceCallback; this.connected = false; this.subscriptions = {}; + this.deviceSubscriptions = {}; } public createLink(socket: string): ApolloLink { @@ -317,10 +371,12 @@ export class ConiqlPlugin implements Connection { public connect( connectionCallback: ConnectionChangedCallback, - valueCallback: ValueChangedCallback + valueCallback: ValueChangedCallback, + deviceCallback: DeviceCallback ): void { this.onConnectionUpdate = connectionCallback; this.onValueUpdate = valueCallback; + this.onDeviceUpdate = deviceCallback; this.connected = true; } @@ -341,6 +397,10 @@ export class ConiqlPlugin implements Connection { this.onValueUpdate(pvName, dtype); } + private _processDevice(data: any, device: string, operation: string): void { + this.onDeviceUpdate(device, JSON.stringify(data.data)); + } + private _subscribe(pvName: string): Subscription { return this.client .subscribe({ @@ -365,6 +425,29 @@ export class ConiqlPlugin implements Connection { }); } + private _subscribeDevice(device: string): Subscription { + return this.client + .subscribe({ + query: DEVICE_SUBSCRIPTION, + variables: { device: device } + }) + .subscribe({ + next: (data): void => { + this._processDevice(data, device, "subscribeDevice"); + }, + error: (err): void => { + log.error("err", err); + }, + complete: (): void => { + this.onConnectionUpdate(device, { + isConnected: false, + isReadonly: true + }); + this.disconnectedDevices.push(device); + } + }); + } + public subscribe(pvName: string, type: SubscriptionType): string { // TODO: How to handle multiple subscriptions of different types to the same channel? if (this.subscriptions[pvName] === undefined) { @@ -373,6 +456,13 @@ export class ConiqlPlugin implements Connection { return pvName; } + public subscribeDevice(device: string): string { + if (this.deviceSubscriptions[device] === undefined) { + this.deviceSubscriptions[device] = this._subscribeDevice(device); + } + return device; + } + public putPv(pvName: string, value: DType): void { log.debug(`Putting ${value} to ${pvName}.`); const variables = { @@ -397,4 +487,9 @@ export class ConiqlPlugin implements Connection { this.subscriptions[pvName].unsubscribe(); delete this.subscriptions[pvName]; } + + public unsubscribeDevice(device: string): void { + this.deviceSubscriptions[device].unsubscribe(); + delete this.deviceSubscriptions[device]; + } } diff --git a/src/connection/forwarder.ts b/src/connection/forwarder.ts index 7a644e9f..c1974162 100644 --- a/src/connection/forwarder.ts +++ b/src/connection/forwarder.ts @@ -2,6 +2,7 @@ import { Connection, ConnectionChangedCallback, ValueChangedCallback, + DeviceCallback, SubscriptionType } from "./plugin"; @@ -35,6 +36,18 @@ export class ConnectionForwarder implements Connection { return connection.unsubscribe(pvName); } + // TODO: Finish this function + public subscribeDevice(device: string): string { + const connection = this.getConnection(device); + return connection.subscribeDevice(device); + } + + // TODO: Finish this function + public unsubscribeDevice(device: string): void { + const connection = this.getConnection(device); + return connection.unsubscribeDevice(device); + } + public isConnected(): boolean { return this.connected; } @@ -46,12 +59,13 @@ export class ConnectionForwarder implements Connection { public connect( connectionCallback: ConnectionChangedCallback, - valueCallback: ValueChangedCallback + valueCallback: ValueChangedCallback, + deviceCallback: DeviceCallback ): void { for (const [, connection] of this.prefixConnections) { if (connection !== undefined) { if (!connection.isConnected()) { - connection.connect(connectionCallback, valueCallback); + connection.connect(connectionCallback, valueCallback, deviceCallback); } } } diff --git a/src/connection/plugin.ts b/src/connection/plugin.ts index 4df4de0b..aafef3e6 100644 --- a/src/connection/plugin.ts +++ b/src/connection/plugin.ts @@ -11,6 +11,7 @@ export interface SubscriptionType { export const nullConnCallback: ConnectionChangedCallback = (_p, _v): void => {}; export const nullValueCallback: ValueChangedCallback = (_p, _v): void => {}; +export const nullDeviceCallback: DeviceCallback = (_d, _v): void => {}; export interface ConnectionState { isConnected: boolean; @@ -22,14 +23,18 @@ export type ConnectionChangedCallback = ( value: ConnectionState ) => void; export type ValueChangedCallback = (pvName: string, value: DType) => void; +export type DeviceCallback = (device: string, value: {}) => void; export interface Connection { subscribe: (pvName: string, type: SubscriptionType) => string; // must be idempotent + subscribeDevice: (device: string) => string; putPv: (pvName: string, value: DType) => void; connect: ( connectionCallback: ConnectionChangedCallback, - valueCallback: ValueChangedCallback + valueCallback: ValueChangedCallback, + deviceCallback: DeviceCallback ) => void; isConnected: () => boolean; unsubscribe: (pvName: string) => void; + unsubscribeDevice: (device: string) => void; } diff --git a/src/connection/sim.ts b/src/connection/sim.ts index ee294f0d..207da071 100644 --- a/src/connection/sim.ts +++ b/src/connection/sim.ts @@ -26,6 +26,7 @@ abstract class SimPv { private onConnectionUpdate: ConnectionChangedCallback; private onValueUpdate: ValueChangedCallback; protected subscribed: boolean; + protected subscribedDevice: boolean; public pvName: string; protected updateRate?: number; abstract getValue(): DType; @@ -42,6 +43,7 @@ abstract class SimPv { this.updateRate = updateRate; this.publishConnection(); this.subscribed = false; + this.subscribedDevice = false; } public getConnection(): ConnectionState { @@ -53,16 +55,32 @@ abstract class SimPv { this.publish(); } + public subscribeDevice(): void { + this.subscribedDevice = true; + this.publishDevice(); + } + public unsubscribe(): void { this.subscribed = false; } + public unsubscribeDevice(): void { + this.subscribedDevice = false; + } + public publish(): void { if (this.subscribed) { this.onValueUpdate(this.pvName, this.getValue()); } } + // TODO: This needs filling out + public publishDevice(): void { + if (this.subscribedDevice) { + this.onValueUpdate("fake device", this.getValue()); + } + } + public publishConnection(): void { this.onConnectionUpdate(this.pvName, this.getConnection()); } @@ -358,6 +376,11 @@ export class SimulatorPlugin implements Connection { return (simulator && simulator.pvName) || pvName; } + // TODO: Finish this function + public subscribeDevice(device: string): string { + return ""; + } + public connect( connectionCallback: ConnectionChangedCallback, valueCallback: ValueChangedCallback @@ -531,4 +554,9 @@ export class SimulatorPlugin implements Connection { } } } + + // TODO: Finish this function + public unsubscribeDevice(device: string): void { + log.debug(`Unsubscribing from ${device}`); + } } diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 5a3dbfbf..172b0b75 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -7,15 +7,10 @@ export const UNSUBSCRIBE = "unsubscribe"; export const VALUE_CHANGED = "value_changed"; export const VALUES_CHANGED = "values_changed"; export const WRITE_PV = "write_pv"; - -/* The never type in the constructor ensures that TypeScript - won't allow this error to be created. This is useful in - switch blocks to check that all cases have been handled. */ -export class InvalidAction extends Error { - public constructor(val: never) { - super(`Invalid action: ${val}`); - } -} +export const SUBSCRIBE_DEVICE = "subscribe_device"; +export const DEVICE_CHANGED = "device_changed"; +export const DEVICES_CHANGED = "devices_changed"; +export const UNSUBSCRIBE_DEVICE = "unsubscribe_device"; export interface ConnectionChanged { type: typeof CONNECTION_CHANGED; @@ -56,6 +51,38 @@ export interface ValuesChanged { payload: ValueChanged[]; } +// TODO: Be more specific with type on value here +export interface SubscribeDevice { + type: typeof SUBSCRIBE_DEVICE; + payload: { + device: string; + componentId: string; + }; +} + +export interface UnsubscribeDevice { + type: typeof UNSUBSCRIBE_DEVICE; + payload: { + componentId: string; + device: string; + }; +} + +// TODO: Be more specific with type on value here +export interface DeviceChanged { + type: typeof DEVICE_CHANGED; + payload: { + device: string; + componentId: string; + value: string; + }; +} + +export interface DevicesChanged { + type: typeof DEVICES_CHANGED; + payload: DeviceChanged[]; +} + export interface WritePv { type: typeof WRITE_PV; payload: { @@ -70,4 +97,8 @@ export type Action = | Unsubscribe | ValueChanged | ValuesChanged - | WritePv; + | WritePv + | DeviceChanged + | DevicesChanged + | UnsubscribeDevice + | SubscribeDevice; diff --git a/src/redux/connectionMiddleware.ts b/src/redux/connectionMiddleware.ts index 136ff96a..2846e312 100644 --- a/src/redux/connectionMiddleware.ts +++ b/src/redux/connectionMiddleware.ts @@ -6,8 +6,12 @@ import { SUBSCRIBE, WRITE_PV, VALUE_CHANGED, + DEVICE_CHANGED, + SUBSCRIBE_DEVICE, UNSUBSCRIBE, - Action + Action, + SubscribeDevice, + UNSUBSCRIBE_DEVICE } from "./actions"; import { DType } from "../types/dtypes"; @@ -33,6 +37,17 @@ function valueChanged( }); } +function deviceChanged( + store: MiddlewareAPI, + device: string, + value: string +): void { + store.dispatch({ + type: DEVICE_CHANGED, + payload: { device: device, value: value } + }); +} + export const connectionMiddleware = (connection: Connection) => ( store: MiddlewareAPI ) => (next: Dispatch): any => (action: Action): Action => { @@ -41,7 +56,10 @@ export const connectionMiddleware = (connection: Connection) => ( // Partial function application. (pvName: string, value: ConnectionState): void => connectionChanged(store, pvName, value), - (pvName: string, value: DType): void => valueChanged(store, pvName, value) + (pvName: string, value: DType): void => + valueChanged(store, pvName, value), + // TODO: Be more specific with type here + (device: string, value: any): void => deviceChanged(store, device, value) ); } @@ -69,6 +87,38 @@ export const connectionMiddleware = (connection: Connection) => ( }; break; } + case SUBSCRIBE_DEVICE: { + const { device } = (action as SubscribeDevice).payload; + + try { + connection.subscribeDevice(device); + } catch (error) { + log.error(`Failed to subscribe to device ${device}`); + log.error(error); + } + + action = { + ...action, + payload: { + ...action.payload + } + }; + break; + } + case UNSUBSCRIBE_DEVICE: { + const { componentId, device } = action.payload; + const subs = store.getState().deviceSubscriptions; + + if (subs[device].length === 1 && subs[device][0] === componentId) { + try { + connection.unsubscribeDevice(device); + } catch (error) { + log.error(`Failed to unsubscribe from device ${device}`); + log.error(error); + } + } + break; + } case WRITE_PV: { const { pvName, value } = action.payload; const effectivePvName = diff --git a/src/redux/csState.ts b/src/redux/csState.ts index f0a92fc9..e3434c73 100644 --- a/src/redux/csState.ts +++ b/src/redux/csState.ts @@ -4,10 +4,15 @@ import { VALUES_CHANGED, Action, SUBSCRIBE, + SUBSCRIBE_DEVICE, WRITE_PV, CONNECTION_CHANGED, UNSUBSCRIBE, - ValueChanged + ValueChanged, + UNSUBSCRIBE_DEVICE, + DEVICE_CHANGED, + DEVICES_CHANGED, + DeviceChanged } from "./actions"; import { MacroMap } from "../types/macros"; import { DType, mergeDType } from "../types/dtypes"; @@ -16,7 +21,9 @@ const initialState: CsState = { valueCache: {}, globalMacros: { SUFFIX: "1" }, effectivePvNameMap: {}, - subscriptions: {} + subscriptions: {}, + deviceSubscriptions: {}, + deviceCache: {} }; export interface PvState { @@ -33,6 +40,11 @@ export interface ValueCache { [key: string]: FullPvState; } +// TODO: Be more specific with type here +export interface DeviceCache { + [key: string]: any; +} + export interface Subscriptions { [pv: string]: string[]; } @@ -43,8 +55,17 @@ export interface CsState { effectivePvNameMap: { [pvName: string]: string }; globalMacros: MacroMap; subscriptions: Subscriptions; + deviceSubscriptions: Subscriptions; + deviceCache: DeviceCache; } +/** + * Merges new value of a pv with the old + * pv metadata + * @param oldValueCache + * @param newValueCache + * @param action + */ function updateValueCache( oldValueCache: ValueCache, newValueCache: ValueCache, @@ -59,6 +80,17 @@ function updateValueCache( newValueCache[action.payload.pvName] = newPvState; } +// TODO: Should probably make this a bit more extensive +function updateDeviceCache( + oldDeviceCache: DeviceCache, + newDeviceCache: DeviceCache, + action: DeviceChanged +): void { + const { value } = action.payload; + console.log("new value", value); + newDeviceCache[action.payload.device] = value; +} + export function csReducer(state = initialState, action: Action): CsState { log.debug(action); switch (action.type) { @@ -67,6 +99,18 @@ export function csReducer(state = initialState, action: Action): CsState { updateValueCache(state.valueCache, newValueCache, action); return { ...state, valueCache: newValueCache }; } + case DEVICE_CHANGED: { + const newDeviceCache: DeviceCache = { ...state.deviceCache }; + updateDeviceCache(state.deviceCache, newDeviceCache, action); + return { ...state, deviceCache: newDeviceCache }; + } + case DEVICES_CHANGED: { + const newDeviceCache: DeviceCache = { ...state.deviceCache }; + for (const changedAction of action.payload) { + updateDeviceCache(state.deviceCache, newDeviceCache, changedAction); + } + return { ...state, deviceCache: newDeviceCache }; + } case VALUES_CHANGED: { const newValueCache: ValueCache = { ...state.valueCache }; for (const changedAction of action.payload) { @@ -139,6 +183,37 @@ export function csReducer(state = initialState, action: Action): CsState { // Handled by middleware. break; } + case SUBSCRIBE_DEVICE: { + const { device, componentId } = action.payload; + const newDeviceSubscriptions = { ...state.deviceSubscriptions }; + if (newDeviceSubscriptions.hasOwnProperty(device)) { + newDeviceSubscriptions[device].push(componentId); + } else { + newDeviceSubscriptions[device] = [componentId]; + } + return { + ...state, + deviceSubscriptions: newDeviceSubscriptions + }; + } + case UNSUBSCRIBE_DEVICE: { + const { device, componentId } = action.payload; + + const newDeviceSubscriptions = { ...state.deviceSubscriptions }; + const newDeviceSubs = state.deviceSubscriptions[device].filter( + (id): boolean => id !== componentId + ); + if (newDeviceSubs.length > 0) { + newDeviceSubscriptions[device] = newDeviceSubs; + } else { + delete newDeviceSubscriptions[device]; + } + + return { + ...state, + deviceSubscriptions: newDeviceSubscriptions + }; + } } return state; } diff --git a/src/redux/store.ts b/src/redux/store.ts index bab8b233..3943e61d 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -26,6 +26,7 @@ if (CONIQL_SOCKET !== undefined) { plugins.unshift(["pva://", coniql]); plugins.unshift(["ca://", coniql]); plugins.unshift(["ssim://", coniql]); + plugins.unshift(["Xspress", coniql]); } const connection = new ConnectionForwarder(plugins); @@ -34,10 +35,11 @@ const composeEnhancers = export const store = createStore( csReducer, - /* preloadedState, */ composeEnhancers( + composeEnhancers( applyMiddleware( connectionMiddleware(connection), throttleMiddleware(new UpdateThrottle(THROTTLE_PERIOD)) ) + // other store enhancers here ) ); diff --git a/src/redux/throttleMiddleware.ts b/src/redux/throttleMiddleware.ts index e2ed89db..6672d493 100644 --- a/src/redux/throttleMiddleware.ts +++ b/src/redux/throttleMiddleware.ts @@ -1,4 +1,10 @@ -import { VALUE_CHANGED, Action, VALUES_CHANGED } from "./actions"; +import { + VALUE_CHANGED, + Action, + VALUES_CHANGED, + DEVICE_CHANGED, + DEVICES_CHANGED +} from "./actions"; import { MiddlewareAPI, Dispatch } from "redux"; /** @@ -7,24 +13,32 @@ import { MiddlewareAPI, Dispatch } from "redux"; * @param updateMillis: the interval to flush the queue at (in milliseconds) */ export class UpdateThrottle { - private queue: Action[]; + private pvQueue: Action[]; + private deviceQueue: Action[]; public ready: boolean; constructor(updateMillis: number) { - this.queue = []; + this.pvQueue = []; + this.deviceQueue = []; this.ready = true; setInterval(() => (this.ready = true), updateMillis); } public queueUpdate(action: Action, store: MiddlewareAPI): void { - this.queue.push(action); + if (action.type === VALUE_CHANGED) { + this.pvQueue.push(action); + } else if (action.type === DEVICE_CHANGED) { + this.deviceQueue.push(action); + } if (this.ready) { this.sendQueue(store); } } public sendQueue(store: MiddlewareAPI): void { - store.dispatch({ type: VALUES_CHANGED, payload: [...this.queue] }); - this.queue = []; + store.dispatch({ type: VALUES_CHANGED, payload: [...this.pvQueue] }); + store.dispatch({ type: DEVICES_CHANGED, payload: [...this.deviceQueue] }); + this.pvQueue = []; + this.deviceQueue = []; this.ready = false; } } @@ -35,7 +49,7 @@ export const throttleMiddleware = (updater: UpdateThrottle) => ( // we don't call next(action) so return undefined. // this makes the return value 'Action | undefined' ) => (next: Dispatch) => (action: Action): Action | undefined => { - if (action.type === VALUE_CHANGED) { + if (action.type === VALUE_CHANGED || action.type === DEVICE_CHANGED) { updater.queueUpdate(action, store); } else { return next(action); diff --git a/src/ui/hooks/useDevice.tsx b/src/ui/hooks/useDevice.tsx new file mode 100644 index 00000000..9be9c6ea --- /dev/null +++ b/src/ui/hooks/useDevice.tsx @@ -0,0 +1,13 @@ +import { useSelector } from "react-redux"; +import { CsState } from "../../redux/csState"; +import { deviceSelector, deviceComparator } from "./utils"; +import { useDeviceSubscription } from "./useDeviceSubscription"; + +export function useDevice(componentId: string, device: string): {} { + useDeviceSubscription(componentId, device); + const description = useSelector( + (state: CsState): {} => deviceSelector(device, state), + deviceComparator + ); + return description; +} diff --git a/src/ui/hooks/useDeviceSubscription.tsx b/src/ui/hooks/useDeviceSubscription.tsx new file mode 100644 index 00000000..2736b990 --- /dev/null +++ b/src/ui/hooks/useDeviceSubscription.tsx @@ -0,0 +1,23 @@ +import { useDispatch } from "react-redux"; +import { useEffect } from "react"; +import { SUBSCRIBE_DEVICE, UNSUBSCRIBE_DEVICE } from "../../redux/actions"; + +export function useDeviceSubscription( + componentId: string, + device: string +): void { + const dispatch = useDispatch(); + + useEffect((): (() => void) => { + dispatch({ + type: SUBSCRIBE_DEVICE, + payload: { device: device, componentId } + }); + return (): void => { + dispatch({ + type: UNSUBSCRIBE_DEVICE, + payload: { device: device, componentId } + }); + }; + }, [dispatch, componentId, device]); +} diff --git a/src/ui/hooks/utils.ts b/src/ui/hooks/utils.ts index fe371050..e6ab4767 100644 --- a/src/ui/hooks/utils.ts +++ b/src/ui/hooks/utils.ts @@ -16,6 +16,10 @@ export function pvStateSelector( return results; } +export function deviceSelector(device: string, state: CsState): {} { + return state.deviceCache[device]; +} + /* Used for preventing re-rendering if the results are equivalent. Note that if the state for a particular PV hasn't changed, we will get back the same object as last time so we are safe to compare them. @@ -40,3 +44,10 @@ export function pvStateComparator( } return true; } + +export function deviceComparator(before: {}, after: {}): boolean { + if (Object.keys(before).length !== Object.keys(after).length) { + return false; + } + return true; +} diff --git a/src/ui/widgets/Device/coniqlParser.ts b/src/ui/widgets/Device/coniqlParser.ts new file mode 100644 index 00000000..b27b7915 --- /dev/null +++ b/src/ui/widgets/Device/coniqlParser.ts @@ -0,0 +1,79 @@ +function coniqlToWidget(coniql: string): string { + if (coniql === "TEXTUPDATE") { + return "readback"; + } + return "readback"; +} + +export function coniqlToJSON(coniqlQuery: {}): string { + let jsonStr = '{"type":"display","position":"relative","overflow":"auto",'; + + if (coniqlQuery !== undefined) { + jsonStr += '"children":['; + + const obj = JSON.parse(JSON.stringify(coniqlQuery)); + + for (const entry in obj) { + // Hack for excess ',' - FIX ME + if (entry !== "0") jsonStr += ","; + + const { label, child } = obj[entry]; + + // Parse Label + + jsonStr += + '{"type": "flexcontainer",\ + "position": "relative",\ + "children": [\ + {\ + "type": "label",\ + "position": "relative",\ + "text":"' + + label + + '",\ + "width":"50%",\ + "backgroundColor": "transparent"\ + }'; + + //Parse child + + const { __typename, id } = child; + + if (__typename === "Channel") { + const { widget } = child; + + jsonStr += + ', {\ + "type": "' + + coniqlToWidget(widget) + + '",\ + "position": "relative",\ + "width":"50%",\ + "pvName": "' + + id + + '"\ + }'; + } + + if (__typename === "Device") { + jsonStr += + ', {\ + "type": "device",\ + "position": "relative",\ + "width":"50%",\ + "deviceName": "' + + id + + '"\ + }'; + } + + jsonStr += "]}"; + } + + jsonStr += "]}"; + } else { + jsonStr += '"children":[]}'; + } + + return jsonStr; +} diff --git a/src/ui/widgets/Device/device.tsx b/src/ui/widgets/Device/device.tsx new file mode 100644 index 00000000..6013ab3b --- /dev/null +++ b/src/ui/widgets/Device/device.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { Widget } from "./../widget"; +import { WidgetPropType } from "./../widgetProps"; +import { InferWidgetProps, StringPropOpt, StringProp } from "./../propTypes"; +import { parseJson } from "./../EmbeddedDisplay/jsonParser"; +import { widgetDescriptionToComponent } from "./../createComponent"; +import { registerWidget } from "./../register"; +import { useId } from "react-id-generator"; +import { RelativePosition } from "../../../types/position"; +import { coniqlToJSON } from "./coniqlParser"; +import { useDevice } from "../../hooks/useDevice"; + +const DeviceProps = { + deviceName: StringProp, + id: StringPropOpt +}; + +const DeviceComponent = ( + props: InferWidgetProps +): JSX.Element => { + // let components = ""; + const device = useDevice(props.id || "", props.deviceName); + // const components = coniqlToJSON(device); + + // const description = parseJson(components, "pva"); + + // const component = widgetDescriptionToComponent({ + // position: new RelativePosition("100%", "100%"), + // type: "display", + // children: [description] + // }); + + return ( +
+ {device} + {/* {component} */} +
+ ); +}; + +const DeviceWidgetProps = { + ...DeviceProps, + ...WidgetPropType +}; + +export const Device = ( + props: InferWidgetProps +): JSX.Element => ; + +registerWidget(Device, DeviceWidgetProps, "device"); diff --git a/src/ui/widgets/EmbeddedDisplay/opiParser.ts b/src/ui/widgets/EmbeddedDisplay/opiParser.ts index ba594b4e..058c17d9 100644 --- a/src/ui/widgets/EmbeddedDisplay/opiParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/opiParser.ts @@ -55,7 +55,8 @@ const OPI_WIDGET_MAPPING: { [key: string]: any } = { "org.csstudio.opibuilder.widgets.polyline": "line", "org.csstudio.opibuilder.widgets.symbol.multistate.MultistateMonitorWidget": "symbol", - "org.csstudio.opibuilder.widgets.LED": "led" + "org.csstudio.opibuilder.widgets.LED": "led", + "org.csstudio.opibuilder.widgets.detailpanel": "device" }; /** diff --git a/src/ui/widgets/index.ts b/src/ui/widgets/index.ts index d8d50a38..69d8facf 100644 --- a/src/ui/widgets/index.ts +++ b/src/ui/widgets/index.ts @@ -2,6 +2,7 @@ import log from "loglevel"; export { ActionButton } from "./ActionButton/actionButton"; export { Checkbox } from "./Checkbox/checkbox"; +export { Device } from "./Device/device"; export { Display } from "./Display/display"; export { DrawerWidget } from "./Drawer/drawer"; export { DropDown } from "./DropDown/dropDown"; From 42ba7ed184a41c7243ebe000fc6994e0a9527359 Mon Sep 17 00:00:00 2001 From: George Sheppard Date: Wed, 25 Nov 2020 16:33:50 +0000 Subject: [PATCH 4/9] Modifications to forwarder to create a new class for devices, updates elsewhere cleaning up --- .env | 2 +- public/json/xpress.json | 2 +- src/connection/coniql.ts | 225 ++++++++++++++---------------- src/connection/forwarder.ts | 46 +++--- src/connection/plugin.ts | 26 ++-- src/connection/sim.ts | 26 +--- src/redux/actions.ts | 3 +- src/redux/connectionMiddleware.ts | 25 ++-- src/redux/csState.ts | 36 +++-- src/redux/store.ts | 19 +-- src/ui/hooks/useDevice.tsx | 12 +- src/ui/widgets/Device/device.tsx | 7 +- 12 files changed, 211 insertions(+), 218 deletions(-) diff --git a/.env b/.env index 3ae173d7..d473d790 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # React app settings. Restart the process for changes to take effect. # Set to connect to a Coniql server. -REACT_APP_CONIQL_SOCKET=cs03r-sc-serv-18.diamond.ac.uk:8080 +REACT_APP_CONIQL_SOCKET=cs03r-sc-serv-19.diamond.ac.uk:8080 REACT_APP_BASE_URL=http://localhost:3000 REACT_APP_SIMULATION_TIME=100 diff --git a/public/json/xpress.json b/public/json/xpress.json index a8237223..741bd9c1 100644 --- a/public/json/xpress.json +++ b/public/json/xpress.json @@ -5,7 +5,7 @@ "children": [ { "type": "device", - "deviceName": "Xspress3", + "deviceName": "device://Xspress3", "position": "relative", "width": "100%" } diff --git a/src/connection/coniql.ts b/src/connection/coniql.ts index 3cfcba1a..b64c35ba 100644 --- a/src/connection/coniql.ts +++ b/src/connection/coniql.ts @@ -17,13 +17,15 @@ import { import { onError } from "apollo-link-error"; import introspectionQueryResultData from "./fragmentTypes.json"; import { - Connection, ConnectionChangedCallback, ValueChangedCallback, nullConnCallback, nullValueCallback, SubscriptionType, - DeviceCallback, + Connection, + ConiqlDeviceConnection, + PvConnection, + DeviceChangedCallback, nullDeviceCallback } from "./plugin"; import { SubscriptionClient } from "subscriptions-transport-ws"; @@ -264,19 +266,18 @@ const DEVICE_SUBSCRIPTION = gql` } `; -export class ConiqlPlugin implements Connection { - private client: ApolloClient; - private onConnectionUpdate: ConnectionChangedCallback; - private onValueUpdate: ValueChangedCallback; - private onDeviceUpdate: DeviceCallback; +class ConiqlPlugin implements Connection { + protected _client: ApolloClient; + protected _onConnectionUpdate: ConnectionChangedCallback; + protected _onValueUpdate: ValueChangedCallback; + protected _onDeviceUpdate: DeviceChangedCallback; private connected: boolean; private wsClient: SubscriptionClient; - private disconnected: string[] = []; - private disconnectedDevices: string[] = []; - private subscriptions: { [pvName: string]: Subscription }; - private deviceSubscriptions: { [device: string]: Subscription }; + protected _disconnected: string[] = []; + // NOTE: This class handles devices and PVs hence pvDevice + private subscriptions: { [pvDevice: string]: Subscription }; - public constructor(socket: string) { + public constructor(socket: string, type: string) { const fragmentMatcher = new IntrospectionFragmentMatcher({ introspectionQueryResultData }); @@ -285,57 +286,48 @@ export class ConiqlPlugin implements Connection { this.wsClient = new SubscriptionClient(`ws://${socket}/ws`, { reconnect: true }); + this.wsClient.onReconnecting((): void => { - for (const pvName of this.disconnected) { - this._subscribe(pvName); + for (const pvDevice of this._disconnected) { + this._subscribe(pvDevice); } - for (const device of this.disconnectedDevices) { - this._subscribeDevice(device); - } - this.disconnected = []; - this.disconnectedDevices = []; + this._disconnected = []; }); + this.wsClient.onDisconnected((): void => { log.error("Websockect client disconnected."); - for (const pvName of Object.keys(this.subscriptions)) { - if ( - this.subscriptions.hasOwnProperty(pvName) && - this.subscriptions[pvName] - ) { - this.subscriptions[pvName].unsubscribe(); - delete this.subscriptions[pvName]; - } else { - log.error(`Attempt to unsubscribe from ${pvName} failed`); - } - this.onConnectionUpdate(pvName, { - isConnected: false, - isReadonly: true - }); - } - for (const device of Object.keys(this.deviceSubscriptions)) { - if ( - this.deviceSubscriptions.hasOwnProperty(device) && - this.deviceSubscriptions[device] - ) { - this.deviceSubscriptions[device].unsubscribe(); - delete this.deviceSubscriptions[device]; - } else { - log.error(`Attempt to unsubscribe from ${device} failed`); - } - this.onConnectionUpdate(device, { + for (const pvDevice of Object.keys(this.subscriptions)) { + this.unsubscribe(pvDevice); + this._onConnectionUpdate(pvDevice, type, { isConnected: false, isReadonly: true }); } }); + const link = this.createLink(socket); - this.client = new ApolloClient({ link, cache }); - this.onConnectionUpdate = nullConnCallback; - this.onValueUpdate = nullValueCallback; - this.onDeviceUpdate = nullDeviceCallback; + this._client = new ApolloClient({ link, cache }); + this._onConnectionUpdate = nullConnCallback; + this._onValueUpdate = nullValueCallback; + this._onDeviceUpdate = nullDeviceCallback; this.connected = false; this.subscriptions = {}; - this.deviceSubscriptions = {}; + } + + public connect( + // TODO: connectionCallback needs to be changed to accomodate devices + connectionCallback: ConnectionChangedCallback, + valueCallback: ValueChangedCallback, + deviceCallback: DeviceChangedCallback + ): void { + this._onConnectionUpdate = connectionCallback; + this._onValueUpdate = valueCallback; + this._onDeviceUpdate = deviceCallback; + this.connected = true; + } + + public isConnected(): boolean { + return this.connected; } public createLink(socket: string): ApolloLink { @@ -369,127 +361,120 @@ export class ConiqlPlugin implements Connection { return link; } - public connect( - connectionCallback: ConnectionChangedCallback, - valueCallback: ValueChangedCallback, - deviceCallback: DeviceCallback - ): void { - this.onConnectionUpdate = connectionCallback; - this.onValueUpdate = valueCallback; - this.onDeviceUpdate = deviceCallback; - this.connected = true; + protected _subscribe(pvDevice: string): Subscription { + throw new Error("_subscribe method not implemented"); } - public isConnected(): boolean { - return this.connected; + public subscribe(pvDevice: string, type: SubscriptionType): string { + if (this.subscriptions[pvDevice] === undefined) { + this.subscriptions[pvDevice] = this._subscribe(pvDevice); + } + return pvDevice; } - private _process(data: any, pvName: string, operation: string): void { - // Process an update to a channel either from getChannel or subscribeChannel. - const { value, time, status, display } = data.data[operation]; - if (status) { - this.onConnectionUpdate(pvName, { - isConnected: true, - isReadonly: !status.mutable - }); + public unsubscribe(pvDevice: string): void { + if ( + this.subscriptions.hasOwnProperty(pvDevice) && + this.subscriptions[pvDevice] + ) { + this.subscriptions[pvDevice].unsubscribe(); + delete this.subscriptions[pvDevice]; + } else { + log.error(`Attempt to unsubscribe from ${pvDevice} failed`); } - const dtype = coniqlToDType(value, time, status, display); - this.onValueUpdate(pvName, dtype); } +} - private _processDevice(data: any, device: string, operation: string): void { - this.onDeviceUpdate(device, JSON.stringify(data.data)); +export class ConiqlDevicePlugin extends ConiqlPlugin + implements ConiqlDeviceConnection { + constructor(socket: string) { + super(socket, "device"); } - private _subscribe(pvName: string): Subscription { - return this.client + private _process(data: any, device: string, operation: string): void { + this._onDeviceUpdate( + device, + new DType({ stringValue: JSON.stringify(data.data) }) + ); + } + + protected _subscribe(pvDevice: string): Subscription { + return this._client .subscribe({ - query: PV_SUBSCRIPTION, - variables: { pvName: pvName } + query: DEVICE_SUBSCRIPTION, + variables: { device: pvDevice } }) .subscribe({ next: (data): void => { - this._process(data, pvName, "subscribeChannel"); + this._process(data, pvDevice, "subscribeDevice"); }, error: (err): void => { log.error("err", err); }, complete: (): void => { - // complete is called when the websocket is disconnected. - this.onConnectionUpdate(pvName, { + this._onConnectionUpdate(pvDevice, "device", { isConnected: false, isReadonly: true }); - this.disconnected.push(pvName); + this._disconnected.push(pvDevice); } }); } +} + +export class ConiqlPvPlugin extends ConiqlPlugin implements PvConnection { + constructor(socket: string) { + super(socket, "pv"); + } + + private _process(data: any, pvName: string, operation: string): void { + // Process an update to a channel either from getChannel or subscribeChannel. + const { value, time, status, display } = data.data[operation]; + if (status) { + this._onConnectionUpdate(pvName, "pv", { + isConnected: true, + isReadonly: !status.mutable + }); + } + const dtype = coniqlToDType(value, time, status, display); + this._onValueUpdate(pvName, dtype); + } - private _subscribeDevice(device: string): Subscription { - return this.client + protected _subscribe(pvName: string): Subscription { + return this._client .subscribe({ - query: DEVICE_SUBSCRIPTION, - variables: { device: device } + query: PV_SUBSCRIPTION, + variables: { pvName: pvName } }) .subscribe({ next: (data): void => { - this._processDevice(data, device, "subscribeDevice"); + this._process(data, pvName, "subscribeChannel"); }, error: (err): void => { log.error("err", err); }, complete: (): void => { - this.onConnectionUpdate(device, { + // complete is called when the websocket is disconnected. + this._onConnectionUpdate(pvName, "pv", { isConnected: false, isReadonly: true }); - this.disconnectedDevices.push(device); + this._disconnected.push(pvName); } }); } - public subscribe(pvName: string, type: SubscriptionType): string { - // TODO: How to handle multiple subscriptions of different types to the same channel? - if (this.subscriptions[pvName] === undefined) { - this.subscriptions[pvName] = this._subscribe(pvName); - } - return pvName; - } - - public subscribeDevice(device: string): string { - if (this.deviceSubscriptions[device] === undefined) { - this.deviceSubscriptions[device] = this._subscribeDevice(device); - } - return device; - } - public putPv(pvName: string, value: DType): void { log.debug(`Putting ${value} to ${pvName}.`); const variables = { pvName: pvName, value: DType.coerceString(value) }; - this.client + this._client .mutate({ mutation: PV_MUTATION, variables: variables }) .catch(error => { log.error(`Failed to write ${value} to ${pvName}`); log.error(error); }); } - - public unsubscribe(pvName: string): void { - // Note that connectionMiddleware handles multiple subscriptions - // for the same PV at present, so if this method is called then - // there is no further need for this PV. - // Note that a bug in tartiflette-aiohttp means that the - // unsubscribe does not work on Python 3.8. - // https://github.com/tartiflette/tartiflette-aiohttp/pull/81 - this.subscriptions[pvName].unsubscribe(); - delete this.subscriptions[pvName]; - } - - public unsubscribeDevice(device: string): void { - this.deviceSubscriptions[device].unsubscribe(); - delete this.deviceSubscriptions[device]; - } } diff --git a/src/connection/forwarder.ts b/src/connection/forwarder.ts index c1974162..03a4b3b6 100644 --- a/src/connection/forwarder.ts +++ b/src/connection/forwarder.ts @@ -1,9 +1,11 @@ import { Connection, + ConiqlDeviceConnection, ConnectionChangedCallback, ValueChangedCallback, - DeviceCallback, - SubscriptionType + SubscriptionType, + DeviceChangedCallback, + PvConnection } from "./plugin"; export class ConnectionForwarder implements Connection { @@ -13,9 +15,11 @@ export class ConnectionForwarder implements Connection { this.prefixConnections = prefixConnections; this.connected = false; } - private getConnection(pvName: string): Connection { + private getConnection( + pvDevice: string + ): ConiqlDeviceConnection | PvConnection { for (const [prefix, connection] of this.prefixConnections) { - if (pvName.startsWith(prefix)) { + if (pvDevice.startsWith(prefix)) { if (connection !== undefined) { return connection; } else { @@ -23,44 +27,32 @@ export class ConnectionForwarder implements Connection { } } } - throw new Error(`No connections for ${pvName}`); + throw new Error(`No connections for ${pvDevice}`); } - public subscribe(pvName: string, type: SubscriptionType): string { - const connection = this.getConnection(pvName); - return connection.subscribe(pvName, type); + public subscribe(pvDevice: string, type: SubscriptionType): string { + const connection = this.getConnection(pvDevice); + return connection.subscribe(pvDevice, type); } - public unsubscribe(pvName: string): void { - const connection = this.getConnection(pvName); - return connection.unsubscribe(pvName); - } - - // TODO: Finish this function - public subscribeDevice(device: string): string { - const connection = this.getConnection(device); - return connection.subscribeDevice(device); - } - - // TODO: Finish this function - public unsubscribeDevice(device: string): void { - const connection = this.getConnection(device); - return connection.unsubscribeDevice(device); + public unsubscribe(pvDevice: string): void { + const connection = this.getConnection(pvDevice); + return connection.unsubscribe(pvDevice); } public isConnected(): boolean { return this.connected; } - public putPv(pvName: string, value: any): void { - const connection = this.getConnection(pvName); - return connection.putPv(pvName, value); + public putPv(pvDevice: string, value: any): void { + const connection = this.getConnection(pvDevice) as PvConnection; + return connection.putPv(pvDevice, value); } public connect( connectionCallback: ConnectionChangedCallback, valueCallback: ValueChangedCallback, - deviceCallback: DeviceCallback + deviceCallback: DeviceChangedCallback ): void { for (const [, connection] of this.prefixConnections) { if (connection !== undefined) { diff --git a/src/connection/plugin.ts b/src/connection/plugin.ts index aafef3e6..c3bf091f 100644 --- a/src/connection/plugin.ts +++ b/src/connection/plugin.ts @@ -11,7 +11,7 @@ export interface SubscriptionType { export const nullConnCallback: ConnectionChangedCallback = (_p, _v): void => {}; export const nullValueCallback: ValueChangedCallback = (_p, _v): void => {}; -export const nullDeviceCallback: DeviceCallback = (_d, _v): void => {}; +export const nullDeviceCallback: DeviceChangedCallback = (_d, _v): void => {}; export interface ConnectionState { isConnected: boolean; @@ -19,22 +19,30 @@ export interface ConnectionState { } export type ConnectionChangedCallback = ( - pvName: string, + pvDevice: string, + type: string, value: ConnectionState ) => void; export type ValueChangedCallback = (pvName: string, value: DType) => void; -export type DeviceCallback = (device: string, value: {}) => void; +export type DeviceChangedCallback = (device: string, value: DType) => void; -export interface Connection { - subscribe: (pvName: string, type: SubscriptionType) => string; // must be idempotent - subscribeDevice: (device: string) => string; - putPv: (pvName: string, value: DType) => void; +export type Connection = { + subscribe: (pvDevice: string, type: SubscriptionType) => string; // must be idempotent connect: ( connectionCallback: ConnectionChangedCallback, valueCallback: ValueChangedCallback, - deviceCallback: DeviceCallback + deviceCallback: DeviceChangedCallback ) => void; isConnected: () => boolean; unsubscribe: (pvName: string) => void; - unsubscribeDevice: (device: string) => void; +}; + +// Left for easy expansion and easier to understand type definitions +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ConiqlDeviceConnection extends Connection {} + +export interface PvConnection extends Connection { + putPv: (pvName: string, value: DType) => void; } + +export type ConnectionTypes = PvConnection | ConiqlDeviceConnection; diff --git a/src/connection/sim.ts b/src/connection/sim.ts index 207da071..14e88014 100644 --- a/src/connection/sim.ts +++ b/src/connection/sim.ts @@ -1,11 +1,11 @@ import log from "loglevel"; import { - Connection, ConnectionState, ConnectionChangedCallback, ValueChangedCallback, nullConnCallback, - nullValueCallback + nullValueCallback, + PvConnection } from "./plugin"; import { DType, @@ -26,7 +26,6 @@ abstract class SimPv { private onConnectionUpdate: ConnectionChangedCallback; private onValueUpdate: ValueChangedCallback; protected subscribed: boolean; - protected subscribedDevice: boolean; public pvName: string; protected updateRate?: number; abstract getValue(): DType; @@ -43,7 +42,6 @@ abstract class SimPv { this.updateRate = updateRate; this.publishConnection(); this.subscribed = false; - this.subscribedDevice = false; } public getConnection(): ConnectionState { @@ -55,34 +53,18 @@ abstract class SimPv { this.publish(); } - public subscribeDevice(): void { - this.subscribedDevice = true; - this.publishDevice(); - } - public unsubscribe(): void { this.subscribed = false; } - public unsubscribeDevice(): void { - this.subscribedDevice = false; - } - public publish(): void { if (this.subscribed) { this.onValueUpdate(this.pvName, this.getValue()); } } - // TODO: This needs filling out - public publishDevice(): void { - if (this.subscribedDevice) { - this.onValueUpdate("fake device", this.getValue()); - } - } - public publishConnection(): void { - this.onConnectionUpdate(this.pvName, this.getConnection()); + this.onConnectionUpdate(this.pvName, "pv", this.getConnection()); } public updateValue(_: DType): void { @@ -352,7 +334,7 @@ class SimCache { } } -export class SimulatorPlugin implements Connection { +export class SimulatorPlugin implements PvConnection { private simPvs: SimCache; private onConnectionUpdate: ConnectionChangedCallback; private onValueUpdate: ValueChangedCallback; diff --git a/src/redux/actions.ts b/src/redux/actions.ts index 172b0b75..fd21f3ae 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -15,7 +15,8 @@ export const UNSUBSCRIBE_DEVICE = "unsubscribe_device"; export interface ConnectionChanged { type: typeof CONNECTION_CHANGED; payload: { - pvName: string; + pvDevice: string; + type: string; value: ConnectionState; }; } diff --git a/src/redux/connectionMiddleware.ts b/src/redux/connectionMiddleware.ts index 2846e312..3e283289 100644 --- a/src/redux/connectionMiddleware.ts +++ b/src/redux/connectionMiddleware.ts @@ -1,12 +1,16 @@ import log from "loglevel"; import { MiddlewareAPI, Dispatch } from "redux"; -import { Connection, ConnectionState } from "../connection/plugin"; import { - CONNECTION_CHANGED, + ConnectionState, + ConnectionTypes, + PvConnection +} from "../connection/plugin"; +import { SUBSCRIBE, WRITE_PV, VALUE_CHANGED, DEVICE_CHANGED, + CONNECTION_CHANGED, SUBSCRIBE_DEVICE, UNSUBSCRIBE, Action, @@ -17,12 +21,13 @@ import { DType } from "../types/dtypes"; function connectionChanged( store: MiddlewareAPI, - pvName: string, + pvDevice: string, + type: string, value: ConnectionState ): void { store.dispatch({ type: CONNECTION_CHANGED, - payload: { pvName: pvName, value: value } + payload: { pvDevice, value, type } }); } @@ -48,14 +53,14 @@ function deviceChanged( }); } -export const connectionMiddleware = (connection: Connection) => ( +export const connectionMiddleware = (connection: ConnectionTypes) => ( store: MiddlewareAPI ) => (next: Dispatch): any => (action: Action): Action => { if (!connection.isConnected()) { connection.connect( // Partial function application. - (pvName: string, value: ConnectionState): void => - connectionChanged(store, pvName, value), + (pvDevice: string, type: string, value: ConnectionState): void => + connectionChanged(store, pvDevice, type, value), (pvName: string, value: DType): void => valueChanged(store, pvName, value), // TODO: Be more specific with type here @@ -91,7 +96,7 @@ export const connectionMiddleware = (connection: Connection) => ( const { device } = (action as SubscribeDevice).payload; try { - connection.subscribeDevice(device); + connection.subscribe(device, { string: true }); } catch (error) { log.error(`Failed to subscribe to device ${device}`); log.error(error); @@ -111,7 +116,7 @@ export const connectionMiddleware = (connection: Connection) => ( if (subs[device].length === 1 && subs[device][0] === componentId) { try { - connection.unsubscribeDevice(device); + connection.unsubscribe(device); } catch (error) { log.error(`Failed to unsubscribe from device ${device}`); log.error(error); @@ -124,7 +129,7 @@ export const connectionMiddleware = (connection: Connection) => ( const effectivePvName = store.getState().effectivePvNameMap[pvName] || pvName; try { - connection.putPv(effectivePvName, value); + (connection as PvConnection).putPv(effectivePvName, value); } catch (error) { log.error(`Failed to put to pv ${pvName}`); log.error(error); diff --git a/src/redux/csState.ts b/src/redux/csState.ts index e3434c73..ef902220 100644 --- a/src/redux/csState.ts +++ b/src/redux/csState.ts @@ -119,16 +119,32 @@ export function csReducer(state = initialState, action: Action): CsState { return { ...state, valueCache: newValueCache }; } case CONNECTION_CHANGED: { - const newValueCache: ValueCache = { ...state.valueCache }; - const { pvName, value } = action.payload; - const pvState = state.valueCache[pvName]; - const newPvState = { - ...pvState, - connected: value.isConnected, - readonly: value.isReadonly - }; - newValueCache[action.payload.pvName] = newPvState; - return { ...state, valueCache: newValueCache }; + console.log(action); + // TODO: Make this better + let cache; + const { pvDevice, type, value } = action.payload; + if (type === "pv") { + cache = state.valueCache; + const oldState = cache[pvDevice]; + const newState = { + ...oldState, + connected: value.isConnected, + readonly: value.isReadonly + }; + cache[pvDevice] = newState; + return { ...state, valueCache: cache as ValueCache }; + } else if (type === "device") { + cache = state.deviceCache; + const oldState = cache[pvDevice]; + const newState = { + ...oldState, + connected: value.isConnected, + readonly: value.isReadonly + }; + cache[pvDevice] = newState; + return { ...state, deviceCache: cache }; + } + break; } case SUBSCRIBE: { const { componentId, effectivePvName } = action.payload; diff --git a/src/redux/store.ts b/src/redux/store.ts index 3943e61d..95dcff92 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,11 +1,10 @@ import { createStore, applyMiddleware, compose } from "redux"; - +import { Connection } from "../connection/plugin"; import { csReducer } from "./csState"; import { connectionMiddleware } from "./connectionMiddleware"; import { throttleMiddleware, UpdateThrottle } from "./throttleMiddleware"; -import { Connection } from "../connection/plugin"; import { SimulatorPlugin } from "../connection/sim"; -import { ConiqlPlugin } from "../connection/coniql"; +import { ConiqlPvPlugin, ConiqlDevicePlugin } from "../connection/coniql"; import { ConnectionForwarder } from "../connection/forwarder"; const CONIQL_SOCKET = process.env.REACT_APP_CONIQL_SOCKET; @@ -16,17 +15,21 @@ const THROTTLE_PERIOD = parseFloat( const SIMULATION_TIME = parseFloat( process.env.REACT_APP_SIMULATION_TIME ?? "100" ); + const simulator = new SimulatorPlugin(SIMULATION_TIME); const plugins: [string, Connection][] = [ ["sim://", simulator], ["loc://", simulator] ]; + if (CONIQL_SOCKET !== undefined) { - const coniql = new ConiqlPlugin(CONIQL_SOCKET); - plugins.unshift(["pva://", coniql]); - plugins.unshift(["ca://", coniql]); - plugins.unshift(["ssim://", coniql]); - plugins.unshift(["Xspress", coniql]); + const coniqlPv = new ConiqlPvPlugin(CONIQL_SOCKET); + plugins.unshift(["pva://", coniqlPv]); + plugins.unshift(["ca://", coniqlPv]); + plugins.unshift(["ssim://", coniqlPv]); + const coniqlDevice = new ConiqlDevicePlugin(CONIQL_SOCKET); + // TODO: Change this to device:// + plugins.unshift(["device://", coniqlDevice]); } const connection = new ConnectionForwarder(plugins); diff --git a/src/ui/hooks/useDevice.tsx b/src/ui/hooks/useDevice.tsx index 9be9c6ea..94ea6bfc 100644 --- a/src/ui/hooks/useDevice.tsx +++ b/src/ui/hooks/useDevice.tsx @@ -3,11 +3,17 @@ import { CsState } from "../../redux/csState"; import { deviceSelector, deviceComparator } from "./utils"; import { useDeviceSubscription } from "./useDeviceSubscription"; -export function useDevice(componentId: string, device: string): {} { +export function useDevice( + componentId: string, + device: string +): string | undefined { useDeviceSubscription(componentId, device); - const description = useSelector( + const description: any = useSelector( (state: CsState): {} => deviceSelector(device, state), deviceComparator ); - return description; + + if (description) { + return description.value.stringValue; + } } diff --git a/src/ui/widgets/Device/device.tsx b/src/ui/widgets/Device/device.tsx index 6013ab3b..fa6e6208 100644 --- a/src/ui/widgets/Device/device.tsx +++ b/src/ui/widgets/Device/device.tsx @@ -30,12 +30,7 @@ const DeviceComponent = ( // children: [description] // }); - return ( -
- {device} - {/* {component} */} -
- ); + return
{device || ""}
; }; const DeviceWidgetProps = { From 6fbc12e73869d783a870b729446a8a7b0c15808b Mon Sep 17 00:00:00 2001 From: George Sheppard Date: Thu, 26 Nov 2020 14:17:00 +0000 Subject: [PATCH 5/9] Cleaning up --- .env | 2 +- src/connection/coniql.ts | 10 ++++++--- src/connection/sim.ts | 10 --------- src/redux/actions.ts | 4 +--- src/redux/connectionMiddleware.ts | 10 ++++----- src/redux/csState.ts | 24 ++++++++++++--------- src/redux/store.ts | 1 - src/ui/hooks/useDevice.tsx | 15 ++++++++----- src/ui/hooks/utils.ts | 9 +++++++- src/ui/widgets/Device/device.tsx | 5 ++--- src/ui/widgets/EmbeddedDisplay/opiParser.ts | 1 - 11 files changed, 48 insertions(+), 43 deletions(-) diff --git a/.env b/.env index d473d790..eeb6dae9 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # React app settings. Restart the process for changes to take effect. # Set to connect to a Coniql server. -REACT_APP_CONIQL_SOCKET=cs03r-sc-serv-19.diamond.ac.uk:8080 +REACT_APP_CONIQL_SOCKET=cs03r-sc-serv-39.diamond.ac.uk:8080 REACT_APP_BASE_URL=http://localhost:3000 REACT_APP_SIMULATION_TIME=100 diff --git a/src/connection/coniql.ts b/src/connection/coniql.ts index b64c35ba..884435d0 100644 --- a/src/connection/coniql.ts +++ b/src/connection/coniql.ts @@ -238,7 +238,6 @@ const PV_MUTATION = gql` } `; -// TODO: Turn id into a variable const DEVICE_SUBSCRIPTION = gql` query { getDevice(id: "Xspress3") { @@ -315,7 +314,6 @@ class ConiqlPlugin implements Connection { } public connect( - // TODO: connectionCallback needs to be changed to accomodate devices connectionCallback: ConnectionChangedCallback, valueCallback: ValueChangedCallback, deviceCallback: DeviceChangedCallback @@ -361,6 +359,11 @@ class ConiqlPlugin implements Connection { return link; } + /** + * This should be overridden in the child class, and it's implementation + * will depend on the type of subscription (currently device of PV) + * @param pvDevice + */ protected _subscribe(pvDevice: string): Subscription { throw new Error("_subscribe method not implemented"); } @@ -402,7 +405,8 @@ export class ConiqlDevicePlugin extends ConiqlPlugin return this._client .subscribe({ query: DEVICE_SUBSCRIPTION, - variables: { device: pvDevice } + // variables: { pvDevice: pvDevice.split("://")[1] } + variables: { pvDevice: "Xspress3" } }) .subscribe({ next: (data): void => { diff --git a/src/connection/sim.ts b/src/connection/sim.ts index 14e88014..9980ac66 100644 --- a/src/connection/sim.ts +++ b/src/connection/sim.ts @@ -358,11 +358,6 @@ export class SimulatorPlugin implements PvConnection { return (simulator && simulator.pvName) || pvName; } - // TODO: Finish this function - public subscribeDevice(device: string): string { - return ""; - } - public connect( connectionCallback: ConnectionChangedCallback, valueCallback: ValueChangedCallback @@ -536,9 +531,4 @@ export class SimulatorPlugin implements PvConnection { } } } - - // TODO: Finish this function - public unsubscribeDevice(device: string): void { - log.debug(`Unsubscribing from ${device}`); - } } diff --git a/src/redux/actions.ts b/src/redux/actions.ts index fd21f3ae..ca5e86c2 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -52,7 +52,6 @@ export interface ValuesChanged { payload: ValueChanged[]; } -// TODO: Be more specific with type on value here export interface SubscribeDevice { type: typeof SUBSCRIBE_DEVICE; payload: { @@ -69,13 +68,12 @@ export interface UnsubscribeDevice { }; } -// TODO: Be more specific with type on value here export interface DeviceChanged { type: typeof DEVICE_CHANGED; payload: { device: string; componentId: string; - value: string; + value: DType; }; } diff --git a/src/redux/connectionMiddleware.ts b/src/redux/connectionMiddleware.ts index 3e283289..26440116 100644 --- a/src/redux/connectionMiddleware.ts +++ b/src/redux/connectionMiddleware.ts @@ -38,18 +38,18 @@ function valueChanged( ): void { store.dispatch({ type: VALUE_CHANGED, - payload: { pvName: pvName, value: value } + payload: { pvName, value } }); } function deviceChanged( store: MiddlewareAPI, device: string, - value: string + value: DType ): void { store.dispatch({ type: DEVICE_CHANGED, - payload: { device: device, value: value } + payload: { device, value } }); } @@ -63,8 +63,8 @@ export const connectionMiddleware = (connection: ConnectionTypes) => ( connectionChanged(store, pvDevice, type, value), (pvName: string, value: DType): void => valueChanged(store, pvName, value), - // TODO: Be more specific with type here - (device: string, value: any): void => deviceChanged(store, device, value) + (device: string, value: DType): void => + deviceChanged(store, device, value) ); } diff --git a/src/redux/csState.ts b/src/redux/csState.ts index ef902220..da32f166 100644 --- a/src/redux/csState.ts +++ b/src/redux/csState.ts @@ -40,9 +40,12 @@ export interface ValueCache { [key: string]: FullPvState; } -// TODO: Be more specific with type here +export interface FullDeviceState extends PvState { + device: string; +} + export interface DeviceCache { - [key: string]: any; + [key: string]: FullDeviceState; } export interface Subscriptions { @@ -80,15 +83,16 @@ function updateValueCache( newValueCache[action.payload.pvName] = newPvState; } -// TODO: Should probably make this a bit more extensive function updateDeviceCache( oldDeviceCache: DeviceCache, newDeviceCache: DeviceCache, action: DeviceChanged ): void { - const { value } = action.payload; - console.log("new value", value); - newDeviceCache[action.payload.device] = value; + newDeviceCache[action.payload.device] = { + ...action.payload, + connected: true, + readonly: false + }; } export function csReducer(state = initialState, action: Action): CsState { @@ -119,10 +123,9 @@ export function csReducer(state = initialState, action: Action): CsState { return { ...state, valueCache: newValueCache }; } case CONNECTION_CHANGED: { - console.log(action); - // TODO: Make this better - let cache; const { pvDevice, type, value } = action.payload; + + let cache; if (type === "pv") { cache = state.valueCache; const oldState = cache[pvDevice]; @@ -143,8 +146,9 @@ export function csReducer(state = initialState, action: Action): CsState { }; cache[pvDevice] = newState; return { ...state, deviceCache: cache }; + } else { + break; } - break; } case SUBSCRIBE: { const { componentId, effectivePvName } = action.payload; diff --git a/src/redux/store.ts b/src/redux/store.ts index 95dcff92..7cade465 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -28,7 +28,6 @@ if (CONIQL_SOCKET !== undefined) { plugins.unshift(["ca://", coniqlPv]); plugins.unshift(["ssim://", coniqlPv]); const coniqlDevice = new ConiqlDevicePlugin(CONIQL_SOCKET); - // TODO: Change this to device:// plugins.unshift(["device://", coniqlDevice]); } const connection = new ConnectionForwarder(plugins); diff --git a/src/ui/hooks/useDevice.tsx b/src/ui/hooks/useDevice.tsx index 94ea6bfc..bda552f9 100644 --- a/src/ui/hooks/useDevice.tsx +++ b/src/ui/hooks/useDevice.tsx @@ -2,18 +2,23 @@ import { useSelector } from "react-redux"; import { CsState } from "../../redux/csState"; import { deviceSelector, deviceComparator } from "./utils"; import { useDeviceSubscription } from "./useDeviceSubscription"; +import { DType } from "../../types/dtypes"; + +export interface Device { + connected: boolean; + device: string; + readonly: boolean; + value: DType; +} export function useDevice( componentId: string, device: string -): string | undefined { +): Device | undefined { useDeviceSubscription(componentId, device); const description: any = useSelector( (state: CsState): {} => deviceSelector(device, state), deviceComparator ); - - if (description) { - return description.value.stringValue; - } + return description; } diff --git a/src/ui/hooks/utils.ts b/src/ui/hooks/utils.ts index e6ab4767..a555ef65 100644 --- a/src/ui/hooks/utils.ts +++ b/src/ui/hooks/utils.ts @@ -4,6 +4,10 @@ export interface PvArrayResults { [pvName: string]: [PvState, string]; } +export interface DeviceArrayResults { + [device: string]: [PvState, string]; +} + export function pvStateSelector( pvNames: string[], state: CsState @@ -45,7 +49,10 @@ export function pvStateComparator( return true; } -export function deviceComparator(before: {}, after: {}): boolean { +export function deviceComparator( + before: DeviceArrayResults, + after: DeviceArrayResults +): boolean { if (Object.keys(before).length !== Object.keys(after).length) { return false; } diff --git a/src/ui/widgets/Device/device.tsx b/src/ui/widgets/Device/device.tsx index fa6e6208..41418b30 100644 --- a/src/ui/widgets/Device/device.tsx +++ b/src/ui/widgets/Device/device.tsx @@ -19,7 +19,7 @@ const DeviceComponent = ( props: InferWidgetProps ): JSX.Element => { // let components = ""; - const device = useDevice(props.id || "", props.deviceName); + const description = useDevice(props.id || "", props.deviceName); // const components = coniqlToJSON(device); // const description = parseJson(components, "pva"); @@ -29,8 +29,7 @@ const DeviceComponent = ( // type: "display", // children: [description] // }); - - return
{device || ""}
; + return
{(description && description.value.toString()) || ""}
; }; const DeviceWidgetProps = { diff --git a/src/ui/widgets/EmbeddedDisplay/opiParser.ts b/src/ui/widgets/EmbeddedDisplay/opiParser.ts index 058c17d9..c11ab520 100644 --- a/src/ui/widgets/EmbeddedDisplay/opiParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/opiParser.ts @@ -205,7 +205,6 @@ export function opiParseActions( (action.description && action.description._text) || undefined, file: { path: action.path._text, - // TODO: Should probably be accessing properties of the element here macros: {}, defaultProtocol: "pva" } From 3f5a8386cd5e1939f21bbf9a4688cf715897cfd0 Mon Sep 17 00:00:00 2001 From: George Sheppard Date: Thu, 26 Nov 2020 14:42:32 +0000 Subject: [PATCH 6/9] Finished coniql aspect of device widget --- src/connection/coniql.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/connection/coniql.ts b/src/connection/coniql.ts index 884435d0..28c756f0 100644 --- a/src/connection/coniql.ts +++ b/src/connection/coniql.ts @@ -239,8 +239,8 @@ const PV_MUTATION = gql` `; const DEVICE_SUBSCRIPTION = gql` - query { - getDevice(id: "Xspress3") { + query deviceQuery($pvDevice: ID!) { + getDevice(id: $pvDevice) { id children(flatten: true) { name @@ -405,8 +405,7 @@ export class ConiqlDevicePlugin extends ConiqlPlugin return this._client .subscribe({ query: DEVICE_SUBSCRIPTION, - // variables: { pvDevice: pvDevice.split("://")[1] } - variables: { pvDevice: "Xspress3" } + variables: { pvDevice: pvDevice.split("://")[1] } }) .subscribe({ next: (data): void => { From dc2e4a7bce3d602df7a8560db276526511204482 Mon Sep 17 00:00:00 2001 From: George Sheppard Date: Fri, 27 Nov 2020 12:14:19 +0000 Subject: [PATCH 7/9] Fixed tests and linting warnings --- src/connection/coniql.test.ts | 10 ++-- src/connection/sim.test.ts | 55 ++++++++++++------- src/connection/sim.ts | 8 ++- src/redux/csState.test.ts | 8 ++- src/redux/throttleMiddleware.test.ts | 23 +++++++- src/setupTests.tsx | 4 +- src/ui/hooks/useConnection.test.tsx | 4 +- src/ui/hooks/useRules.test.tsx | 4 +- src/ui/hooks/utils.test.ts | 4 +- src/ui/widgets/Device/coniqlParser.ts | 79 --------------------------- src/ui/widgets/Device/device.tsx | 5 -- src/ui/widgets/widget.test.tsx | 4 +- 12 files changed, 91 insertions(+), 117 deletions(-) delete mode 100644 src/ui/widgets/Device/coniqlParser.ts diff --git a/src/connection/coniql.test.ts b/src/connection/coniql.test.ts index 9c351ed5..e3b1ed34 100644 --- a/src/connection/coniql.test.ts +++ b/src/connection/coniql.test.ts @@ -1,6 +1,6 @@ import { ApolloClient } from "apollo-client"; import { - ConiqlPlugin, + ConiqlPvPlugin, ConiqlStatus, ConiqlTime, ConiqlBase64Array @@ -48,14 +48,16 @@ class MockObservable { } describe("ConiqlPlugin", (): void => { - let cp: ConiqlPlugin; + let cp: ConiqlPvPlugin; let mockConnUpdate: jest.Mock; let mockValUpdate: jest.Mock; + let mockDevUpdate: jest.Mock; beforeEach((): void => { - cp = new ConiqlPlugin("a.b.c:100"); + cp = new ConiqlPvPlugin("a.b.c:100"); mockConnUpdate = jest.fn(); mockValUpdate = jest.fn(); - cp.connect(mockConnUpdate, mockValUpdate); + mockDevUpdate = jest.fn(); + cp.connect(mockConnUpdate, mockValUpdate, mockDevUpdate); }); it("handles update to value", (): void => { diff --git a/src/connection/sim.test.ts b/src/connection/sim.test.ts index bd46ce56..2fc34104 100644 --- a/src/connection/sim.test.ts +++ b/src/connection/sim.test.ts @@ -1,5 +1,9 @@ import { SimulatorPlugin } from "./sim"; -import { nullConnCallback, nullValueCallback } from "./plugin"; +import { + nullConnCallback, + nullValueCallback, + nullDeviceCallback +} from "./plugin"; import { ddouble, dstring } from "../setupTests"; import { DType } from "../types/dtypes"; @@ -9,13 +13,17 @@ beforeEach((): void => { }); function getValue(pvName: string, callback: Function): void { - simulator.connect(nullConnCallback, function(updatePvName, value): void { - const nameInfo1 = SimulatorPlugin.parseName(updatePvName); - const nameInfo2 = SimulatorPlugin.parseName(updatePvName); - if (nameInfo1.keyName === nameInfo2.keyName) { - callback(value); - } - }); + simulator.connect( + nullConnCallback, + function(updatePvName, value): void { + const nameInfo1 = SimulatorPlugin.parseName(updatePvName); + const nameInfo2 = SimulatorPlugin.parseName(updatePvName); + if (nameInfo1.keyName === nameInfo2.keyName) { + callback(value); + } + }, + nullDeviceCallback + ); } const assertValue = ( @@ -209,9 +217,13 @@ describe("LimitData", (): void => { const iter = repeatedCallback(); iter.next(); - simulator.connect(nullConnCallback, function(name, value): void { - iter.next({ name: name, value: value }); - }); + simulator.connect( + nullConnCallback, + function(name, value): void { + iter.next({ name: name, value: value }); + }, + nullDeviceCallback + ); simulator.subscribe("sim://limit#one"); simulator.subscribe("sim://limit#two"); @@ -224,7 +236,7 @@ it("test disconnector", (done): void => { let wasConnected = false; let wasDisconnected = false; simulator = new SimulatorPlugin(50); - function callback(pvName: string, state: any): void { + function callback(pvName: string, type: string, state: any): void { expect(pvName).toBe("sim://disconnector"); if (state.isConnected) { wasConnected = true; @@ -236,8 +248,7 @@ it("test disconnector", (done): void => { done(); } } - - simulator.connect(callback, nullValueCallback); + simulator.connect(callback, nullValueCallback, nullDeviceCallback); simulator.subscribe("sim://disconnector"); }); @@ -288,9 +299,13 @@ it("distinguish sine values", (done): void => { } } - simulator.connect(nullConnCallback, function(name, value): void { - callback({ name: name, value: value }); - }); + simulator.connect( + nullConnCallback, + function(name, value): void { + callback({ name: name, value: value }); + }, + nullDeviceCallback + ); simulator.subscribe("sim://sine#one"); simulator.subscribe("sim://sine#two"); @@ -400,7 +415,8 @@ it("unsubscribe stops updates for simulated values", (done): void => { simulator.connect( nullConnCallback, - callbacks.callback(client.callback(callback)) + callbacks.callback(client.callback(callback)), + nullDeviceCallback ); client.subscribe(); }); @@ -436,7 +452,8 @@ it("unsubscribe stops updates, but maintains value", (done): void => { simulator.connect( nullConnCallback, - callbacks.callback(client.callback(callback)) + callbacks.callback(client.callback(callback)), + nullDeviceCallback ); client.subscribe(); }); diff --git a/src/connection/sim.ts b/src/connection/sim.ts index 9980ac66..541bcdf4 100644 --- a/src/connection/sim.ts +++ b/src/connection/sim.ts @@ -3,8 +3,10 @@ import { ConnectionState, ConnectionChangedCallback, ValueChangedCallback, + DeviceChangedCallback, nullConnCallback, nullValueCallback, + nullDeviceCallback, PvConnection } from "./plugin"; import { @@ -338,6 +340,7 @@ export class SimulatorPlugin implements PvConnection { private simPvs: SimCache; private onConnectionUpdate: ConnectionChangedCallback; private onValueUpdate: ValueChangedCallback; + private onDeviceUpdate: DeviceChangedCallback; private updateRate: number; private connected: boolean; @@ -345,6 +348,7 @@ export class SimulatorPlugin implements PvConnection { this.simPvs = new SimCache(); this.onConnectionUpdate = nullConnCallback; this.onValueUpdate = nullValueCallback; + this.onDeviceUpdate = nullDeviceCallback; this.putPv = this.putPv.bind(this); this.updateRate = updateRate || 2000; this.connected = false; @@ -360,7 +364,8 @@ export class SimulatorPlugin implements PvConnection { public connect( connectionCallback: ConnectionChangedCallback, - valueCallback: ValueChangedCallback + valueCallback: ValueChangedCallback, + deviceCallback: DeviceChangedCallback ): void { if (this.connected) { throw new Error("Can only connect once"); @@ -368,6 +373,7 @@ export class SimulatorPlugin implements PvConnection { this.onConnectionUpdate = connectionCallback; this.onValueUpdate = valueCallback; + this.onDeviceUpdate = deviceCallback; this.connected = true; } diff --git a/src/redux/csState.test.ts b/src/redux/csState.test.ts index e9933aaf..89f44297 100644 --- a/src/redux/csState.test.ts +++ b/src/redux/csState.test.ts @@ -23,8 +23,10 @@ const initialState: CsState = { initializingPvName: "" } }, + deviceCache: {}, globalMacros: {}, subscriptions: {}, + deviceSubscriptions: {}, effectivePvNameMap: {} }; @@ -98,7 +100,11 @@ describe("CONNECTION_CHANGED", (): void => { test("csReducer handles value update", (): void => { const action: ConnectionChanged = { type: CONNECTION_CHANGED, - payload: { pvName: "PV", value: { isConnected: false, isReadonly: true } } + payload: { + pvDevice: "PV", + type: "pv", + value: { isConnected: false, isReadonly: true } + } }; const newState = csReducer(initialState, action); expect(newState.valueCache["PV"].connected).toEqual(false); diff --git a/src/redux/throttleMiddleware.test.ts b/src/redux/throttleMiddleware.test.ts index d543aada..172da6e7 100644 --- a/src/redux/throttleMiddleware.test.ts +++ b/src/redux/throttleMiddleware.test.ts @@ -3,10 +3,12 @@ import { VALUE_CHANGED, CONNECTION_CHANGED, VALUES_CHANGED, + DEVICE_CHANGED, ValueChanged, ConnectionChanged } from "./actions"; import { ddouble } from "../setupTests"; +import { DType } from "../types/dtypes"; // Mock setInterval. jest.useFakeTimers(); @@ -38,7 +40,18 @@ describe("UpdateThrottle", (): void => { }, mockStore ); - expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + updater.queueUpdate( + { + type: DEVICE_CHANGED, + payload: { + device: "test", + componentId: "12", + value: new DType({ stringValue: "15" }) + } + }, + mockStore + ); + expect(mockStore.dispatch).toHaveBeenCalledTimes(2); }); }); @@ -59,7 +72,7 @@ describe("throttleMidddlware", (): void => { payload: { pvName: "pv", value: ddouble(0) } }; actionHandler(valueAction); - expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockStore.dispatch).toHaveBeenCalledTimes(2); // The value update is not passed on. expect(mockNext).not.toHaveBeenCalled(); expect(mockStore.dispatch.mock.calls[0][0].type).toEqual(VALUES_CHANGED); @@ -73,7 +86,11 @@ describe("throttleMidddlware", (): void => { const actionHandler = nextHandler(mockNext); const connectionAction: ConnectionChanged = { type: CONNECTION_CHANGED, - payload: { pvName: "pv", value: { isReadonly: false, isConnected: true } } + payload: { + pvDevice: "pv", + type: "pv", + value: { isReadonly: false, isConnected: true } + } }; actionHandler(connectionAction); expect(mockStore.dispatch).toHaveBeenCalledTimes(0); diff --git a/src/setupTests.tsx b/src/setupTests.tsx index 2cba0803..e3db80af 100644 --- a/src/setupTests.tsx +++ b/src/setupTests.tsx @@ -58,7 +58,9 @@ export function contextRender( effectivePvNameMap: {}, globalMacros: {}, subscriptions: {}, - valueCache: {} + valueCache: {}, + deviceCache: {}, + deviceSubscriptions: {} } ): RenderResult { const ParentComponent = (props: { child: JSX.Element }): JSX.Element => { diff --git a/src/ui/hooks/useConnection.test.tsx b/src/ui/hooks/useConnection.test.tsx index 787df475..bdc4f765 100644 --- a/src/ui/hooks/useConnection.test.tsx +++ b/src/ui/hooks/useConnection.test.tsx @@ -33,7 +33,9 @@ function getConnectionState(pvName: string, value: DType): CsState { }, subscriptions: { [pvName]: [] }, globalMacros: {}, - effectivePvNameMap: {} + effectivePvNameMap: {}, + deviceCache: {}, + deviceSubscriptions: {} }; } diff --git a/src/ui/hooks/useRules.test.tsx b/src/ui/hooks/useRules.test.tsx index b794246d..9919cdcc 100644 --- a/src/ui/hooks/useRules.test.tsx +++ b/src/ui/hooks/useRules.test.tsx @@ -27,7 +27,9 @@ function getCsState(value: DType): CsState { }, subscriptions: { "ca://PV1": [] }, globalMacros: {}, - effectivePvNameMap: {} + effectivePvNameMap: {}, + deviceCache: {}, + deviceSubscriptions: {} }; } diff --git a/src/ui/hooks/utils.test.ts b/src/ui/hooks/utils.test.ts index f07298f7..89d487af 100644 --- a/src/ui/hooks/utils.test.ts +++ b/src/ui/hooks/utils.test.ts @@ -15,7 +15,9 @@ const state: CsState = { valueCache: { pv1: pvState }, globalMacros: {}, effectivePvNameMap: { pv1: "pv1", pv2: "pv3" }, - subscriptions: {} + subscriptions: {}, + deviceCache: {}, + deviceSubscriptions: {} }; describe("pvStateSelector", (): void => { diff --git a/src/ui/widgets/Device/coniqlParser.ts b/src/ui/widgets/Device/coniqlParser.ts deleted file mode 100644 index b27b7915..00000000 --- a/src/ui/widgets/Device/coniqlParser.ts +++ /dev/null @@ -1,79 +0,0 @@ -function coniqlToWidget(coniql: string): string { - if (coniql === "TEXTUPDATE") { - return "readback"; - } - return "readback"; -} - -export function coniqlToJSON(coniqlQuery: {}): string { - let jsonStr = '{"type":"display","position":"relative","overflow":"auto",'; - - if (coniqlQuery !== undefined) { - jsonStr += '"children":['; - - const obj = JSON.parse(JSON.stringify(coniqlQuery)); - - for (const entry in obj) { - // Hack for excess ',' - FIX ME - if (entry !== "0") jsonStr += ","; - - const { label, child } = obj[entry]; - - // Parse Label - - jsonStr += - '{"type": "flexcontainer",\ - "position": "relative",\ - "children": [\ - {\ - "type": "label",\ - "position": "relative",\ - "text":"' + - label + - '",\ - "width":"50%",\ - "backgroundColor": "transparent"\ - }'; - - //Parse child - - const { __typename, id } = child; - - if (__typename === "Channel") { - const { widget } = child; - - jsonStr += - ', {\ - "type": "' + - coniqlToWidget(widget) + - '",\ - "position": "relative",\ - "width":"50%",\ - "pvName": "' + - id + - '"\ - }'; - } - - if (__typename === "Device") { - jsonStr += - ', {\ - "type": "device",\ - "position": "relative",\ - "width":"50%",\ - "deviceName": "' + - id + - '"\ - }'; - } - - jsonStr += "]}"; - } - - jsonStr += "]}"; - } else { - jsonStr += '"children":[]}'; - } - - return jsonStr; -} diff --git a/src/ui/widgets/Device/device.tsx b/src/ui/widgets/Device/device.tsx index 41418b30..db7935c6 100644 --- a/src/ui/widgets/Device/device.tsx +++ b/src/ui/widgets/Device/device.tsx @@ -2,12 +2,7 @@ import React from "react"; import { Widget } from "./../widget"; import { WidgetPropType } from "./../widgetProps"; import { InferWidgetProps, StringPropOpt, StringProp } from "./../propTypes"; -import { parseJson } from "./../EmbeddedDisplay/jsonParser"; -import { widgetDescriptionToComponent } from "./../createComponent"; import { registerWidget } from "./../register"; -import { useId } from "react-id-generator"; -import { RelativePosition } from "../../../types/position"; -import { coniqlToJSON } from "./coniqlParser"; import { useDevice } from "../../hooks/useDevice"; const DeviceProps = { diff --git a/src/ui/widgets/widget.test.tsx b/src/ui/widgets/widget.test.tsx index 086ca229..8dc12bd3 100644 --- a/src/ui/widgets/widget.test.tsx +++ b/src/ui/widgets/widget.test.tsx @@ -37,7 +37,9 @@ describe("", (): void => { effectivePvNameMap: {}, globalMacros: {}, subscriptions: { [PV_NAME]: [] }, - valueCache: {} + valueCache: {}, + deviceCache: {}, + deviceSubscriptions: {} }; const { getByText } = contextRender( From 1b48bc613e52f87a69a8323c1ef4204f2269bb9a Mon Sep 17 00:00:00 2001 From: George Sheppard Date: Tue, 1 Dec 2020 15:46:49 +0000 Subject: [PATCH 8/9] Updating and adding tests --- src/connection/coniql.test.ts | 39 +++++++++++++++- src/redux/connectionMiddleware.test.ts | 29 +++++++++++- src/redux/csState.test.ts | 62 +++++++++++++++++++++++- src/redux/csState.ts | 1 + src/redux/throttleMiddleware.test.ts | 65 ++++++++++++++++---------- src/redux/throttleMiddleware.ts | 12 +++-- src/ui/hooks/useDevice.tsx | 6 +-- src/ui/hooks/utils.test.ts | 58 +++++++++++++++++++++-- src/ui/hooks/utils.ts | 22 +++++---- src/ui/widgets/Device/device.tsx | 2 +- 10 files changed, 247 insertions(+), 49 deletions(-) diff --git a/src/connection/coniql.test.ts b/src/connection/coniql.test.ts index e3b1ed34..b49629ee 100644 --- a/src/connection/coniql.test.ts +++ b/src/connection/coniql.test.ts @@ -3,7 +3,8 @@ import { ConiqlPvPlugin, ConiqlStatus, ConiqlTime, - ConiqlBase64Array + ConiqlBase64Array, + ConiqlDevicePlugin } from "./coniql"; import { DType } from "../types/dtypes"; @@ -47,7 +48,41 @@ class MockObservable { } } -describe("ConiqlPlugin", (): void => { +describe("ConiqlDevicePlugin", (): void => { + let cp: ConiqlDevicePlugin; + let mockConnUpdate: jest.Mock; + let mockValUpdate: jest.Mock; + let mockDevUpdate: jest.Mock; + beforeEach((): void => { + cp = new ConiqlDevicePlugin("x.y.z:1000"); + mockConnUpdate = jest.fn(); + mockValUpdate = jest.fn(); + mockDevUpdate = jest.fn(); + cp.connect(mockConnUpdate, mockValUpdate, mockDevUpdate); + }); + + it("subscribes to a device", (): void => { + ApolloClient.prototype.subscribe = jest.fn( + (_): MockObservable => new MockObservable(42) + ) as jest.Mock; + cp.subscribe("pvDevice", {}); + expect(ApolloClient.prototype.subscribe).toHaveBeenCalled(); + expect(mockDevUpdate).toHaveBeenCalledWith( + "pvDevice", + new DType({ + stringValue: JSON.stringify({ + subscribeChannel: { + value: { + float: 42 + } + } + }) + }) + ); + }); +}); + +describe("ConiqlPvPlugin", (): void => { let cp: ConiqlPvPlugin; let mockConnUpdate: jest.Mock; let mockValUpdate: jest.Mock; diff --git a/src/redux/connectionMiddleware.test.ts b/src/redux/connectionMiddleware.test.ts index 3192fa4c..8d657fa9 100644 --- a/src/redux/connectionMiddleware.test.ts +++ b/src/redux/connectionMiddleware.test.ts @@ -1,4 +1,11 @@ -import { SUBSCRIBE, Subscribe, WRITE_PV, WritePv } from "./actions"; +import { + SUBSCRIBE, + Subscribe, + WRITE_PV, + WritePv, + SUBSCRIBE_DEVICE, + SubscribeDevice +} from "./actions"; import { connectionMiddleware } from "./connectionMiddleware"; import { ddouble } from "../setupTests"; @@ -41,6 +48,26 @@ describe("connectionMiddleware", (): void => { expect(mockNext).toHaveBeenCalledTimes(1); expect(mockNext.mock.calls[0][0].type).toEqual(SUBSCRIBE); }); + it("calls subscribe() when receiving SUBSCRIBE_DEVICE", (): void => { + const middleware = connectionMiddleware(mockConnection); + // nextHandler takes next() and returns the actual middleware function + const nextHandler = middleware(mockStore); + const mockNext = jest.fn(); + // actionHandler takes an action + const actionHandler = nextHandler(mockNext); + const subscribeAction: SubscribeDevice = { + type: SUBSCRIBE_DEVICE, + payload: { + device: "device", + componentId: "2" + } + }; + actionHandler(subscribeAction); + expect(mockConnection.subscribe).toHaveBeenCalledTimes(1); + // The action is passed on. + expect(mockNext).toHaveBeenCalledTimes(1); + expect(mockNext.mock.calls[0][0].type).toEqual(SUBSCRIBE_DEVICE); + }); it("calls putPv() when receiving WritePv", (): void => { // Set up state mockStore.getState.mockReturnValue({ effectivePvNameMap: {} }); diff --git a/src/redux/csState.test.ts b/src/redux/csState.test.ts index 89f44297..06c37ade 100644 --- a/src/redux/csState.test.ts +++ b/src/redux/csState.test.ts @@ -9,9 +9,13 @@ import { Unsubscribe, ValueChanged, ValuesChanged, - VALUES_CHANGED + VALUES_CHANGED, + DeviceChanged, + DEVICE_CHANGED, + DevicesChanged, + DEVICES_CHANGED } from "./actions"; -import { DAlarm } from "../types/dtypes"; +import { DAlarm, DType } from "../types/dtypes"; import { ddouble, dstring, ddoubleArray } from "../setupTests"; const initialState: CsState = { @@ -96,6 +100,47 @@ describe("VALUE_CHANGED", (): void => { }); }); +describe("DEVICE_CHANGED", (): void => { + test("csReducer handles DEVICE_CHANGED", (): void => { + const value = new DType({ stringValue: "6" }); + const action: DeviceChanged = { + type: DEVICE_CHANGED, + payload: { + device: "new device", + componentId: "5", + value: value + } + }; + const newState = csReducer(initialState, action); + expect(newState.deviceCache["new device"].value).toEqual(value); + }); + + test("csReducer handles DEVICES_CHANGED", (): void => { + const value1 = new DType({ stringValue: "1" }); + const value2 = new DType({ stringValue: "2" }); + const payload1 = { + device: "device1", + componentId: "1", + value: value1 + }; + const payload2 = { + device: "device2", + componentId: "2", + value: value2 + }; + const devicesChanged: DevicesChanged = { + type: DEVICES_CHANGED, + payload: [ + { type: DEVICE_CHANGED, payload: payload1 }, + { type: DEVICE_CHANGED, payload: payload2 } + ] + }; + const newState = csReducer(initialState, devicesChanged); + expect(newState.deviceCache["device1"].value).toEqual(value1); + expect(newState.deviceCache["device2"].value).toEqual(value2); + }); +}); + describe("CONNECTION_CHANGED", (): void => { test("csReducer handles value update", (): void => { const action: ConnectionChanged = { @@ -109,6 +154,19 @@ describe("CONNECTION_CHANGED", (): void => { const newState = csReducer(initialState, action); expect(newState.valueCache["PV"].connected).toEqual(false); }); + + test("csReducer handles device update", (): void => { + const action: ConnectionChanged = { + type: CONNECTION_CHANGED, + payload: { + pvDevice: "device", + type: "device", + value: { isConnected: false, isReadonly: true } + } + }; + const newState = csReducer(initialState, action); + expect(newState.deviceCache["device"].connected).toEqual(false); + }); }); test("handles initializers", (): void => { diff --git a/src/redux/csState.ts b/src/redux/csState.ts index da32f166..a565bf6f 100644 --- a/src/redux/csState.ts +++ b/src/redux/csState.ts @@ -41,6 +41,7 @@ export interface ValueCache { } export interface FullDeviceState extends PvState { + [index: string]: any; device: string; } diff --git a/src/redux/throttleMiddleware.test.ts b/src/redux/throttleMiddleware.test.ts index 172da6e7..0f51ce26 100644 --- a/src/redux/throttleMiddleware.test.ts +++ b/src/redux/throttleMiddleware.test.ts @@ -4,8 +4,10 @@ import { CONNECTION_CHANGED, VALUES_CHANGED, DEVICE_CHANGED, + DEVICES_CHANGED, ValueChanged, - ConnectionChanged + ConnectionChanged, + Action } from "./actions"; import { ddouble } from "../setupTests"; import { DType } from "../types/dtypes"; @@ -19,7 +21,7 @@ describe("UpdateThrottle", (): void => { mockStore.dispatch.mockClear(); }); it("collects updates", (): void => { - const updater = new UpdateThrottle(100); + const updater = new UpdateThrottle(10); updater.queueUpdate( { type: VALUE_CHANGED, @@ -30,28 +32,41 @@ describe("UpdateThrottle", (): void => { }, mockStore ); - updater.queueUpdate( - { - type: VALUE_CHANGED, - payload: { - pvName: "PV", - value: ddouble(1) - } - }, - mockStore - ); - updater.queueUpdate( - { - type: DEVICE_CHANGED, - payload: { - device: "test", - componentId: "12", - value: new DType({ stringValue: "15" }) - } - }, - mockStore - ); - expect(mockStore.dispatch).toHaveBeenCalledTimes(2); + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + }); + + it("VALUE_CHANGED queues and is dispatched", (): void => { + const updater = new UpdateThrottle(10); + const query: Action = { + type: VALUE_CHANGED, + payload: { + pvName: "PV", + value: ddouble(0) + } + }; + updater.queueUpdate(query, mockStore); + expect(mockStore.dispatch).toHaveBeenCalledWith({ + type: VALUES_CHANGED, + payload: [query] + }); + }); + + it("DEVICE_CHANGED queues and is dispatched", (): void => { + const updater = new UpdateThrottle(10); + const query: Action = { + type: DEVICE_CHANGED, + payload: { + device: "PV", + componentId: "device", + value: new DType({ stringValue: "15" }) + } + }; + updater.queueUpdate(query, mockStore); + + expect(mockStore.dispatch).toHaveBeenCalledWith({ + type: DEVICES_CHANGED, + payload: [query] + }); }); }); @@ -72,7 +87,7 @@ describe("throttleMidddlware", (): void => { payload: { pvName: "pv", value: ddouble(0) } }; actionHandler(valueAction); - expect(mockStore.dispatch).toHaveBeenCalledTimes(2); + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); // The value update is not passed on. expect(mockNext).not.toHaveBeenCalled(); expect(mockStore.dispatch.mock.calls[0][0].type).toEqual(VALUES_CHANGED); diff --git a/src/redux/throttleMiddleware.ts b/src/redux/throttleMiddleware.ts index 6672d493..d83dd87f 100644 --- a/src/redux/throttleMiddleware.ts +++ b/src/redux/throttleMiddleware.ts @@ -35,10 +35,14 @@ export class UpdateThrottle { } public sendQueue(store: MiddlewareAPI): void { - store.dispatch({ type: VALUES_CHANGED, payload: [...this.pvQueue] }); - store.dispatch({ type: DEVICES_CHANGED, payload: [...this.deviceQueue] }); - this.pvQueue = []; - this.deviceQueue = []; + if (this.pvQueue.length > 0) { + store.dispatch({ type: VALUES_CHANGED, payload: [...this.pvQueue] }); + this.pvQueue = []; + } + if (this.deviceQueue.length > 0) { + store.dispatch({ type: DEVICES_CHANGED, payload: [...this.deviceQueue] }); + this.deviceQueue = []; + } this.ready = false; } } diff --git a/src/ui/hooks/useDevice.tsx b/src/ui/hooks/useDevice.tsx index bda552f9..f4a5863c 100644 --- a/src/ui/hooks/useDevice.tsx +++ b/src/ui/hooks/useDevice.tsx @@ -1,5 +1,5 @@ import { useSelector } from "react-redux"; -import { CsState } from "../../redux/csState"; +import { CsState, FullDeviceState } from "../../redux/csState"; import { deviceSelector, deviceComparator } from "./utils"; import { useDeviceSubscription } from "./useDeviceSubscription"; import { DType } from "../../types/dtypes"; @@ -16,8 +16,8 @@ export function useDevice( device: string ): Device | undefined { useDeviceSubscription(componentId, device); - const description: any = useSelector( - (state: CsState): {} => deviceSelector(device, state), + const description: any = useSelector( + (state: CsState): FullDeviceState => deviceSelector(device, state), deviceComparator ); return description; diff --git a/src/ui/hooks/utils.test.ts b/src/ui/hooks/utils.test.ts index 89d487af..71b1748e 100644 --- a/src/ui/hooks/utils.test.ts +++ b/src/ui/hooks/utils.test.ts @@ -1,5 +1,16 @@ -import { CsState, PvState, FullPvState } from "../../redux/csState"; -import { pvStateSelector, pvStateComparator, PvArrayResults } from "./utils"; +import { + CsState, + PvState, + FullPvState, + FullDeviceState +} from "../../redux/csState"; +import { + pvStateSelector, + pvStateComparator, + PvArrayResults, + deviceSelector, + deviceComparator +} from "./utils"; const pv1 = "pv1"; const pv2 = "pv2"; @@ -10,13 +21,19 @@ const pvState: FullPvState = { readonly: true, initializingPvName: pv1 }; +const deviceState: FullDeviceState = { + value: undefined, + connected: true, + readonly: true, + device: "fakeDevice" +}; const state: CsState = { valueCache: { pv1: pvState }, globalMacros: {}, effectivePvNameMap: { pv1: "pv1", pv2: "pv3" }, subscriptions: {}, - deviceCache: {}, + deviceCache: { fakeDevice: deviceState }, deviceSubscriptions: {} }; @@ -71,3 +88,38 @@ describe("pvStateComparator", (): void => { expect(pvStateComparator(singleResult, anotherSingleResult)).toBe(false); }); }); + +describe("deviceSelector", (): void => { + it("returns appropriate device value if available", () => { + const result = deviceSelector("fakeDevice", state); + expect(result.device).toEqual("fakeDevice"); + }); + + it("returns undefined if a device is not available", (): void => { + const result = deviceSelector("anotherFakeDevice", state); + expect(result).toBeUndefined(); + }); +}); + +describe("deviceComparator", (): void => { + it("returns true if the same object is passed twice", (): void => { + expect(deviceComparator(deviceState, deviceState)).toBe(true); + }); + + it("returns true if one is a copy of the other", (): void => { + const newDevice: FullDeviceState = { ...deviceState }; + expect(deviceComparator(deviceState, newDevice)).toBe(true); + }); + + it("returns false if both match but one has an extra property", (): void => { + const newDevice = { ...deviceState }; + newDevice.newValue = "5"; + expect(deviceComparator(deviceState, newDevice)).toBe(false); + }); + + it("returns false one property doesn't match", (): void => { + const newDevice = { ...deviceState }; + newDevice.connected = !newDevice.connected; + expect(deviceComparator(deviceState, newDevice)).toBe(false); + }); +}); diff --git a/src/ui/hooks/utils.ts b/src/ui/hooks/utils.ts index a555ef65..8b141d25 100644 --- a/src/ui/hooks/utils.ts +++ b/src/ui/hooks/utils.ts @@ -1,13 +1,9 @@ -import { PvState, CsState } from "../../redux/csState"; +import { PvState, CsState, FullDeviceState } from "../../redux/csState"; export interface PvArrayResults { [pvName: string]: [PvState, string]; } -export interface DeviceArrayResults { - [device: string]: [PvState, string]; -} - export function pvStateSelector( pvNames: string[], state: CsState @@ -20,7 +16,10 @@ export function pvStateSelector( return results; } -export function deviceSelector(device: string, state: CsState): {} { +export function deviceSelector( + device: string, + state: CsState +): FullDeviceState { return state.deviceCache[device]; } @@ -50,11 +49,18 @@ export function pvStateComparator( } export function deviceComparator( - before: DeviceArrayResults, - after: DeviceArrayResults + before: FullDeviceState, + after: FullDeviceState ): boolean { if (Object.keys(before).length !== Object.keys(after).length) { return false; } + + for (const [property, beforeValue] of Object.entries(before)) { + const afterValue = after[property]; + if (afterValue !== beforeValue) { + return false; + } + } return true; } diff --git a/src/ui/widgets/Device/device.tsx b/src/ui/widgets/Device/device.tsx index db7935c6..6ba48344 100644 --- a/src/ui/widgets/Device/device.tsx +++ b/src/ui/widgets/Device/device.tsx @@ -24,7 +24,7 @@ const DeviceComponent = ( // type: "display", // children: [description] // }); - return
{(description && description.value.toString()) || ""}
; + return
{(description && description.value?.toString()) || ""}
; }; const DeviceWidgetProps = { From 7ed14608501af515931f709d348f51afe0c8e638 Mon Sep 17 00:00:00 2001 From: George Sheppard Date: Tue, 1 Dec 2020 15:49:23 +0000 Subject: [PATCH 9/9] Uncomment .env variable --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index eeb6dae9..0db0d5d7 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # React app settings. Restart the process for changes to take effect. # Set to connect to a Coniql server. -REACT_APP_CONIQL_SOCKET=cs03r-sc-serv-39.diamond.ac.uk:8080 +# REACT_APP_CONIQL_SOCKET=cs03r-sc-serv-39.diamond.ac.uk:8080 REACT_APP_BASE_URL=http://localhost:3000 REACT_APP_SIMULATION_TIME=100