node-red-contrib-meshtastic-advanced 1.0.0
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/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/decoder-BvBAtm2U.js +166 -0
- package/dist/decoder-BvBAtm2U.js.map +1 -0
- package/dist/encoder-DU3wTIEA.js +126 -0
- package/dist/encoder-DU3wTIEA.js.map +1 -0
- package/dist/message-types-FA26fPjF.js +91 -0
- package/dist/message-types-FA26fPjF.js.map +1 -0
- package/dist/nodes/decode/decode.html +108 -0
- package/dist/nodes/decode/decode.js +140 -0
- package/dist/nodes/decode/decode.js.map +1 -0
- package/dist/nodes/decrypt/decrypt.html +127 -0
- package/dist/nodes/decrypt/decrypt.js +163 -0
- package/dist/nodes/decrypt/decrypt.js.map +1 -0
- package/dist/nodes/encode/encode.html +143 -0
- package/dist/nodes/encode/encode.js +94 -0
- package/dist/nodes/encode/encode.js.map +1 -0
- package/dist/nodes/encrypt/encrypt.html +164 -0
- package/dist/nodes/encrypt/encrypt.js +199 -0
- package/dist/nodes/encrypt/encrypt.js.map +1 -0
- package/dist/validation-BegQyBSh.js +34 -0
- package/dist/validation-BegQyBSh.js.map +1 -0
- package/dist/x25519-D0dlDGB0.js +771 -0
- package/dist/x25519-D0dlDGB0.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Node-RED Meshtastic Advanced Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# node-red-contrib-meshtastic-advanced
|
|
2
|
+
|
|
3
|
+
Advanced Node-RED nodes for Meshtastic message encoding, decoding, encryption, and decryption.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Full Encryption Support**: Both channel (PSK) and direct message (PKC) encryption
|
|
8
|
+
- ✅ **All Message Types**: Supports all Meshtastic message types (TEXT, POSITION, TELEMETRY, etc.)
|
|
9
|
+
- ✅ **Modern Architecture**: TypeScript-based with proper error handling
|
|
10
|
+
- ✅ **Secure Key Storage**: Uses Node-RED credentials API for encrypted key storage
|
|
11
|
+
- ✅ **Flexible Design**: Modular nodes that can be chained for custom workflows
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install node-red-contrib-meshtastic-advanced
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly in Node-RED:
|
|
20
|
+
1. Go to Menu → Manage palette
|
|
21
|
+
2. Select the Install tab
|
|
22
|
+
3. Search for `node-red-contrib-meshtastic-advanced`
|
|
23
|
+
4. Click Install
|
|
24
|
+
|
|
25
|
+
## Nodes
|
|
26
|
+
|
|
27
|
+
### 🔓 meshtastic-decrypt
|
|
28
|
+
|
|
29
|
+
Decrypts encrypted Meshtastic packets using either:
|
|
30
|
+
- **Channel encryption (PSK)**: AES-128/256-CTR with pre-shared key
|
|
31
|
+
- **Direct message encryption (PKC)**: x25519 + AES-256-CCM
|
|
32
|
+
|
|
33
|
+
**Configuration:**
|
|
34
|
+
- **Mode**: Auto-detect, Channel (PSK), or Direct Message (PKC)
|
|
35
|
+
- **Channel Key**: Pre-shared key for channel decryption (16 or 32 bytes)
|
|
36
|
+
- **Private Key**: Your x25519 private key for DM decryption
|
|
37
|
+
- **Public Keys**: JSON mapping of sender node numbers to public keys
|
|
38
|
+
|
|
39
|
+
**Input:** Binary ServiceEnvelope with encrypted packet
|
|
40
|
+
|
|
41
|
+
**Output:** ServiceEnvelope with decrypted payload
|
|
42
|
+
|
|
43
|
+
### 📖 meshtastic-decode
|
|
44
|
+
|
|
45
|
+
Decodes Meshtastic protobuf payloads to readable JSON.
|
|
46
|
+
|
|
47
|
+
**Configuration:**
|
|
48
|
+
- **Output Format**: Flat (all fields at top level) or Nested (organized structure)
|
|
49
|
+
- **Include Raw**: Optionally include raw protobuf bytes
|
|
50
|
+
|
|
51
|
+
**Input:** ServiceEnvelope (binary or object) with decoded payload
|
|
52
|
+
|
|
53
|
+
**Output:** JSON object with decoded message content
|
|
54
|
+
|
|
55
|
+
**Supported Message Types:**
|
|
56
|
+
- TEXT_MESSAGE_APP
|
|
57
|
+
- POSITION_APP
|
|
58
|
+
- NODEINFO_APP
|
|
59
|
+
- TELEMETRY_APP
|
|
60
|
+
- ROUTING_APP
|
|
61
|
+
- ADMIN_APP
|
|
62
|
+
- WAYPOINT_APP
|
|
63
|
+
- NEIGHBORINFO_APP
|
|
64
|
+
- TRACEROUTE_APP
|
|
65
|
+
- And more...
|
|
66
|
+
|
|
67
|
+
### ✏️ meshtastic-encode
|
|
68
|
+
|
|
69
|
+
Encodes JSON messages to Meshtastic protobuf format.
|
|
70
|
+
|
|
71
|
+
**Configuration:**
|
|
72
|
+
- **Default To**: Destination node (default: broadcast 0xFFFFFFFF)
|
|
73
|
+
- **Default Channel**: Channel index 0-7 (default: 0)
|
|
74
|
+
- **Default Hop Limit**: Max hops 0-7 (default: 3)
|
|
75
|
+
- **Output Type**: ServiceEnvelope (full packet) or Data only
|
|
76
|
+
- **Output Format**: Object or Buffer
|
|
77
|
+
|
|
78
|
+
**Input Example:**
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"portNum": "TEXT_MESSAGE_APP",
|
|
82
|
+
"message": "Hello, mesh!",
|
|
83
|
+
"to": 123456,
|
|
84
|
+
"channel": 0,
|
|
85
|
+
"wantAck": false
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 🔒 meshtastic-encrypt
|
|
90
|
+
|
|
91
|
+
Encrypts Meshtastic payloads using channel PSK or direct message PKC.
|
|
92
|
+
|
|
93
|
+
**Configuration:**
|
|
94
|
+
- **Mode**: Channel (PSK) or Direct Message (PKC)
|
|
95
|
+
- **From Node**: Source node number
|
|
96
|
+
- **Channel Key**: Pre-shared key for channel encryption
|
|
97
|
+
- **My Private Key**: Your x25519 private key for DM encryption
|
|
98
|
+
- **Recipient Public Key**: Recipient's x25519 public key
|
|
99
|
+
|
|
100
|
+
**Input:** Data protobuf or ServiceEnvelope
|
|
101
|
+
|
|
102
|
+
**Output:** Encrypted ServiceEnvelope
|
|
103
|
+
|
|
104
|
+
## Example Flows
|
|
105
|
+
|
|
106
|
+
### Receive and Decode Encrypted Messages
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
[MQTT In] → [meshtastic-decrypt] → [meshtastic-decode] → [Debug]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
1. Subscribe to MQTT topic for incoming Meshtastic messages
|
|
113
|
+
2. Decrypt using channel key
|
|
114
|
+
3. Decode to readable JSON
|
|
115
|
+
4. Display in debug panel
|
|
116
|
+
|
|
117
|
+
### Send Encrypted Text Messages
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
[Inject] → [meshtastic-encode] → [meshtastic-encrypt] → [MQTT Out]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
1. Create message payload
|
|
124
|
+
2. Encode to protobuf
|
|
125
|
+
3. Encrypt with channel key
|
|
126
|
+
4. Publish to MQTT
|
|
127
|
+
|
|
128
|
+
### Multi-Channel Monitoring
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
[MQTT In] → [Switch] → [meshtastic-decrypt (ch 0)]
|
|
132
|
+
→ [meshtastic-decrypt (ch 1)]
|
|
133
|
+
→ [meshtastic-decrypt (ch 2)]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Monitor multiple channels with different encryption keys.
|
|
137
|
+
|
|
138
|
+
## Security Considerations
|
|
139
|
+
|
|
140
|
+
### Default Key Warning
|
|
141
|
+
|
|
142
|
+
⚠️ **The default Meshtastic channel key is `AQ==` (base64) which is INSECURE.**
|
|
143
|
+
|
|
144
|
+
Always configure a secure channel key. The nodes will warn you if you use the default key.
|
|
145
|
+
|
|
146
|
+
### Key Storage
|
|
147
|
+
|
|
148
|
+
All encryption keys are stored using Node-RED's credentials system, which encrypts them at rest. Keys are never exported in flow JSON files.
|
|
149
|
+
|
|
150
|
+
### Supported Key Formats
|
|
151
|
+
|
|
152
|
+
- **Base64**: Standard base64 encoding (e.g., `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=`)
|
|
153
|
+
- **Hex**: Hexadecimal encoding (e.g., `0000000000000000000000000000000000000000000000000000000000000000`)
|
|
154
|
+
|
|
155
|
+
### Key Lengths
|
|
156
|
+
|
|
157
|
+
- **Channel PSK**: 16 bytes (AES-128) or 32 bytes (AES-256)
|
|
158
|
+
- **x25519 Keys**: 32 bytes
|
|
159
|
+
|
|
160
|
+
## Encryption Details
|
|
161
|
+
|
|
162
|
+
### Channel Encryption (PSK + AES-CTR)
|
|
163
|
+
|
|
164
|
+
Meshtastic uses AES-CTR for channel encryption with a nonce constructed from:
|
|
165
|
+
- Node number (from field): 32 bits
|
|
166
|
+
- Packet ID: 32 bits
|
|
167
|
+
- Counter: Increments per 16-byte block
|
|
168
|
+
|
|
169
|
+
### Direct Message Encryption (PKC + AES-CCM)
|
|
170
|
+
|
|
171
|
+
For v2.5+ direct messages:
|
|
172
|
+
1. Each node has an x25519 key pair
|
|
173
|
+
2. Shared secret derived using ECDH
|
|
174
|
+
3. Messages encrypted with AES-256-CCM (provides authentication)
|
|
175
|
+
4. Random nonce per message
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
### Build
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npm install
|
|
183
|
+
npm run build
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Run Tests
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npm test
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Local Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Link to local Node-RED
|
|
196
|
+
cd ~/.node-red
|
|
197
|
+
npm link /path/to/node-red-contrib-meshtastic-advanced
|
|
198
|
+
|
|
199
|
+
# Watch mode
|
|
200
|
+
npm run dev
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Troubleshooting
|
|
204
|
+
|
|
205
|
+
### "Packet is still encrypted" error
|
|
206
|
+
|
|
207
|
+
Make sure you use the **meshtastic-decrypt** node before the decode node. The decode node only works with already-decrypted packets.
|
|
208
|
+
|
|
209
|
+
### "Channel decryption requires a channelKey" error
|
|
210
|
+
|
|
211
|
+
Configure the channel key in the decrypt node settings. Make sure it matches the key used by your Meshtastic devices.
|
|
212
|
+
|
|
213
|
+
### "No public key found for sender" error
|
|
214
|
+
|
|
215
|
+
For DM decryption, you need to configure the sender's public key in the decrypt node's "Public Keys" field.
|
|
216
|
+
|
|
217
|
+
### "Invalid key length" error
|
|
218
|
+
|
|
219
|
+
Check that your keys are the correct length:
|
|
220
|
+
- Channel keys: 16 bytes (AES-128) or 32 bytes (AES-256)
|
|
221
|
+
- x25519 keys: 32 bytes
|
|
222
|
+
|
|
223
|
+
## Migration from Old Plugin
|
|
224
|
+
|
|
225
|
+
The old `@meshtastic/node-red-contrib-meshtastic` plugin had limited functionality:
|
|
226
|
+
- Only had a decode node (no decrypt, encode, or encrypt)
|
|
227
|
+
- Decrypt node was non-functional
|
|
228
|
+
- No PKC/DM support
|
|
229
|
+
|
|
230
|
+
This new plugin is a complete rewrite with full functionality.
|
|
231
|
+
|
|
232
|
+
### Migration Steps
|
|
233
|
+
|
|
234
|
+
1. Uninstall old plugin: `npm uninstall @meshtastic/node-red-contrib-meshtastic`
|
|
235
|
+
2. Install new plugin: `npm install node-red-contrib-meshtastic-advanced`
|
|
236
|
+
3. Update flows to use new node names (meshtastic-decrypt, meshtastic-decode, etc.)
|
|
237
|
+
4. Configure encryption keys in decrypt/encrypt nodes
|
|
238
|
+
|
|
239
|
+
## References
|
|
240
|
+
|
|
241
|
+
- [Meshtastic Official Site](https://meshtastic.org/)
|
|
242
|
+
- [Meshtastic Encryption Docs](https://meshtastic.org/docs/overview/encryption/)
|
|
243
|
+
- [Meshtastic Protobufs](https://github.com/meshtastic/protobufs)
|
|
244
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
249
|
+
|
|
250
|
+
## Contributing
|
|
251
|
+
|
|
252
|
+
Contributions are welcome! Please open an issue or pull request on GitHub.
|
|
253
|
+
|
|
254
|
+
## Acknowledgments
|
|
255
|
+
|
|
256
|
+
- [Meshtastic Project](https://github.com/meshtastic) for the amazing mesh networking platform
|
|
257
|
+
- [@noble/curves](https://github.com/paulmillr/noble-curves) for x25519 implementation
|
|
258
|
+
- Node-RED community for the excellent flow-based programming tool
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const protobufs = require("@meshtastic/protobufs");
|
|
3
|
+
class MeshtasticError extends Error {
|
|
4
|
+
constructor(message, cause) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
this.name = "MeshtasticError";
|
|
8
|
+
if (cause) {
|
|
9
|
+
this.stack = `${this.stack}
|
|
10
|
+
Caused by: ${cause.stack}`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class MeshtasticDecryptError extends MeshtasticError {
|
|
15
|
+
constructor(message, cause) {
|
|
16
|
+
super(message, cause);
|
|
17
|
+
this.name = "MeshtasticDecryptError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class MeshtasticDecodeError extends MeshtasticError {
|
|
21
|
+
constructor(message, cause) {
|
|
22
|
+
super(message, cause);
|
|
23
|
+
this.name = "MeshtasticDecodeError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class MeshtasticEncodeError extends MeshtasticError {
|
|
27
|
+
constructor(message, cause) {
|
|
28
|
+
super(message, cause);
|
|
29
|
+
this.name = "MeshtasticEncodeError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class MeshtasticEncryptError extends MeshtasticError {
|
|
33
|
+
constructor(message, cause) {
|
|
34
|
+
super(message, cause);
|
|
35
|
+
this.name = "MeshtasticEncryptError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class InvalidKeyError extends MeshtasticError {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "InvalidKeyError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
class InvalidNonceError extends MeshtasticError {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "InvalidNonceError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class ProtobufError extends MeshtasticError {
|
|
51
|
+
constructor(message, cause) {
|
|
52
|
+
super(message, cause);
|
|
53
|
+
this.name = "ProtobufError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function ensureBuffer(input) {
|
|
57
|
+
if (Buffer.isBuffer(input)) {
|
|
58
|
+
return input;
|
|
59
|
+
}
|
|
60
|
+
return Buffer.from(input);
|
|
61
|
+
}
|
|
62
|
+
function ensureUint8Array(input) {
|
|
63
|
+
if (input instanceof Uint8Array && !(input instanceof Buffer)) {
|
|
64
|
+
return input;
|
|
65
|
+
}
|
|
66
|
+
return new Uint8Array(input);
|
|
67
|
+
}
|
|
68
|
+
function decodeServiceEnvelope(buffer) {
|
|
69
|
+
try {
|
|
70
|
+
const uint8 = ensureUint8Array(buffer);
|
|
71
|
+
return protobufs.Protobuf.Mqtt.ServiceEnvelope.fromBinary(uint8);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new ProtobufError(
|
|
74
|
+
`Failed to decode ServiceEnvelope: ${error instanceof Error ? error.message : String(error)}`,
|
|
75
|
+
error instanceof Error ? error : void 0
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function decodeData(buffer) {
|
|
80
|
+
try {
|
|
81
|
+
const uint8 = ensureUint8Array(buffer);
|
|
82
|
+
return protobufs.Protobuf.Mesh.Data.fromBinary(uint8);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new ProtobufError(
|
|
85
|
+
`Failed to decode Data: ${error instanceof Error ? error.message : String(error)}`,
|
|
86
|
+
error instanceof Error ? error : void 0
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function decodeMessageByPortNum(portNum, payload) {
|
|
91
|
+
try {
|
|
92
|
+
const uint8 = ensureUint8Array(payload);
|
|
93
|
+
switch (portNum) {
|
|
94
|
+
case protobufs.Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP:
|
|
95
|
+
return new TextDecoder().decode(uint8);
|
|
96
|
+
case protobufs.Protobuf.Portnums.PortNum.POSITION_APP:
|
|
97
|
+
return protobufs.Protobuf.Mesh.Position.fromBinary(uint8);
|
|
98
|
+
case protobufs.Protobuf.Portnums.PortNum.NODEINFO_APP:
|
|
99
|
+
return protobufs.Protobuf.Mesh.User.fromBinary(uint8);
|
|
100
|
+
case protobufs.Protobuf.Portnums.PortNum.TELEMETRY_APP:
|
|
101
|
+
return protobufs.Protobuf.Telemetry.Telemetry.fromBinary(uint8);
|
|
102
|
+
case protobufs.Protobuf.Portnums.PortNum.ROUTING_APP:
|
|
103
|
+
return protobufs.Protobuf.Mesh.Routing.fromBinary(uint8);
|
|
104
|
+
case protobufs.Protobuf.Portnums.PortNum.ADMIN_APP:
|
|
105
|
+
return protobufs.Protobuf.Admin.AdminMessage.fromBinary(uint8);
|
|
106
|
+
case protobufs.Protobuf.Portnums.PortNum.WAYPOINT_APP:
|
|
107
|
+
return protobufs.Protobuf.Mesh.Waypoint.fromBinary(uint8);
|
|
108
|
+
case protobufs.Protobuf.Portnums.PortNum.NEIGHBORINFO_APP:
|
|
109
|
+
return protobufs.Protobuf.Mesh.NeighborInfo.fromBinary(uint8);
|
|
110
|
+
case protobufs.Protobuf.Portnums.PortNum.STORE_FORWARD_APP:
|
|
111
|
+
return protobufs.Protobuf.StoreForward.StoreAndForward.fromBinary(uint8);
|
|
112
|
+
case protobufs.Protobuf.Portnums.PortNum.RANGE_TEST_APP:
|
|
113
|
+
return new TextDecoder("ascii").decode(uint8);
|
|
114
|
+
case protobufs.Protobuf.Portnums.PortNum.REMOTE_HARDWARE_APP:
|
|
115
|
+
return protobufs.Protobuf.RemoteHardware.HardwareMessage.fromBinary(uint8);
|
|
116
|
+
case protobufs.Protobuf.Portnums.PortNum.DETECTION_SENSOR_APP:
|
|
117
|
+
return new TextDecoder().decode(uint8);
|
|
118
|
+
case protobufs.Protobuf.Portnums.PortNum.REPLY_APP:
|
|
119
|
+
return new TextDecoder("ascii").decode(uint8);
|
|
120
|
+
case protobufs.Protobuf.Portnums.PortNum.TRACEROUTE_APP:
|
|
121
|
+
return protobufs.Protobuf.Mesh.RouteDiscovery.fromBinary(uint8);
|
|
122
|
+
case protobufs.Protobuf.Portnums.PortNum.PAXCOUNTER_APP:
|
|
123
|
+
return protobufs.Protobuf.Paxcount.Paxcount.fromBinary(uint8);
|
|
124
|
+
case protobufs.Protobuf.Portnums.PortNum.MAP_REPORT_APP:
|
|
125
|
+
return protobufs.Protobuf.Mqtt.MapReport.fromBinary(uint8);
|
|
126
|
+
case protobufs.Protobuf.Portnums.PortNum.UNKNOWN_APP:
|
|
127
|
+
case protobufs.Protobuf.Portnums.PortNum.IP_TUNNEL_APP:
|
|
128
|
+
case protobufs.Protobuf.Portnums.PortNum.SERIAL_APP:
|
|
129
|
+
case protobufs.Protobuf.Portnums.PortNum.AUDIO_APP:
|
|
130
|
+
case protobufs.Protobuf.Portnums.PortNum.ZPS_APP:
|
|
131
|
+
case protobufs.Protobuf.Portnums.PortNum.SIMULATOR_APP:
|
|
132
|
+
case protobufs.Protobuf.Portnums.PortNum.PRIVATE_APP:
|
|
133
|
+
case protobufs.Protobuf.Portnums.PortNum.ATAK_FORWARDER:
|
|
134
|
+
case protobufs.Protobuf.Portnums.PortNum.TEXT_MESSAGE_COMPRESSED_APP:
|
|
135
|
+
default:
|
|
136
|
+
return {
|
|
137
|
+
raw: Buffer.from(uint8),
|
|
138
|
+
portNum,
|
|
139
|
+
note: `Unsupported or binary message type: ${getPortNumName(portNum)}`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new MeshtasticDecodeError(
|
|
144
|
+
`Failed to decode message for portNum ${portNum}: ${error instanceof Error ? error.message : String(error)}`,
|
|
145
|
+
error instanceof Error ? error : void 0
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function getPortNumName(portNum) {
|
|
150
|
+
const name = protobufs.Protobuf.Portnums.PortNum[portNum];
|
|
151
|
+
return name || `UNKNOWN(${portNum})`;
|
|
152
|
+
}
|
|
153
|
+
exports.InvalidKeyError = InvalidKeyError;
|
|
154
|
+
exports.InvalidNonceError = InvalidNonceError;
|
|
155
|
+
exports.MeshtasticDecodeError = MeshtasticDecodeError;
|
|
156
|
+
exports.MeshtasticDecryptError = MeshtasticDecryptError;
|
|
157
|
+
exports.MeshtasticEncodeError = MeshtasticEncodeError;
|
|
158
|
+
exports.MeshtasticEncryptError = MeshtasticEncryptError;
|
|
159
|
+
exports.MeshtasticError = MeshtasticError;
|
|
160
|
+
exports.ProtobufError = ProtobufError;
|
|
161
|
+
exports.decodeData = decodeData;
|
|
162
|
+
exports.decodeMessageByPortNum = decodeMessageByPortNum;
|
|
163
|
+
exports.decodeServiceEnvelope = decodeServiceEnvelope;
|
|
164
|
+
exports.ensureBuffer = ensureBuffer;
|
|
165
|
+
exports.getPortNumName = getPortNumName;
|
|
166
|
+
//# sourceMappingURL=decoder-BvBAtm2U.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decoder-BvBAtm2U.js","sources":["../src/lib/utils/error-types.ts","../src/lib/utils/buffer-utils.ts","../src/lib/protobuf/decoder.ts"],"sourcesContent":["/**\n * Custom error types for Meshtastic operations\n */\n\nexport class MeshtasticError extends Error {\n constructor(message: string, public cause?: Error) {\n super(message);\n this.name = 'MeshtasticError';\n if (cause) {\n this.stack = `${this.stack}\\nCaused by: ${cause.stack}`;\n }\n }\n}\n\nexport class MeshtasticDecryptError extends MeshtasticError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'MeshtasticDecryptError';\n }\n}\n\nexport class MeshtasticDecodeError extends MeshtasticError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'MeshtasticDecodeError';\n }\n}\n\nexport class MeshtasticEncodeError extends MeshtasticError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'MeshtasticEncodeError';\n }\n}\n\nexport class MeshtasticEncryptError extends MeshtasticError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'MeshtasticEncryptError';\n }\n}\n\nexport class InvalidKeyError extends MeshtasticError {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidKeyError';\n }\n}\n\nexport class InvalidNonceError extends MeshtasticError {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidNonceError';\n }\n}\n\nexport class ProtobufError extends MeshtasticError {\n constructor(message: string, cause?: Error) {\n super(message, cause);\n this.name = 'ProtobufError';\n }\n}\n","/**\n * Buffer manipulation utilities\n */\n\n/**\n * Convert a Uint8Array to Buffer\n * @param uint8Array - Uint8Array to convert\n * @returns Buffer\n */\nexport function uint8ArrayToBuffer(uint8Array: Uint8Array): Buffer {\n return Buffer.from(uint8Array);\n}\n\n/**\n * Convert a Buffer to Uint8Array\n * @param buffer - Buffer to convert\n * @returns Uint8Array\n */\nexport function bufferToUint8Array(buffer: Buffer): Uint8Array {\n return new Uint8Array(buffer);\n}\n\n/**\n * Check if input is a Buffer or Uint8Array\n * @param input - Input to check\n * @returns true if input is Buffer or Uint8Array\n */\nexport function isBufferLike(input: any): input is Buffer | Uint8Array {\n return Buffer.isBuffer(input) || input instanceof Uint8Array;\n}\n\n/**\n * Ensure input is a Buffer (convert if needed)\n * @param input - Buffer or Uint8Array\n * @returns Buffer\n */\nexport function ensureBuffer(input: Buffer | Uint8Array): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n return Buffer.from(input);\n}\n\n/**\n * Ensure input is a Uint8Array (convert if needed)\n * @param input - Buffer or Uint8Array\n * @returns Uint8Array\n */\nexport function ensureUint8Array(input: Buffer | Uint8Array): Uint8Array {\n if (input instanceof Uint8Array && !(input instanceof Buffer)) {\n return input;\n }\n return new Uint8Array(input);\n}\n\n/**\n * Convert a hex string to Buffer\n * @param hex - Hex string\n * @returns Buffer\n */\nexport function hexToBuffer(hex: string): Buffer {\n return Buffer.from(hex, 'hex');\n}\n\n/**\n * Convert a base64 string to Buffer\n * @param base64 - Base64 string\n * @returns Buffer\n */\nexport function base64ToBuffer(base64: string): Buffer {\n return Buffer.from(base64, 'base64');\n}\n\n/**\n * Convert a Buffer to hex string\n * @param buffer - Buffer\n * @returns Hex string\n */\nexport function bufferToHex(buffer: Buffer): string {\n return buffer.toString('hex');\n}\n\n/**\n * Convert a Buffer to base64 string\n * @param buffer - Buffer\n * @returns Base64 string\n */\nexport function bufferToBase64(buffer: Buffer): string {\n return buffer.toString('base64');\n}\n\n/**\n * Check if two buffers are equal\n * @param a - First buffer\n * @param b - Second buffer\n * @returns true if buffers are equal\n */\nexport function buffersEqual(a: Buffer, b: Buffer): boolean {\n return a.equals(b);\n}\n","/**\n * Protobuf decoding utilities for Meshtastic messages\n */\n\nimport { Protobuf } from '@meshtastic/protobufs';\nimport { ProtobufError, MeshtasticDecodeError } from '../utils/error-types';\nimport { ensureUint8Array } from '../utils/buffer-utils';\n\n/**\n * Decode a ServiceEnvelope from binary data\n * @param buffer - Binary data containing ServiceEnvelope\n * @returns Decoded ServiceEnvelope\n * @throws {ProtobufError} If decoding fails\n */\nexport function decodeServiceEnvelope(buffer: Buffer | Uint8Array): Protobuf.Mqtt.ServiceEnvelope {\n try {\n const uint8 = ensureUint8Array(buffer);\n return Protobuf.Mqtt.ServiceEnvelope.fromBinary(uint8);\n } catch (error) {\n throw new ProtobufError(\n `Failed to decode ServiceEnvelope: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Decode a Data protobuf from binary data\n * @param buffer - Binary data containing Data message\n * @returns Decoded Data\n * @throws {ProtobufError} If decoding fails\n */\nexport function decodeData(buffer: Buffer | Uint8Array): Protobuf.Mesh.Data {\n try {\n const uint8 = ensureUint8Array(buffer);\n return Protobuf.Mesh.Data.fromBinary(uint8);\n } catch (error) {\n throw new ProtobufError(\n `Failed to decode Data: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Decode a message payload based on its port number\n * @param portNum - Port number (message type)\n * @param payload - Binary payload data\n * @returns Decoded message (type depends on portNum)\n * @throws {MeshtasticDecodeError} If decoding fails\n */\nexport function decodeMessageByPortNum(\n portNum: Protobuf.Portnums.PortNum | number,\n payload: Uint8Array | Buffer\n): any {\n try {\n const uint8 = ensureUint8Array(payload);\n\n switch (portNum) {\n case Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP:\n return new TextDecoder().decode(uint8);\n\n case Protobuf.Portnums.PortNum.POSITION_APP:\n return Protobuf.Mesh.Position.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.NODEINFO_APP:\n return Protobuf.Mesh.User.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.TELEMETRY_APP:\n return Protobuf.Telemetry.Telemetry.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.ROUTING_APP:\n return Protobuf.Mesh.Routing.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.ADMIN_APP:\n return Protobuf.Admin.AdminMessage.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.WAYPOINT_APP:\n return Protobuf.Mesh.Waypoint.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.NEIGHBORINFO_APP:\n return Protobuf.Mesh.NeighborInfo.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.STORE_FORWARD_APP:\n return Protobuf.StoreForward.StoreAndForward.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.RANGE_TEST_APP:\n return new TextDecoder('ascii').decode(uint8);\n\n case Protobuf.Portnums.PortNum.REMOTE_HARDWARE_APP:\n return Protobuf.RemoteHardware.HardwareMessage.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.DETECTION_SENSOR_APP:\n return new TextDecoder().decode(uint8);\n\n case Protobuf.Portnums.PortNum.REPLY_APP:\n return new TextDecoder('ascii').decode(uint8);\n\n case Protobuf.Portnums.PortNum.TRACEROUTE_APP:\n return Protobuf.Mesh.RouteDiscovery.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.PAXCOUNTER_APP:\n return Protobuf.Paxcount.Paxcount.fromBinary(uint8);\n\n case Protobuf.Portnums.PortNum.MAP_REPORT_APP:\n return Protobuf.Mqtt.MapReport.fromBinary(uint8);\n\n // For unknown or unsupported message types, return raw bytes\n case Protobuf.Portnums.PortNum.UNKNOWN_APP:\n case Protobuf.Portnums.PortNum.IP_TUNNEL_APP:\n case Protobuf.Portnums.PortNum.SERIAL_APP:\n case Protobuf.Portnums.PortNum.AUDIO_APP:\n case Protobuf.Portnums.PortNum.ZPS_APP:\n case Protobuf.Portnums.PortNum.SIMULATOR_APP:\n case Protobuf.Portnums.PortNum.PRIVATE_APP:\n case Protobuf.Portnums.PortNum.ATAK_FORWARDER:\n case Protobuf.Portnums.PortNum.TEXT_MESSAGE_COMPRESSED_APP:\n default:\n return {\n raw: Buffer.from(uint8),\n portNum,\n note: `Unsupported or binary message type: ${getPortNumName(portNum)}`\n };\n }\n } catch (error) {\n throw new MeshtasticDecodeError(\n `Failed to decode message for portNum ${portNum}: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Get the name of a port number\n * @param portNum - Port number\n * @returns Port number name (e.g., \"TEXT_MESSAGE_APP\")\n */\nexport function getPortNumName(portNum: Protobuf.Portnums.PortNum | number): string {\n const name = Protobuf.Portnums.PortNum[portNum];\n return name || `UNKNOWN(${portNum})`;\n}\n\n/**\n * Format a ServiceEnvelope as a JSON object for output\n * @param envelope - ServiceEnvelope to format\n * @param decodePayload - Whether to decode the payload (default: true)\n * @returns Formatted JSON object\n */\nexport function formatServiceEnvelopeAsJson(\n envelope: Protobuf.Mqtt.ServiceEnvelope,\n decodePayload: boolean = true\n): any {\n const packet = envelope.packet;\n\n if (!packet) {\n return {\n channelId: envelope.channelId,\n gatewayId: envelope.gatewayId,\n error: 'No packet in ServiceEnvelope'\n };\n }\n\n const result: any = {\n channelId: envelope.channelId,\n gatewayId: envelope.gatewayId,\n from: packet.from,\n to: packet.to,\n channel: packet.channel,\n packetId: packet.id,\n rxTime: packet.rxTime,\n rxSnr: packet.rxSnr,\n rxRssi: packet.rxRssi,\n hopLimit: packet.hopLimit,\n wantAck: packet.wantAck,\n priority: packet.priority,\n hopStart: packet.hopStart,\n };\n\n // Handle payload based on variant type\n if (packet.payloadVariant.case === 'encrypted') {\n result.encrypted = true;\n result.encryptedData = Buffer.from(packet.payloadVariant.value).toString('base64');\n } else if (packet.payloadVariant.case === 'decoded') {\n const decoded = packet.payloadVariant.value;\n result.encrypted = false;\n result.portNum = getPortNumName(decoded.portnum);\n result.portNumValue = decoded.portnum;\n result.wantResponse = decoded.wantResponse;\n result.requestId = decoded.requestId;\n result.emoji = decoded.emoji;\n\n if (decodePayload && decoded.payload && decoded.payload.length > 0) {\n try {\n result.payload = decodeMessageByPortNum(decoded.portnum, decoded.payload);\n } catch (error) {\n result.payload = {\n raw: Buffer.from(decoded.payload).toString('base64'),\n error: error instanceof Error ? error.message : String(error)\n };\n }\n } else {\n result.payload = decoded.payload ? Buffer.from(decoded.payload).toString('base64') : null;\n }\n }\n\n return result;\n}\n"],"names":["Protobuf"],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const protobufs = require("@meshtastic/protobufs");
|
|
3
|
+
const decoder = require("./decoder-BvBAtm2U.js");
|
|
4
|
+
function encodeMessageByPortNum(portNum, message) {
|
|
5
|
+
try {
|
|
6
|
+
switch (portNum) {
|
|
7
|
+
case protobufs.Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP:
|
|
8
|
+
if (typeof message !== "string") {
|
|
9
|
+
throw new Error("TEXT_MESSAGE_APP requires a string message");
|
|
10
|
+
}
|
|
11
|
+
return new TextEncoder().encode(message);
|
|
12
|
+
case protobufs.Protobuf.Portnums.PortNum.POSITION_APP:
|
|
13
|
+
return protobufs.Protobuf.Mesh.Position.toBinary(message);
|
|
14
|
+
case protobufs.Protobuf.Portnums.PortNum.NODEINFO_APP:
|
|
15
|
+
return protobufs.Protobuf.Mesh.User.toBinary(message);
|
|
16
|
+
case protobufs.Protobuf.Portnums.PortNum.TELEMETRY_APP:
|
|
17
|
+
return protobufs.Protobuf.Telemetry.Telemetry.toBinary(message);
|
|
18
|
+
case protobufs.Protobuf.Portnums.PortNum.ROUTING_APP:
|
|
19
|
+
return protobufs.Protobuf.Mesh.Routing.toBinary(message);
|
|
20
|
+
case protobufs.Protobuf.Portnums.PortNum.ADMIN_APP:
|
|
21
|
+
return protobufs.Protobuf.Admin.AdminMessage.toBinary(message);
|
|
22
|
+
case protobufs.Protobuf.Portnums.PortNum.WAYPOINT_APP:
|
|
23
|
+
return protobufs.Protobuf.Mesh.Waypoint.toBinary(message);
|
|
24
|
+
case protobufs.Protobuf.Portnums.PortNum.NEIGHBORINFO_APP:
|
|
25
|
+
return protobufs.Protobuf.Mesh.NeighborInfo.toBinary(message);
|
|
26
|
+
case protobufs.Protobuf.Portnums.PortNum.STORE_FORWARD_APP:
|
|
27
|
+
return protobufs.Protobuf.StoreForward.StoreAndForward.toBinary(message);
|
|
28
|
+
case protobufs.Protobuf.Portnums.PortNum.RANGE_TEST_APP:
|
|
29
|
+
case protobufs.Protobuf.Portnums.PortNum.DETECTION_SENSOR_APP:
|
|
30
|
+
case protobufs.Protobuf.Portnums.PortNum.REPLY_APP:
|
|
31
|
+
if (typeof message !== "string") {
|
|
32
|
+
throw new Error(`${portNum} requires a string message`);
|
|
33
|
+
}
|
|
34
|
+
return new TextEncoder().encode(message);
|
|
35
|
+
case protobufs.Protobuf.Portnums.PortNum.REMOTE_HARDWARE_APP:
|
|
36
|
+
return protobufs.Protobuf.RemoteHardware.HardwareMessage.toBinary(message);
|
|
37
|
+
case protobufs.Protobuf.Portnums.PortNum.TRACEROUTE_APP:
|
|
38
|
+
return protobufs.Protobuf.Mesh.RouteDiscovery.toBinary(message);
|
|
39
|
+
case protobufs.Protobuf.Portnums.PortNum.PAXCOUNTER_APP:
|
|
40
|
+
return protobufs.Protobuf.Paxcount.Paxcount.toBinary(message);
|
|
41
|
+
case protobufs.Protobuf.Portnums.PortNum.MAP_REPORT_APP:
|
|
42
|
+
return protobufs.Protobuf.Mqtt.MapReport.toBinary(message);
|
|
43
|
+
default:
|
|
44
|
+
throw new Error(`Encoding not supported for portNum: ${portNum}`);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new decoder.MeshtasticEncodeError(
|
|
48
|
+
`Failed to encode message for portNum ${portNum}: ${error instanceof Error ? error.message : String(error)}`,
|
|
49
|
+
error instanceof Error ? error : void 0
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function createDataPayload(portNum, payload, options) {
|
|
54
|
+
return {
|
|
55
|
+
portnum: portNum,
|
|
56
|
+
payload,
|
|
57
|
+
wantResponse: (options == null ? void 0 : options.wantResponse) ?? false,
|
|
58
|
+
requestId: (options == null ? void 0 : options.requestId) ?? 0,
|
|
59
|
+
emoji: (options == null ? void 0 : options.emoji) ?? 0
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function createServiceEnvelope(data, from, to = 4294967295, channel = 0, packetId, options) {
|
|
63
|
+
return {
|
|
64
|
+
packet: {
|
|
65
|
+
from,
|
|
66
|
+
to,
|
|
67
|
+
channel,
|
|
68
|
+
id: packetId ?? 0,
|
|
69
|
+
rxTime: 0,
|
|
70
|
+
rxSnr: 0,
|
|
71
|
+
rxRssi: 0,
|
|
72
|
+
hopLimit: (options == null ? void 0 : options.hopLimit) ?? 3,
|
|
73
|
+
wantAck: (options == null ? void 0 : options.wantAck) ?? false,
|
|
74
|
+
priority: (options == null ? void 0 : options.priority) ?? protobufs.Protobuf.Mesh.MeshPacket_Priority.UNSET,
|
|
75
|
+
hopStart: (options == null ? void 0 : options.hopLimit) ?? 3,
|
|
76
|
+
payloadVariant: {
|
|
77
|
+
case: "decoded",
|
|
78
|
+
value: data
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
channelId: (options == null ? void 0 : options.channelId) ?? "",
|
|
82
|
+
gatewayId: (options == null ? void 0 : options.gatewayId) ?? ""
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function encodeServiceEnvelope(envelope) {
|
|
86
|
+
try {
|
|
87
|
+
const binary = protobufs.Protobuf.Mqtt.ServiceEnvelope.toBinary(envelope);
|
|
88
|
+
return decoder.ensureBuffer(binary);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw new decoder.ProtobufError(
|
|
91
|
+
`Failed to encode ServiceEnvelope: ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
+
error instanceof Error ? error : void 0
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function encodeData(data) {
|
|
97
|
+
try {
|
|
98
|
+
const binary = protobufs.Protobuf.Mesh.Data.toBinary(data);
|
|
99
|
+
return decoder.ensureBuffer(binary);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new decoder.ProtobufError(
|
|
102
|
+
`Failed to encode Data: ${error instanceof Error ? error.message : String(error)}`,
|
|
103
|
+
error instanceof Error ? error : void 0
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function parsePortNum(portNum) {
|
|
108
|
+
if (typeof portNum === "number") {
|
|
109
|
+
if (portNum in protobufs.Protobuf.Portnums.PortNum) {
|
|
110
|
+
return portNum;
|
|
111
|
+
}
|
|
112
|
+
throw new decoder.MeshtasticEncodeError(`Invalid port number: ${portNum}`);
|
|
113
|
+
}
|
|
114
|
+
const enumValue = protobufs.Protobuf.Portnums.PortNum[portNum];
|
|
115
|
+
if (enumValue === void 0) {
|
|
116
|
+
throw new decoder.MeshtasticEncodeError(`Unknown port number name: ${portNum}`);
|
|
117
|
+
}
|
|
118
|
+
return enumValue;
|
|
119
|
+
}
|
|
120
|
+
exports.createDataPayload = createDataPayload;
|
|
121
|
+
exports.createServiceEnvelope = createServiceEnvelope;
|
|
122
|
+
exports.encodeData = encodeData;
|
|
123
|
+
exports.encodeMessageByPortNum = encodeMessageByPortNum;
|
|
124
|
+
exports.encodeServiceEnvelope = encodeServiceEnvelope;
|
|
125
|
+
exports.parsePortNum = parsePortNum;
|
|
126
|
+
//# sourceMappingURL=encoder-DU3wTIEA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder-DU3wTIEA.js","sources":["../src/lib/protobuf/encoder.ts"],"sourcesContent":["/**\n * Protobuf encoding utilities for Meshtastic messages\n */\n\nimport { Protobuf } from '@meshtastic/protobufs';\nimport { MeshtasticEncodeError, ProtobufError } from '../utils/error-types';\nimport { ensureBuffer } from '../utils/buffer-utils';\n\n/**\n * Encode a message payload based on its port number\n * @param portNum - Port number (message type)\n * @param message - Message data to encode\n * @returns Encoded binary payload\n * @throws {MeshtasticEncodeError} If encoding fails\n */\nexport function encodeMessageByPortNum(\n portNum: Protobuf.Portnums.PortNum | number,\n message: any\n): Uint8Array {\n try {\n switch (portNum) {\n case Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP:\n // Encode string as UTF-8 bytes\n if (typeof message !== 'string') {\n throw new Error('TEXT_MESSAGE_APP requires a string message');\n }\n return new TextEncoder().encode(message);\n\n case Protobuf.Portnums.PortNum.POSITION_APP:\n return Protobuf.Mesh.Position.toBinary(message as Protobuf.Mesh.Position);\n\n case Protobuf.Portnums.PortNum.NODEINFO_APP:\n return Protobuf.Mesh.User.toBinary(message as Protobuf.Mesh.User);\n\n case Protobuf.Portnums.PortNum.TELEMETRY_APP:\n return Protobuf.Telemetry.Telemetry.toBinary(message as Protobuf.Telemetry.Telemetry);\n\n case Protobuf.Portnums.PortNum.ROUTING_APP:\n return Protobuf.Mesh.Routing.toBinary(message as Protobuf.Mesh.Routing);\n\n case Protobuf.Portnums.PortNum.ADMIN_APP:\n return Protobuf.Admin.AdminMessage.toBinary(message as Protobuf.Admin.AdminMessage);\n\n case Protobuf.Portnums.PortNum.WAYPOINT_APP:\n return Protobuf.Mesh.Waypoint.toBinary(message as Protobuf.Mesh.Waypoint);\n\n case Protobuf.Portnums.PortNum.NEIGHBORINFO_APP:\n return Protobuf.Mesh.NeighborInfo.toBinary(message as Protobuf.Mesh.NeighborInfo);\n\n case Protobuf.Portnums.PortNum.STORE_FORWARD_APP:\n return Protobuf.StoreForward.StoreAndForward.toBinary(message as Protobuf.StoreForward.StoreAndForward);\n\n case Protobuf.Portnums.PortNum.RANGE_TEST_APP:\n case Protobuf.Portnums.PortNum.DETECTION_SENSOR_APP:\n case Protobuf.Portnums.PortNum.REPLY_APP:\n // Encode string as ASCII/UTF-8 bytes\n if (typeof message !== 'string') {\n throw new Error(`${portNum} requires a string message`);\n }\n return new TextEncoder().encode(message);\n\n case Protobuf.Portnums.PortNum.REMOTE_HARDWARE_APP:\n return Protobuf.RemoteHardware.HardwareMessage.toBinary(message as Protobuf.RemoteHardware.HardwareMessage);\n\n case Protobuf.Portnums.PortNum.TRACEROUTE_APP:\n return Protobuf.Mesh.RouteDiscovery.toBinary(message as Protobuf.Mesh.RouteDiscovery);\n\n case Protobuf.Portnums.PortNum.PAXCOUNTER_APP:\n return Protobuf.Paxcount.Paxcount.toBinary(message as Protobuf.Paxcount.Paxcount);\n\n case Protobuf.Portnums.PortNum.MAP_REPORT_APP:\n return Protobuf.Mqtt.MapReport.toBinary(message as Protobuf.Mqtt.MapReport);\n\n default:\n throw new Error(`Encoding not supported for portNum: ${portNum}`);\n }\n } catch (error) {\n throw new MeshtasticEncodeError(\n `Failed to encode message for portNum ${portNum}: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Create a Data protobuf from encoded payload\n * @param portNum - Port number (message type)\n * @param payload - Encoded message payload\n * @param options - Additional options (wantResponse, requestId, emoji)\n * @returns Data protobuf\n */\nexport function createDataPayload(\n portNum: Protobuf.Portnums.PortNum | number,\n payload: Uint8Array,\n options?: {\n wantResponse?: boolean;\n requestId?: number;\n emoji?: number;\n }\n): Protobuf.Mesh.Data {\n return {\n portnum: portNum as Protobuf.Portnums.PortNum,\n payload,\n wantResponse: options?.wantResponse ?? false,\n requestId: options?.requestId ?? 0,\n emoji: options?.emoji ?? 0,\n };\n}\n\n/**\n * Create a ServiceEnvelope with a Data payload\n * @param data - Data protobuf\n * @param from - Source node number\n * @param to - Destination node number (0xFFFFFFFF for broadcast)\n * @param channel - Channel index\n * @param packetId - Packet ID (if undefined, needs to be set later)\n * @param options - Additional options\n * @returns ServiceEnvelope\n */\nexport function createServiceEnvelope(\n data: Protobuf.Mesh.Data,\n from: number,\n to: number = 0xFFFFFFFF,\n channel: number = 0,\n packetId?: number,\n options?: {\n wantAck?: boolean;\n hopLimit?: number;\n priority?: Protobuf.Mesh.MeshPacket_Priority;\n channelId?: string;\n gatewayId?: string;\n }\n): Protobuf.Mqtt.ServiceEnvelope {\n return {\n packet: {\n from,\n to,\n channel,\n id: packetId ?? 0,\n rxTime: 0,\n rxSnr: 0,\n rxRssi: 0,\n hopLimit: options?.hopLimit ?? 3,\n wantAck: options?.wantAck ?? false,\n priority: options?.priority ?? Protobuf.Mesh.MeshPacket_Priority.UNSET,\n hopStart: options?.hopLimit ?? 3,\n payloadVariant: {\n case: 'decoded',\n value: data,\n },\n },\n channelId: options?.channelId ?? '',\n gatewayId: options?.gatewayId ?? '',\n };\n}\n\n/**\n * Encode a complete ServiceEnvelope to binary\n * @param envelope - ServiceEnvelope to encode\n * @returns Binary encoded ServiceEnvelope\n * @throws {ProtobufError} If encoding fails\n */\nexport function encodeServiceEnvelope(envelope: Protobuf.Mqtt.ServiceEnvelope): Buffer {\n try {\n const binary = Protobuf.Mqtt.ServiceEnvelope.toBinary(envelope);\n return ensureBuffer(binary);\n } catch (error) {\n throw new ProtobufError(\n `Failed to encode ServiceEnvelope: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Encode a Data protobuf to binary\n * @param data - Data protobuf to encode\n * @returns Binary encoded Data\n * @throws {ProtobufError} If encoding fails\n */\nexport function encodeData(data: Protobuf.Mesh.Data): Buffer {\n try {\n const binary = Protobuf.Mesh.Data.toBinary(data);\n return ensureBuffer(binary);\n } catch (error) {\n throw new ProtobufError(\n `Failed to encode Data: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Parse port number from string name or number\n * @param portNum - Port number as string name or number\n * @returns Port number as enum value\n * @throws {MeshtasticEncodeError} If port number is invalid\n */\nexport function parsePortNum(portNum: string | number): Protobuf.Portnums.PortNum {\n if (typeof portNum === 'number') {\n // Validate that the number is a valid PortNum\n if (portNum in Protobuf.Portnums.PortNum) {\n return portNum as Protobuf.Portnums.PortNum;\n }\n throw new MeshtasticEncodeError(`Invalid port number: ${portNum}`);\n }\n\n // Parse string name to enum value\n const enumValue = (Protobuf.Portnums.PortNum as any)[portNum];\n if (enumValue === undefined) {\n throw new MeshtasticEncodeError(`Unknown port number name: ${portNum}`);\n }\n\n return enumValue;\n}\n"],"names":["Protobuf","MeshtasticEncodeError","ensureBuffer","ProtobufError"],"mappings":"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"}
|