UNPKG

netvar

Version:

Communicate to your codeSys plc over Network Variable Lists easily

327 lines (326 loc) 13.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.client = exports.t = void 0; const dgram_1 = require("dgram"); exports.t = __importStar(require("./types")); /// endpoint: default value '255.255.255.255' const client = (endpoint = '255.255.255.255', clientopts) => { const listeners = []; const port = (clientopts === null || clientopts === void 0 ? void 0 : clientopts.port) || 1202; const write_port = (clientopts === null || clientopts === void 0 ? void 0 : clientopts.send_port) || port; const debug = (clientopts === null || clientopts === void 0 ? void 0 : clientopts.debug) || false; const socket = (0, dgram_1.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) => { 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) => { const lngBuf = Buffer.alloc(2); lngBuf.writeUInt16LE(lng + 20); // 20 is for the header return lngBuf.toString('hex'); }; const d2h = (d, l) => { 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) => { 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); }; const list = (options, vars) => { const { listId, onChange, cyclic, cycleInterval, packed } = options; const nodeId = '002d5333'; const listIdStr = d2h(listId, 4); let packedSendCounter = 0; const write_state = 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 = Object.assign({}, vars); let cycleIntervalTimer = undefined; if (cyclic) { const interval = cycleInterval || 1000; cycleIntervalTimer = setInterval(() => (packed ? sendPacked(write_state) : send(write_state)), interval); } const getVarName = (idx) => { return Object.entries(vars) .filter(([, v]) => v.idx === idx) .map(([key]) => key) .shift(); }; const getNextSendCounter = Object.keys(state).reduce((acc, n) => (Object.assign(Object.assign({}, acc), { [n]: -1 })), {}); const getCounter = (key) => { return getNextSendCounter[key]++; }; const getPackedSendCounter = () => { packedSendCounter += 1; if (packedSendCounter > 65535) packedSendCounter = 0; return packedSendCounter; }; const sendPacked = (write_state) => { 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) => { Object.entries(send) .filter((toSend) => 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, data, offset) => { 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 (_a) { } return bytesRead; }; const onMessage = (varId, data) => { 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) => { if (name in state) { write_state[name].value = value; packed ? sendPacked(write_state) : send({ [name]: write_state[name] }); // eslint-disable-line return true; } return false; }, setMore: (set) => { try { state = Object.entries(set).reduce((acc, [name, value]) => (Object.assign(Object.assign({}, acc), { [name]: Object.assign(Object.assign({}, acc[name]), { value }) })), state); const newSet = Object.entries(set).reduce((acc, [name, value]) => (Object.assign(Object.assign({}, acc), { [name]: Object.assign(Object.assign({}, state[name]), { value }) })), {}); packed ? sendPacked(write_state) : send(newSet); return true; } catch (_a) { return false; } }, get: (name) => (name in state ? state[name].value : undefined), definition, dispose: () => cycleIntervalTimer && clearInterval(cycleIntervalTimer), }; }; return { openList: list, }; }; exports.client = client;