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/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/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 c2845096..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 { NType } from "../ntypes"; +import { VType, vdoubleOf } 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 vdoubleOf(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..33f55c2c 100644 --- a/src/connection/sim.ts +++ b/src/connection/sim.ts @@ -6,14 +6,16 @@ import { nullConnCallback, nullValueCallback } from "./plugin"; -import { NType } from "../ntypes"; +import { VType, vdoubleOf, VNumber } from "../vtypes/vtypes"; +import { alarm } from "../vtypes/alarm"; +import { timeNow } from "../vtypes/time"; 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,11 +46,11 @@ class SinePv extends SimPv { this.updateRate ); } - public getValue(): NType { + public getValue(): VType { const val = Math.sin( new Date().getSeconds() + new Date().getMilliseconds() * 0.001 ); - return { type: "NTScalarDouble", value: val }; + return vdoubleOf(val); } } @@ -71,14 +73,14 @@ class Disconnector extends SimPv { return { isConnected: randomBool }; } - public getValue(): NType { + public getValue(): VType { const value = Math.random(); - return { type: "NTScalarDouble", value: value }; + return vdoubleOf(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 +90,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 = vdoubleOf(50); this.onValueUpdate(this.pvName, this.getValue()); setInterval( (): void => this.onConnectionUpdate(this.pvName, this.getConnection()), @@ -108,40 +98,21 @@ 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; - - // 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 - } - }; + 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; + this.value = vdoubleOf( + value.getValue(), + alarm(alarmSeverity, 0, ""), + timeNow() + ); + } } - public getValue(): NType { + public getValue(): VType { return this.value; } } @@ -151,7 +122,7 @@ interface SimCache { } interface ValueCache { - [pvName: string]: NType; + [pvName: string]: VType; } interface MetaCache { @@ -190,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,7 +190,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,17 +201,17 @@ 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://")) { 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/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 new file mode 100644 index 00000000..9ec1d89f --- /dev/null +++ b/src/vtypes/alarm.ts @@ -0,0 +1,74 @@ +export enum AlarmSeverity { + NONE, + MINOR, + MAJOR, + INVALID, + UNDEFINED +} + +export enum AlarmStatus { + NONE, + DEVICE, + DRIVER, + RECORD, + DB, + CONF, + UNDEFINED, + CLIENT +} + +export interface AlarmProvider { + getAlarm(): Alarm; +} + +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 alarm = ( + severity: AlarmSeverity, + status: AlarmStatus, + name: string +): Alarm => { + return new IAlarm(severity, status, name); +}; + +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/display.ts b/src/vtypes/display.ts new file mode 100644 index 00000000..b8dfac54 --- /dev/null +++ b/src/vtypes/display.ts @@ -0,0 +1,72 @@ +/* Currently missing NumberFormat. */ +export interface Range { + min: number; + max: number; +} + +export const RANGE_NONE: Range = { + min: 0, + max: 0 +}; + +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); + +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 new file mode 100644 index 00000000..e5c63e8d --- /dev/null +++ b/src/vtypes/time.ts @@ -0,0 +1,62 @@ +export interface Instant { + secondsPastEpoch: number; + nanoseconds: number; +} + +export interface TimeProvider { + getTime(): Time; +} + +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 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(); + const secs = nowMillis / 1000; + const nanos = (nowMillis % 1000) * 1e6; + return { + secondsPastEpoch: secs, + nanoseconds: nanos + }; +}; +export const timeNow = (): Time => { + 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.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(""); + }); +}); diff --git a/src/vtypes/vtypes.ts b/src/vtypes/vtypes.ts new file mode 100644 index 00000000..d24826eb --- /dev/null +++ b/src/vtypes/vtypes.ts @@ -0,0 +1,96 @@ +import { Alarm, AlarmProvider, ALARM_NONE } from "./alarm"; +import { Display, DISPLAY_NONE } from "./display"; +import { Time, timeNow, TimeProvider } from "./time"; + +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 Scalar { + public abstract getValue(): number; +} + +export abstract class VDouble extends VNumber { + 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 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);