-
-
Notifications
You must be signed in to change notification settings - Fork 628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support Z-Wave Long Range #6401
Changes from 18 commits
666257e
acfb9ea
5f81ca9
b4ddd89
5eb0762
fcaacb3
29ef9af
1b56690
63d142b
6005dfa
cd27d02
8f15e3a
d1e23fc
70038ab
9b679c3
9637127
8223ee9
e8f9023
02052ef
c9184ef
d00b084
15bc808
7b268b4
1bb54b2
9b5c7ba
32c0c34
25908de
e8c3134
6e52505
e55e479
f0d0dde
2e7971d
8715fce
f9585f0
fe446eb
79b1a6a
8f22fd1
a42243d
683db90
ea170ce
f226a82
1e09888
098b8d1
c58c2fa
01039de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -215,34 +215,49 @@ | |
export function parseNodeProtocolInfo( | ||
buffer: Buffer, | ||
offset: number, | ||
isLongRange: boolean = false, | ||
): NodeProtocolInfo { | ||
validatePayload(buffer.length >= offset + 3); | ||
|
||
const isListening = !!(buffer[offset] & 0b10_000_000); | ||
const isRouting = !!(buffer[offset] & 0b01_000_000); | ||
let isRouting = false; | ||
if (!isLongRange) { | ||
isRouting = !!(buffer[offset] & 0b01_000_000); | ||
} | ||
|
||
const supportedDataRates: DataRate[] = []; | ||
const maxSpeed = buffer[offset] & 0b00_011_000; | ||
const speedExtension = buffer[offset + 2] & 0b111; | ||
if (maxSpeed & 0b00_010_000) { | ||
supportedDataRates.push(40000); | ||
} | ||
if (maxSpeed & 0b00_001_000) { | ||
supportedDataRates.push(9600); | ||
} | ||
if (speedExtension & 0b001) { | ||
supportedDataRates.push(100000); | ||
} | ||
if (supportedDataRates.length === 0) { | ||
supportedDataRates.push(9600); | ||
if (isLongRange) { | ||
const speedExtension = buffer[offset + 2] & 0b111; | ||
if (speedExtension & 0b010) { | ||
supportedDataRates.push(100000); | ||
} | ||
} else { | ||
const maxSpeed = buffer[offset] & 0b00_011_000; | ||
const speedExtension = buffer[offset + 2] & 0b111; | ||
if (maxSpeed & 0b00_010_000) { | ||
supportedDataRates.push(40000); | ||
} | ||
if (maxSpeed & 0b00_001_000) { | ||
supportedDataRates.push(9600); | ||
} | ||
if (speedExtension & 0b001) { | ||
supportedDataRates.push(100000); | ||
} | ||
if (supportedDataRates.length === 0) { | ||
supportedDataRates.push(9600); | ||
} | ||
} | ||
|
||
const protocolVersion = buffer[offset] & 0b111; | ||
// BUGBUG: what's the correct protocol version here for long range? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't find this in the specs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that the NIF for Long Range lists these bits as reserved, I'd guess it's either 1 or not interesting. What do the frames received from your controller contain here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question, I'm not sure. I'll have to check. |
||
const protocolVersion = isLongRange | ||
? 0 | ||
: buffer[offset] & 0b111; | ||
|
||
const capability = buffer[offset + 1]; | ||
const optionalFunctionality = !!(capability & 0b1000_0000); | ||
const optionalFunctionality = (!isLongRange) | ||
&& !!(capability & 0b1000_0000); | ||
let isFrequentListening: FLiRS; | ||
switch (capability & 0b0110_0000) { | ||
switch (capability & (isLongRange ? 0b0100_0000 : 0b0110_0000)) { | ||
case 0b0100_0000: | ||
isFrequentListening = "1000ms"; | ||
break; | ||
|
@@ -252,21 +267,29 @@ | |
default: | ||
isFrequentListening = false; | ||
} | ||
const supportsBeaming = !!(capability & 0b0001_0000); | ||
const supportsBeaming = (!isLongRange) && !!(capability & 0b0001_0000); | ||
|
||
let nodeType: NodeType; | ||
switch (capability & 0b1010) { | ||
case 0b1000: | ||
|
||
switch ( | ||
isLongRange | ||
? (0b1_0000_0000 | (capability & 0b0010)) | ||
: (capability && 0b1010) | ||
) { | ||
case 0b0_0000_1000: | ||
case 0b1_0000_0000: | ||
nodeType = NodeType["End Node"]; | ||
break; | ||
case 0b0010: | ||
case 0b0_0000_0010: | ||
case 0b1_0000_0010: | ||
default: | ||
// BUGBUG: is Controller correct for default && isLongRange? | ||
nodeType = NodeType.Controller; | ||
break; | ||
} | ||
|
||
const hasSpecificDeviceClass = !!(capability & 0b100); | ||
const supportsSecurity = !!(capability & 0b1); | ||
const hasSpecificDeviceClass = isLongRange || !!(capability & 0b100); | ||
const supportsSecurity = isLongRange || !!(capability & 0b1); | ||
|
||
return { | ||
isListening, | ||
|
@@ -282,39 +305,61 @@ | |
}; | ||
} | ||
|
||
export function encodeNodeProtocolInfo(info: NodeProtocolInfo): Buffer { | ||
export function encodeNodeProtocolInfo( | ||
info: NodeProtocolInfo, | ||
isLongRange: boolean = false, | ||
): Buffer { | ||
const ret = Buffer.alloc(3, 0); | ||
// Byte 0 and 2 | ||
if (info.isListening) ret[0] |= 0b10_000_000; | ||
if (info.isRouting) ret[0] |= 0b01_000_000; | ||
if (info.supportedDataRates.includes(40000)) ret[0] |= 0b00_010_000; | ||
if (info.supportedDataRates.includes(9600)) ret[0] |= 0b00_001_000; | ||
if (info.supportedDataRates.includes(100000)) ret[2] |= 0b001; | ||
ret[0] |= info.protocolVersion & 0b111; | ||
if (isLongRange) { | ||
if (info.supportedDataRates.includes(100000)) ret[2] |= 0b010; | ||
} else { | ||
if (info.isRouting) ret[0] |= 0b01_000_000; | ||
if (info.supportedDataRates.includes(40000)) ret[0] |= 0b00_010_000; | ||
if (info.supportedDataRates.includes(9600)) ret[0] |= 0b00_001_000; | ||
if (info.supportedDataRates.includes(100000)) ret[2] |= 0b001; | ||
ret[0] |= info.protocolVersion & 0b111; | ||
} | ||
|
||
// Byte 1 | ||
if (info.optionalFunctionality) ret[1] |= 0b1000_0000; | ||
if (!isLongRange) { | ||
if (info.optionalFunctionality) ret[1] |= 0b1000_0000; | ||
} | ||
if (info.isFrequentListening === "1000ms") ret[1] |= 0b0100_0000; | ||
else if (info.isFrequentListening === "250ms") ret[1] |= 0b0010_0000; | ||
else if (!isLongRange && info.isFrequentListening === "250ms") { | ||
ret[1] |= 0b0010_0000; | ||
} | ||
|
||
if (!isLongRange) { | ||
if (info.supportsBeaming) ret[1] |= 0b0001_0000; | ||
if (info.supportsSecurity) ret[1] |= 0b1; | ||
} | ||
|
||
if (info.supportsBeaming) ret[1] |= 0b0001_0000; | ||
if (info.supportsSecurity) ret[1] |= 0b1; | ||
if (info.nodeType === NodeType["End Node"]) ret[1] |= 0b1000; | ||
else ret[1] |= 0b0010; // Controller | ||
if (info.nodeType === NodeType["End Node"]) { | ||
if (!isLongRange) ret[1] |= 0b1000; | ||
} else ret[1] |= 0b0010; // Controller | ||
|
||
if (info.hasSpecificDeviceClass) ret[1] |= 0b100; | ||
if (!isLongRange && info.hasSpecificDeviceClass) ret[1] |= 0b100; | ||
|
||
return ret; | ||
} | ||
|
||
export function parseNodeProtocolInfoAndDeviceClass(buffer: Buffer): { | ||
export function parseNodeProtocolInfoAndDeviceClass( | ||
buffer: Buffer, | ||
isLongRange: boolean = false, | ||
): { | ||
info: NodeProtocolInfoAndDeviceClass; | ||
bytesRead: number; | ||
} { | ||
validatePayload(buffer.length >= 5); | ||
const protocolInfo = parseNodeProtocolInfo(buffer, 0); | ||
const protocolInfo = parseNodeProtocolInfo(buffer, 0, isLongRange); | ||
let offset = 3; | ||
const basic = buffer[offset++]; | ||
// BUGBUG: 4.3.2.1.1.14 says this is omitted if the Controller field is set to 0, yet we always parse it? | ||
let basic = 0x100; // BUGBUG: is there an assume one here, or...? | ||
if (!isLongRange) { | ||
basic = buffer[offset++]; | ||
} | ||
const generic = buffer[offset++]; | ||
let specific = 0; | ||
if (protocolInfo.hasSpecificDeviceClass) { | ||
|
@@ -334,36 +379,64 @@ | |
|
||
export function encodeNodeProtocolInfoAndDeviceClass( | ||
info: NodeProtocolInfoAndDeviceClass, | ||
isLongRange: boolean = false, | ||
): Buffer { | ||
return Buffer.concat([ | ||
encodeNodeProtocolInfo({ ...info, hasSpecificDeviceClass: true }), | ||
Buffer.from([ | ||
const deviceClasses = isLongRange | ||
? Buffer.from([ | ||
info.genericDeviceClass, | ||
info.specificDeviceClass, | ||
]) | ||
: Buffer.from([ | ||
info.basicDeviceClass, | ||
info.genericDeviceClass, | ||
info.specificDeviceClass, | ||
]), | ||
]); | ||
return Buffer.concat([ | ||
encodeNodeProtocolInfo({ ...info, hasSpecificDeviceClass: true }), | ||
deviceClasses, | ||
]); | ||
} | ||
|
||
export function parseNodeInformationFrame( | ||
buffer: Buffer, | ||
isLongRange: boolean = false, | ||
): NodeInformationFrame { | ||
const { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass( | ||
let { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass( | ||
buffer, | ||
isLongRange, | ||
); | ||
const supportedCCs = parseCCList(buffer.subarray(offset)).supportedCCs; | ||
var ccListLength; | ||
if (isLongRange) { | ||
ccListLength = buffer[offset]; | ||
offset += 1; | ||
} else { | ||
ccListLength = buffer.length - offset; | ||
} | ||
const supportedCCs = | ||
parseCCList(buffer.subarray(offset, ccListLength)).supportedCCs; | ||
|
||
return { | ||
...info, | ||
supportedCCs, | ||
}; | ||
} | ||
|
||
export function encodeNodeInformationFrame(info: NodeInformationFrame): Buffer { | ||
return Buffer.concat([ | ||
encodeNodeProtocolInfoAndDeviceClass(info), | ||
encodeCCList(info.supportedCCs, []), | ||
]); | ||
export function encodeNodeInformationFrame( | ||
info: NodeInformationFrame, | ||
isLongRange: boolean = false, | ||
): Buffer { | ||
const protocolInfo = encodeNodeProtocolInfoAndDeviceClass(info, isLongRange); | ||
const ccList = encodeCCList(info.supportedCCs, []); | ||
|
||
var buffers = [protocolInfo] | ||
if (isLongRange) { | ||
const ccListLength = Buffer.allocUnsafe(1); | ||
ccListLength[0] = ccList.length; | ||
buffers.concat(ccListLength); | ||
|
||
} | ||
buffers.concat(ccList); | ||
|
||
|
||
return Buffer.concat(buffers); | ||
} | ||
|
||
export function parseNodeID( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
import { MAX_NODES, NUM_NODEMASK_BYTES } from "../consts"; | ||
import { | ||
MAX_NODES, | ||
NUM_LR_NODES_PER_SEGMENT, | ||
NUM_NODEMASK_BYTES, | ||
} from "../consts"; | ||
import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError"; | ||
import { | ||
getBitMaskWidth, | ||
|
@@ -231,15 +235,17 @@ export function encodeFloatWithScale( | |
} | ||
|
||
/** Parses a bit mask into a numeric array */ | ||
export function parseBitMask(mask: Buffer, startValue: number = 1): number[] { | ||
const numBits = mask.length * 8; | ||
|
||
export function parseBitMask( | ||
mask: Buffer, | ||
startValue: number = 1, | ||
numBits: number = mask.length * 8, | ||
): number[] { | ||
const ret: number[] = []; | ||
for (let index = 1; index <= numBits; index++) { | ||
const byteNum = (index - 1) >>> 3; // id / 8 | ||
const bitNum = (index - 1) % 8; | ||
for (let index = 0; index < numBits; index++) { | ||
const byteNum = index >>> 3; // id / 8 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally found this confusing... we do 1 based indexing in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why I did it this way. Maybe this was more in line with some texts in the specs. |
||
const bitNum = index % 8; | ||
if ((mask[byteNum] & (2 ** bitNum)) !== 0) { | ||
ret.push(index + startValue - 1); | ||
ret.push(index + startValue); | ||
} | ||
} | ||
return ret; | ||
|
@@ -266,10 +272,28 @@ export function parseNodeBitMask(mask: Buffer): number[] { | |
return parseBitMask(mask.subarray(0, NUM_NODEMASK_BYTES)); | ||
} | ||
|
||
export function parseLongRangeNodeBitMask( | ||
mask: Buffer, | ||
startValue: number, | ||
): number[] { | ||
return parseBitMask(mask, startValue); | ||
} | ||
|
||
export function encodeNodeBitMask(nodeIDs: readonly number[]): Buffer { | ||
return encodeBitMask(nodeIDs, MAX_NODES); | ||
} | ||
|
||
export function encodeLongRangeNodeBitMask( | ||
nodeIDs: readonly number[], | ||
startValue: number, | ||
): Buffer { | ||
return encodeBitMask( | ||
nodeIDs, | ||
startValue + NUM_LR_NODES_PER_SEGMENT - 1, | ||
startValue, | ||
); | ||
} | ||
|
||
/** | ||
* Parses a partial value from a "full" value. Example: | ||
* ```txt | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where did you find the information you used to update this method? I'm not sure a change is necessary, as the controller should forward a properly formatted NIF, no matter if the protocol-level frame it received is using the Long Range CC or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See
specs\Z-Wave Stack Specifications\Z-Wave and Z-Wave Long Range Network Layer Specification.pdf
. Long Range added their own Node Information Frame Command in section 6.3.1.2. The non-long-range frame (section 4.3.2.1) has 3 bytes for Basic/Generic/Specific device classes, and then a list of command classes for the remainder of the packet.The long range frame has Generic and Specific device class bytes (no basic), and then a byte that is the "command list length" (in bytes, as I recall), and then the list of command classes.
Why they felt the need to replace these fields and add the seemingly redundant length, I'm not sure.
If we don't need/want the long range command class in this change (it wasn't needed to get pairing and comms to work). I think this stuff can be backed out.