UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

820 lines 38.8 kB
import { CommandClass, Security2CCMessageEncapsulation, Security2CCNonceGet, Security2CCNonceReport, SecurityCCNonceReport, registerCCs, } from "@zwave-js/cc"; import { DeviceConfig } from "@zwave-js/config"; import { CommandClasses, MPDUHeaderType, NODE_ID_BROADCAST, NODE_ID_BROADCAST_LR, SPANState, SecurityClass, SecurityManager, SecurityManager2, ZWaveError, ZWaveErrorCodes, ZnifferLRChannelConfig, ZnifferRegion, ZnifferRegionLegacy, getChipTypeAndVersion, isLongRangeNodeId, isZWaveError, sdkVersionGte, securityClassIsS2, } from "@zwave-js/core"; import { ZnifferFrameType, ZnifferGetFrequenciesRequest, ZnifferGetFrequenciesResponse, ZnifferGetFrequencyInfoRequest, ZnifferGetFrequencyInfoResponse, ZnifferGetLRChannelConfigInfoRequest, ZnifferGetLRChannelConfigInfoResponse, ZnifferGetLRChannelConfigsRequest, ZnifferGetLRChannelConfigsResponse, ZnifferGetLRRegionsRequest, ZnifferGetLRRegionsResponse, ZnifferGetVersionRequest, ZnifferGetVersionResponse, ZnifferMessage, ZnifferMessageType, ZnifferSerialFrameType, ZnifferSerialStreamFactory, ZnifferSetBaudRateRequest, ZnifferSetBaudRateResponse, ZnifferSetFrequencyRequest, ZnifferSetFrequencyResponse, ZnifferSetLRChannelConfigRequest, ZnifferSetLRChannelConfigResponse, ZnifferStartRequest, ZnifferStartResponse, ZnifferStopRequest, ZnifferStopResponse, isZWaveSerialPortImplementation, wrapLegacySerialBinding, } from "@zwave-js/serial"; import { Bytes, TypedEventTarget, getEnumMemberName, getErrorMessage, isAbortError, isEnumMember, noop, num2hex, pick, } from "@zwave-js/shared"; import { createDeferredPromise, } from "alcalzone-shared/deferred-promise"; import { ZnifferLogger } from "../log/Zniffer.js"; import { BeamStop, LongRangeBeamStart, LongRangeMPDU, ZWaveBeamStart, ZWaveMPDU, beamToFrame, mpduToFrame, parseBeamFrame, parseMPDU, znifferDataMessageToCorruptedFrame, } from "./MPDU.js"; import { ZLFEntryKind, captureToZLFEntry, parseZLFEntry, parseZLFHeader, } from "./ZLFEntry.js"; // Force-load all Command Classes: registerCCs(); const logo = ` ███████╗ ███╗ ██╗ ██╗ ██████╗ ██████╗ ███████╗ ██████╗ ██╗ ███████╗ ╚══███╔╝ ████╗ ██║ ██║ ██╔═══╝ ██╔═══╝ ██╔════╝ ██╔══██╗ ██║ ██╔════╝ ███╔╝ ██╔██╗ ██║ ██║ ████╗ ████╗ █████╗ ██████╔╝ ██║ ███████╗ ███╔╝ ██║╚██╗██║ ██║ ██╔═╝ ██╔═╝ ██╔══╝ ██╔══██╗ ██ ██║ ╚════██║ ███████╗ ██║ ╚████║ ██║ ██║ ██║ ███████╗ ██║ ██║ ╚█████╔╝ ███████║ ╚══════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚════╝ ╚══════╝ `.trim(); function is700PlusSeries(chipType) { if (typeof chipType !== "string") { return chipType.type >= 0x07; } const chipTypeNumeric = getChipTypeAndVersion(chipType); if (chipTypeNumeric) { return chipTypeNumeric.type >= 0x07; } return false; } function tryConvertRSSI(rssi, chipType) { // For 400/500 series, the conversion is documented in the Zniffer user guide. // The conversion for 700/800 series was reverse-engineered from the Zniffer firmware. // Here, we assume that only these two representations exist: if (is700PlusSeries(chipType)) { return rssi * 4 - 256; } else { return rssi * 1.5 - 153.5; } } export class Zniffer extends TypedEventTarget { port; constructor(port, options = {}) { super(); this.port = port; // Ensure the given serial port is valid if (typeof port !== "string" && !isZWaveSerialPortImplementation(port)) { throw new ZWaveError(`The port must be a string or a valid custom serial port implementation!`, ZWaveErrorCodes.Driver_InvalidOptions); } this._options = options; this._active = false; this.parsingContext = { getHighestSecurityClass(_nodeId) { return SecurityClass.S2_AccessControl; }, hasSecurityClass(_nodeId, _securityClass) { // We don't actually know. Attempt parsing with all security classes return true; }, setSecurityClass(_nodeId, _securityClass, _granted) { // Do nothing }, getDeviceConfig(_nodeId) { // Disable strict validation while parsing certain CCs // Most of this stuff isn't actually needed, only the compat flags... return new DeviceConfig("unknown.json", false, "UNKNOWN_MANUFACTURER", 0x0000, "UNKNOWN_PRODUCT", "UNKNOWN_DESCRIPTION", [], { min: "0.0", max: "255.255", }, true, undefined, undefined, undefined, undefined, undefined, // ...down here: { disableStrictEntryControlDataValidation: true, disableStrictMeasurementValidation: true, }); }, }; } _options; /** * The host bindings used to access file system etc. */ // This is set during `init()` and should not be accessed before bindings; serialFactory; /** The serial port instance */ serial; parsingContext; _destroyPromise; get wasDestroyed() { return !!this._destroyPromise; } _chipType; _currentFrequency; /** The currently configured frequency */ get currentFrequency() { return this._currentFrequency; } _supportedFrequencies = new Map(); /** A map of supported frequency identifiers and their names */ get supportedFrequencies() { return this._supportedFrequencies; } _lrRegions = new Set(); /** A list regions that are Long Range capable */ get lrRegions() { return this._lrRegions; } _currentLRChannelConfig; /** The currently configured Long Range channel configuration */ get currentLRChannelConfig() { return this._currentLRChannelConfig; } _supportedLRChannelConfigs = new Map(); /** A map of supported Long Range channel configurations and their names */ get supportedLRChannelConfigs() { return this._supportedLRChannelConfigs; } // This is set during `start()` and should not be accessed before _logContainer; // This is set during `start()` and should not be accessed before znifferLog; /** The security managers for each node */ securityManagers = new Map(); /** A list of awaited messages */ awaitedMessages = []; _active; /** Whether the Zniffer instance is currently capturing */ get active() { return this._active; } _capturedFrames = []; /** A list of raw captured frames that can be saved to a .zlf file later */ get capturedFrames() { return this._capturedFrames.filter((f) => f.parsedFrame !== undefined) .map((f) => ({ timestamp: f.timestamp, frameData: f.frameData, parsedFrame: f.parsedFrame, })); } async init() { if (this.wasDestroyed) { throw new ZWaveError("The Zniffer was destroyed. Create a new instance and initialize that one.", ZWaveErrorCodes.Driver_Destroyed); } // Populate default bindings. This has to happen asynchronously, so the driver does not have a hard dependency // on Node.js internals this.bindings = { fs: this._options.host?.fs ?? (await import("#default_bindings/fs")).fs, serial: this._options.host?.serial ?? (await import("#default_bindings/serial")).serial, log: this._options.host?.log ?? (await import("#default_bindings/log")).log, }; // Initialize logging this._logContainer = this.bindings.log(this._options.logConfig); this.znifferLog = new ZnifferLogger(this, this._logContainer); // Open the serial port let binding; if (typeof this.port === "string") { if (typeof this.bindings.serial.createFactoryByPath === "function") { this.znifferLog.print(`opening serial port ${this.port}`); binding = await this.bindings.serial.createFactoryByPath(this.port); } else { throw new ZWaveError("This platform does not support creating a serial connection by path", ZWaveErrorCodes.Driver_Failed); } } else if (isZWaveSerialPortImplementation(this.port)) { this.znifferLog.print("opening serial port using the provided custom implementation"); this.znifferLog.print("This is deprecated! Switch to the factory pattern instead.", "warn"); binding = wrapLegacySerialBinding(this.port); } else { this.znifferLog.print("opening serial port using the provided custom factory"); binding = this.port; } this.serialFactory = new ZnifferSerialStreamFactory(binding, this._logContainer); this.serial = await this.serialFactory.createStream(); void this.handleSerialData(this.serial); this.znifferLog.print(logo, "info"); await this.stop(); const versionInfo = await this.getVersion(); this._chipType = versionInfo.chipType; this.znifferLog.print(`received Zniffer info: Chip type: ${typeof versionInfo.chipType === "string" ? versionInfo.chipType : `unknown (${num2hex(versionInfo.chipType.type)}, ${num2hex(versionInfo.chipType.version)})`} Zniffer version: ${versionInfo.majorVersion}.${versionInfo.minorVersion}`, "info"); await this.setBaudrate(0); const freqs = await this.getFrequencies(); this._currentFrequency = freqs.currentFrequency; if (is700PlusSeries(this._chipType)) { // The frequencies match the ZnifferRegion enum for (const freq of freqs.supportedFrequencies) { this._supportedFrequencies.set(freq, getEnumMemberName(ZnifferRegion, freq)); } // ... but there might be unknown regions. Query those from the Zniffer const unknownRegions = freqs.supportedFrequencies.filter((f) => !isEnumMember(ZnifferRegion, f)); for (const freq of unknownRegions) { const freqInfo = await this.getFrequencyInfo(freq); this._supportedFrequencies.set(freq, freqInfo.frequencyName); } } else if ( // Version 2.55+ supports querying the frequency names sdkVersionGte(`${versionInfo.majorVersion}.${versionInfo.minorVersion}`, "2.55")) { // The frequencies are firmware-specific. Query them from the Zniffer for (const freq of freqs.supportedFrequencies) { const freqInfo = await this.getFrequencyInfo(freq); this._supportedFrequencies.set(freq, freqInfo.frequencyName); } } else { // The frequencies match the ZnifferRegionLegacy enum, and their info cannot be queried for (const freq of freqs.supportedFrequencies) { this._supportedFrequencies.set(freq, getEnumMemberName(ZnifferRegionLegacy, freq)); } } this.znifferLog.print(`received frequency info: current frequency: ${this._supportedFrequencies.get(freqs.currentFrequency) ?? `unknown (${num2hex(freqs.currentFrequency)})`} supported frequencies: ${[...this._supportedFrequencies].map(([region, name]) => `\n · ${region.toString().padStart(2, " ")}: ${name}`).join("")}`, "info"); if (typeof this._options.defaultFrequency === "number" && freqs.currentFrequency !== this._options.defaultFrequency && this._supportedFrequencies.has(this._options.defaultFrequency)) { await this.setFrequency(this._options.defaultFrequency); } if (sdkVersionGte(`${versionInfo.majorVersion}.${versionInfo.minorVersion}`, "10.22")) { // Simplicity SDK 2024.6 added commands to query and configure LR channels for (const region of await this.getLRRegions()) { this._lrRegions.add(region); } const channels = await this.getLRChannelConfigs(); this._currentLRChannelConfig = channels.currentConfig; // The channel configs match the ZnifferLRChannelConfig enum for (const channel of channels.supportedConfigs) { this._supportedLRChannelConfigs.set(channel, getEnumMemberName(ZnifferLRChannelConfig, channel)); } // ... but there might be unknown configurations. Query those from the Zniffer const unknownConfigs = channels.supportedConfigs.filter((f) => !isEnumMember(ZnifferLRChannelConfig, f)); for (const channel of unknownConfigs) { const channelInfo = await this.getLRChannelConfigInfo(channel); this._supportedLRChannelConfigs.set(channel, channelInfo.configName); } this.znifferLog.print(`received LR channel info: current channel: ${this._supportedLRChannelConfigs.get(this._currentLRChannelConfig) ?? `unknown (${num2hex(this._currentLRChannelConfig)})`} supported channels: ${[...this._supportedLRChannelConfigs].map(([channel, name]) => `\n · ${channel.toString()}: ${name}`).join("")}`, "info"); if (this._lrRegions.has(this._currentFrequency) && typeof this._options.defaultLRChannelConfig === "number" && this._currentLRChannelConfig !== this._options.defaultLRChannelConfig && this._supportedLRChannelConfigs.has(this._options.defaultLRChannelConfig)) { await this.setLRChannelConfig(this._options.defaultLRChannelConfig); } } this.emit("ready"); } async handleSerialData(serial) { try { for await (const frame of serial.readable) { setImmediate(() => { if (frame.type === ZnifferSerialFrameType.SerialAPI) { void this.serialport_onData(frame.data); } else { // Handle discarded data? } }); } } catch (e) { if (isAbortError(e)) { return; } else if (isZWaveError(e) && e.code === ZWaveErrorCodes.Driver_Failed) { this.emit("error", e); return this.destroy(); } throw e; } } /** * Is called when the serial port has received a Zniffer frame */ async serialport_onData(data) { let msg; let bytesRead; try { ({ msg, bytesRead } = ZnifferMessage.parse(data)); } catch (e) { console.error(e); return; } if (bytesRead < data.length) { // This should not actually happen this.znifferLog.print(`Possible data loss: read only ${bytesRead} of ${data.length} bytes!`, "warn"); } if (msg.type === ZnifferMessageType.Command) { this.handleResponse(msg); } else { const dataMsg = msg; const capture = { timestamp: new Date(), rawData: data, frameData: dataMsg.payload, }; this._capturedFrames.push(capture); if (this._options.maxCapturedFrames != undefined && this._capturedFrames.length > this._options.maxCapturedFrames) { this._capturedFrames.shift(); } await this.handleDataMessage(dataMsg, capture); } } /** * Is called when a Request-type message was received */ handleResponse(msg) { // Check if we have a dynamic handler waiting for this message for (const entry of this.awaitedMessages) { if (entry.predicate(msg)) { // We do entry.handler(msg); return; } } } /** * Is called when a Request-type message was received */ async handleDataMessage(msg, capture) { try { const frame = await this.parseFrame(msg); capture.parsedFrame = frame.external; if (frame.internal instanceof ZWaveBeamStart || frame.internal instanceof LongRangeBeamStart || frame.internal instanceof BeamStop) { this.znifferLog.beam(frame.internal); this.emit("frame", frame.external, capture.frameData); return; } if (frame.internal === undefined) { // Corrupted frame, expose as a CRC error this.znifferLog.crcError(msg); this.emit("corrupted frame", frame.external, capture.frameData); return; } if (frame.internal instanceof ZWaveMPDU || frame.internal instanceof LongRangeMPDU) { this.znifferLog.mpdu(frame.internal, frame.cc); this.emit("frame", frame.external, capture.frameData); return; } } catch (e) { console.error(e); } } /** * Waits until a certain serial message is received or a timeout has elapsed. Returns the received message. * @param timeout The number of milliseconds to wait. If the timeout elapses, the returned promise will be rejected * @param predicate A predicate function to test all incoming messages. */ waitForMessage(predicate, timeout) { return new Promise((resolve, reject) => { const promise = createDeferredPromise(); const entry = { predicate, handler: (msg) => promise.resolve(msg), timeout: undefined, }; this.awaitedMessages.push(entry); const removeEntry = () => { if (entry.timeout) clearTimeout(entry.timeout); const index = this.awaitedMessages.indexOf(entry); if (index !== -1) this.awaitedMessages.splice(index, 1); }; // When the timeout elapses, remove the wait entry and reject the returned Promise entry.timeout = setTimeout(() => { removeEntry(); reject(new ZWaveError(`Received no matching message within the provided timeout!`, ZWaveErrorCodes.Controller_Timeout)); }, timeout); // When the promise is resolved, remove the wait entry and resolve the returned Promise void promise.then((cc) => { removeEntry(); resolve(cc); }); }); } async getVersion() { const req = new ZnifferGetVersionRequest(); await this.serial?.writeAsync(req.serialize()); const res = await this.waitForMessage((msg) => msg instanceof ZnifferGetVersionResponse, 1000); return pick(res, ["chipType", "majorVersion", "minorVersion"]); } async getFrequencies() { const req = new ZnifferGetFrequenciesRequest(); await this.serial?.writeAsync(req.serialize()); const res = await this.waitForMessage((msg) => msg instanceof ZnifferGetFrequenciesResponse, 1000); return pick(res, [ "currentFrequency", "supportedFrequencies", ]); } async setFrequency(frequency) { const req = new ZnifferSetFrequencyRequest({ frequency }); await this.serial?.writeAsync(req.serialize()); await this.waitForMessage((msg) => msg instanceof ZnifferSetFrequencyResponse, 1000); this._currentFrequency = frequency; } async getFrequencyInfo(frequency) { const req = new ZnifferGetFrequencyInfoRequest({ frequency }); await this.serial?.writeAsync(req.serialize()); const res = await this.waitForMessage((msg) => msg instanceof ZnifferGetFrequencyInfoResponse && msg.frequency === frequency, 1000); return pick(res, ["numChannels", "frequencyName"]); } async getLRRegions() { const req = new ZnifferGetLRRegionsRequest(); await this.serial?.writeAsync(req.serialize()); const res = await this.waitForMessage((msg) => msg instanceof ZnifferGetLRRegionsResponse, 1000); return res.regions; } async getLRChannelConfigs() { const req = new ZnifferGetLRChannelConfigsRequest(); await this.serial?.writeAsync(req.serialize()); const res = await this.waitForMessage((msg) => msg instanceof ZnifferGetLRChannelConfigsResponse, 1000); return pick(res, [ "currentConfig", "supportedConfigs", ]); } async setLRChannelConfig(channelConfig) { if (this._currentFrequency == undefined || !this._lrRegions.has(this._currentFrequency)) { throw new ZWaveError(`The LR channel configuration can only be set for LR regions!`, ZWaveErrorCodes.Controller_NotSupported); } const req = new ZnifferSetLRChannelConfigRequest({ channelConfig }); await this.serial?.writeAsync(req.serialize()); await this.waitForMessage((msg) => msg instanceof ZnifferSetLRChannelConfigResponse, 1000); this._currentLRChannelConfig = channelConfig; } async getLRChannelConfigInfo(channelConfig) { const req = new ZnifferGetLRChannelConfigInfoRequest({ channelConfig }); await this.serial?.writeAsync(req.serialize()); const res = await this.waitForMessage((msg) => msg instanceof ZnifferGetLRChannelConfigInfoResponse && msg.channelConfig === channelConfig, 1000); return pick(res, ["numChannels", "configName"]); } /** Starts the capture and discards all previously captured frames */ async start() { if (this.wasDestroyed) { throw new ZWaveError("The Zniffer is not ready or has been destroyed", ZWaveErrorCodes.Driver_NotReady); } if (this._active) return; this._capturedFrames = []; this._active = true; const req = new ZnifferStartRequest(); await this.serial?.writeAsync(req.serialize()); await this.waitForMessage((msg) => msg instanceof ZnifferStartResponse, 1000); } async stop() { if (!this._active) return; this._active = false; if (!this.serial) return; const req = new ZnifferStopRequest(); await this.serial?.writeAsync(req.serialize()); await this.waitForMessage((msg) => msg instanceof ZnifferStopResponse, 1000); } async setBaudrate(baudrate) { const req = new ZnifferSetBaudRateRequest({ baudrate }); await this.serial?.writeAsync(req.serialize()); await this.waitForMessage((msg) => msg instanceof ZnifferSetBaudRateResponse, 1000); } async getSecurityManagers(sourceNodeId) { if (this.securityManagers.has(sourceNodeId)) { return this.securityManagers.get(sourceNodeId); } // Initialize security // Set up the S0 security manager. We can only do that after the controller // interview because we need to know the controller node id. const S0Key = this._options.securityKeys?.S0_Legacy; let securityManager; if (S0Key) { // this.znifferLog.print( // "Network key for S0 configured, enabling S0 security manager...", // ); securityManager = new SecurityManager({ networkKey: S0Key, // FIXME: Track nonces separately for each destination node ownNodeId: sourceNodeId, nonceTimeout: Number.POSITIVE_INFINITY, }); // } else { // this.znifferLog.print( // "No network key for S0 configured, cannot decrypt communication from secure (S0) devices!", // "warn", // ); } let securityManager2; if (this._options.securityKeys // Only set it up if we have security keys for at least one S2 security class && Object.keys(this._options.securityKeys).some((key) => key.startsWith("S2_") && key in SecurityClass && securityClassIsS2(SecurityClass[key]))) { // this.znifferLog.print( // "At least one network key for S2 configured, enabling S2 security manager...", // ); securityManager2 = await SecurityManager2.create(); // Small hack: Zniffer does not care about S2 duplicates securityManager2.isDuplicateSinglecast = () => false; // Set up all keys for (const secClass of [ "S2_Unauthenticated", "S2_Authenticated", "S2_AccessControl", "S0_Legacy", ]) { const key = this._options.securityKeys[secClass]; if (key) { await securityManager2.setKey(SecurityClass[secClass], key); } } // } else { // this.znifferLog.print( // "No network key for S2 configured, cannot decrypt communication from secure (S2) devices!", // "warn", // ); } let securityManagerLR; if (this._options.securityKeysLongRange?.S2_AccessControl || this._options.securityKeysLongRange?.S2_Authenticated) { // this.znifferLog.print( // "At least one network key for Z-Wave Long Range configured, enabling security manager...", // ); securityManagerLR = await SecurityManager2.create(); // Small hack: Zniffer does not care about S2 duplicates securityManagerLR.isDuplicateSinglecast = () => false; // Set up all keys if (this._options.securityKeysLongRange?.S2_AccessControl) { await securityManagerLR.setKey(SecurityClass.S2_AccessControl, this._options.securityKeysLongRange.S2_AccessControl); } if (this._options.securityKeysLongRange?.S2_Authenticated) { await securityManagerLR.setKey(SecurityClass.S2_Authenticated, this._options.securityKeysLongRange.S2_Authenticated); } // } else { // this.znifferLog.print( // "No network key for Z-Wave Long Range configured, cannot decrypt Long Range communication!", // "warn", // ); } const ret = { securityManager, securityManager2, securityManagerLR, }; this.securityManagers.set(sourceNodeId, ret); return ret; } /** Clears the list of captured frames */ clearCapturedFrames() { this._capturedFrames = []; } /** * Get the captured frames in the official Zniffer application format. * @param frameFilter Optional predicate function to filter the frames included in the capture */ getCaptureAsZLFBuffer(frameFilter) { // Mimics the current Zniffer software, without using features like sessions and comments const header = new Bytes(2048).fill(0); header[0] = 0x68; // zniffer version header.writeUInt16BE(0x2312, 0x07fe); // checksum let filteredFrames = this._capturedFrames; if (frameFilter) { filteredFrames = filteredFrames.filter((f) => // Always include Zniffer-protocol frames f.parsedFrame == undefined // Apply the filter to all other frames || frameFilter({ frameData: f.frameData, parsedFrame: f.parsedFrame, timestamp: f.timestamp, })); } return Bytes.concat([ header, ...filteredFrames.map(captureToZLFEntry), ]); } /** * Saves the captured frames in a `.zlf` file that can be read by the official Zniffer application. * @param frameFilter Optional predicate function to filter the frames included in the capture */ async saveCaptureToFile(filePath, frameFilter) { await this.bindings.fs.writeFile(filePath, this.getCaptureAsZLFBuffer(frameFilter)); } /** * Terminates the Zniffer instance and closes the underlying serial connection. * Must be called under any circumstances. */ async destroy() { // Ensure this is only called once and all subsequent calls block if (this._destroyPromise) return this._destroyPromise; this._destroyPromise = createDeferredPromise(); this.znifferLog.print("Destroying Zniffer instance..."); if (this._active) { await this.stop().catch(noop); } if (this.serial != undefined) { // Avoid spewing errors if the port was in the middle of receiving something if (this.serial.isOpen) await this.serial.close(); this.serial = undefined; } this.znifferLog.print("Zniffer instance destroyed"); // destroy loggers as the very last thing this._logContainer.destroy(); this._destroyPromise.resolve(); } /** * Loads captured frames from a `.zlf` file that was written by the official Zniffer application or Z-Wave JS. */ async loadCaptureFromFile(filePath) { const buffer = await this.bindings.fs.readFile(filePath); await this.loadCaptureFromBuffer(buffer); } /** * Load captured frames from a buffer */ async loadCaptureFromBuffer(buffer) { // Parse and validate header let { bytesRead: offset } = parseZLFHeader(buffer); this.clearCapturedFrames(); let accumulator; while (offset < buffer.length) { const { bytesRead, complete, accumulator: newAccumulator, entries, } = parseZLFEntry(buffer, offset, accumulator); // console.log( // `parsing offset ${num2hex(offset)}, len ${bytesRead} (${ // num2hex(bytesRead) // }) - timestamp: ${ // (entries[0] as any)?.capture?.timestamp.toISOString() // }`, // ); // Avoid infinite loops if (bytesRead <= 0) break; offset += bytesRead; accumulator = complete ? undefined : newAccumulator; let index = 0; for (const entry of entries) { // Skip other entry types for now // We may want to make use of the network key entries in the future if (entry.kind !== ZLFEntryKind.Zniffer) continue; try { if (entry.type === ZnifferMessageType.Data) { entry.capture.parsedFrame = // FIXME: Figure out which values the Zniffer application actually stores // as RSSI. Both the attempted conversion and the raw values seem wrong. (await this.parseFrame(entry.msg, false)).external; this._capturedFrames.push(entry.capture); } } catch (e) { console.warn(`Failed to parse entry #${index} at offset ${num2hex(offset - bytesRead)}:`, getErrorMessage(e, true)); } index++; } } } async parseFrame(msg, convertRSSI = this._options.convertRSSI ?? false) { let convertedRSSI; if (convertRSSI && this._chipType) { convertedRSSI = tryConvertRSSI(msg.rssiRaw, this._chipType); } // Short-circuit if we're dealing with beam frames if (msg.frameType === ZnifferFrameType.BeamStart || msg.frameType === ZnifferFrameType.BeamStop) { const beam = parseBeamFrame(msg); beam.frameInfo.rssi = convertedRSSI; return { internal: beam, external: beamToFrame(beam), }; } // Only handle messages with a valid checksum, expose the others as CRC errors if (!msg.checksumOK) { return { internal: undefined, external: znifferDataMessageToCorruptedFrame(msg), }; } const mpdu = parseMPDU(msg); mpdu.frameInfo.rssi = convertedRSSI; // Try to decode the CC while assuming the role of the receiver let destSecurityManager; let destSecurityManager2; let destSecurityManagerLR; // Only frames with a destination node id contains something that requires access to the own node ID let destNodeId = 0xff; let cc; // FIXME: Cache data => parsed CC, so we can understand re-transmitted S2 frames if (mpdu.payload.length > 0 && mpdu.headerType !== MPDUHeaderType.Acknowledgement) { if ("destinationNodeId" in mpdu) { destNodeId = mpdu.destinationNodeId; ({ securityManager: destSecurityManager, securityManager2: destSecurityManager2, securityManagerLR: destSecurityManagerLR, } = await this.getSecurityManagers(mpdu.destinationNodeId)); } // TODO: Support parsing multicast S2 frames const frameType = mpdu.headerType === MPDUHeaderType.Multicast ? "multicast" : (destNodeId === NODE_ID_BROADCAST || destNodeId === NODE_ID_BROADCAST_LR) ? "broadcast" : "singlecast"; try { cc = await CommandClass.parse(mpdu.payload, { homeId: mpdu.homeId, ownNodeId: destNodeId, sourceNodeId: mpdu.sourceNodeId, frameType, securityManager: destSecurityManager, securityManager2: destSecurityManager2, securityManagerLR: destSecurityManagerLR, ...this.parsingContext, }); } catch (e) { // Ignore console.error(e.stack); } } // Update the security managers when nonces are exchanged, so we can // decrypt the communication if (cc?.ccId === CommandClasses["Security 2"]) { const securityManagers = await this.getSecurityManagers(mpdu.sourceNodeId); const isLR = isLongRangeNodeId(mpdu.sourceNodeId) || isLongRangeNodeId(destNodeId); const senderSecurityManager = isLR ? securityManagers.securityManagerLR : securityManagers.securityManager2; const destSecurityManager = isLR ? destSecurityManagerLR : destSecurityManager2; if (senderSecurityManager && destSecurityManager) { if (cc instanceof Security2CCNonceGet) { // Nonce Get -> all nonces are now invalid senderSecurityManager.deleteNonce(destNodeId); destSecurityManager.deleteNonce(mpdu.sourceNodeId); } else if (cc instanceof Security2CCNonceReport && cc.SOS) { // Nonce Report (SOS) -> We only know the receiver's nonce senderSecurityManager.setSPANState(destNodeId, { type: SPANState.LocalEI, receiverEI: cc.receiverEI, }); destSecurityManager.storeRemoteEI(mpdu.sourceNodeId, cc.receiverEI); } else if (cc instanceof Security2CCMessageEncapsulation) { const senderEI = cc.getSenderEI(); if (senderEI) { // The receiver should now have a valid SPAN state, since decoding the S2 CC updates it. // The security manager for the sender however, does not. Therefore, update it manually, // if the receiver SPAN is indeed valid. const receiverSPANState = destSecurityManager .getSPANState(mpdu.sourceNodeId); if (receiverSPANState.type === SPANState.SPAN) { senderSecurityManager.setSPANState(destNodeId, receiverSPANState); } } } } } else if (cc?.ccId === CommandClasses.Security && cc instanceof SecurityCCNonceReport) { const senderSecurityManager = (await this.getSecurityManagers(mpdu.sourceNodeId)) .securityManager; const destSecurityManager = (await this.getSecurityManagers(destNodeId)) .securityManager; if (senderSecurityManager && destSecurityManager) { // Both nodes have a shared nonce now senderSecurityManager.setNonce({ issuer: mpdu.sourceNodeId, nonceId: senderSecurityManager.getNonceId(cc.nonce), }, { nonce: cc.nonce, receiver: destNodeId, }, { free: true }); destSecurityManager.setNonce({ issuer: mpdu.sourceNodeId, nonceId: senderSecurityManager.getNonceId(cc.nonce), }, { nonce: cc.nonce, receiver: destNodeId, }, { free: true }); } } return { internal: mpdu, cc, external: mpduToFrame(mpdu, cc), }; } } //# sourceMappingURL=Zniffer.js.map