ota-hub-reactjs 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base/device-whisperer.d.ts +6 -0
- package/dist/base/device-whisperer.js +3 -0
- package/dist/message_layers/protobuf-wrapper.d.ts +2 -0
- package/dist/transport_layers/esp32-device-whisperer.d.ts +2 -0
- package/dist/transport_layers/mqtt-device-whisperer.d.ts +13 -0
- package/dist/transport_layers/mqtt-device-whisperer.js +102 -8
- package/dist/transport_layers/serial-device-whisperer.d.ts +2 -0
- package/dist/transport_layers/websocket-device-whisperer.d.ts +2 -0
- package/package.json +1 -1
|
@@ -13,6 +13,10 @@ export type DeviceConnectionState = {
|
|
|
13
13
|
uuid: string;
|
|
14
14
|
deviceMac?: string;
|
|
15
15
|
name: string;
|
|
16
|
+
/**
|
|
17
|
+
* Default device send helper.
|
|
18
|
+
* This is a utility function which may better be replaced with transport specific send functions (e.g. MQTT has publish, Serial has write etc.)
|
|
19
|
+
*/
|
|
16
20
|
send: (data: string | Uint8Array) => void | Promise<void>;
|
|
17
21
|
onReceive?: (data: string | Uint8Array) => void;
|
|
18
22
|
onConnect?: () => void | Promise<void>;
|
|
@@ -29,6 +33,8 @@ export type DeviceWhispererProps<T extends DeviceConnectionState> = {
|
|
|
29
33
|
createInitialConnectionState?: (uuid: string) => Partial<T>;
|
|
30
34
|
};
|
|
31
35
|
export declare function MultiDeviceWhisperer<T extends DeviceConnectionState>({ createInitialConnectionState, }?: DeviceWhispererProps<T>): {
|
|
36
|
+
focussedConnection: string | undefined;
|
|
37
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
32
38
|
connections: T[];
|
|
33
39
|
addConnection: ({ uuid, propCreator }: AddConnectionProps<T>) => Promise<string>;
|
|
34
40
|
removeConnection: (uuid: string) => void;
|
|
@@ -21,6 +21,7 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
|
|
|
21
21
|
const [connections, setConnections] = useState([]);
|
|
22
22
|
const connectionsRef = useRef(new Map());
|
|
23
23
|
const [isReady, setIsReady] = useState(false);
|
|
24
|
+
const [focussedConnection, setFocussedConnection] = useState();
|
|
24
25
|
// ---------- GET ----------
|
|
25
26
|
const getConnection = (uuid) => connectionsRef.current.get(uuid);
|
|
26
27
|
// ---------- UPDATE ----------
|
|
@@ -67,6 +68,8 @@ export function MultiDeviceWhisperer({ createInitialConnectionState = createDefa
|
|
|
67
68
|
setConnections(Array.from(connectionsRef.current.values()));
|
|
68
69
|
}, []);
|
|
69
70
|
return {
|
|
71
|
+
focussedConnection,
|
|
72
|
+
setFocussedConnection,
|
|
70
73
|
connections,
|
|
71
74
|
addConnection,
|
|
72
75
|
removeConnection,
|
|
@@ -19,6 +19,8 @@ export declare function ProtobufMultiDeviceWhisperer<AppLayer extends DeviceConn
|
|
|
19
19
|
addConnection: ({ uuid, propCreator }: AddConnectionProps<AppLayer>) => Promise<string>;
|
|
20
20
|
sendProtobuf: (uuid: string, message: MessageTX) => void;
|
|
21
21
|
protoBufOnReceiveHandler: (uuid: string, data: string | Uint8Array) => void;
|
|
22
|
+
focussedConnection: string | undefined;
|
|
23
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
22
24
|
connections: AppLayer[];
|
|
23
25
|
removeConnection: (uuid: string) => void;
|
|
24
26
|
connect: (_uuid: string) => void;
|
|
@@ -27,6 +27,8 @@ export declare function ESP32MultiDeviceWhisperer<AppOrMessageLayer extends ESP3
|
|
|
27
27
|
blob: Blob;
|
|
28
28
|
address: number;
|
|
29
29
|
}[]) => Promise<void>;
|
|
30
|
+
focussedConnection: string | undefined;
|
|
31
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
30
32
|
connections: AppOrMessageLayer[];
|
|
31
33
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
32
34
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { DeviceConnectionState, AddConnectionProps, DeviceWhispererProps } from "../base/device-whisperer.js";
|
|
2
|
+
type MQTTPayload = string | Uint8Array | ArrayBuffer;
|
|
3
|
+
type MQTTTopicCallback = (payload: MQTTPayload, topic: string) => void;
|
|
2
4
|
export type MQTTConnectionState = DeviceConnectionState & {
|
|
5
|
+
/**
|
|
6
|
+
* Publishes a payload to any explicit MQTT topic.
|
|
7
|
+
*/
|
|
8
|
+
publish?: (topic: string, payload: MQTTPayload) => void | Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Subscribes a callback for a specific topic. Returns an unsubscribe function.
|
|
11
|
+
*/
|
|
12
|
+
subscribeCallbackToTopic?: (topic: string, callback: MQTTTopicCallback) => () => void;
|
|
3
13
|
pingFunction?: (props?: any) => void;
|
|
4
14
|
touchHeartbeat?: () => void;
|
|
5
15
|
};
|
|
@@ -21,6 +31,8 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
|
|
|
21
31
|
disconnect: (uuid: string) => Promise<void>;
|
|
22
32
|
reconnectAll: () => Promise<void>;
|
|
23
33
|
connectToMQTTServer: () => (() => void) | undefined;
|
|
34
|
+
focussedConnection: string | undefined;
|
|
35
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
24
36
|
connections: AppOrMessageLayer[];
|
|
25
37
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
26
38
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
@@ -30,3 +42,4 @@ export declare function MQTTMultiDeviceWhisperer<AppOrMessageLayer extends MQTTC
|
|
|
30
42
|
setIsReady: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
31
43
|
createInitialConnectionState: (uuid: string) => Partial<AppOrMessageLayer>;
|
|
32
44
|
};
|
|
45
|
+
export {};
|
|
@@ -7,6 +7,83 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
7
7
|
const isUnmountedRef = useRef(false);
|
|
8
8
|
const watchdogTimers = useRef({});
|
|
9
9
|
const addingConnections = useRef(new Set());
|
|
10
|
+
const topicCallbacksRef = useRef(new Map());
|
|
11
|
+
const hasCallbackForTopic = (topic) => {
|
|
12
|
+
const callbacks = topicCallbacksRef.current.get(topic);
|
|
13
|
+
return !!callbacks && callbacks.size > 0;
|
|
14
|
+
};
|
|
15
|
+
const hasOtherConnectionUsingTopic = (topic, excludingUuid) => {
|
|
16
|
+
return base.connections.some((connection) => {
|
|
17
|
+
if (excludingUuid && connection.uuid === excludingUuid)
|
|
18
|
+
return false;
|
|
19
|
+
const connectionTopic = subTopicFromUuid?.(connection.uuid) ?? connection.uuid;
|
|
20
|
+
return connectionTopic === topic;
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
const normalizePayload = (payload) => {
|
|
24
|
+
if (typeof payload === "string") {
|
|
25
|
+
return new TextEncoder().encode(payload);
|
|
26
|
+
}
|
|
27
|
+
if (payload instanceof ArrayBuffer) {
|
|
28
|
+
return new Uint8Array(payload);
|
|
29
|
+
}
|
|
30
|
+
return payload;
|
|
31
|
+
};
|
|
32
|
+
const publish = async (topic, payload, uuidForLogs) => {
|
|
33
|
+
const client = clientRef.current;
|
|
34
|
+
if (!client || isUnmountedRef.current)
|
|
35
|
+
return;
|
|
36
|
+
const bytes = normalizePayload(payload);
|
|
37
|
+
if (uuidForLogs) {
|
|
38
|
+
base.appendLog(uuidForLogs, {
|
|
39
|
+
level: 5,
|
|
40
|
+
message: bytes,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
await new Promise((resolve, reject) => {
|
|
44
|
+
client.publish(topic, bytes, { qos: 1 }, (err) => {
|
|
45
|
+
if (err) {
|
|
46
|
+
reject(err);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const subscribeCallbackToTopic = (topic, callback) => {
|
|
54
|
+
if (!topic)
|
|
55
|
+
return () => { };
|
|
56
|
+
const topicCallbacks = topicCallbacksRef.current.get(topic) ?? new Set();
|
|
57
|
+
const topicAlreadyRegistered = topicCallbacksRef.current.has(topic);
|
|
58
|
+
topicCallbacks.add(callback);
|
|
59
|
+
topicCallbacksRef.current.set(topic, topicCallbacks);
|
|
60
|
+
const client = clientRef.current;
|
|
61
|
+
if (client?.connected && !client.disconnecting && !topicAlreadyRegistered) {
|
|
62
|
+
client.subscribe(topic, { qos: 1 }, (err) => {
|
|
63
|
+
if (err)
|
|
64
|
+
console.error("Topic callback subscribe failed:", err);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
const currentCallbacks = topicCallbacksRef.current.get(topic);
|
|
69
|
+
if (!currentCallbacks)
|
|
70
|
+
return;
|
|
71
|
+
currentCallbacks.delete(callback);
|
|
72
|
+
if (currentCallbacks.size > 0)
|
|
73
|
+
return;
|
|
74
|
+
topicCallbacksRef.current.delete(topic);
|
|
75
|
+
// Keep topic subscribed when at least one managed connection still uses it.
|
|
76
|
+
if (hasOtherConnectionUsingTopic(topic))
|
|
77
|
+
return;
|
|
78
|
+
const latestClient = clientRef.current;
|
|
79
|
+
if (latestClient?.connected && !latestClient.disconnecting) {
|
|
80
|
+
latestClient.unsubscribe(topic, (err) => {
|
|
81
|
+
if (err)
|
|
82
|
+
console.error("Topic callback unsubscribe failed:", err);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
};
|
|
10
87
|
const connectToMQTTServer = () => {
|
|
11
88
|
isUnmountedRef.current = false;
|
|
12
89
|
if (clientRef.current) {
|
|
@@ -29,6 +106,13 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
29
106
|
new_client.on("connect", () => {
|
|
30
107
|
console.log("MQTT Whisperer Connected");
|
|
31
108
|
base.setIsReady(true);
|
|
109
|
+
// Re-subscribe custom topic callbacks after reconnect.
|
|
110
|
+
topicCallbacksRef.current.forEach((_callbacks, topic) => {
|
|
111
|
+
new_client.subscribe(topic, { qos: 1 }, (err) => {
|
|
112
|
+
if (err)
|
|
113
|
+
console.error("Topic callback subscribe failed:", err);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
32
116
|
});
|
|
33
117
|
new_client.on("reconnect", () => {
|
|
34
118
|
console.log("MQTT Whisperer Reconnecting...");
|
|
@@ -47,6 +131,15 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
47
131
|
return;
|
|
48
132
|
const uuid = uuidFromMessage(topic, payload);
|
|
49
133
|
const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
|
|
134
|
+
const topicCallbacks = topicCallbacksRef.current.get(topic);
|
|
135
|
+
topicCallbacks?.forEach((cb) => {
|
|
136
|
+
try {
|
|
137
|
+
cb(bytes, topic);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error("Topic callback failed:", err);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
50
143
|
if (!uuid)
|
|
51
144
|
return;
|
|
52
145
|
const conn = base.getConnection(uuid);
|
|
@@ -75,6 +168,7 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
75
168
|
}
|
|
76
169
|
});
|
|
77
170
|
watchdogTimers.current = {};
|
|
171
|
+
topicCallbacksRef.current.clear();
|
|
78
172
|
if (clientRef.current) {
|
|
79
173
|
clientRef.current.removeAllListeners();
|
|
80
174
|
clientRef.current.end(true);
|
|
@@ -105,6 +199,10 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
105
199
|
if (!clientRef.current || isUnmountedRef.current)
|
|
106
200
|
return;
|
|
107
201
|
const topic = subTopicFromUuid?.(uuid) ?? uuid;
|
|
202
|
+
// Keep topic subscribed if callbacks or other device-connections still depend on it.
|
|
203
|
+
if (hasCallbackForTopic(topic) || hasOtherConnectionUsingTopic(topic, uuid)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
108
206
|
if (clientRef.current.connected && !clientRef.current.disconnecting) {
|
|
109
207
|
clientRef.current.unsubscribe(topic, async (err) => {
|
|
110
208
|
if (err)
|
|
@@ -173,14 +271,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
173
271
|
const conn = base.getConnection(uuid);
|
|
174
272
|
if (!conn)
|
|
175
273
|
return;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
: data;
|
|
179
|
-
base.appendLog(uuid, {
|
|
180
|
-
level: 5,
|
|
181
|
-
message: payload,
|
|
182
|
-
});
|
|
183
|
-
clientRef.current?.publish(pubTopicFromUuid?.(uuid) ?? uuid, payload); // TS is wrong here!
|
|
274
|
+
const topic = pubTopicFromUuid?.(uuid) ?? subTopicFromUuid?.(uuid) ?? uuid;
|
|
275
|
+
await publish(topic, data, uuid);
|
|
184
276
|
};
|
|
185
277
|
const addConnection = async ({ uuid, propCreator }) => {
|
|
186
278
|
if (!clientRef.current || isUnmountedRef.current)
|
|
@@ -199,6 +291,8 @@ export function MQTTMultiDeviceWhisperer({ serverUrl, uuidFromMessage, subTopicF
|
|
|
199
291
|
return {
|
|
200
292
|
// Defaults, may be overridden by props
|
|
201
293
|
send: (d) => defaultSend(id, d),
|
|
294
|
+
publish: (topic, payload) => publish(topic, payload, id),
|
|
295
|
+
subscribeCallbackToTopic,
|
|
202
296
|
onReceive: (d) => defaultOnReceive(id, d),
|
|
203
297
|
touchHeartbeat: () => touchHeartbeat(id),
|
|
204
298
|
// Initial connection state
|
|
@@ -46,6 +46,8 @@ export declare function SerialMultiDeviceWhisperer<AppOrMessageLayer extends Ser
|
|
|
46
46
|
reconnectAll: (...connectionProps: any) => Promise<void>;
|
|
47
47
|
releasePortByDefault: boolean;
|
|
48
48
|
setReleasePortByDefault: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
49
|
+
focussedConnection: string | undefined;
|
|
50
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
49
51
|
connections: AppOrMessageLayer[];
|
|
50
52
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
51
53
|
updateConnectionName: (uuid: string, name: string) => void;
|
|
@@ -19,6 +19,8 @@ export declare function WebsocketMultiDeviceWhisperer<AppOrMessageLayer extends
|
|
|
19
19
|
disconnect: (uuid: string) => Promise<void>;
|
|
20
20
|
checkForNewDevices: () => Promise<DeviceObjectResponse[]>;
|
|
21
21
|
reconnectAll: (...connectionProps: any) => Promise<void>;
|
|
22
|
+
focussedConnection: string | undefined;
|
|
23
|
+
setFocussedConnection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
22
24
|
connections: AppOrMessageLayer[];
|
|
23
25
|
updateConnection: (uuid: string, updater: (c: AppOrMessageLayer) => AppOrMessageLayer) => void;
|
|
24
26
|
updateConnectionName: (uuid: string, name: string) => void;
|
package/package.json
CHANGED