UNPKG

netvar

Version:

Communicate to your codeSys plc over Network Variable Lists easily

377 lines (337 loc) 11.1 kB
import { createSocket } from 'dgram' import * as t from './types' export * as t from './types' export type Options = { listId: number // todo change to unknown onChange?: (name: string, value: any) => void // eslint-disable-line cyclic?: boolean cycleInterval?: number packed?: boolean } interface ClientOptions { /// Port to listen to port?: number /// Optional port to use when sending data /// (if not specified, `port` is used to send data as well) send_port?: number /// If true, sent and received packages are printed to the terminal debug?: boolean } type OnMessage = (varId: number, value: Buffer) => void type ListenerList = { listId: number; cb: OnMessage }[] /// endpoint: default value '255.255.255.255' export const client = (endpoint: string = '255.255.255.255', clientopts?: ClientOptions) => { const listeners: ListenerList = [] const port = clientopts?.port || 1202 const write_port = clientopts?.send_port || port const debug = clientopts?.debug || false const socket = createSocket('udp4', (msg) => { if (msg.length < 20) { return } const data = msg.toString('hex') const varId = parseInt(data.substring(18, 22), 16) const listId = parseInt(data.substring(16, 18), 16) if (debug) { console.log(`RECV (listId: ${listId}, from ${endpoint}:${write_port}): ${data}`) } listeners.filter((l) => l.listId == listId).forEach((l) => l.cb(varId, msg.subarray(20))) }) socket.bind(port) const mkValue = (def: t.Types): { data: string; lng: number } => { const out = Buffer.alloc(250) let lng = 0 switch (def.type) { case 'BOOL': return { data: def.value ? '01' : '00', lng: 1 } case 'BYTE': lng = out.writeInt8(def.value) break case 'WORD': lng = out.writeUInt16LE(def.value) break case 'DWORD': lng = out.writeUInt32LE(def.value) case 'TIME': lng = out.writeInt32LE(def.value) break case 'REAL': lng = out.writeFloatLE(def.value) break case 'LREAL': lng = out.writeDoubleLE(def.value) break case 'STRING': lng = out.write(def.value, 'ascii') lng = out.writeInt8(0, lng) break case 'WSTRING': lng = out.write(def.value, 'utf16le') lng = out.writeInt16LE(0, lng) break } return { data: out.subarray(0, lng).toString('hex'), lng, } } const mkLng = (lng: number): string => { const lngBuf = Buffer.alloc(2) lngBuf.writeUInt16LE(lng + 20) // 20 is for the header return lngBuf.toString('hex') } const d2h = (d: number, l: number) => { let bn = BigInt(d) let pos = true if (bn < 0) { pos = false bn = bitnot(bn) } let hex = bn.toString(16) if (hex.length % 2) { hex = '0' + hex } if (pos && 0x80 & parseInt(hex.slice(0, 2), 16)) { hex = '00' + hex } return (hex.length % 2 ? '0' + hex : hex).padEnd(l, '0') } const bitnot = (bn: bigint) => { bn = BigInt(-bn) let bin = bn.toString(2) let prefix = '' while (bin.length % 8) { bin = '0' + bin } if ('1' === bin[0] && -1 !== bin.slice(1).indexOf('1')) { prefix = '11111111' } bin = bin .split('') .map(function (i) { return '0' === i ? '1' : '0' }) .join('') return BigInt('0b' + prefix + bin) + BigInt(1) } type Return<T extends { [key: string]: t.Types }> = { set: <K extends keyof T>(name: K, value: T[K]['value']) => boolean setMore: (set: { [K in keyof T]?: T[K]['value'] }) => boolean get: <K extends keyof T>(name: K) => T[K]['value'] | undefined definition: string dispose: () => void } const list = <T extends { [k: string]: t.Types }>(options: Options, vars: T): Return<T> => { const { listId, onChange, cyclic, cycleInterval, packed } = options const nodeId = '002d5333' const listIdStr = d2h(listId, 4) let packedSendCounter = 0 const write_state: T = JSON.parse(JSON.stringify(vars)) //clone to save write state separately const sortedIdx = Object.entries(write_state) .sort((a, b) => a[1].idx - b[1].idx) .map(([name, _]) => name) let state = { ...vars } let cycleIntervalTimer: NodeJS.Timeout | undefined = undefined if (cyclic) { const interval = cycleInterval || 1000 cycleIntervalTimer = setInterval( () => (packed ? sendPacked(write_state) : send(write_state)), interval, ) } const getVarName = (idx: number): keyof T | undefined => { return Object.entries(vars) .filter(([, v]) => v.idx === idx) .map(([key]) => key) .shift() } const getNextSendCounter: Record<keyof T, number> = Object.keys(state).reduce( (acc, n) => ({ ...acc, [n]: -1 }), {} as Record<keyof T, number>, ) const getCounter = (key: keyof T) => { return getNextSendCounter[key]++ } const getPackedSendCounter = (): number => { packedSendCounter += 1 if (packedSendCounter > 65535) packedSendCounter = 0 return packedSendCounter } const sendPacked = (write_state: T) => { const counter = d2h(getPackedSendCounter(), 4) const vars = sortedIdx.map((name) => { return mkValue(write_state[name]) }) const lng = d2h(vars.reduce((sum, current) => sum + current.lng, 0) + 20, 4) //add 20 bytes for the header const data = vars.map((current, _lng) => current.data).join('') const items = d2h(vars.length, 4) const cmdStr = `${nodeId}00000000${listIdStr}0000${items}${lng}${counter}0000${data}` const cmd = Buffer.from(cmdStr, 'hex') if (debug) { console.log(`SEND (${endpoint}:${write_port}): ${cmdStr}`) } socket.send(cmd, write_port, endpoint) } const send = (send: Partial<T>) => { Object.entries(send) .filter((toSend): toSend is [string, t.Types] => true) .map(([name, toSend]) => { const { data, lng } = mkValue(toSend) return { idx: d2h(toSend.idx, 2), counter: d2h(getCounter(name), 2), data, lng: mkLng(lng), } }) .map(({ idx, lng, counter, data }) => { const str = `${nodeId}00000000${listIdStr}${idx}0100${lng}${counter}0000${data}` if (debug) { console.log(`SEND (${endpoint}:${write_port}): ${str}`) } return Buffer.from(str, 'hex') }) .forEach((cmd) => { socket.send(cmd, write_port, endpoint) }) } const readIntoVar = (varName: string, data: Buffer, offset: number): number => { let bytesRead = 0 //TODO - maybe improve error handling at some point try { const selVar = state[varName] const oldValue = selVar.value switch (selVar.type) { case 'BOOL': selVar.value = data.readInt8(offset) !== 0 bytesRead = 1 break case 'BYTE': selVar.value = data.readInt8(offset) bytesRead = 1 break case 'WORD': selVar.value = data.readInt16LE(offset) bytesRead = 2 break case 'DWORD': selVar.value = data.readInt32LE(offset) bytesRead = 4 break case 'STRING': { const strdata = data.slice(offset) const length = strdata.findIndex((c) => c === 0) selVar.value = strdata.toString('ascii', offset, length === -1 ? undefined : length) bytesRead = length === -1 ? 0 : length break } case 'WSTRING': { const strdata = data.slice(offset) const length = strdata.findIndex((c) => c === 0) selVar.value = strdata.toString('utf16le', offset, length === -1 ? undefined : length) bytesRead = length === -1 ? 0 : length break } case 'TIME': selVar.value = data.readInt32LE(offset) bytesRead = 4 break case 'REAL': selVar.value = data.readFloatLE(offset) bytesRead = 4 break case 'LREAL': selVar.value = data.readDoubleLE(offset) bytesRead = 8 break default: { //selVar.value = data.readInt8() } } if (oldValue !== selVar.value && onChange) { onChange(`${varName}`, selVar.value) } } catch {} return bytesRead } const onMessage = (varId: number, data: Buffer) => { if (varId === 0) { let offset = 0 sortedIdx.forEach((name) => { if (name) { offset += readIntoVar(name, data, offset) } }) } else { const varName = getVarName(varId) if (typeof varName === 'string') { readIntoVar(varName, data, 0) } } } listeners.push({ listId, cb: onMessage }) const definition = `<GVL> <Declarations><![CDATA[VAR_GLOBAL ${Object.entries(state) .sort((a, b) => a[1].idx - b[1].idx) .map(([name, def]) => ` ${name}: ${def.type};`) .join('\n')} END_VAR]]></Declarations> <NetvarSettings Protocol="UDP"> <ListIdentifier>${listId}</ListIdentifier> <Pack>False</Pack> <Checksum>False</Checksum> <Acknowledge>False</Acknowledge> <CyclicTransmission>${options.cyclic ? 'True' : 'False'}</CyclicTransmission> <TransmissionOnChange>True</TransmissionOnChange> <TransmissionOnEvent>False</TransmissionOnEvent> <Interval>T#${options.cycleInterval || 9000000}ms</Interval> <MinGap>T#1ms</MinGap> <EventVariable> </EventVariable> <ProtocolSettings> <ProtocolSetting Name="Broadcast Adr." Value="${endpoint}"/> <ProtocolSetting Name="Port" Value="${port}"/> </ProtocolSettings> </NetvarSettings> </GVL>` return { set: (name, value): boolean => { if (name in state) { write_state[name].value = value packed ? sendPacked(write_state) : send({ [name]: write_state[name] } as any as Partial<T>) // eslint-disable-line return true } return false }, setMore: (set): boolean => { try { state = Object.entries(set).reduce( (acc, [name, value]) => ({ ...acc, [name]: { ...acc[name], value } }), state, ) const newSet = Object.entries(set).reduce( (acc, [name, value]) => ({ ...acc, [name]: { ...state[name], value }, }), {}, ) packed ? sendPacked(write_state) : send(newSet) return true } catch { return false } }, get: (name) => (name in state ? state[name].value : undefined), definition, dispose: () => cycleIntervalTimer && clearInterval(cycleIntervalTimer), } } return { openList: list, } }