Skip to content

Commit

Permalink
feat: expose RSSI and routes as part of node statistics (zwave-js#4022)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone committed Mar 2, 2022
1 parent 0c5d5cd commit 9edf81c
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 20 deletions.
65 changes: 65 additions & 0 deletions docs/api/controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,71 @@ To get around this:
2. Batch all `getNodeNeighbors` requests together
3. Turn the radio back on with `controller.toggleRF(true`)

### `getKnownLifelineRoutes`

The routing table of the controller is stored in its memory and not easily accessible during normal operation. Z-Wave JS gets around this by keeping statistics for each node that include the last used routes, the used repeaters, procotol and speed, as well as RSSI readings. This information can be read using

```ts
getKnownLifelineRoutes(): ReadonlyMap<number, LifelineRoutes>
```

This has some limitations:

- The information is dynamically built using TX status reports and may not be accurate at all times.
- It may not be available immediately after startup or at all if the controller doesn't support this feature.
- It only includes information about the routes between the controller and nodes, not between individual nodes.

> [!NOTE] To keep information returned by this method updated, subscribe to each node's `"statistics"` event and use the included information.
The returned objects have the following shape:

<!-- #import LifelineRoutes from "zwave-js" -->

```ts
interface LifelineRoutes {
/** The last working route from the controller to this node. */
lwr?: RouteStatistics;
/** The next to last working route from the controller to this node. */
nlwr?: RouteStatistics;
}
```

<!-- #import RouteStatistics from "zwave-js" -->

```ts
interface RouteStatistics {
/** The protocol and used data rate for this route */
protocolDataRate: ProtocolDataRate;
/** Which nodes are repeaters for this route */
repeaters: number[];

/** The RSSI of the ACK frame received by the controller */
rssi?: RSSI;
/**
* The RSSI of the ACK frame received by each repeater.
* If this is set, it has the same length as the repeaters array.
*/
repeaterRSSI?: RSSI[];

/**
* The node IDs of the nodes between which the transmission failed most recently.
* Is only set if there recently was a transmission failure.
*/
routeFailedBetween?: [number, number];
}
```

<!-- #import ProtocolDataRate from "zwave-js" -->

```ts
declare enum ProtocolDataRate {
ZWave_9k6 = 1,
ZWave_40k = 2,
ZWave_100k = 3,
LongRange_100k = 4,
}
```

### `healNode`

```ts
Expand Down
36 changes: 36 additions & 0 deletions docs/api/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,5 +1175,41 @@ interface NodeStatistics {
* Consecutive measurements are combined using an exponential moving average.
*/
rtt?: number;

/**
* Average RSSI of frames received by this node in dBm.
* Consecutive non-error measurements are combined using an exponential moving average.
*/
rssi?: RSSI;

/** The last working route from the controller to this node. */
lwr?: RouteStatistics;
/** The next to last working route from the controller to this node. */
nlwr?: RouteStatistics;
}
```

<!-- #import RouteStatistics from "zwave-js" -->

