matterbridge 3.1.6-dev-20250721-75fab6b → 3.1.7-dev-20250723-aab81fe
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/CHANGELOG.md +36 -3
- package/README-DOCKER.md +35 -0
- package/bin/mb_coap.js +2 -0
- package/bin/mb_mdns.js +2 -0
- package/dist/dgram/coap.js +252 -0
- package/dist/dgram/dgram.js +237 -0
- package/dist/dgram/mb_coap.js +52 -0
- package/dist/dgram/mb_mdns.js +60 -0
- package/dist/dgram/mdns.js +595 -0
- package/dist/dgram/multicast.js +113 -0
- package/dist/dgram/unicast.js +37 -0
- package/dist/matterbridge.js +7 -3
- package/dist/matterbridgeEndpoint.js +9 -9
- package/dist/matterbridgeEndpointHelpers.js +1 -0
- package/npm-shrinkwrap.json +5 -3
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -12,11 +12,44 @@ If you like this project and find it useful, please consider giving it a star on
|
|
|
12
12
|
|
|
13
13
|
### Added
|
|
14
14
|
|
|
15
|
-
- [
|
|
15
|
+
- [operationalState]: Improved documentation on createDefaultOperationalStateClusterServer() and added the optional attribute countdownTime. Thanks Ludovic BOUÉ (https://github.com/Luligu/matterbridge/pull/363).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- [package]: Updated dependencies.
|
|
20
|
+
|
|
21
|
+
<a href="https://www.buymeacoffee.com/luligugithub">
|
|
22
|
+
<img src="bmc-button.svg" alt="Buy me a coffee" width="80">
|
|
23
|
+
</a>
|
|
24
|
+
|
|
25
|
+
## [3.1.7] - 2025-07-24
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- [docker]: Added trigger of Build Docker Image latest from publish.yml.
|
|
30
|
+
- [docker]: Added on demand trigger for Build Docker Image latest from other plugins.
|
|
31
|
+
- [mdns]: Added bin mb_mdns.
|
|
32
|
+
- [coap]: Added bin mb_coap.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- [package]: Updated dependencies.
|
|
37
|
+
|
|
38
|
+
<a href="https://www.buymeacoffee.com/luligugithub">
|
|
39
|
+
<img src="bmc-button.svg" alt="Buy me a coffee" width="80">
|
|
40
|
+
</a>
|
|
41
|
+
|
|
42
|
+
## [3.1.6] - 2025-07-22
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- [reset]: Improved "Reset all devices" command in the frontend. It will shutdown all the plugins and recreate the devices with new state and enpoint numbers even if the device is not selected.
|
|
16
47
|
- [enpoint]: Enhanced HEPA and Activated Carbon Filter Monitoring Cluster Server methods with additional features and improved default parameters.
|
|
17
|
-
- [enpoint]: Added resetCondition command for HEPA and Activated Carbon Filter Monitoring Cluster Server.
|
|
48
|
+
- [enpoint]: Added resetCondition MAtter command for HEPA and Activated Carbon Filter Monitoring Cluster Server.
|
|
18
49
|
- [dishwasher]: Added Dishwasher class and Jest test. It is not supported by the Home app.
|
|
19
|
-
- [extractorHood]: Added ExtractorHood class and Jest test.
|
|
50
|
+
- [extractorHood]: Added ExtractorHood class and Jest test. It is not supported by the Home app.
|
|
51
|
+
- [fan]: Added the createCompleteFanControlClusterServer() cluster helper that create a fan device with all the features. Thanks Ludovic BOUÉ (https://github.com/Luligu/matterbridge/pull/362).
|
|
52
|
+
- [docker]: Added logging configuration instructions to [docker setup](README-DOCKER.md).
|
|
20
53
|
|
|
21
54
|
### Changed
|
|
22
55
|
|
package/README-DOCKER.md
CHANGED
|
@@ -171,3 +171,38 @@ docker logs \
|
|
|
171
171
|
```bash
|
|
172
172
|
docker logs --tail 1000 -f matterbridge
|
|
173
173
|
```
|
|
174
|
+
|
|
175
|
+
### Prevent the logs to grow
|
|
176
|
+
|
|
177
|
+
If you want to prevent the docker logs to grow too much, you can configure Docker's logging options globally.
|
|
178
|
+
|
|
179
|
+
**Warning**: This will restart Docker and affect all running containers.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
sudo nano /etc/docker/daemon.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Add or update the logging configuration in the daemon.json file:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"log-driver": "json-file",
|
|
190
|
+
"log-opts": {
|
|
191
|
+
"max-size": "100m",
|
|
192
|
+
"max-file": "3"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Where:
|
|
198
|
+
|
|
199
|
+
- `max-size`: Maximum size of each log file (e.g., "10m", "100m", "1g")
|
|
200
|
+
- `max-file`: Maximum number of log files to keep
|
|
201
|
+
|
|
202
|
+
Save the file and restart Docker:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
sudo systemctl restart docker
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Note**: This configuration applies to new containers. Existing containers will need to be recreated to use the new logging settings.
|
package/bin/mb_coap.js
ADDED
package/bin/mb_mdns.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { BLUE, db, GREEN, MAGENTA, nf } from 'node-ansi-logger';
|
|
2
|
+
import { COAP_MULTICAST_IPV4_ADDRESS, COAP_MULTICAST_PORT, Multicast } from './multicast.js';
|
|
3
|
+
export const COAP_OPTION_URI_PATH = 11;
|
|
4
|
+
export const COIOT_OPTION_DEVID = 3332;
|
|
5
|
+
export const COIOT_OPTION_VALIDITY = 3412;
|
|
6
|
+
export const COIOT_OPTION_SERIAL = 3420;
|
|
7
|
+
export const COIOT_REQUEST_STATUS_ID = 56831;
|
|
8
|
+
export const COIOT_REQUEST_DESCRIPTION_ID = 56832;
|
|
9
|
+
export class Coap extends Multicast {
|
|
10
|
+
constructor(name, multicastAddress, multicastPort, socketType, reuseAddr = true, interfaceName, interfaceAddress) {
|
|
11
|
+
super(name, multicastAddress, multicastPort, socketType, reuseAddr, interfaceName, interfaceAddress);
|
|
12
|
+
}
|
|
13
|
+
onCoapMessage(message, rinfo) {
|
|
14
|
+
this.log.debug(`Coap message received from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
|
|
15
|
+
}
|
|
16
|
+
onMessage(msg, rinfo) {
|
|
17
|
+
this.log.info(`Dgram multicast socket received a CoAP message from ${BLUE}${rinfo.family}${nf} ${BLUE}${rinfo.address}${nf}:${BLUE}${rinfo.port}${nf}`);
|
|
18
|
+
try {
|
|
19
|
+
const result = this.decodeCoapMessage(msg);
|
|
20
|
+
this.onCoapMessage(result, rinfo);
|
|
21
|
+
this.logCoapMessage(result);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
this.log.error(`Error decoding CoAP message: ${error instanceof Error ? error.message : error}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
decodeCoapMessage(msg) {
|
|
28
|
+
if (msg.length < 4) {
|
|
29
|
+
throw new Error('Message too short to be a valid CoAP message');
|
|
30
|
+
}
|
|
31
|
+
const version = (msg[0] & 0xc0) >> 6;
|
|
32
|
+
const type = (msg[0] & 0x30) >> 4;
|
|
33
|
+
const tokenLength = msg[0] & 0x0f;
|
|
34
|
+
const code = msg[1];
|
|
35
|
+
const messageId = msg.readUInt16BE(2);
|
|
36
|
+
let offset = 4;
|
|
37
|
+
let token = Buffer.alloc(0);
|
|
38
|
+
if (tokenLength > 0) {
|
|
39
|
+
if (msg.length < offset + tokenLength) {
|
|
40
|
+
throw new Error('Message too short for the token length specified');
|
|
41
|
+
}
|
|
42
|
+
token = msg.slice(offset, offset + tokenLength);
|
|
43
|
+
offset += tokenLength;
|
|
44
|
+
}
|
|
45
|
+
const options = [];
|
|
46
|
+
let currentOptionNumber = 0;
|
|
47
|
+
while (offset < msg.length) {
|
|
48
|
+
if (msg[offset] === 0xff) {
|
|
49
|
+
offset++;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
const optionHeader = msg[offset++];
|
|
53
|
+
let delta = (optionHeader & 0xf0) >> 4;
|
|
54
|
+
let length = optionHeader & 0x0f;
|
|
55
|
+
if (delta === 13) {
|
|
56
|
+
if (offset >= msg.length) {
|
|
57
|
+
throw new Error('Invalid extended option delta');
|
|
58
|
+
}
|
|
59
|
+
delta = msg[offset++] + 13;
|
|
60
|
+
}
|
|
61
|
+
else if (delta === 14) {
|
|
62
|
+
if (offset + 1 >= msg.length) {
|
|
63
|
+
throw new Error('Invalid extended option delta');
|
|
64
|
+
}
|
|
65
|
+
delta = msg.readUInt16BE(offset) + 269;
|
|
66
|
+
offset += 2;
|
|
67
|
+
}
|
|
68
|
+
else if (delta === 15) {
|
|
69
|
+
throw new Error('Reserved option delta value encountered');
|
|
70
|
+
}
|
|
71
|
+
if (length === 13) {
|
|
72
|
+
if (offset >= msg.length) {
|
|
73
|
+
throw new Error('Invalid extended option length');
|
|
74
|
+
}
|
|
75
|
+
length = msg[offset++] + 13;
|
|
76
|
+
}
|
|
77
|
+
else if (length === 14) {
|
|
78
|
+
if (offset + 1 >= msg.length) {
|
|
79
|
+
throw new Error('Invalid extended option length');
|
|
80
|
+
}
|
|
81
|
+
length = msg.readUInt16BE(offset) + 269;
|
|
82
|
+
offset += 2;
|
|
83
|
+
}
|
|
84
|
+
else if (length === 15) {
|
|
85
|
+
throw new Error('Reserved option length value encountered');
|
|
86
|
+
}
|
|
87
|
+
currentOptionNumber += delta;
|
|
88
|
+
if (offset + length > msg.length) {
|
|
89
|
+
throw new Error('Option length exceeds message length');
|
|
90
|
+
}
|
|
91
|
+
const optionValue = msg.slice(offset, offset + length);
|
|
92
|
+
offset += length;
|
|
93
|
+
options.push({
|
|
94
|
+
number: currentOptionNumber,
|
|
95
|
+
value: optionValue,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const payload = offset < msg.length ? msg.slice(offset) : undefined;
|
|
99
|
+
return {
|
|
100
|
+
version,
|
|
101
|
+
type,
|
|
102
|
+
tokenLength,
|
|
103
|
+
code,
|
|
104
|
+
messageId,
|
|
105
|
+
token,
|
|
106
|
+
options,
|
|
107
|
+
payload,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
encodeCoapMessage(msg) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
const token = msg.token || Buffer.alloc(0);
|
|
113
|
+
const tokenLength = token.length;
|
|
114
|
+
if (tokenLength > 8) {
|
|
115
|
+
throw new Error('Token length cannot exceed 8 bytes');
|
|
116
|
+
}
|
|
117
|
+
const header = Buffer.alloc(4);
|
|
118
|
+
header[0] = ((msg.version & 0x03) << 6) | ((msg.type & 0x03) << 4) | (tokenLength & 0x0f);
|
|
119
|
+
header[1] = msg.code;
|
|
120
|
+
header.writeUInt16BE(msg.messageId, 2);
|
|
121
|
+
parts.push(header);
|
|
122
|
+
if (tokenLength > 0) {
|
|
123
|
+
parts.push(token);
|
|
124
|
+
}
|
|
125
|
+
const sortedOptions = msg.options.slice().sort((a, b) => a.number - b.number);
|
|
126
|
+
let previousOptionNumber = 0;
|
|
127
|
+
for (const option of sortedOptions) {
|
|
128
|
+
const optionDelta = option.number - previousOptionNumber;
|
|
129
|
+
const optionValueLength = option.value.length;
|
|
130
|
+
let deltaNibble;
|
|
131
|
+
let deltaExtended = null;
|
|
132
|
+
let lengthNibble;
|
|
133
|
+
let lengthExtended = null;
|
|
134
|
+
if (optionDelta < 13) {
|
|
135
|
+
deltaNibble = optionDelta;
|
|
136
|
+
}
|
|
137
|
+
else if (optionDelta < 269) {
|
|
138
|
+
deltaNibble = 13;
|
|
139
|
+
deltaExtended = Buffer.from([optionDelta - 13]);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
deltaNibble = 14;
|
|
143
|
+
deltaExtended = Buffer.alloc(2);
|
|
144
|
+
deltaExtended.writeUInt16BE(optionDelta - 269, 0);
|
|
145
|
+
}
|
|
146
|
+
if (optionValueLength < 13) {
|
|
147
|
+
lengthNibble = optionValueLength;
|
|
148
|
+
}
|
|
149
|
+
else if (optionValueLength < 269) {
|
|
150
|
+
lengthNibble = 13;
|
|
151
|
+
lengthExtended = Buffer.from([optionValueLength - 13]);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
lengthNibble = 14;
|
|
155
|
+
lengthExtended = Buffer.alloc(2);
|
|
156
|
+
lengthExtended.writeUInt16BE(optionValueLength - 269, 0);
|
|
157
|
+
}
|
|
158
|
+
const optionHeader = Buffer.alloc(1);
|
|
159
|
+
optionHeader[0] = (deltaNibble << 4) | (lengthNibble & 0x0f);
|
|
160
|
+
parts.push(optionHeader);
|
|
161
|
+
if (deltaExtended) {
|
|
162
|
+
parts.push(deltaExtended);
|
|
163
|
+
}
|
|
164
|
+
if (lengthExtended) {
|
|
165
|
+
parts.push(lengthExtended);
|
|
166
|
+
}
|
|
167
|
+
parts.push(option.value);
|
|
168
|
+
previousOptionNumber = option.number;
|
|
169
|
+
}
|
|
170
|
+
if (msg.payload && msg.payload.length > 0) {
|
|
171
|
+
parts.push(Buffer.from([0xff]));
|
|
172
|
+
parts.push(msg.payload);
|
|
173
|
+
}
|
|
174
|
+
return Buffer.concat(parts);
|
|
175
|
+
}
|
|
176
|
+
coapTypeToString(type) {
|
|
177
|
+
switch (type) {
|
|
178
|
+
case 0:
|
|
179
|
+
return 'Confirmable (CON)';
|
|
180
|
+
case 1:
|
|
181
|
+
return 'Non-confirmable (NON)';
|
|
182
|
+
case 2:
|
|
183
|
+
return 'Acknowledgement (ACK)';
|
|
184
|
+
case 3:
|
|
185
|
+
return 'Reset (RST)';
|
|
186
|
+
default:
|
|
187
|
+
return `Unknown Type (${type})`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
coapCodeToString(code) {
|
|
191
|
+
const cls = code >> 5;
|
|
192
|
+
const detail = code & 0x1f;
|
|
193
|
+
const codeStr = `${cls}.${detail.toString().padStart(2, '0')}`;
|
|
194
|
+
return codeStr;
|
|
195
|
+
}
|
|
196
|
+
sendRequest(messageId, options, payload, token, address, port) {
|
|
197
|
+
const coapMessage = {
|
|
198
|
+
version: 1,
|
|
199
|
+
type: 1,
|
|
200
|
+
tokenLength: token ? Buffer.from(token).length : 0,
|
|
201
|
+
code: 1,
|
|
202
|
+
messageId,
|
|
203
|
+
token: token ? Buffer.from(token) : Buffer.alloc(0),
|
|
204
|
+
options,
|
|
205
|
+
payload: payload ? Buffer.from(JSON.stringify(payload)) : undefined,
|
|
206
|
+
};
|
|
207
|
+
const encodedBuffer = this.encodeCoapMessage(coapMessage);
|
|
208
|
+
this.send(encodedBuffer, address ?? COAP_MULTICAST_IPV4_ADDRESS, port ?? COAP_MULTICAST_PORT);
|
|
209
|
+
}
|
|
210
|
+
logCoapMessage(msg) {
|
|
211
|
+
this.log.info(`Decoded CoAP message: version ${MAGENTA}${msg.version}${nf} type ${MAGENTA}${this.coapTypeToString(msg.type)}${nf} ` +
|
|
212
|
+
`code ${MAGENTA}${this.coapCodeToString(msg.code)}${nf} messageId ${MAGENTA}${msg.messageId}${nf} ` +
|
|
213
|
+
`tokenLength ${MAGENTA}${msg.tokenLength}${nf} token ${MAGENTA}${msg.tokenLength == 0 ? 'no token' : msg.token.toString('hex')}${nf} ` +
|
|
214
|
+
`options ${MAGENTA}${msg.options.length}${nf} payload ${MAGENTA}${msg.payload ? msg.payload.length + ' bytes' : undefined}${nf}`);
|
|
215
|
+
msg.options.forEach((option) => {
|
|
216
|
+
if (option.number === COAP_OPTION_URI_PATH) {
|
|
217
|
+
this.log.info(`Option: COAP_OPTION_URI_PATH => ${GREEN}${option.value}${nf}`);
|
|
218
|
+
}
|
|
219
|
+
else if (option.number === COIOT_OPTION_DEVID) {
|
|
220
|
+
const parts = option.value.toString().split('#');
|
|
221
|
+
const deviceModel = parts[0];
|
|
222
|
+
const deviceMac = parts[1];
|
|
223
|
+
const protocolRevision = parts[2];
|
|
224
|
+
this.log.info(`Option: COIOT_OPTION_DEVID => ${option.value} => Model: ${GREEN}${deviceModel}${nf}, MAC: ${GREEN}${deviceMac}${nf}, Protocol: ${GREEN}${protocolRevision}${nf}`);
|
|
225
|
+
}
|
|
226
|
+
else if (option.number === COIOT_OPTION_SERIAL) {
|
|
227
|
+
const serial = option.value.readUInt16BE(0);
|
|
228
|
+
this.log.info(`Option: COIOT_OPTION_SERIAL => 0x${option.value.toString('hex')} => serial: ${GREEN}${serial}${nf}`);
|
|
229
|
+
}
|
|
230
|
+
else if (option.number === COIOT_OPTION_VALIDITY) {
|
|
231
|
+
const validity = option.value.readUInt16BE(0);
|
|
232
|
+
let validFor = 0;
|
|
233
|
+
if ((validity & 0x1) === 0) {
|
|
234
|
+
validFor = Math.floor(validity / 10);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
validFor = validity * 4;
|
|
238
|
+
}
|
|
239
|
+
this.log.info(`Option: COIOT_OPTION_VALIDITY => 0x${option.value.toString('hex')} => valid for: ${GREEN}${validFor}${nf} seconds`);
|
|
240
|
+
}
|
|
241
|
+
else
|
|
242
|
+
this.log.info(`Option: ${option.number} - ${option.value
|
|
243
|
+
.toString('hex')
|
|
244
|
+
.match(/.{1,2}/g)
|
|
245
|
+
?.join(' ')}`);
|
|
246
|
+
});
|
|
247
|
+
if (msg.payload && msg.payload.length > 0)
|
|
248
|
+
this.log.info(`Payload:`, JSON.parse(msg.payload.toString()), '\n');
|
|
249
|
+
else
|
|
250
|
+
this.log.info(`No payload`, '\n');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import dgram from 'node:dgram';
|
|
2
|
+
import EventEmitter from 'node:events';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { AnsiLogger, BLUE, db, idn, nf, rs } from 'node-ansi-logger';
|
|
5
|
+
export class Dgram extends EventEmitter {
|
|
6
|
+
log;
|
|
7
|
+
socket;
|
|
8
|
+
bound = false;
|
|
9
|
+
socketType;
|
|
10
|
+
interfaceName;
|
|
11
|
+
interfaceAddress;
|
|
12
|
+
interfaceNetmask;
|
|
13
|
+
constructor(name, socketType, reuseAddr = true, interfaceName, interfaceAddress) {
|
|
14
|
+
super();
|
|
15
|
+
this.log = new AnsiLogger({ logName: name, logTimestampFormat: 4, logLevel: "debug" });
|
|
16
|
+
this.socket = dgram.createSocket({ type: socketType, reuseAddr });
|
|
17
|
+
this.socketType = socketType;
|
|
18
|
+
this.interfaceName = interfaceName;
|
|
19
|
+
this.interfaceAddress = interfaceAddress;
|
|
20
|
+
this.socket.on('error', (error) => {
|
|
21
|
+
this.log.debug(`Socket error: ${error instanceof Error ? error.message : error}`);
|
|
22
|
+
this.emit('error', error);
|
|
23
|
+
this.onError(error);
|
|
24
|
+
});
|
|
25
|
+
this.socket.on('close', () => {
|
|
26
|
+
this.log.debug('Socket closed');
|
|
27
|
+
this.bound = false;
|
|
28
|
+
this.emit('close');
|
|
29
|
+
this.onClose();
|
|
30
|
+
});
|
|
31
|
+
this.socket.on('connect', () => {
|
|
32
|
+
this.log.info('Socket connected');
|
|
33
|
+
this.emit('connect');
|
|
34
|
+
this.onConnect();
|
|
35
|
+
});
|
|
36
|
+
this.socket.on('message', (msg, rinfo) => {
|
|
37
|
+
this.log.debug(`Socket received a message from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
|
|
38
|
+
this.emit('message', msg, rinfo);
|
|
39
|
+
this.onMessage(msg, rinfo);
|
|
40
|
+
});
|
|
41
|
+
this.socket.on('listening', () => {
|
|
42
|
+
this.bound = true;
|
|
43
|
+
const address = this.socket.address();
|
|
44
|
+
this.log.debug(`Socket listening on ${BLUE}${address.family}${db} ${BLUE}${address.address}${db}:${BLUE}${address.port}${db}`);
|
|
45
|
+
this.emit('listening', address);
|
|
46
|
+
this.onListening(address);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
send(msg, serverAddress, serverPort) {
|
|
50
|
+
this.socket.send(msg, 0, msg.length, serverPort, serverAddress, (error) => {
|
|
51
|
+
if (error) {
|
|
52
|
+
this.log.error(`Socket failed to send a message: ${error instanceof Error ? error.message : error}`);
|
|
53
|
+
this.emit('error', error);
|
|
54
|
+
this.onError(error);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.log.debug(`Socket sent a message to ${BLUE}${serverAddress}${db}:${BLUE}${serverPort}${db}`);
|
|
58
|
+
this.emit('sent', msg, serverAddress, serverPort);
|
|
59
|
+
this.onSent(msg, serverAddress, serverPort);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
onError(error) {
|
|
64
|
+
this.log.error(`Socket error: ${error instanceof Error ? error.message : error}`);
|
|
65
|
+
}
|
|
66
|
+
onClose() {
|
|
67
|
+
this.log.info(`Socket closed`);
|
|
68
|
+
}
|
|
69
|
+
onConnect() {
|
|
70
|
+
this.log.info(`Socket connected`);
|
|
71
|
+
}
|
|
72
|
+
onSent(msg, serverAddress, serverPort) {
|
|
73
|
+
this.log.info(`Socket sent a message to ${BLUE}${serverAddress}${db}:${BLUE}${serverPort}${db}`);
|
|
74
|
+
}
|
|
75
|
+
onMessage(msg, rinfo) {
|
|
76
|
+
this.log.info(`Socket received a message from ${BLUE}${rinfo.family}${nf} ${BLUE}${rinfo.address}${nf}:${BLUE}${rinfo.port}${nf}`);
|
|
77
|
+
}
|
|
78
|
+
onListening(address) {
|
|
79
|
+
this.log.info(`Socket listening on ${BLUE}${address.family}${nf} ${BLUE}${address.address}${nf}:${BLUE}${address.port}${nf}`);
|
|
80
|
+
this.onReady(address);
|
|
81
|
+
}
|
|
82
|
+
onReady(address) {
|
|
83
|
+
this.log.info(`Socket ready on ${BLUE}${address.family}${nf} ${BLUE}${address.address}${nf}:${BLUE}${address.port}${nf}`);
|
|
84
|
+
this.emit('ready', address);
|
|
85
|
+
}
|
|
86
|
+
getIpv4InterfaceAddress(networkInterface) {
|
|
87
|
+
if (networkInterface === '')
|
|
88
|
+
networkInterface = undefined;
|
|
89
|
+
const interfaces = os.networkInterfaces();
|
|
90
|
+
if (networkInterface && !interfaces[networkInterface]) {
|
|
91
|
+
this.log.warn(`Interface "${networkInterface}" not found. Using first external IPv4 interface.`);
|
|
92
|
+
networkInterface = undefined;
|
|
93
|
+
}
|
|
94
|
+
if (!networkInterface) {
|
|
95
|
+
for (const [interfaceName, interfaceDetails] of Object.entries(interfaces)) {
|
|
96
|
+
if (!interfaceDetails)
|
|
97
|
+
continue;
|
|
98
|
+
for (const detail of interfaceDetails) {
|
|
99
|
+
if (detail.family === 'IPv4' && !detail.internal) {
|
|
100
|
+
networkInterface = interfaceName;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (networkInterface)
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!networkInterface) {
|
|
109
|
+
throw new Error(`Didn't find an external IPv4 network interface`);
|
|
110
|
+
}
|
|
111
|
+
const addresses = interfaces[networkInterface];
|
|
112
|
+
const ipv4Address = addresses?.find((addr) => addr.family === 'IPv4' && !addr.internal);
|
|
113
|
+
if (!ipv4Address) {
|
|
114
|
+
throw new Error(`Interface ${networkInterface} does not have an external IPv4 address`);
|
|
115
|
+
}
|
|
116
|
+
return ipv4Address.address;
|
|
117
|
+
}
|
|
118
|
+
getIpv6InterfaceAddress(networkInterface) {
|
|
119
|
+
if (networkInterface === '')
|
|
120
|
+
networkInterface = undefined;
|
|
121
|
+
const interfaces = os.networkInterfaces();
|
|
122
|
+
if (networkInterface && !interfaces[networkInterface]) {
|
|
123
|
+
this.log.warn(`Interface "${networkInterface}" not found. Using first external IPv6 interface.`);
|
|
124
|
+
networkInterface = undefined;
|
|
125
|
+
}
|
|
126
|
+
if (!networkInterface) {
|
|
127
|
+
for (const [interfaceName, interfaceDetails] of Object.entries(interfaces)) {
|
|
128
|
+
if (!interfaceDetails)
|
|
129
|
+
continue;
|
|
130
|
+
for (const detail of interfaceDetails) {
|
|
131
|
+
if (detail.family === 'IPv6' && !detail.internal) {
|
|
132
|
+
networkInterface = interfaceName;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (networkInterface)
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!networkInterface) {
|
|
141
|
+
throw new Error(`Didn't find an external IPv6 network interface`);
|
|
142
|
+
}
|
|
143
|
+
const addresses = interfaces[networkInterface];
|
|
144
|
+
const linkLocalAddress = addresses?.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.address.startsWith('fe80'));
|
|
145
|
+
if (linkLocalAddress) {
|
|
146
|
+
this.log.debug('Found IPv6 link-local address');
|
|
147
|
+
return linkLocalAddress.scopeid ? `${linkLocalAddress.address}%${process.platform !== 'win32' ? networkInterface : linkLocalAddress.scopeid}` : linkLocalAddress.address;
|
|
148
|
+
}
|
|
149
|
+
this.log.debug('No IPv6 link-local address found');
|
|
150
|
+
const ulaAddress = addresses?.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.address.startsWith('fd') && addr.netmask === 'ffff:ffff:ffff:ffff::');
|
|
151
|
+
if (ulaAddress) {
|
|
152
|
+
this.log.debug('Found IPv6 Unique Local Addresses (ULA) unicast address');
|
|
153
|
+
return ulaAddress.address;
|
|
154
|
+
}
|
|
155
|
+
this.log.debug('No IPv6 Unique Local Addresses (ULA) unicast address found');
|
|
156
|
+
const uniqueLocalAddress = addresses?.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.address.startsWith('fd'));
|
|
157
|
+
if (uniqueLocalAddress) {
|
|
158
|
+
this.log.debug('Found IPv6 Unique Local Addresses (ULA) address');
|
|
159
|
+
return uniqueLocalAddress.address;
|
|
160
|
+
}
|
|
161
|
+
this.log.debug('No IPv6 Unique Local Addresses (ULA) address found');
|
|
162
|
+
throw new Error(`Interface ${networkInterface} does not have a suitable external IPv6 address`);
|
|
163
|
+
}
|
|
164
|
+
getInterfacesNames() {
|
|
165
|
+
const interfaces = os.networkInterfaces();
|
|
166
|
+
const interfaceNames = [];
|
|
167
|
+
for (const name in interfaces) {
|
|
168
|
+
if (interfaces[name]) {
|
|
169
|
+
interfaceNames.push(name);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return interfaceNames;
|
|
173
|
+
}
|
|
174
|
+
getIpv6ScopeIdForAllInterfacesAddress() {
|
|
175
|
+
const interfaces = os.networkInterfaces();
|
|
176
|
+
for (const name in interfaces) {
|
|
177
|
+
const iface = interfaces[name];
|
|
178
|
+
if (iface) {
|
|
179
|
+
const ipv6Address = iface.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.scopeid);
|
|
180
|
+
if (ipv6Address) {
|
|
181
|
+
return process.platform === 'win32' ? '%' + String(ipv6Address.scopeid) : '%' + name;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
getInterfaceNameFromScopeId(scopeId) {
|
|
188
|
+
const nets = os.networkInterfaces();
|
|
189
|
+
for (const ifaceName in nets) {
|
|
190
|
+
const addresses = nets[ifaceName] || [];
|
|
191
|
+
for (const addr of addresses) {
|
|
192
|
+
if (addr.family === 'IPv6' && addr.scopeid === scopeId) {
|
|
193
|
+
return ifaceName;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
getNetmask(interfaceAddress) {
|
|
200
|
+
const cleanedAddress = interfaceAddress.includes('%') ? interfaceAddress.split('%')[0] : interfaceAddress;
|
|
201
|
+
const nets = os.networkInterfaces();
|
|
202
|
+
for (const ifaceName in nets) {
|
|
203
|
+
const ifaceAddresses = nets[ifaceName];
|
|
204
|
+
if (!ifaceAddresses)
|
|
205
|
+
continue;
|
|
206
|
+
for (const addr of ifaceAddresses) {
|
|
207
|
+
if (addr.address === cleanedAddress) {
|
|
208
|
+
return addr.netmask;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
getIpv4BroadcastAddress(ipAddress, netmask) {
|
|
215
|
+
if (!ipAddress || !netmask) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
const ipParts = ipAddress.split('.').map(Number);
|
|
219
|
+
const maskParts = netmask.split('.').map(Number);
|
|
220
|
+
const broadcastParts = ipParts.map((octet, i) => (octet & maskParts[i]) | (255 - maskParts[i]));
|
|
221
|
+
return broadcastParts.join('.');
|
|
222
|
+
}
|
|
223
|
+
getIpv6BroadcastAddress() {
|
|
224
|
+
return 'ff02::1';
|
|
225
|
+
}
|
|
226
|
+
listNetworkInterfaces() {
|
|
227
|
+
const interfaces = os.networkInterfaces();
|
|
228
|
+
for (const [name, addresses] of Object.entries(interfaces)) {
|
|
229
|
+
if (!addresses)
|
|
230
|
+
continue;
|
|
231
|
+
this.log.debug(`Interface: ${idn}${name}${rs}${db}`);
|
|
232
|
+
for (const address of addresses) {
|
|
233
|
+
this.log.debug(`- address ${BLUE}${address.address}${db} netmask ${BLUE}${address.netmask}${db} ${address.mac ? 'MAC: ' + BLUE + address.mac + db : ''} type: ${BLUE}${address.family}${db} ${BLUE}${address.internal ? 'internal' : 'external'}${db} ${address.scopeid !== undefined ? 'scopeid: ' + BLUE + address.scopeid + db : ''} cidr: ${BLUE}${address.cidr}${db}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { COAP_MULTICAST_IPV4_ADDRESS, COAP_MULTICAST_IPV6_ADDRESS, COAP_MULTICAST_PORT } from './multicast.js';
|
|
2
|
+
import { Coap, COAP_OPTION_URI_PATH } from './coap.js';
|
|
3
|
+
{
|
|
4
|
+
const coapIpv4 = new Coap('CoAP Server udp4', COAP_MULTICAST_IPV4_ADDRESS, COAP_MULTICAST_PORT, 'udp4', true);
|
|
5
|
+
const coapIpv6 = new Coap('CoAP Server udp6', COAP_MULTICAST_IPV6_ADDRESS, COAP_MULTICAST_PORT, 'udp6', true);
|
|
6
|
+
coapIpv4.listNetworkInterfaces();
|
|
7
|
+
function cleanupAndLogAndExit() {
|
|
8
|
+
if (process.argv.includes('--coap-udp4'))
|
|
9
|
+
coapIpv4.stop();
|
|
10
|
+
if (process.argv.includes('--coap-udp6'))
|
|
11
|
+
coapIpv6.stop();
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
const requestUdp4 = () => {
|
|
15
|
+
coapIpv4.sendRequest(32000, [
|
|
16
|
+
{ number: COAP_OPTION_URI_PATH, value: Buffer.from('cit') },
|
|
17
|
+
{ number: COAP_OPTION_URI_PATH, value: Buffer.from('d') },
|
|
18
|
+
], {}, undefined, COAP_MULTICAST_IPV4_ADDRESS, COAP_MULTICAST_PORT);
|
|
19
|
+
};
|
|
20
|
+
const requestUdp6 = () => {
|
|
21
|
+
coapIpv6.sendRequest(32000, [
|
|
22
|
+
{ number: COAP_OPTION_URI_PATH, value: Buffer.from('cit') },
|
|
23
|
+
{ number: COAP_OPTION_URI_PATH, value: Buffer.from('d') },
|
|
24
|
+
], {}, undefined, COAP_MULTICAST_IPV6_ADDRESS, COAP_MULTICAST_PORT);
|
|
25
|
+
};
|
|
26
|
+
process.on('SIGINT', () => {
|
|
27
|
+
cleanupAndLogAndExit();
|
|
28
|
+
});
|
|
29
|
+
coapIpv4.start();
|
|
30
|
+
coapIpv4.on('ready', (address) => {
|
|
31
|
+
coapIpv4.log.info(`coapIpv4 server ready on ${address.family} ${address.address}:${address.port}`);
|
|
32
|
+
if (!process.argv.includes('--coap-request'))
|
|
33
|
+
return;
|
|
34
|
+
requestUdp4();
|
|
35
|
+
setInterval(() => {
|
|
36
|
+
requestUdp4();
|
|
37
|
+
}, 10000).unref();
|
|
38
|
+
});
|
|
39
|
+
coapIpv6.start();
|
|
40
|
+
coapIpv6.on('ready', (address) => {
|
|
41
|
+
coapIpv6.log.info(`coapIpv6 server ready on ${address.family} ${address.address}:${address.port}`);
|
|
42
|
+
if (!process.argv.includes('--coap-request'))
|
|
43
|
+
return;
|
|
44
|
+
requestUdp6();
|
|
45
|
+
setInterval(() => {
|
|
46
|
+
requestUdp6();
|
|
47
|
+
}, 10000).unref();
|
|
48
|
+
});
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
cleanupAndLogAndExit();
|
|
51
|
+
}, 600000);
|
|
52
|
+
}
|