Skip to content

Commit

Permalink
fix: refresh CC response timeout for possibly matching partial respon…
Browse files Browse the repository at this point in the history
…ses (zwave-js#5683)
  • Loading branch information
AlCalzone authored Apr 19, 2023
1 parent 0ed12bf commit c0fb1fc
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 44 deletions.
16 changes: 12 additions & 4 deletions packages/cc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4130,21 +4130,24 @@ export class ConfigurationCCNameGet extends ConfigurationCC {
//
// @public (undocumented)
export class ConfigurationCCNameReport extends ConfigurationCC {
constructor(host: ZWaveHost_2, options: CommandClassDeserializationOptions);
// Warning: (ae-forgotten-export) The symbol "ConfigurationCCNameReportOptions" needs to be exported by the entry point index.d.ts
constructor(host: ZWaveHost_2, options: CommandClassDeserializationOptions | ConfigurationCCNameReportOptions);
// (undocumented)
expectMoreMessages(): boolean;
// (undocumented)
getPartialCCSessionId(): Record<string, any> | undefined;
// (undocumented)
mergePartialCCs(applHost: ZWaveApplicationHost_2, partials: ConfigurationCCNameReport[]): void;
// (undocumented)
get name(): string;
name: string;
// (undocumented)
get parameter(): number;
readonly parameter: number;
// (undocumented)
persistValues(applHost: ZWaveApplicationHost_2): boolean;
// (undocumented)
get reportsToFollow(): number;
readonly reportsToFollow: number;
// (undocumented)
serialize(): Buffer;
// (undocumented)
toLogEntry(applHost: ZWaveApplicationHost_2): MessageOrCCLogEntry_2;
}
Expand Down Expand Up @@ -6767,6 +6770,11 @@ export function getImplementedVersion<T extends CommandClass>(cc: T | CommandCla
// @public
export function getImplementedVersionStatic<T extends CCConstructor<CommandClass>>(classConstructor: T): number;

// Warning: (ae-missing-release-tag) "getInnermostCommandClass" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function getInnermostCommandClass(cc: CommandClass): CommandClass;

// Warning: (tsdoc-undefined-tag) The TSDoc tag "@publicAPI" is not defined in this configuration
// Warning: (ae-missing-release-tag) "getManufacturerId" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
Expand Down
71 changes: 41 additions & 30 deletions packages/cc/src/cc/ConfigurationCC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2077,24 +2077,43 @@ export class ConfigurationCCBulkGet extends ConfigurationCC {
}
}

export interface ConfigurationCCNameReportOptions extends CCCommandOptions {
parameter: number;
name: string;
reportsToFollow: number;
}

@CCCommand(ConfigurationCommand.NameReport)
export class ConfigurationCCNameReport extends ConfigurationCC {
public constructor(
host: ZWaveHost,
options: CommandClassDeserializationOptions,
options:
| CommandClassDeserializationOptions
| ConfigurationCCNameReportOptions,
) {
super(host, options);
// Parameter and # of reports must be present
validatePayload(this.payload.length >= 3);
this._parameter = this.payload.readUInt16BE(0);
this._reportsToFollow = this.payload[2];
if (this._reportsToFollow > 0) {
// If more reports follow, the info must at least be one byte
validatePayload(this.payload.length >= 4);

if (gotDeserializationOptions(options)) {
// Parameter and # of reports must be present
validatePayload(this.payload.length >= 3);
this.parameter = this.payload.readUInt16BE(0);
this.reportsToFollow = this.payload[2];
if (this.reportsToFollow > 0) {
// If more reports follow, the info must at least be one byte
validatePayload(this.payload.length >= 4);
}
this.name = this.payload.slice(3).toString("utf8");
} else {
this.parameter = options.parameter;
this.name = options.name;
this.reportsToFollow = options.reportsToFollow;
}
this._name = this.payload.slice(3).toString("utf8");
}

public readonly parameter: number;
public name: string;
public readonly reportsToFollow: number;

public persistValues(applHost: ZWaveApplicationHost): boolean {
if (!super.persistValues(applHost)) return false;

Expand All @@ -2104,37 +2123,32 @@ export class ConfigurationCCNameReport extends ConfigurationCC {
return true;
}

private _parameter: number;
public get parameter(): number {
return this._parameter;
}

private _name: string;
public get name(): string {
return this._name;
}
public serialize(): Buffer {
const nameBuffer = Buffer.from(this.name, "utf8");
this.payload = Buffer.allocUnsafe(3 + nameBuffer.length);
this.payload.writeUInt16BE(this.parameter, 0);
this.payload[2] = this.reportsToFollow;
nameBuffer.copy(this.payload, 3);

private _reportsToFollow: number;
public get reportsToFollow(): number {
return this._reportsToFollow;
return super.serialize();
}

public getPartialCCSessionId(): Record<string, any> | undefined {
// Distinguish sessions by the parameter number
return { parameter: this._parameter };
return { parameter: this.parameter };
}

public expectMoreMessages(): boolean {
return this._reportsToFollow > 0;
return this.reportsToFollow > 0;
}

public mergePartialCCs(
applHost: ZWaveApplicationHost,
partials: ConfigurationCCNameReport[],
): void {
// Concat the name
this._name = [...partials, this]
.map((report) => report._name)
this.name = [...partials, this]
.map((report) => report.name)
.reduce((prev, cur) => prev + cur, "");
}

Expand All @@ -2159,11 +2173,8 @@ export class ConfigurationCCNameGet extends ConfigurationCC {
) {
super(host, options);
if (gotDeserializationOptions(options)) {
// TODO: Deserialize payload
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
validatePayload(this.payload.length >= 2);
this.parameter = this.payload.readUInt16BE(0);
} else {
this.parameter = options.parameter;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/cc/src/lib/EncapsulatingCommandClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export function isEncapsulatingCommandClass(
return false;
}

export function getInnermostCommandClass(cc: CommandClass): CommandClass {
if (isEncapsulatingCommandClass(cc)) {
return getInnermostCommandClass(cc.encapsulated);
} else {
return cc;
}
}

/** Defines the static side of an encapsulating command class */
export interface MultiEncapsulatingCommandClassStatic {
new (
Expand Down
3 changes: 2 additions & 1 deletion packages/zwave-js/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,9 @@ export class Driver extends TypedEventEmitter<DriverEventCallbacks> implements Z
waitForCommand<T extends ICommandClass>(predicate: (cc: ICommandClass) => boolean, timeout: number): Promise<T>;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "zwave-js" does not have an export "waitForCommand"
waitForMessage<T extends Message>(predicate: (msg: Message) => boolean, timeout: number): Promise<T>;
waitForMessage<T extends Message>(predicate: (msg: Message) => boolean, timeout: number, refreshPredicate?: (msg: Message) => boolean): Promise<T>;
}

// Warning: (ae-missing-release-tag) "DriverLogContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down
18 changes: 16 additions & 2 deletions packages/zwave-js/src/lib/driver/Driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ interface AwaitedThing<T> {
handler: (thing: T) => void;
timeout?: NodeJS.Timeout;
predicate: (msg: T) => boolean;
refreshPredicate?: (msg: T) => boolean;
}

type AwaitedMessageEntry = AwaitedThing<Message>;
Expand Down Expand Up @@ -2894,7 +2895,17 @@ export class Driver
}

// Assemble partial CCs on the driver level. Only forward complete messages to the send thread machine
if (!this.assemblePartialCCs(msg)) return;
if (!this.assemblePartialCCs(msg)) {
// Check if a message timer needs to be refreshed.
for (const entry of this.awaitedMessages) {
if (entry.refreshPredicate?.(msg)) {
entry.timeout?.refresh();
// Since this is a partial message there may be no clear 1:1 match.
// Therefore we loop through all awaited messages
}
}
return;
}

// Make sure we are allowed to handle this command
if (this.shouldDiscardCC(msg.command)) {
Expand Down Expand Up @@ -4510,16 +4521,19 @@ ${handlers.length} left`,
*
* **Note:** To wait for a certain CommandClass, better use {@link waitForCommand}.
* @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
* @param predicate A predicate function to test all incoming messages.
* @param refreshPredicate A predicate function to test partial messages. If this returns `true` for a message, the timer will be restarted.
*/
public waitForMessage<T extends Message>(
predicate: (msg: Message) => boolean,
timeout: number,
refreshPredicate?: (msg: Message) => boolean,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const promise = createDeferredPromise<Message>();
const entry: AwaitedMessageEntry = {
predicate,
refreshPredicate,
handler: (msg) => promise.resolve(msg),
timeout: undefined,
};
Expand Down
31 changes: 28 additions & 3 deletions packages/zwave-js/src/lib/driver/MessageGenerators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
CommandClass,
getInnermostCommandClass,
isCommandClassContainer,
MGRPExtension,
MPANExtension,
Expand Down Expand Up @@ -71,15 +72,39 @@ export type MessageGeneratorImplementation = (
additionalCommandTimeoutMs?: number,
) => AsyncGenerator<Message, Message, Message>;

function maybePartialNodeUpdate(sent: Message, received: Message): boolean {
// Some commands are returned in multiple segments, which may take longer than
// the configured timeout.
if (!isCommandClassContainer(sent) || !isCommandClassContainer(received)) {
return false;
}

if (sent.getNodeId() !== received.getNodeId()) return false;

if (received.command.ccId === CommandClasses["Transport Service"]) {
// We don't know what's in there. It may be the expected update
return true;
}

// Let the sent CC test if the received one is a match.
// These predicates don't check if the received CC is complete, we can use them here.
// This also doesn't check for correct encapsulation, but that is good enough to refresh the timer.
const sentCommand = getInnermostCommandClass(sent.command);
const receivedCommand = getInnermostCommandClass(received.command);
return sentCommand.isExpectedCCResponse(receivedCommand);
}

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);
return await driver.waitForMessage<T>(
(received) => msg.isExpectedNodeUpdate(received),
timeoutMs,
(received) => maybePartialNodeUpdate(msg, received),
);
} catch (e) {
throw new ZWaveError(
`Timed out while waiting for a response from the node`,
Expand Down
Loading

0 comments on commit c0fb1fc

Please sign in to comment.