From b62548a3a749621760e8816673a19ad2afbc1e9f Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Fri, 4 Oct 2019 12:12:22 +0100 Subject: [PATCH 1/4] First pass at vtypes. --- src/vtypes/alarm.ts | 57 +++++++++++++++++++++++++++++++++++++++++ src/vtypes/display.ts | 59 +++++++++++++++++++++++++++++++++++++++++++ src/vtypes/time.ts | 37 +++++++++++++++++++++++++++ src/vtypes/vtypes.ts | 50 ++++++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 src/vtypes/alarm.ts create mode 100644 src/vtypes/display.ts create mode 100644 src/vtypes/time.ts create mode 100644 src/vtypes/vtypes.ts diff --git a/src/vtypes/alarm.ts b/src/vtypes/alarm.ts new file mode 100644 index 00000000..26eca32c --- /dev/null +++ b/src/vtypes/alarm.ts @@ -0,0 +1,57 @@ +export enum AlarmSeverity { + NONE, + MINOR, + MAJOR, + INVALID, + UNDEFINED +} + +export enum AlarmStatus { + NONE, + DEVICE, + DRIVER, + RECORD, + DB, + CONF, + UNDEFINED, + CLIENT +} + +export abstract class Alarm { + public abstract getSeverity(): AlarmSeverity; + public abstract getStatus(): AlarmStatus; + public abstract getName(): string; +} + +class IAlarm extends Alarm { + private severity: AlarmSeverity; + private status: AlarmStatus; + private name: string; + public constructor( + severity: AlarmSeverity, + status: AlarmStatus, + name: string + ) { + super(); + this.severity = severity; + this.status = status; + this.name = name; + } + public getSeverity(): AlarmSeverity { + return this.severity; + } + public getStatus(): AlarmStatus { + return this.status; + } + public getName(): string { + return this.name; + } +} + +export const alarmOf = ( + severity: AlarmSeverity, + status: AlarmStatus, + name: string +): Alarm => { + return new IAlarm(severity, status, name); +}; diff --git a/src/vtypes/display.ts b/src/vtypes/display.ts new file mode 100644 index 00000000..ab6f9026 --- /dev/null +++ b/src/vtypes/display.ts @@ -0,0 +1,59 @@ +/* Currently missing NumberFormat. */ +export interface Range { + min: number; + max: number; +} + +export abstract class Display { + public abstract getDisplayRange(): Range; + public abstract getWarningRange(): Range; + public abstract getAlarmRange(): Range; + public abstract getControlRange(): Range; + public abstract getUnit(): string; +} + +class IDisplay { + private displayRange: Range; + private alarmRange: Range; + private warningRange: Range; + private controlRange: Range; + private unit: string; + + public constructor( + displayRange: Range, + alarmRange: Range, + warningRange: Range, + controlRange: Range, + unit: string + ) { + this.displayRange = displayRange; + this.alarmRange = alarmRange; + this.warningRange = warningRange; + this.controlRange = controlRange; + this.unit = unit; + } + public getDisplayRange(): Range { + return this.displayRange; + } + public getAlarmRange(): Range { + return this.alarmRange; + } + public getWarningRange(): Range { + return this.warningRange; + } + public getControlRange(): Range { + return this.controlRange; + } + public getUnit(): string { + return this.unit; + } +} + +export const displayOf = ( + displayRange: Range, + alarmRange: Range, + warningRange: Range, + controlRange: Range, + unit: string +): Display => + new IDisplay(displayRange, alarmRange, warningRange, controlRange, unit); diff --git a/src/vtypes/time.ts b/src/vtypes/time.ts new file mode 100644 index 00000000..9d409804 --- /dev/null +++ b/src/vtypes/time.ts @@ -0,0 +1,37 @@ +interface Instant { + secondsPastEpoch: number; + nanoseconds: number; +} + +export abstract class Time { + public abstract getInstant(): Instant; + public abstract getUserTag(): number; + public abstract isValid(): boolean; +} + +class ITime extends Time { + private instant: Instant; + private userTag: number; + private valid: boolean; + public constructor(instant: Instant, userTag: number, valid: boolean) { + super(); + this.instant = instant; + this.userTag = userTag; + this.valid = valid; + } + public getInstant(): Instant { + return this.instant; + } + public getUserTag(): number { + return this.userTag; + } + public isValid(): boolean { + return this.valid; + } +} + +export const timeOf = ( + instant: Instant, + userTag: number, + valid: boolean +): Time => new ITime(instant, userTag, valid); diff --git a/src/vtypes/vtypes.ts b/src/vtypes/vtypes.ts new file mode 100644 index 00000000..deede649 --- /dev/null +++ b/src/vtypes/vtypes.ts @@ -0,0 +1,50 @@ +import { Alarm } from "./alarm"; +import { Display } from "./display"; +import { Time } from "./time"; + +class VType {} + +export abstract class VDouble extends VType { + public abstract getValue(): number; + public abstract getAlarm(): Alarm; + public abstract getTime(): Time; + public abstract getDisplay(): Display; +} + +class IVDouble extends VDouble { + private value: number; + private alarm: Alarm; + private time: Time; + private display: Display; + public constructor( + value: number, + alarm: Alarm, + time: Time, + display: Display + ) { + super(); + this.value = value; + this.alarm = alarm; + this.time = time; + this.display = display; + } + public getValue(): number { + return this.value; + } + public getAlarm(): Alarm { + return this.alarm; + } + public getTime(): Time { + return this.time; + } + public getDisplay(): Display { + return this.display; + } +} + +export const doubleOf = ( + double: number, + alarm: Alarm, + time: Time, + display: Display +): VDouble => new IVDouble(double, alarm, time, display); From bff607bb5e57e9c7eeea7b36eabe12616d1e07f5 Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Fri, 4 Oct 2019 12:41:37 +0100 Subject: [PATCH 2/4] Partially-working conversion to VTypes. --- .../ConnectionWrapper/connectionWrapper.tsx | 4 +- src/connection/coniql.ts | 8 +-- src/connection/plugin.ts | 8 +-- src/connection/sim.ts | 49 ++++++------------- src/redux/connectionMiddleware.ts | 4 +- src/redux/csState.ts | 4 +- src/vtypes/alarm.ts | 2 + src/vtypes/display.ts | 13 +++++ src/vtypes/time.ts | 13 +++++ src/vtypes/vtypes.ts | 20 +++++--- 10 files changed, 70 insertions(+), 55 deletions(-) diff --git a/src/components/ConnectionWrapper/connectionWrapper.tsx b/src/components/ConnectionWrapper/connectionWrapper.tsx index 3a9d5855..4c42cda5 100644 --- a/src/components/ConnectionWrapper/connectionWrapper.tsx +++ b/src/components/ConnectionWrapper/connectionWrapper.tsx @@ -3,7 +3,7 @@ import { useId } from "react-id-generator"; import { useSubscription } from "../../hooks/useCs"; import { useSelector } from "react-redux"; import { CsState } from "../../redux/csState"; -import { NType } from "../../ntypes"; +import { VType } from "../../vtypes/vtypes"; export interface PvProps extends React.PropsWithChildren { pvName: string; @@ -22,7 +22,7 @@ export const connectionWrapper =

( useSubscription(id, props.pvName); const [connected, latestValue] = useSelector((state: CsState): [ boolean, - NType? + VType? ] => { let pvState = state.valueCache[props.pvName]; let connected = false; diff --git a/src/connection/coniql.ts b/src/connection/coniql.ts index c2845096..1fc59f2b 100644 --- a/src/connection/coniql.ts +++ b/src/connection/coniql.ts @@ -5,7 +5,7 @@ import { WebSocketLink } from "apollo-link-ws"; import { getMainDefinition } from "apollo-utilities"; import gql from "graphql-tag"; import { InMemoryCache, NormalizedCacheObject } from "apollo-cache-inmemory"; -import { NType } from "../ntypes"; +import { VType, doubleOf } from "../vtypes/vtypes"; import { Connection, ConnectionChangedCallback, @@ -87,12 +87,12 @@ export class ConiqlPlugin implements Connection { }); } - public putPv(pvName: string, value: NType): void { + public putPv(pvName: string, value: VType): void { // noop } - public getValue(pvName: string): NType { - return { type: "NTScalarDouble", value: "" }; + public getValue(pvName: string): VType { + return doubleOf(0); } public unsubscribe(pvName: string): void { diff --git a/src/connection/plugin.ts b/src/connection/plugin.ts index 57e4ba53..b67a293d 100644 --- a/src/connection/plugin.ts +++ b/src/connection/plugin.ts @@ -1,4 +1,4 @@ -import { NType } from "../ntypes"; +import { VType } from "../vtypes/vtypes"; export const nullConnCallback: ConnectionChangedCallback = (_p, _v): void => {}; export const nullValueCallback: ValueChangedCallback = (_p, _v): void => {}; @@ -11,12 +11,12 @@ export type ConnectionChangedCallback = ( pvName: string, value: ConnectionState ) => void; -export type ValueChangedCallback = (pvName: string, value: NType) => void; +export type ValueChangedCallback = (pvName: string, value: VType) => void; export interface Connection { subscribe: (pvName: string) => void; - putPv: (pvName: string, value: NType) => void; - getValue: (pvName: string) => NType; + putPv: (pvName: string, value: VType) => void; + getValue: (pvName: string) => VType; connect: ( connectionCallback: ConnectionChangedCallback, valueCallback: ValueChangedCallback diff --git a/src/connection/sim.ts b/src/connection/sim.ts index 4696fca4..61de3152 100644 --- a/src/connection/sim.ts +++ b/src/connection/sim.ts @@ -6,14 +6,14 @@ import { nullConnCallback, nullValueCallback } from "./plugin"; -import { NType } from "../ntypes"; +import { VType, doubleOf, VNumber } from "../vtypes/vtypes"; abstract class SimPv { protected onConnectionUpdate: ConnectionChangedCallback; protected onValueUpdate: ValueChangedCallback; protected pvName: string; protected updateRate: number; - abstract getValue(): NType; + abstract getValue(): VType; public constructor( pvName: string, onConnectionUpdate: ConnectionChangedCallback, @@ -44,7 +44,7 @@ class SinePv extends SimPv { this.updateRate ); } - public getValue(): NType { + public getValue(): VType { const val = Math.sin( new Date().getSeconds() + new Date().getMilliseconds() * 0.001 ); @@ -71,14 +71,14 @@ class Disconnector extends SimPv { return { isConnected: randomBool }; } - public getValue(): NType { + public getValue(): VType { const value = Math.random(); return { type: "NTScalarDouble", value: value }; } } class MetaData extends SimPv { - private value: NType; + private value: VType; // Class to provide PV value along with Alarm and Timestamp data // Initial limits will be 10, 20, 80 and 90 - with expected range between 0 and 100 public constructor( @@ -88,19 +88,7 @@ class MetaData extends SimPv { updateRate: number ) { super(pvName, onConnectionUpdate, onValueUpdate, updateRate); - let currentTime = new Date(); - let seconds = Math.round(currentTime.getTime() / 1000), - nanoseconds = Math.round(currentTime.getTime() % 1000); - this.value = { - type: "NTScalar", - value: 50, - alarm: { severity: 0, status: 0, message: "" }, - time: { - secondsPastEpoch: seconds, - nanoseconds: nanoseconds, - userTag: 0 - } - }; + this.value = doubleOf(50); this.onValueUpdate(this.pvName, this.getValue()); setInterval( (): void => this.onConnectionUpdate(this.pvName, this.getConnection()), @@ -108,18 +96,13 @@ class MetaData extends SimPv { ); } - public updateValue(value: NType): void { + public updateValue(value: VType): void { // Set alarm status - let alarmSeverity = - value.value < 10 - ? 2 - : value.value > 90 - ? 2 - : value.value < 20 - ? 1 - : value.value > 80 - ? 1 - : 0; + let alarmSeverity = 0; + if (value instanceof VNumber) { + let v = value.getValue(); + alarmSeverity = v < 10 ? 2 : v > 90 ? 2 : v < 20 ? 1 : v > 80 ? 1 : 0; + } // Produce timestamp info let currentTime = new Date().getTime(); @@ -141,7 +124,7 @@ class MetaData extends SimPv { }; } - public getValue(): NType { + public getValue(): VType { return this.value; } } @@ -151,7 +134,7 @@ interface SimCache { } interface ValueCache { - [pvName: string]: NType; + [pvName: string]: VType; } interface MetaCache { @@ -219,7 +202,7 @@ export class SimulatorPlugin implements Connection { } } - public putPv(pvName: string, value: NType): void { + public putPv(pvName: string, value: VType): void { if (pvName.startsWith("loc://")) { this.localPvs[pvName] = value; this.onValueUpdate(pvName, value); @@ -230,7 +213,7 @@ export class SimulatorPlugin implements Connection { } } - public getValue(pvName: string): NType { + public getValue(pvName: string): VType { if (pvName.startsWith("loc://")) { return this.localPvs[pvName]; } else if (pvName.startsWith("sim://")) { diff --git a/src/redux/connectionMiddleware.ts b/src/redux/connectionMiddleware.ts index 38fcb511..709a9d5c 100644 --- a/src/redux/connectionMiddleware.ts +++ b/src/redux/connectionMiddleware.ts @@ -7,7 +7,7 @@ import { VALUE_CHANGED, UNSUBSCRIBE } from "./actions"; -import { NType } from "../ntypes"; +import { VType } from "../vtypes/vtypes"; function connectionChanged( store: Store, @@ -20,7 +20,7 @@ function connectionChanged( }); } -function valueChanged(store: Store, pvName: string, value: NType): void { +function valueChanged(store: Store, pvName: string, value: VType): void { store.dispatch({ type: VALUE_CHANGED, payload: { pvName: pvName, value: value } diff --git a/src/redux/csState.ts b/src/redux/csState.ts index 4072240d..6efd2bb4 100644 --- a/src/redux/csState.ts +++ b/src/redux/csState.ts @@ -6,7 +6,7 @@ import { CONNECTION_CHANGED, UNSUBSCRIBE } from "./actions"; -import { NType } from "../ntypes"; +import { VType } from "../vtypes/vtypes"; const initialState: CsState = { valueCache: {}, @@ -14,7 +14,7 @@ const initialState: CsState = { }; export interface PvState { - value: NType; + value: VType; connected: boolean; } diff --git a/src/vtypes/alarm.ts b/src/vtypes/alarm.ts index 26eca32c..7fa0cb27 100644 --- a/src/vtypes/alarm.ts +++ b/src/vtypes/alarm.ts @@ -55,3 +55,5 @@ export const alarmOf = ( ): Alarm => { return new IAlarm(severity, status, name); }; + +export const ALARM_NONE = alarmOf(AlarmSeverity.NONE, AlarmStatus.NONE, ""); diff --git a/src/vtypes/display.ts b/src/vtypes/display.ts index ab6f9026..b8dfac54 100644 --- a/src/vtypes/display.ts +++ b/src/vtypes/display.ts @@ -4,6 +4,11 @@ export interface Range { max: number; } +export const RANGE_NONE: Range = { + min: 0, + max: 0 +}; + export abstract class Display { public abstract getDisplayRange(): Range; public abstract getWarningRange(): Range; @@ -57,3 +62,11 @@ export const displayOf = ( unit: string ): Display => new IDisplay(displayRange, alarmRange, warningRange, controlRange, unit); + +export const DISPLAY_NONE = displayOf( + RANGE_NONE, + RANGE_NONE, + RANGE_NONE, + RANGE_NONE, + "" +); diff --git a/src/vtypes/time.ts b/src/vtypes/time.ts index 9d409804..e0559aea 100644 --- a/src/vtypes/time.ts +++ b/src/vtypes/time.ts @@ -35,3 +35,16 @@ export const timeOf = ( userTag: number, valid: boolean ): Time => new ITime(instant, userTag, valid); + +export const instantNow = (): Instant => { + const nowMillis = new Date().getTime(); + const secs = nowMillis / 1000; + const nanos = (nowMillis % 1000) * 1e6; + return { + secondsPastEpoch: secs, + nanoseconds: nanos + }; +}; +export const timeNow = (): Time => { + return timeOf(instantNow(), 0, true); +}; diff --git a/src/vtypes/vtypes.ts b/src/vtypes/vtypes.ts index deede649..0aeca04a 100644 --- a/src/vtypes/vtypes.ts +++ b/src/vtypes/vtypes.ts @@ -1,10 +1,14 @@ -import { Alarm } from "./alarm"; -import { Display } from "./display"; -import { Time } from "./time"; +import { Alarm, ALARM_NONE } from "./alarm"; +import { Display, DISPLAY_NONE } from "./display"; +import { Time, timeNow } from "./time"; -class VType {} +export class VType {} -export abstract class VDouble extends VType { +export abstract class VNumber extends VType { + public abstract getValue(): number; +} + +export abstract class VDouble extends VNumber { public abstract getValue(): number; public abstract getAlarm(): Alarm; public abstract getTime(): Time; @@ -44,7 +48,7 @@ class IVDouble extends VDouble { export const doubleOf = ( double: number, - alarm: Alarm, - time: Time, - display: Display + alarm = ALARM_NONE, + time = timeNow(), + display = DISPLAY_NONE ): VDouble => new IVDouble(double, alarm, time, display); From b3937fc2d9a68613784778595578148c339663d3 Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Fri, 4 Oct 2019 16:25:12 +0100 Subject: [PATCH 3/4] Complete vtypification. --- src/components/AlarmBorder/alarmBorder.tsx | 14 +-- src/components/CopyWrapper/copyWrapper.tsx | 19 +-- src/components/Input/input.tsx | 16 ++- src/components/ProgressBar/progressBar.tsx | 12 +- src/components/Readback/readback.test.tsx | 14 +-- src/components/Readback/readback.tsx | 30 ++--- src/components/SlideControl/slideControl.tsx | 12 +- src/connection/coniql.ts | 4 +- src/connection/sim.ts | 42 +++---- src/hooks/useCs.ts | 4 +- src/ntypes.ts | 118 ------------------- src/vtypes/alarm.ts | 19 ++- src/vtypes/time.ts | 26 ++-- src/vtypes/utils.ts | 53 +++++++++ src/vtypes/vtypes.ts | 52 +++++++- 15 files changed, 207 insertions(+), 228 deletions(-) delete mode 100644 src/ntypes.ts create mode 100644 src/vtypes/utils.ts diff --git a/src/components/AlarmBorder/alarmBorder.tsx b/src/components/AlarmBorder/alarmBorder.tsx index 3db3e000..a2b80291 100644 --- a/src/components/AlarmBorder/alarmBorder.tsx +++ b/src/components/AlarmBorder/alarmBorder.tsx @@ -1,25 +1,23 @@ import React, { ReactNode } from "react"; -import { Alarm, NType } from "../../ntypes"; +import { VType } from "../../vtypes/vtypes"; +import { Alarm, alarmOf, AlarmSeverity } from "../../vtypes/alarm"; import classes from "./alarmBorder.module.css"; export const AlarmBorder = (props: { connected: boolean; - value?: NType; + value?: VType; children: ReactNode; }): JSX.Element => { let { connected, value = null } = props; - let alarm: Alarm = { severity: 0, status: 0, message: "" }; - if (value && value.alarm) { - alarm = value.alarm; - } + let alarm: Alarm = alarmOf(value); // Sort out alarm border classes let alarmClasses = [classes.Border, classes.Children]; if (connected === false) { alarmClasses.push(classes.NotConnected); - } else if (alarm.severity === 1) { + } else if (alarm.getSeverity() === AlarmSeverity.MINOR) { alarmClasses.push(classes.MinorAlarm); - } else if (alarm.severity === 2) { + } else if (alarm.getSeverity() === AlarmSeverity.MAJOR) { alarmClasses.push(classes.MajorAlarm); } diff --git a/src/components/CopyWrapper/copyWrapper.tsx b/src/components/CopyWrapper/copyWrapper.tsx index 879ae1b7..a95f8b11 100644 --- a/src/components/CopyWrapper/copyWrapper.tsx +++ b/src/components/CopyWrapper/copyWrapper.tsx @@ -8,18 +8,21 @@ import copyToClipboard from "clipboard-copy"; import Popover from "react-tiny-popover"; import { connectionWrapper } from "../ConnectionWrapper/connectionWrapper"; -import { NType } from "../../ntypes"; +import { VType } from "../../vtypes/vtypes"; import classes from "./copyWrapper.module.css"; +import { vtypeToString } from "../../vtypes/utils"; +import { timeOf } from "../../vtypes/time"; +import { alarmOf } from "../../vtypes/alarm"; export const CopyWrapper = (props: { pvName: string; connected: boolean; - value?: NType; + value?: VType; children: ReactNode; style?: object; }): JSX.Element => { const [popoverOpen, setPopoverOpen] = useState(false); - let { connected, pvName, value = null } = props; + let { connected, pvName, value } = props; let displayValue = ""; if (!connected) { @@ -28,7 +31,7 @@ export const CopyWrapper = (props: { if (!value) { displayValue = "Warning: Waiting for value"; } else { - displayValue = value.value.toString(); + displayValue = vtypeToString(value); } } @@ -48,14 +51,16 @@ export const CopyWrapper = (props: { } }; // Compose the text which should be shown on the tooltip + let time = timeOf(value); + let alarm = alarmOf(value); let toolTipText = [ displayValue, value - ? value.time - ? new Date(value.time.secondsPastEpoch * 1000) + ? time + ? new Date(time.getInstant().secondsPastEpoch * 1000) : "" : "", - value ? (value.alarm ? value.alarm.message : "") : "" + value ? (alarm ? alarm.getName() : "") : "" ] .filter((word): boolean => word !== "") .join(", "); diff --git a/src/components/Input/input.tsx b/src/components/Input/input.tsx index 603f6035..87922a1c 100644 --- a/src/components/Input/input.tsx +++ b/src/components/Input/input.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; import { connectionWrapper } from "../ConnectionWrapper/connectionWrapper"; import { writePv } from "../../hooks/useCs"; -import { NType, ntOrNullToString } from "../../ntypes"; +import { VType } from "../../vtypes/vtypes"; import classes from "./input.module.css"; +import { vtypeToString, stringToVtype } from "../../vtypes/utils"; export interface InputProps { pvName: string; @@ -32,7 +33,7 @@ interface ConnectedInputProps { interface SmartInputProps { pvName: string; - value?: NType; + value?: VType; } export const SmartInput: React.FC = ( @@ -42,10 +43,7 @@ export const SmartInput: React.FC = ( const [editing, setEditing] = useState(false); function onKeyDown(event: React.KeyboardEvent): void { if (event.key === "Enter") { - writePv(props.pvName, { - type: "NTScalar", - value: event.currentTarget.value - }); + writePv(props.pvName, stringToVtype(event.currentTarget.value)); setInputValue(""); setEditing(false); } @@ -63,11 +61,11 @@ export const SmartInput: React.FC = ( function onBlur(event: React.ChangeEvent): void { setEditing(false); /* When focus lost show PV value. */ - setInputValue(ntOrNullToString(props.value)); + setInputValue(vtypeToString(props.value)); } - if (!editing && inputValue !== ntOrNullToString(props.value)) { - setInputValue(ntOrNullToString(props.value)); + if (!editing && inputValue !== vtypeToString(props.value)) { + setInputValue(vtypeToString(props.value)); } return ( diff --git a/src/components/ProgressBar/progressBar.tsx b/src/components/ProgressBar/progressBar.tsx index 53f46f44..eca13dd6 100644 --- a/src/components/ProgressBar/progressBar.tsx +++ b/src/components/ProgressBar/progressBar.tsx @@ -1,15 +1,16 @@ import React from "react"; -import { NType, ntOrNullToNumber } from "../../ntypes"; +import { VType } from "../../vtypes/vtypes"; import classes from "./progressBar.module.css"; import { connectionWrapper } from "../ConnectionWrapper/connectionWrapper"; import { CopyWrapper } from "../CopyWrapper/copyWrapper"; import { AlarmBorder } from "../AlarmBorder/alarmBorder"; +import { vtypeOrUndefinedToNumber } from "../../vtypes/utils"; interface ProgressBarProps { connected: boolean; - value?: NType; + value?: VType; min: number; max: number; vertical?: boolean; @@ -25,7 +26,7 @@ interface ProgressBarProps { // Same as ProgressBarProps but without connected and value as these are // collected from the store interface ConnectedProgressBarProps { - value?: NType; + value?: VType; pvName: string; min: number; max: number; @@ -62,7 +63,8 @@ export const ProgressBar: React.FC = ( height: height, width: width }; - let numValue = ntOrNullToNumber(value); + // eslint-disable-next-line no-undef + let numValue = vtypeOrUndefinedToNumber(value); let onPercent = numValue < min ? 0 @@ -116,7 +118,7 @@ export const ConnectedProgressBar: React.FC< export const StandaloneProgressBar = (props: { pvName: string; - value: NType; + value: VType; connected: boolean; min: number; max: number; diff --git a/src/components/Readback/readback.test.tsx b/src/components/Readback/readback.test.tsx index f95425d0..2b0d3de5 100644 --- a/src/components/Readback/readback.test.tsx +++ b/src/components/Readback/readback.test.tsx @@ -2,13 +2,12 @@ import React from "react"; import { Readback } from "./readback"; import { shallow } from "enzyme"; import renderer from "react-test-renderer"; +import { vstringOf } from "../../vtypes/vtypes"; +import { stringToVtype } from "../../vtypes/utils"; test("snapshot matches", (): void => { const readback = renderer.create( - + ); let json = readback.toJSON(); expect(json).toMatchSnapshot(); @@ -16,10 +15,7 @@ test("snapshot matches", (): void => { it("renders a basic element", (): void => { const wrapper = shallow( - + ); expect(wrapper.text()).toEqual("hello"); }); @@ -28,7 +24,7 @@ it("applies precision to numbers", (): void => { const wrapper = shallow( ); diff --git a/src/components/Readback/readback.tsx b/src/components/Readback/readback.tsx index 24515247..626f7a77 100644 --- a/src/components/Readback/readback.tsx +++ b/src/components/Readback/readback.tsx @@ -1,20 +1,16 @@ import React from "react"; -import { - NType, - Alarm, - NO_ALARM, - ntToNumericString, - ntToString -} from "../../ntypes"; import { connectionWrapper } from "../ConnectionWrapper/connectionWrapper"; import { CopyWrapper } from "../CopyWrapper/copyWrapper"; import { AlarmBorder } from "../AlarmBorder/alarmBorder"; import classes from "./readback.module.css"; +import { VType } from "../../vtypes/vtypes"; +import { Alarm, alarmOf, AlarmSeverity } from "../../vtypes/alarm"; +import { vtypeToString } from "../../vtypes/utils"; export const Readback = (props: { connected: boolean; - value?: NType; + value?: VType; precision?: number; style?: {}; }): JSX.Element => { @@ -24,18 +20,12 @@ export const Readback = (props: { precision = undefined, style = { backgroundColor: "#383838", color: "#00bb00" } } = props; - let alarm = NO_ALARM; - if (value && value.alarm != null) { - alarm = value.alarm; - } + const alarm = alarmOf(value); let displayedValue; if (!value) { displayedValue = "Waiting for value"; - } else if (precision && precision >= 0) { - value = value as NType; - displayedValue = ntToNumericString(value, precision); } else { - displayedValue = ntToString(value); + displayedValue = vtypeToString(value, precision); } // Change text color depending on connection state or alarm @@ -44,13 +34,13 @@ export const Readback = (props: { ...style, color: "#ffffff" }; - } else if (alarm.severity === 1) { + } else if (alarm.getSeverity() === AlarmSeverity.MINOR) { // Minor alarm style = { ...style, color: "#eeee00" }; - } else if (alarm.severity === 2) { + } else if (alarm.getSeverity() === AlarmSeverity.MAJOR) { // Major alarm style = { ...style, @@ -84,7 +74,7 @@ interface ConnectedCopyReadbackProps { export const CopyReadback = (props: { pvName: string; - value: NType; + value: VType; connected: boolean; precision?: number; style?: object; @@ -114,7 +104,7 @@ interface ConnectedStandaloneReadbackProps { export const StandaloneReadback = (props: { pvName: string; - value: NType; + value: VType; connected: boolean; precision?: number; style?: object; diff --git a/src/components/SlideControl/slideControl.tsx b/src/components/SlideControl/slideControl.tsx index 60af6b2e..1b0fd591 100644 --- a/src/components/SlideControl/slideControl.tsx +++ b/src/components/SlideControl/slideControl.tsx @@ -4,15 +4,16 @@ import React, { useState } from "react"; import { ProgressBar } from "../ProgressBar/progressBar"; import { writePv } from "../../hooks/useCs"; -import { NType, ntOrNullToString } from "../../ntypes"; +import { VType } from "../../vtypes/vtypes"; import classes from "./slideControl.module.css"; import { connectionWrapper } from "../ConnectionWrapper/connectionWrapper"; +import { vtypeToString, stringToVtype } from "../../vtypes/utils"; interface SlideControlProps { pvName: string; connected: boolean; - value?: NType; + value?: VType; min: number; max: number; vertical?: boolean; @@ -57,13 +58,10 @@ export const SlideControl: React.FC = ( } function onMouseUp(event: React.MouseEvent): void { setEditing(false); - writePv(pvName, { - type: "NTScalar", - value: event.currentTarget.value - }); + writePv(pvName, stringToVtype(event.currentTarget.value)); } - let stringValue = ntOrNullToString(value); + let stringValue = vtypeToString(value); if (!editing && inputValue !== stringValue) { setInputValue(stringValue); } diff --git a/src/connection/coniql.ts b/src/connection/coniql.ts index 1fc59f2b..fcf33630 100644 --- a/src/connection/coniql.ts +++ b/src/connection/coniql.ts @@ -5,7 +5,7 @@ import { WebSocketLink } from "apollo-link-ws"; import { getMainDefinition } from "apollo-utilities"; import gql from "graphql-tag"; import { InMemoryCache, NormalizedCacheObject } from "apollo-cache-inmemory"; -import { VType, doubleOf } from "../vtypes/vtypes"; +import { VType, vdoubleOf } from "../vtypes/vtypes"; import { Connection, ConnectionChangedCallback, @@ -92,7 +92,7 @@ export class ConiqlPlugin implements Connection { } public getValue(pvName: string): VType { - return doubleOf(0); + return vdoubleOf(0); } public unsubscribe(pvName: string): void { diff --git a/src/connection/sim.ts b/src/connection/sim.ts index 61de3152..33f55c2c 100644 --- a/src/connection/sim.ts +++ b/src/connection/sim.ts @@ -6,7 +6,9 @@ import { nullConnCallback, nullValueCallback } from "./plugin"; -import { VType, doubleOf, VNumber } from "../vtypes/vtypes"; +import { VType, vdoubleOf, VNumber } from "../vtypes/vtypes"; +import { alarm } from "../vtypes/alarm"; +import { timeNow } from "../vtypes/time"; abstract class SimPv { protected onConnectionUpdate: ConnectionChangedCallback; @@ -48,7 +50,7 @@ class SinePv extends SimPv { const val = Math.sin( new Date().getSeconds() + new Date().getMilliseconds() * 0.001 ); - return { type: "NTScalarDouble", value: val }; + return vdoubleOf(val); } } @@ -73,7 +75,7 @@ class Disconnector extends SimPv { public getValue(): VType { const value = Math.random(); - return { type: "NTScalarDouble", value: value }; + return vdoubleOf(value); } } @@ -88,7 +90,7 @@ class MetaData extends SimPv { updateRate: number ) { super(pvName, onConnectionUpdate, onValueUpdate, updateRate); - this.value = doubleOf(50); + this.value = vdoubleOf(50); this.onValueUpdate(this.pvName, this.getValue()); setInterval( (): void => this.onConnectionUpdate(this.pvName, this.getConnection()), @@ -102,26 +104,12 @@ class MetaData extends SimPv { if (value instanceof VNumber) { let v = value.getValue(); alarmSeverity = v < 10 ? 2 : v > 90 ? 2 : v < 20 ? 1 : v > 80 ? 1 : 0; + this.value = vdoubleOf( + value.getValue(), + alarm(alarmSeverity, 0, ""), + timeNow() + ); } - - // Produce timestamp info - let currentTime = new Date().getTime(); - let seconds = Math.floor(currentTime / 1000); - let nanoseconds = Math.floor(currentTime % 1000); - - this.value = { - ...value, - alarm: { - severity: alarmSeverity, - status: 0, - message: "" - }, - time: { - secondsPastEpoch: seconds, - nanoseconds: nanoseconds, - userTag: 0 - } - }; } public getValue(): VType { @@ -173,9 +161,9 @@ export class SimulatorPlugin implements Connection { public subscribe(pvName: string): void { console.log(`subscribing to ${pvName}`); //eslint-disable-line no-console if (pvName.startsWith("loc://")) { - this.localPvs[pvName] = { type: "NTScalarDouble", value: 0 }; + this.localPvs[pvName] = vdoubleOf(0); this.onConnectionUpdate(pvName, { isConnected: true }); - this.onValueUpdate(pvName, { type: "NTScalarDouble", value: 0 }); + this.onValueUpdate(pvName, vdoubleOf(0)); } else if (pvName === "sim://disconnector") { this.simPvs[pvName] = new Disconnector( "sim://disconnector", @@ -219,11 +207,11 @@ export class SimulatorPlugin implements Connection { } else if (pvName.startsWith("sim://")) { this.simPvs[pvName].getValue(); } else if (pvName === "sim://random") { - return { type: "NTScalarDouble", value: Math.random() }; + return vdoubleOf(Math.random()); } else if (pvName.startsWith("meta://")) { return this.localPvs[pvName]; } - return { type: "NTScalarDouble", value: 0 }; + return vdoubleOf(0); } public unsubscribe(pvName: string): void { diff --git a/src/hooks/useCs.ts b/src/hooks/useCs.ts index ac75fabc..33f03ee5 100644 --- a/src/hooks/useCs.ts +++ b/src/hooks/useCs.ts @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { getStore } from "../redux/store"; import { SUBSCRIBE, UNSUBSCRIBE, WRITE_PV } from "../redux/actions"; import { useDispatch } from "react-redux"; -import { NType } from "../ntypes"; +import { VType } from "../vtypes/vtypes"; export function useSubscription(componentId: string, pvName: string): void { const dispatch = useDispatch(); @@ -20,7 +20,7 @@ export function useSubscription(componentId: string, pvName: string): void { }, [dispatch, componentId, pvName]); } -export function writePv(pvName: string, value: NType): void { +export function writePv(pvName: string, value: VType): void { getStore().dispatch({ type: WRITE_PV, payload: { pvName: pvName, value: value } diff --git a/src/ntypes.ts b/src/ntypes.ts deleted file mode 100644 index f13f798f..00000000 --- a/src/ntypes.ts +++ /dev/null @@ -1,118 +0,0 @@ -export type Scalar = number | string; - -export interface Alarm { - severity: number; - status: number; - message: string; -} - -export interface Time { - secondsPastEpoch: number; - nanoseconds: number; - userTag: number; -} - -export interface Display { - limitLow: number; - limitHigh: number; - description: string; - format: string; - units: string; -} - -export interface Control { - limitLow: number; - limitHigh: number; - minStep: number; -} - -export interface NTScalar { - type: string; - value: Scalar; - alarm?: Alarm; - time?: Time; - display?: Display; - control?: Control; -} - -export interface NTScalarArray { - type: string; - value: Scalar[]; - alarm?: Alarm; - time?: Time; - display?: Display; - control?: Control; -} - -/* -We could handle enums in a different way, by defining - -type Scalar: number | string | Enum; - -Then enums would be available in NTScalar and NTScalarArray. This would be -less similar to normative types. -*/ -export interface Enum { - index: number; - choices: string[]; -} - -export interface NTEnum { - type: string; - value: Enum; - descriptor?: string; - alarm?: Alarm; - time?: Time; -} - -export type NType = NTScalar | NTScalarArray | NTEnum; - -export const NO_ALARM: Alarm = { - severity: 0, - status: 0, - message: "" -}; - -export function ntToNumber(ntype: NType): number { - let value = ntype.value; - let numericValue; - if (typeof value === "number") { - numericValue = value; - } else if (typeof value === "string") { - numericValue = parseFloat(value); - } else { - numericValue = 0; - } - return numericValue; -} - -export function ntOrNullToNumber(ntype?: NType): number { - if (ntype === undefined) { - return NaN; - } else { - return ntToNumber(ntype); - } -} - -export function ntToNumericString(ntype: NType, precision = 3): string { - return ntToNumber(ntype).toFixed(precision); -} - -export function ntToString(ntype: NType): string { - let value = ntype.value; - if (typeof value === "number") { - return value.toString(); - } - if (typeof value === "string") { - return value; - } - return ""; -} - -export function ntOrNullToString(ntype?: NType): string { - if (ntype === undefined) { - return "null"; - } else { - return ntToString(ntype); - } -} diff --git a/src/vtypes/alarm.ts b/src/vtypes/alarm.ts index 7fa0cb27..9ec1d89f 100644 --- a/src/vtypes/alarm.ts +++ b/src/vtypes/alarm.ts @@ -17,6 +17,10 @@ export enum AlarmStatus { CLIENT } +export interface AlarmProvider { + getAlarm(): Alarm; +} + export abstract class Alarm { public abstract getSeverity(): AlarmSeverity; public abstract getStatus(): AlarmStatus; @@ -48,7 +52,7 @@ class IAlarm extends Alarm { } } -export const alarmOf = ( +export const alarm = ( severity: AlarmSeverity, status: AlarmStatus, name: string @@ -56,4 +60,15 @@ export const alarmOf = ( return new IAlarm(severity, status, name); }; -export const ALARM_NONE = alarmOf(AlarmSeverity.NONE, AlarmStatus.NONE, ""); +export const isAlarmProvider = (object: any): object is AlarmProvider => { + return "getAlarm" in object; +}; + +export const ALARM_NONE = alarm(AlarmSeverity.NONE, AlarmStatus.NONE, ""); + +export const alarmOf = (object: any): Alarm => { + if (object && isAlarmProvider(object)) { + return object.getAlarm(); + } + return ALARM_NONE; +}; diff --git a/src/vtypes/time.ts b/src/vtypes/time.ts index e0559aea..e5c63e8d 100644 --- a/src/vtypes/time.ts +++ b/src/vtypes/time.ts @@ -1,8 +1,12 @@ -interface Instant { +export interface Instant { secondsPastEpoch: number; nanoseconds: number; } +export interface TimeProvider { + getTime(): Time; +} + export abstract class Time { public abstract getInstant(): Instant; public abstract getUserTag(): number; @@ -30,11 +34,12 @@ class ITime extends Time { } } -export const timeOf = ( - instant: Instant, - userTag: number, - valid: boolean -): Time => new ITime(instant, userTag, valid); +export const isTimeProvider = (object: any): object is TimeProvider => { + return "getTime" in object; +}; + +export const time = (instant: Instant, userTag: number, valid: boolean): Time => + new ITime(instant, userTag, valid); export const instantNow = (): Instant => { const nowMillis = new Date().getTime(); @@ -46,5 +51,12 @@ export const instantNow = (): Instant => { }; }; export const timeNow = (): Time => { - return timeOf(instantNow(), 0, true); + return time(instantNow(), 0, true); +}; + +export const timeOf = (object: any): Time | undefined => { + if (object && isTimeProvider(object)) { + return object.getTime(); + } + return undefined; }; diff --git a/src/vtypes/utils.ts b/src/vtypes/utils.ts new file mode 100644 index 00000000..f94c5590 --- /dev/null +++ b/src/vtypes/utils.ts @@ -0,0 +1,53 @@ +import { VNumber, VType, VString, vdoubleOf, vstringOf } from "./vtypes"; +import { ALARM_NONE } from "./alarm"; +import { timeNow } from "./time"; +import { DISPLAY_NONE } from "./display"; + +export const vtypeToString = (vtype?: VType, precision?: number): string => { + if (vtype instanceof VNumber) { + if (precision) { + return vtype.getValue().toFixed(precision); + } else { + return vtype.getValue().toString(); + } + } + if (vtype instanceof VString) { + return vtype.getValue(); + } + return ""; +}; + +export function vtypeToNumber(vtype: VType): number { + let value = vtype.getValue(); + let numericValue; + if (typeof value === "number") { + numericValue = value; + } else if (typeof value === "string") { + numericValue = parseFloat(value); + } else { + numericValue = 0; + } + return numericValue; +} + +export function vtypeOrUndefinedToNumber(vtype?: VType): number { + if (vtype) { + return vtypeToNumber(vtype); + } else { + return 0; + } +} + +export const stringToVtype = ( + value: string, + alarm = ALARM_NONE, + time = timeNow(), + display = DISPLAY_NONE +): VType => { + try { + let numberValue = parseFloat(value); + return vdoubleOf(numberValue, alarm, time, display); + } catch (error) { + return vstringOf(value, alarm, time); + } +}; diff --git a/src/vtypes/vtypes.ts b/src/vtypes/vtypes.ts index 0aeca04a..d24826eb 100644 --- a/src/vtypes/vtypes.ts +++ b/src/vtypes/vtypes.ts @@ -1,10 +1,18 @@ -import { Alarm, ALARM_NONE } from "./alarm"; +import { Alarm, AlarmProvider, ALARM_NONE } from "./alarm"; import { Display, DISPLAY_NONE } from "./display"; -import { Time, timeNow } from "./time"; +import { Time, timeNow, TimeProvider } from "./time"; -export class VType {} +export abstract class VType { + public abstract getValue(): any; +} + +export abstract class Scalar implements AlarmProvider, TimeProvider { + public abstract getValue(): any; + public abstract getAlarm(): Alarm; + public abstract getTime(): Time; +} -export abstract class VNumber extends VType { +export abstract class VNumber extends Scalar { public abstract getValue(): number; } @@ -46,9 +54,43 @@ class IVDouble extends VDouble { } } -export const doubleOf = ( +export const vdoubleOf = ( double: number, alarm = ALARM_NONE, time = timeNow(), display = DISPLAY_NONE ): VDouble => new IVDouble(double, alarm, time, display); + +export abstract class VString extends Scalar { + public abstract getValue(): string; +} + +class IVString extends VString { + private value: string; + private alarm: Alarm; + private time: Time; + + public constructor(value: string, alarm: Alarm, time: Time) { + super(); + this.value = value; + this.alarm = alarm; + this.time = time; + } + + public getValue(): string { + return this.value; + } + + public getAlarm(): Alarm { + return this.alarm; + } + public getTime(): Time { + return this.time; + } +} + +export const vstringOf = ( + value: string, + alarm = ALARM_NONE, + time = timeNow() +): VString => new IVString(value, alarm, time); From 97d71f46f5f5e4c0e8facdfab0a7c3dfb769ccd0 Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Tue, 8 Oct 2019 15:57:07 +0100 Subject: [PATCH 4/4] Add some tests for vtypes. --- src/vtypes/vtypes.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/vtypes/vtypes.test.ts diff --git a/src/vtypes/vtypes.test.ts b/src/vtypes/vtypes.test.ts new file mode 100644 index 00000000..053b4a5c --- /dev/null +++ b/src/vtypes/vtypes.test.ts @@ -0,0 +1,32 @@ +import { DISPLAY_NONE, RANGE_NONE } from "./display"; +import { ALARM_NONE, AlarmSeverity, AlarmStatus } from "./alarm"; + +describe("Display", (): void => { + test("DISPLAY_NONE has zero alarm range", (): void => { + expect(DISPLAY_NONE.getAlarmRange()).toEqual(RANGE_NONE); + }); + test("DISPLAY_NONE has zero warning range", (): void => { + expect(DISPLAY_NONE.getWarningRange()).toEqual(RANGE_NONE); + }); + test("DISPLAY_NONE has zero control range", (): void => { + expect(DISPLAY_NONE.getControlRange()).toEqual(RANGE_NONE); + }); + test("DISPLAY_NONE has zero display range", (): void => { + expect(DISPLAY_NONE.getDisplayRange()).toEqual(RANGE_NONE); + }); + test("DISPLAY_NONE has no units", (): void => { + expect(DISPLAY_NONE.getUnit()).toEqual(""); + }); +}); + +describe("Alarm", (): void => { + test("ALARM_NONE has severity None", (): void => { + expect(ALARM_NONE.getSeverity()).toEqual(AlarmSeverity.NONE); + }); + test("ALARM_NONE has status None", (): void => { + expect(ALARM_NONE.getStatus()).toEqual(AlarmStatus.NONE); + }); + test("ALARM_NONE has no message", (): void => { + expect(ALARM_NONE.getName()).toEqual(""); + }); +});