```ts
interface RouteStatistics {
/** The protocol and used data rate for this route */
protocolDataRate: ProtocolDataRate;
/** Which nodes are repeaters for this route */
repeaters: number[];

/** The RSSI of the ACK frame received by the controller */
rssi?: RSSI;
/**
* The RSSI of the ACK frame received by each repeater.
* If this is set, it has the same length as the repeaters array.
*/
repeaterRSSI?: RSSI[];

/**
* The node IDs of the nodes between which the transmission failed most recently.
* Is only set if there recently was a transmission failure.
*/
routeFailedBetween?: [number, number];
}
```
7 changes: 6 additions & 1 deletion packages/zwave-js/src/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export type {
export type { ControllerStatistics } from "./lib/controller/ControllerStatistics";
export { ZWaveFeature } from "./lib/controller/Features";
export * from "./lib/controller/Inclusion";
export { RSSI, RssiError, TXReport } from "./lib/controller/SendDataShared";
export {
isRssiError,
RSSI,
RssiError,
TXReport,
} from "./lib/controller/SendDataShared";
export type { ZWaveLibraryTypes } from "./lib/controller/ZWaveLibraryTypes";
export { RFRegion } from "./lib/serialapi/misc/SerialAPISetupMessages";
7 changes: 6 additions & 1 deletion packages/zwave-js/src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ export {
NodeType,
NODE_ID_BROADCAST,
NODE_ID_MAX,
ProtocolDataRate,
ProtocolVersion,
} from "@zwave-js/core";
export { DeviceClass } from "./lib/node/DeviceClass";
export { Endpoint } from "./lib/node/Endpoint";
export { ZWaveNode } from "./lib/node/Node";
export type { NodeStatistics } from "./lib/node/NodeStatistics";
export type {
NodeStatistics,
RouteStatistics,
} from "./lib/node/NodeStatistics";
export {
InterviewStage,
LifelineHealthCheckResult,
LifelineHealthCheckSummary,
LifelineRoutes,
NodeInterviewFailedEventArgs,
NodeStatus,
RefreshInfoOptions,
Expand Down
22 changes: 21 additions & 1 deletion packages/zwave-js/src/lib/controller/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ import type { Message } from "../message/Message";
import type { SuccessIndicator } from "../message/SuccessIndicator";
import { DeviceClass } from "../node/DeviceClass";
import { ZWaveNode } from "../node/Node";
import { InterviewStage, NodeStatus } from "../node/Types";
import { InterviewStage, LifelineRoutes, NodeStatus } from "../node/Types";
import { VirtualNode } from "../node/VirtualNode";
import {
GetBackgroundRSSIRequest,
Expand Down Expand Up @@ -4211,6 +4211,26 @@ ${associatedNodes.join(", ")}`,
}
}

/**
* Returns the known routes the controller will use to communicate with the nodes.
*
* This information is dynamically built using TX status reports and may not be accurate at all times.
* Also, it may not be available immediately after startup or at all if the controller doesn't support this feature.
*
* **Note:** To keep information returned by this method updated, use the information contained in each node's `"statistics"` event.
*/
public getKnownLifelineRoutes(): ReadonlyMap<number, LifelineRoutes> {
const ret = new Map<number, LifelineRoutes>();
for (const node of this.nodes.values()) {
if (node.isControllerNode) continue;
ret.set(node.id, {
lwr: node.statistics.lwr,
nlwr: node.statistics.nlwr,
});
}
return ret;
}

/**
* @internal
* Serializes the controller information and all nodes to store them in a cache.
Expand Down
4 changes: 4 additions & 0 deletions packages/zwave-js/src/lib/controller/SendDataShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export enum RssiError {
NoSignalDetected = 125,
}

export function isRssiError(rssi: RSSI): rssi is RssiError {
return rssi >= RssiError.NoSignalDetected;
}

// const RSSI_RESERVED_START = 11;

/** A number between -128 and +124 dBm or one of the special values in {@link RssiError} indicating an error */
Expand Down
17 changes: 2 additions & 15 deletions packages/zwave-js/src/lib/driver/Driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import {
createDeferredPromise,
DeferredPromise,
} from "alcalzone-shared/deferred-promise";
import { roundTo } from "alcalzone-shared/math";
import { isArray, isObject } from "alcalzone-shared/typeguards";
import { randomBytes } from "crypto";
import type { EventEmitter } from "events";
Expand Down Expand Up @@ -3263,25 +3262,13 @@ ${handlers.length} left`,
node.incrementStatistics("commandsDroppedTX");
} else {
node.incrementStatistics("commandsTX");
if (msg.rtt) {
const rttMs = msg.rtt / 1e6;
node.updateStatistics((current) => ({
...current,
rtt:
current.rtt != undefined
? roundTo(
current.rtt * 0.9 + rttMs * 0.1,
1,
)
: roundTo(rttMs, 1),
}));
}
node.updateRTT(msg);
}

// Notify listeners about the status report if one was received
if (hasTXReport(result)) {
options.onTXReport?.(result.txReport);
// TODO: Update statistics based on the TX report
node.updateRouteStatistics(result.txReport);
}
}

Expand Down
77 changes: 75 additions & 2 deletions packages/zwave-js/src/lib/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ import {
stringify,
TypedEventEmitter,
} from "@zwave-js/shared";
import { roundTo } from "alcalzone-shared/math";
import { padStart } from "alcalzone-shared/strings";
import { isArray, isObject } from "alcalzone-shared/typeguards";
import { randomBytes } from "crypto";
import { EventEmitter } from "events";
import type { Message } from "../..";
import { PowerlevelCCTestNodeReport } from "../commandclass";
import type {
CCAPI,
Expand Down Expand Up @@ -152,7 +154,12 @@ import {
GetNodeProtocolInfoRequest,
GetNodeProtocolInfoResponse,
} from "../controller/GetNodeProtocolInfoMessages";
import { RSSI, RssiError, TXReport } from "../controller/SendDataShared";
import {
isRssiError,
RSSI,
RssiError,
TXReport,
} from "../controller/SendDataShared";
import type { Driver, SendCommandOptions } from "../driver/Driver";
import { Extended, interpretEx } from "../driver/StateMachineShared";
import type { StatisticsEventCallbacksWithSelf } from "../driver/Statistics";
Expand All @@ -169,7 +176,12 @@ import {
createNodeReadyMachine,
NodeReadyInterpreter,
} from "./NodeReadyMachine";
import { NodeStatistics, NodeStatisticsHost } from "./NodeStatistics";
import {
NodeStatistics,
NodeStatisticsHost,
RouteStatistics,
routeStatisticsEquals,
} from "./NodeStatistics";
import {
createNodeStatusMachine,
NodeStatusInterpreter,
Expand Down Expand Up @@ -4484,4 +4496,65 @@ ${formatRouteHealthCheckSummary(this.id, otherNode.id, summary)}`,

return summary;
}

/**
* Updates the average RTT of this node
* @internal
*/
public updateRTT(sentMessage: Message): void {
if (sentMessage.rtt) {
const rttMs = sentMessage.rtt / 1e6;
this.updateStatistics((current) => ({
...current,
rtt:
current.rtt != undefined
? roundTo(current.rtt * 0.75 + rttMs * 0.25, 1)
: roundTo(rttMs, 1),
}));
}
}

/**
* Updates route/transmission statistics for this node
* @internal
*/
public updateRouteStatistics(txReport: TXReport): void {
this.updateStatistics((current) => {
const ret = { ...current };
// Update ACK RSSI
if (txReport.ackRSSI != undefined) {
ret.rssi =
ret.rssi == undefined || isRssiError(txReport.ackRSSI)
? txReport.ackRSSI
: Math.round(ret.rssi * 0.75 + txReport.ackRSSI * 0.25);
}

// Update the LWR's statistics
const newStats: RouteStatistics = {
protocolDataRate: txReport.routeSpeed,
repeaters: (txReport.repeaterNodeIds ?? []) as number[],
rssi:
txReport.ackRSSI ?? ret.lwr?.rssi ?? RssiError.NotAvailable,
};
if (txReport.ackRepeaterRSSI != undefined) {
newStats.repeaterRSSI = txReport.ackRepeaterRSSI as number[];
}
if (
txReport.failedRouteLastFunctionalNodeId &&
txReport.failedRouteFirstNonFunctionalNodeId
) {
newStats.routeFailedBetween = [
txReport.failedRouteLastFunctionalNodeId,
txReport.failedRouteFirstNonFunctionalNodeId,
];
}

if (ret.lwr && !routeStatisticsEquals(ret.lwr, newStats)) {
// The old LWR becomes the NLWR
ret.nlwr = ret.lwr;
}
ret.lwr = newStats;
return ret;
});
}
}
45 changes: 45 additions & 0 deletions packages/zwave-js/src/lib/node/NodeStatistics.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ProtocolDataRate } from "@zwave-js/core";
import type { RSSI } from "../controller/SendDataShared";
import { StatisticsHost } from "../driver/Statistics";

export class NodeStatisticsHost extends StatisticsHost<NodeStatistics> {
Expand Down Expand Up @@ -36,4 +38,47 @@ export interface NodeStatistics {
* Consecutive measurements are combined using an exponential moving average.
*/
rtt?: number;

/**
* Average RSSI of frames received by this node in dBm.
* Consecutive non-error measurements are combined using an exponential moving average.
*/
rssi?: RSSI;

/** The last working route from the controller to this node. */
lwr?: RouteStatistics;
/** The next to last working route from the controller to this node. */
nlwr?: RouteStatistics;
}

export interface RouteStatistics {
/** The protocol and used data rate for this route */
protocolDataRate: ProtocolDataRate;
/** Which nodes are repeaters for this route */
repeaters: number[];

/** The RSSI of the ACK frame received by the controller */
rssi?: RSSI;
/**
* The RSSI of the ACK frame received by each repeater.
* If this is set, it has the same length as the repeaters array.
*/
repeaterRSSI?: RSSI[];

/**
* The node IDs of the nodes between which the transmission failed most recently.
* Is only set if there recently was a transmission failure.
*/
routeFailedBetween?: [number, number];
}

/** Checks if the given route statistics belong to the same route */
export function routeStatisticsEquals(
r1: RouteStatistics,
r2: RouteStatistics,
): boolean {
if (r1.repeaters.length !== r2.repeaters.length) return false;
if (!r1.repeaters.every((node) => r2.repeaters.includes(node)))
return false;
return true;
}
Loading

0 comments on commit 9edf81c

Please sign in to comment.