inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
403 lines (373 loc) • 12.1 kB
text/typescript
import {
CommandClass,
isCommandClassContainer,
Security2CCMessageEncapsulation,
Security2CCNonceGet,
Security2CCNonceReport,
} from "@zwave-js/cc";
import {
SecurityCCCommandEncapsulation,
SecurityCCNonceGet,
SecurityCCNonceReport,
} from "@zwave-js/cc/SecurityCC";
import {
SendCommandOptions,
SPANState,
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core";
import type { Message } from "@zwave-js/serial";
import {
createDeferredPromise,
DeferredPromise,
} from "alcalzone-shared/deferred-promise";
import {
isSendData,
isTransmitReport,
} from "../serialapi/transport/SendDataShared";
import type { Driver } from "./Driver";
import { sendDataErrorToZWaveError } from "./StateMachineShared";
import type { MessageGenerator } from "./Transaction";
export type MessageGeneratorImplementation = (
/** A reference to the driver */
driver: Driver,
/** The "primary" message */
message: Message,
/**
* A hook to get notified about each sent message and the result of the Serial API call
* without waiting for the message generator to finish completely.
*/
onMessageSent: (msg: Message, result: Message | undefined) => void,
/** Can be used to extend the timeout waiting for a response from a node to the sent message */
additionalCommandTimeoutMs?: number,
) => AsyncGenerator<Message, Message, Message>;
export async function waitForNodeUpdate<T extends Message>(
driver: Driver,
msg: Message,
timeoutMs: number,
): Promise<T> {
try {
return await driver.waitForMessage<T>((received) => {
return msg.isExpectedNodeUpdate(received);
}, timeoutMs);
} catch (e) {
throw new ZWaveError(
`Timed out while waiting for a response from the node`,
ZWaveErrorCodes.Controller_NodeTimeout,
);
}
}
/** A simple message generator that simply sends a message, waits for the ACK (and the response if one is expected) */
export const simpleMessageGenerator: MessageGeneratorImplementation =
async function* (
driver,
msg,
onMessageSent,
additionalCommandTimeoutMs = 0,
) {
// Pass this message to the send thread and wait for it to be sent
let result: Message;
let commandTimeMs: number;
try {
// The yield can throw and must be handled here
result = yield msg;
// Figure out how long the message took to be handled
msg.markAsCompleted();
commandTimeMs = Math.ceil(msg.rtt! / 1e6);
onMessageSent(msg, result);
} catch (e) {
msg.markAsCompleted();
throw e;
}
// If the message was sent to a node and came back with a NOK callback,
// we want to inspect the callback, for example to look at TX statistics
// or update the node status.
//
// We now need to throw because the callback was passed through so we could inspect it.
if (isTransmitReport(result) && !result.isOK()) {
// Throw the message in order to short-circuit all possible generators
throw result;
}
// If the sent message expects an update from the node, wait for it
if (msg.expectsNodeUpdate()) {
// CommandTime is measured by the application
// ReportTime timeout SHOULD be set to CommandTime + 1 second.
const timeout =
commandTimeMs +
driver.options.timeouts.report +
additionalCommandTimeoutMs;
return waitForNodeUpdate(driver, msg, timeout);
}
return result;
};
/** A simple (internal) generator that simply sends a command, and optionally returns the response command */
async function* sendCommandGenerator<
TResponse extends CommandClass = CommandClass,
>(
driver: Driver,
command: CommandClass,
onMessageSent: (msg: Message, result: Message | undefined) => void,
options: SendCommandOptions,
) {
const msg = driver.createSendDataMessage(command, options);
const resp = yield* simpleMessageGenerator(driver, msg, onMessageSent);
if (resp && isCommandClassContainer(resp)) {
driver.unwrapCommands(resp);
return resp.command as TResponse;
}
}
/** A message generator for security encapsulated messages (S0) */
export const secureMessageGeneratorS0: MessageGeneratorImplementation =
async function* (driver, msg, onMessageSent) {
if (!isSendData(msg)) {
throw new ZWaveError(
"Cannot use the S0 message generator for a command that's not a SendData message!",
ZWaveErrorCodes.Argument_Invalid,
);
} else if (typeof msg.command.nodeId !== "number") {
throw new ZWaveError(
"Cannot use the S0 message generator for multicast commands!",
ZWaveErrorCodes.Argument_Invalid,
);
} else if (!(msg.command instanceof SecurityCCCommandEncapsulation)) {
throw new ZWaveError(
"The S0 message generator can only be used for Security S0 command encapsulation!",
ZWaveErrorCodes.Argument_Invalid,
);
}
// Step 1: Acquire a nonce
const secMan = driver.securityManager!;
const nodeId = msg.command.nodeId;
let additionalTimeoutMs: number | undefined;
// Try to get a free nonce before requesting a new one
let nonce: Buffer | undefined = secMan.getFreeNonce(nodeId);
if (!nonce) {
// No free nonce, request a new one
const cc = new SecurityCCNonceGet(driver, {
nodeId: nodeId,
endpoint: msg.command.endpointIndex,
});
const nonceResp =
yield* sendCommandGenerator<SecurityCCNonceReport>(
driver,
cc,
(msg, result) => {
additionalTimeoutMs = Math.ceil(msg.rtt! / 1e6);
onMessageSent(msg, result);
},
{
// Only try getting a nonce once
maxSendAttempts: 1,
},
);
if (!nonceResp) {
throw new ZWaveError(
"No nonce received from the node, cannot send secure command!",
ZWaveErrorCodes.SecurityCC_NoNonce,
);
}
nonce = nonceResp.nonce;
}
msg.command.nonce = nonce;
// Now send the actual secure command
return yield* simpleMessageGenerator(
driver,
msg,
onMessageSent,
additionalTimeoutMs,
);
};
/** A message generator for security encapsulated messages (S2) */
export const secureMessageGeneratorS2: MessageGeneratorImplementation =
async function* (driver, msg, onMessageSent) {
if (!isSendData(msg)) {
throw new ZWaveError(
"Cannot use the S2 message generator for a command that's not a SendData message!",
ZWaveErrorCodes.Argument_Invalid,
);
} else if (typeof msg.command.nodeId !== "number") {
throw new ZWaveError(
"Cannot use the S2 message generator for multicast commands!", // (yet)
ZWaveErrorCodes.Argument_Invalid,
);
} else if (!(msg.command instanceof Security2CCMessageEncapsulation)) {
throw new ZWaveError(
"The S2 message generator can only be used for Security S2 command encapsulation!",
ZWaveErrorCodes.Argument_Invalid,
);
}
const secMan = driver.securityManager2!;
const nodeId = msg.command.nodeId;
const spanState = secMan.getSPANState(nodeId);
let additionalTimeoutMs: number | undefined;
if (
spanState.type === SPANState.None ||
spanState.type === SPANState.LocalEI
) {
// Request a new nonce
// No free nonce, request a new one
const cc = new Security2CCNonceGet(driver, {
nodeId: nodeId,
endpoint: msg.command.endpointIndex,
});
const nonceResp =
yield* sendCommandGenerator<Security2CCNonceReport>(
driver,
cc,
(msg, result) => {
additionalTimeoutMs = Math.ceil(msg.rtt! / 1e6);
onMessageSent(msg, result);
},
{
// Only try getting a nonce once
maxSendAttempts: 1,
},
);
if (!nonceResp) {
throw new ZWaveError(
"No nonce received from the node, cannot send secure command!",
ZWaveErrorCodes.Security2CC_NoSPAN,
);
}
// Storing the nonce is not necessary, this will be done automatically when the nonce is received
}
// Now send the actual secure command
let response = yield* simpleMessageGenerator(
driver,
msg,
onMessageSent,
additionalTimeoutMs,
);
if (
isCommandClassContainer(response) &&
response.command instanceof Security2CCNonceReport
) {
const command = response.command;
if (command.SOS && command.receiverEI) {
// The node couldn't decrypt the last command we sent it. Invalidate
// the shared SPAN, since it did the same
secMan.storeRemoteEI(nodeId, command.receiverEI);
}
driver.controllerLog.logNode(nodeId, {
message: `failed to decode the message, retrying with SPAN extension...`,
direction: "none",
});
// Prepare the message for re-transmission
msg.callbackId = undefined;
msg.command.unsetSequenceNumber();
// Send the message again
response = yield* simpleMessageGenerator(
driver,
msg,
onMessageSent,
additionalTimeoutMs,
);
if (
isCommandClassContainer(response) &&
response.command instanceof Security2CCNonceReport
) {
// No dice
driver.controllerLog.logNode(nodeId, {
message: `failed to decode the message after re-transmission with SPAN extension, dropping the message.`,
direction: "none",
level: "warn",
});
throw new ZWaveError(
"The node failed to decode the message.",
ZWaveErrorCodes.Security2CC_CannotDecode,
);
}
}
return response;
};
export function createMessageGenerator<TResponse extends Message = Message>(
driver: Driver,
msg: Message,
onMessageSent: (msg: Message, result: Message | undefined) => void,
): {
generator: MessageGenerator;
resultPromise: DeferredPromise<TResponse>;
} {
const resultPromise = createDeferredPromise<TResponse>();
const generator: MessageGenerator = {
parent: undefined as any, // The transaction will set this field on creation
current: undefined,
self: undefined,
start: () => {
const resetGenerator = () => {
generator.current = undefined;
generator.self = undefined;
};
async function* gen() {
// Determine which message generator implementation should be used
let implementation: MessageGeneratorImplementation =
simpleMessageGenerator;
if (isSendData(msg)) {
if (
msg.command instanceof Security2CCMessageEncapsulation
) {
implementation = secureMessageGeneratorS2;
} else if (
msg.command instanceof SecurityCCCommandEncapsulation
) {
implementation = secureMessageGeneratorS0;
}
}
// Step through the generator so we can easily cancel it and don't
// accidentally forget to unset this.current at the end
const gen = implementation(driver, msg, onMessageSent);
let sendResult: Message | undefined;
let result: Message | undefined;
while (true) {
// This call passes the previous send result (if it exists already) to the generator and saves the
// generated or returned message in `value`. When `done` is true, `value` contains the returned result of the message generator
try {
const { value, done } = await gen.next(sendResult!);
if (done) {
result = value;
break;
}
// Pass the generated message to the transaction machine and remember the result for the next iteration
generator.current = value;
sendResult = yield generator.current;
} catch (e) {
if (e instanceof Error) {
// There was an actual error, reject the transaction
resultPromise.reject(e);
} else if (isTransmitReport(e) && !e.isOK()) {
// The generator was prematurely ended by throwing a NOK transmit report.
// If this cannot be handled (e.g. by moving the messages to the wakeup queue), we need
// to treat this as an error
if (
driver.handleMissingNodeACK(
generator.parent as any,
)
) {
resetGenerator();
return;
} else {
resultPromise.reject(
sendDataErrorToZWaveError(
"callback NOK",
generator.parent,
e,
),
);
}
} else {
// The generator was prematurely ended by throwing a Message
resultPromise.resolve(e as TResponse);
}
break;
}
}
resultPromise.resolve(result as TResponse);
resetGenerator();
return;
}
generator.self = gen();
return generator.self;
},
};
return { resultPromise, generator };
}