-
-
Notifications
You must be signed in to change notification settings - Fork 10
Plan Fire and Forget
Roborock is deprecating the synchronous request-response pattern. Currently, data queries (getNetworkInfo, getSerialNumber, getHomeMap, getMapInfo, getRoomMap, getCleanModeData, getCustomMessage) use AbstractClient.get() — which registers a pending promise in V1PendingResponseTracker / B01PendingResponseTracker, awaits a correlated response, and times out after 10 seconds.
When Roborock drops response support, get() will always time out. Goal: remove the get() / PendingResponseTracker infrastructure and replace data queries with a OneShotResponseListener pattern that resolves on the first matching push message (already flowing through SimpleMessageListener → responseBroadcaster.onMessage()).
BEFORE (data query path)
─────────────────────────────────────────────────────────
Dispatcher.getNetworkInfo()
→ client.get() → responseTracker.waitFor() (pending promise)
→ sendInternal()
→ device push arrives → responseBroadcaster.tryResolve()
→ responseTracker resolves promise
→ return extracted result
AFTER (data query path)
─────────────────────────────────────────────────────────
Dispatcher.getNetworkInfo()
→ client.query(duid, request, parseFn)
→ create OneShotResponseListener, register on responseBroadcaster
→ sendInternal()
→ device push arrives → responseBroadcaster.onMessage()
→ listener.onMessage() → parseFn matches → listener resolves
→ return parsed result
UNCHANGED (command path and async push pipeline)
─────────────────────────────────────────────────────────
Commands: client.send() → fire-and-forget (no change)
Push: MQTTClient/LocalClient → responseBroadcaster.onMessage()
→ SimpleMessageListener → AbstractMessageHandler → PlatformRunner
→ runtimes/handlers/ → robot.updateAttribute()
Delete:
-
routing/services/pendingResponseTracker.ts(interface) routing/services/v1PendingResponseTracker.tsrouting/services/b01PendingResponseTracker.ts-
AbstractClient.get<T>()andresponseTrackerfield ClientRouter.get<T>()-
get<T>()fromClientinterface -
ResponseBroadcaster.tryResolve()from interface -
V1ResponseBroadcaster.tryResolve()andB01ResponseBroadcaster.tryResolve() -
ResponseBroadcasterFactory.waitFor(),cancelAll(), tracker construction,PendingResponseTracker implementsclause -
responseBroadcaster.tryResolve(response)calls inlocalClient.onMessageandmqttClient.onMessage -
PendingResponseTrackerimport/param inLocalNetworkClientandMQTTClientconstructors
Create:
routing/listeners/oneShotResponseListener.ts
Add to existing:
-
query<T>()onClientinterface,AbstractClient,ClientRouter
Keep unchanged:
-
HelloResponseListener— already its own one-shot pattern -
SimpleMessageListener→AbstractMessageHandler→PlatformRunner→runtimes/handlers/ -
BurstPollingManager,PollingService—getDeviceStatuscallers discard return value already - Transport layer connection/serialization logic
For each of these, replace client.get() with client.send():
| Location | Method | Evidence result was ignored |
|---|---|---|
V10MessageDispatcher |
findMyRobot() |
await this.client.get(duid, request) — result discarded |
V10MessageDispatcher |
getDeviceStatus() |
PollingService discards return; push pipeline already updates state. Convert to send(), return void. |
Q10MessageDispatcher |
getNetworkInfo() |
Logs response, returns undefined
|
Q10MessageDispatcher |
getDeviceStatus() |
Result discarded |
Q10MessageDispatcher |
getMapInfo() |
Logs response, returns stub |
Q10MessageDispatcher |
getRoomMap() |
TODO stub — just return []
|
Q7MessageDispatcher |
getMapInfo() |
Logs response, returns stub |
Q7MessageDispatcher |
getRoomMap() |
TODO stub — return []
|
Update AbstractMessageDispatcher interface: getDeviceStatus(duid: string): Promise<void>
Build + test after Phase 1.
File: src/roborockCommunication/routing/listeners/oneShotResponseListener.ts
// Implements AbstractMessageListener
// duid: string
// name: 'OneShotResponseListener'
// parseFn: (msg: ResponseMessage) => T | undefined
// timeoutMs: number (default: MESSAGE_TIMEOUT_MS)
//
// waitFor(): Promise<T | undefined>
// - Resolves with parseFn result on first message where parseFn returns non-undefined
// - Rejects with Error on timeout
// - After settling, onMessage() is a no-op (settled flag)
//
// onMessage(message: ResponseMessage): Promise<void>
// - If settled: return
// - Try parseFn(message); if non-undefined, resolveNo auto-unregister: after settling, onMessage is a no-op. Stays dormant in the listeners array.
Precedent: src/roborockCommunication/routing/listeners/implementation/helloResponseListener.ts
Client interface (routing/client.ts):
query<T>(duid: string, request: RequestMessage, parseFn: (msg: ResponseMessage) => T | undefined, timeoutMs?: number): Promise<T | undefined>;AbstractClient (routing/abstractClient.ts):
public async query<T>(duid, request, parseFn, timeoutMs?): Promise<T | undefined> {
const listener = new OneShotResponseListener<T>(duid, parseFn, timeoutMs);
this.responseBroadcaster.register(listener);
this.sendInternal(duid, request);
return listener.waitFor();
}ClientRouter (routing/clientRouter.ts):
public async query<T>(duid, request, parseFn, timeoutMs?): Promise<T | undefined> {
const listener = new OneShotResponseListener<T>(duid, parseFn, timeoutMs);
this.broadcasterFactory.register(listener);
await this.send(duid, request);
return listener.waitFor();
}Module-level helper in V10MessageDispatcher.ts, reused across all V10 parseFns:
function parseV1Result<T>(
msg: ResponseMessage,
messageId: number,
): T | undefined {
// Extract dps from Protocol.rpc_response, Protocol.general_response, or Protocol.general_request
// Check dps.id === messageId
// Return dps.result[0] as T or dps.result as T
}Replace each this.client.get(...) with this.client.query(duid, request, parseFn):
-
getNetworkInfo(): parseFn extractsresult[0]asNetworkInfowheredps.id === request.messageId -
getSerialNumber(): parseFn extractsresultas{ serial_number: string }[] -
getHomeMap(): parseFn extracts result asMapRoomResponse -
getMapInfo(): parseFn extractsresult[0]asMultipleMapDto -
getRoomMap(): parseFn extracts result asRawRoomMappingData -
getCustomMessage<T>():this.client.query(duid, def, genericParseFn) -
getCleanModeData(): three sequentialquery()calls (structure unchanged)
Remove the getCustomMessage('get_custom_mode') read-before-write. Replace with unconditional sends (F&F makes redundant sends harmless):
// Always attempt to exit smart mode first (no-op on device if not in smart mode)
if (mopRoute && mopRoute !== smartMopRoute) {
await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [customMopRoute] }));
}
// Then set target settings
if (suctionPower && suctionPower !== 0) { ... set_custom_mode }
if (waterFlow && waterFlow !== 0) { ... set_water_box_custom_mode }
if (mopRoute && mopRoute !== 0) { ... set_mop_mode }Replace this.client.get() with this.client.query(). Use a generic B01 parseFn to extract data from flat dps keyed by code.
Build + test after Phase 2.
3.1 Remove get<T>() from Client interface (routing/client.ts)
3.2 Remove get<T>() and responseTracker field from AbstractClient (routing/abstractClient.ts)
3.3 Remove get<T>() from ClientRouter (routing/clientRouter.ts)
3.4 Remove tryResolve() from ResponseBroadcaster interface (routing/listeners/responseBroadcaster.ts)
3.5 Remove tryResolve() from V1ResponseBroadcaster and B01ResponseBroadcaster (including tracker field)
3.6 Simplify ResponseBroadcasterFactory:
- Remove
implements PendingResponseTracker - Remove
waitFor(),cancelAll(), tracker construction, tracker fields and imports
3.7 Remove responseBroadcaster.tryResolve(response) from:
-
LocalNetworkClient.onMessage(line ~221) -
MQTTClient.onMessage(line ~262)
3.8 Remove responseTracker: PendingResponseTracker param from LocalNetworkClient, MQTTClient, and ClientRouter construction.
3.9 Delete files:
routing/services/pendingResponseTracker.tsrouting/services/v1PendingResponseTracker.tsrouting/services/b01PendingResponseTracker.ts
3.10 Update tests:
-
Delete:
src/tests/roborockCommunication/routing/services/v1PendingResponseTracker.test.ts -
Delete:
src/tests/roborockCommunication/routing/services/b01PendingResponseTracker.test.ts -
Update:
responseBroadcasterFactory.test.ts— removewaitFor,cancelAll,tryResolvecases -
Update:
v1ResponseBroadcaster.test.ts— removetryResolvetests -
Update:
b01ResponseBroadcaster.test.ts— removetryResolvetests -
Create:
src/tests/roborockCommunication/routing/listeners/oneShotResponseListener.test.ts- Resolves when parseFn returns a value
- Ignores messages where parseFn returns
undefined - Rejects on timeout
- Is a no-op after settling (subsequent
onMessagecalls ignored)
Build + test after Phase 3.
| File | Phase | Action |
|---|---|---|
routing/client.ts |
2+3 | Add query<T>(), remove get<T>()
|
routing/abstractClient.ts |
2+3 | Add query(), remove get() + responseTracker
|
routing/clientRouter.ts |
2+3 | Add query(), remove get<T>(), remove tracker param in construction |
routing/listeners/oneShotResponseListener.ts |
2 | Create |
routing/listeners/responseBroadcaster.ts |
3 | Remove tryResolve()
|
routing/listeners/responseBroadcasterFactory.ts |
3 | Remove tracker/waitFor/cancelAll/implements |
routing/listeners/v1ResponseBroadcaster.ts |
3 | Remove tryResolve(), tracker field |
routing/listeners/b01ResponseBroadcaster.ts |
3 | Remove tryResolve(), tracker field |
routing/services/pendingResponseTracker.ts |
3 | Delete |
routing/services/v1PendingResponseTracker.ts |
3 | Delete |
routing/services/b01PendingResponseTracker.ts |
3 | Delete |
local/localClient.ts |
3 | Remove tryResolve call, remove responseTracker param |
mqtt/mqttClient.ts |
3 | Remove tryResolve call, remove responseTracker param |
dispatcher/abstractMessageDispatcher.ts |
1 | getDeviceStatus() → Promise<void> |
dispatcher/V10MessageDispatcher.ts |
1+2 | findMyRobot→send, getDeviceStatus→send, all query migrations, changeCleanMode guard removal |
dispatcher/Q10MessageDispatcher.ts |
1+2 | getNetworkInfo/getDeviceStatus/getMapInfo/getRoomMap→send, getCustomMessage→query |
dispatcher/Q7MessageDispatcher.ts |
1+2 | getMapInfo/getRoomMap→send (return []), getCustomMessage→query |
constants/timeouts.ts |
2 | Rename MESSAGE_TIMEOUT_MS → QUERY_TIMEOUT_MS (optional) |
After each phase:
npm run build # must exit 0
npm test # all tests must pass
End-to-end (after Phase 3): Plugin connects, polls device state via push pipeline, and issues commands without awaiting tracker resolution. No timeout errors in logs.