@soapbox.pub/nostr-lora 0.1.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 +173 -0
- package/dist/assembler.d.ts +74 -0
- package/dist/connection-manager.d.ts +30 -0
- package/dist/constants.d.ts +31 -0
- package/dist/event-codec.d.ts +15 -0
- package/dist/gm-protocol.d.ts +30 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/lora-transport.d.ts +109 -0
- package/dist/protocol.d.ts +78 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +57 -0
- package/dist/react.js.map +1 -0
- package/dist/send-queue.d.ts +40 -0
- package/dist/serial-connection.d.ts +41 -0
- package/dist/serial-connection.js +955 -0
- package/dist/serial-connection.js.map +1 -0
- package/dist/use-lora-transport.d.ts +32 -0
- package/dist/utils.d.ts +24 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Soapbox 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,173 @@
|
|
|
1
|
+
# @soapbox.pub/nostr-lora
|
|
2
|
+
|
|
3
|
+
Reference implementation of [NIP-LR](./NIP.md): Nostr over LoRa.
|
|
4
|
+
|
|
5
|
+
Handles packet encoding/decoding, chunked transmission, reassembly,
|
|
6
|
+
retransmission requests, and GM (announce) messages. Designed for use with
|
|
7
|
+
MeshCore LoRa devices over Web Serial.
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
nostr-lora # core — LoRaTransport, SerialConnectionManager, etc.
|
|
13
|
+
nostr-lora/react # React hook — useNostrLora
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
React is an optional peer dependency. The core entry point has no React
|
|
17
|
+
dependency.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install @soapbox.pub/nostr-lora
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### React
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { useNostrLora } from "nostr-lora/react";
|
|
31
|
+
|
|
32
|
+
function App() {
|
|
33
|
+
const { connect, disconnect, isConnected, sendEvent, error } = useNostrLora(
|
|
34
|
+
{
|
|
35
|
+
onEvent(event) {
|
|
36
|
+
console.log("received event", event);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
{isConnected
|
|
44
|
+
? <button onClick={disconnect}>Disconnect</button>
|
|
45
|
+
: <button onClick={connect}>Connect</button>}
|
|
46
|
+
{error && <p>{error.message}</p>}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`connect()` triggers the browser's serial port picker. Once connected, incoming
|
|
53
|
+
Nostr events are delivered via `onEvent`. Call `sendEvent(event)` to broadcast a
|
|
54
|
+
signed Nostr event over LoRa.
|
|
55
|
+
|
|
56
|
+
Full return type:
|
|
57
|
+
|
|
58
|
+
| Field | Type | Description |
|
|
59
|
+
| ------------------ | ------------------------------------------------------ | ----------------------------------------- |
|
|
60
|
+
| `isConnected` | `boolean` | Whether the serial port is open |
|
|
61
|
+
| `connecting` | `boolean` | `true` while the port is being opened |
|
|
62
|
+
| `error` | `Error \| null` | Last error, if any |
|
|
63
|
+
| `portLabel` | `string \| null` | USB VID:PID of the connected device |
|
|
64
|
+
| `deviceName` | `string \| null` | Device name from firmware |
|
|
65
|
+
| `sendQueue` | `QueueSnapshot[]` | Current outbound packet queue |
|
|
66
|
+
| `lastPacket` | `PacketReceiveInfo \| null` | SNR/RSSI/size of last received packet |
|
|
67
|
+
| `connect` | `() => Promise<void>` | Open port and begin transport |
|
|
68
|
+
| `disconnect` | `() => Promise<void>` | Close port |
|
|
69
|
+
| `sendEvent` | `(event: NostrEvent) => Promise<void>` | Send a signed event |
|
|
70
|
+
| `sendGm` | `(nodeId, eventCount?, recentSince?) => Promise<void>` | Broadcast a GM announcement |
|
|
71
|
+
| `setTimingOptions` | `({ delay?, jitter? }) => void` | Adjust inter-packet timing live |
|
|
72
|
+
| `transport` | `LoRaTransport \| null` | Direct access to the underlying transport |
|
|
73
|
+
|
|
74
|
+
### Vanilla (no framework)
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { LoRaTransport, SerialConnectionManager } from "nostr-lora";
|
|
78
|
+
|
|
79
|
+
const connection = new SerialConnectionManager();
|
|
80
|
+
const transport = new LoRaTransport(connection);
|
|
81
|
+
|
|
82
|
+
transport.on("event:receive", (event) => {
|
|
83
|
+
console.log("received event", event);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
transport.on("connect", (portLabel) => {
|
|
87
|
+
console.log("connected to", portLabel);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
transport.on("error", (err) => {
|
|
91
|
+
console.error(err);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Triggers the browser port picker
|
|
95
|
+
await transport.begin();
|
|
96
|
+
|
|
97
|
+
// Send a signed Nostr event
|
|
98
|
+
await transport.sendEvent(signedEvent);
|
|
99
|
+
|
|
100
|
+
// Later
|
|
101
|
+
await transport.end();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Custom connection
|
|
105
|
+
|
|
106
|
+
Implement `ConnectionManager` to use a different transport (e.g. WebSocket,
|
|
107
|
+
BLE):
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import type { ConnectionManager, ConnectionManagerEvents } from "nostr-lora";
|
|
111
|
+
import { LoRaTransport } from "nostr-lora";
|
|
112
|
+
|
|
113
|
+
class MyConnection implements ConnectionManager {
|
|
114
|
+
on<E extends keyof ConnectionManagerEvents>(
|
|
115
|
+
event: E,
|
|
116
|
+
handler: (...args: ConnectionManagerEvents[E]) => void,
|
|
117
|
+
) {/* ... */}
|
|
118
|
+
|
|
119
|
+
async open() {/* ... */}
|
|
120
|
+
async close() {/* ... */}
|
|
121
|
+
async sendRawData(data: Uint8Array) {/* ... */}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const transport = new LoRaTransport(new MyConnection());
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Options
|
|
128
|
+
|
|
129
|
+
All options are optional. Defaults are exported as `Defaults`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { Defaults } from "nostr-lora";
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| Option | Default | Description |
|
|
136
|
+
| ------------------- | ----------- | ----------------------------------------------------------- |
|
|
137
|
+
| `nodeId` | `undefined` | Your node's identity bytes (used in GM packets) |
|
|
138
|
+
| `logger` | `null` | Object with `log/warn/error/debug` methods (e.g. `console`) |
|
|
139
|
+
| `interPacketDelay` | `2000` ms | Base delay between packets in the send queue |
|
|
140
|
+
| `interPacketJitter` | `500` ms | Max random jitter added on top of the delay |
|
|
141
|
+
| `requestInactivity` | `30000` ms | Idle time before sending a retransmission request |
|
|
142
|
+
| `requestMaxRetries` | `3` | Max retransmission attempts before abandoning |
|
|
143
|
+
| `eventTimeout` | `30000` ms | Max wait for all chunks of a partial event |
|
|
144
|
+
| `dedupTimeout` | `900000` ms | How long to remember received event IDs |
|
|
145
|
+
| `sentChunksTtl` | `300000` ms | How long to keep sent chunks for potential retransmission |
|
|
146
|
+
| `initialTtl` | `6` | Hop TTL assigned to locally-created events |
|
|
147
|
+
| `gmBackoffBase` | `300000` ms | Minimum interval between GM responses to the same peer |
|
|
148
|
+
| `gmMaxShareEvents` | `3` | Max events to re-share in response to a GM |
|
|
149
|
+
| `gmJitterMin` | `500` ms | Min random delay before responding to a GM |
|
|
150
|
+
| `gmJitterMax` | `1500` ms | Max random delay before responding to a GM |
|
|
151
|
+
|
|
152
|
+
## Events
|
|
153
|
+
|
|
154
|
+
`LoRaTransport` extends `EventEmitter` and emits:
|
|
155
|
+
|
|
156
|
+
| Event | Args | Description |
|
|
157
|
+
| ----------------- | ------------------------------------ | ------------------------------------------------ |
|
|
158
|
+
| `connect` | `portLabel: string` | Connection opened |
|
|
159
|
+
| `disconnect` | — | Connection closed |
|
|
160
|
+
| `error` | `err: Error` | Error from connection or packet handling |
|
|
161
|
+
| `event:receive` | `event: NostrEvent` | A complete Nostr event was received and verified |
|
|
162
|
+
| `event:send` | `event: NostrEvent` | A Nostr event finished sending |
|
|
163
|
+
| `packet:receive` | `info: PacketReceiveInfo` | Raw packet received (SNR, RSSI, size) |
|
|
164
|
+
| `chunk:receive` | `decoded, byteLength` | Individual DATA chunk received |
|
|
165
|
+
| `gm:receive` | `decoded, byteLength` | GM packet received |
|
|
166
|
+
| `request:receive` | `prefixHex, missingChunks` | Retransmission request received from peer |
|
|
167
|
+
| `request:send` | `eventIdHex, missingChunks, attempt` | Retransmission request sent |
|
|
168
|
+
| `queue:update` | `snapshot: QueueSnapshot[]` | Send queue changed |
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
The source code of this library is provided to you under the terms of the
|
|
173
|
+
[MIT License](./LICENSE).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { FirstChunk, SubsequentChunk } from './protocol.js';
|
|
2
|
+
/** Holds the per-event state while chunks are being collected. Not exported. */
|
|
3
|
+
declare class AssemblySession {
|
|
4
|
+
readonly eventId: Uint8Array;
|
|
5
|
+
readonly eventKind: number;
|
|
6
|
+
readonly totalChunks: number;
|
|
7
|
+
readonly checksum: number;
|
|
8
|
+
readonly chunks: Array<Uint8Array | undefined>;
|
|
9
|
+
receivedChunks: number;
|
|
10
|
+
constructor(first: FirstChunk);
|
|
11
|
+
get isComplete(): boolean;
|
|
12
|
+
/** Returns true if this was a new chunk, false if duplicate. */
|
|
13
|
+
addChunk(index: number, payload: Uint8Array): boolean;
|
|
14
|
+
missingIndices(): number[];
|
|
15
|
+
}
|
|
16
|
+
export interface CompleteAction {
|
|
17
|
+
action: 'complete';
|
|
18
|
+
session: AssemblySession;
|
|
19
|
+
}
|
|
20
|
+
export interface StartedAction {
|
|
21
|
+
action: 'started';
|
|
22
|
+
session: AssemblySession;
|
|
23
|
+
}
|
|
24
|
+
export interface ProgressAction {
|
|
25
|
+
action: 'progress';
|
|
26
|
+
session: AssemblySession;
|
|
27
|
+
eventIdHex: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PendingAction {
|
|
30
|
+
action: 'pending';
|
|
31
|
+
prefix: Uint8Array;
|
|
32
|
+
}
|
|
33
|
+
export interface NoneAction {
|
|
34
|
+
action: 'none';
|
|
35
|
+
}
|
|
36
|
+
export type FirstChunkResult = CompleteAction | StartedAction | NoneAction;
|
|
37
|
+
export type SubsequentChunkResult = CompleteAction | ProgressAction | PendingAction | NoneAction;
|
|
38
|
+
interface PendingEntry {
|
|
39
|
+
eventIdPrefix: Uint8Array;
|
|
40
|
+
chunkIndex: number;
|
|
41
|
+
payload: Uint8Array;
|
|
42
|
+
isLast: boolean;
|
|
43
|
+
arrivedAt: number;
|
|
44
|
+
}
|
|
45
|
+
export declare class Assembler {
|
|
46
|
+
/** In-progress sessions keyed by the full event ID as a hex string. */
|
|
47
|
+
sessions: Map<string, AssemblySession>;
|
|
48
|
+
/** Subsequent chunks that arrived before their first chunk. */
|
|
49
|
+
pendingSubsequent: PendingEntry[];
|
|
50
|
+
handleFirst(decoded: FirstChunk, seenIds: Map<string, number>): FirstChunkResult;
|
|
51
|
+
handleSubsequent(decoded: SubsequentChunk, eventTimeout: number): SubsequentChunkResult;
|
|
52
|
+
/** Returns missing chunk indices for a session, or null if it no longer exists. */
|
|
53
|
+
missingChunks(eventIdHex: string): number[] | null;
|
|
54
|
+
/** Abandon an in-progress session after max retries exceeded. */
|
|
55
|
+
abandonSession(eventIdHex: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Returns missing chunk indices for a pending-subsequent group
|
|
58
|
+
* (chunk 0 still missing), or null if no buffered chunks exist for this prefix.
|
|
59
|
+
*/
|
|
60
|
+
missingChunksForPending(prefixHex: string): number[] | null;
|
|
61
|
+
/** Abandon all pending entries for a given prefix after max retries exceeded. */
|
|
62
|
+
abandonPending(prefixHex: string): void;
|
|
63
|
+
/** True if a session is currently in progress for this event ID. */
|
|
64
|
+
hasSession(eventIdHex: string): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Returns the raw eventId bytes for an in-progress session, or null.
|
|
67
|
+
* Used by the transport to build REQUEST prefix bytes.
|
|
68
|
+
*/
|
|
69
|
+
getSessionEventId(eventIdHex: string): Uint8Array | null;
|
|
70
|
+
private _findSessionForPrefix;
|
|
71
|
+
private _replayPending;
|
|
72
|
+
private _prunePending;
|
|
73
|
+
}
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Events emitted by a ConnectionManager. */
|
|
2
|
+
export type ConnectionManagerEvents = {
|
|
3
|
+
"data": [data: {
|
|
4
|
+
data: Uint8Array;
|
|
5
|
+
snr: number;
|
|
6
|
+
rssi: number;
|
|
7
|
+
}];
|
|
8
|
+
"connect": [portLabel: string];
|
|
9
|
+
"disconnect": [];
|
|
10
|
+
"deviceInfo": [info: {
|
|
11
|
+
name: string;
|
|
12
|
+
firmwareVersion: string;
|
|
13
|
+
}];
|
|
14
|
+
"error": [err: Error];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Abstraction over the physical radio link.
|
|
18
|
+
* Implement this interface to use `LoRaTransport` with a custom connection.
|
|
19
|
+
* `SerialConnectionManager` provides the Web Serial implementation.
|
|
20
|
+
*/
|
|
21
|
+
export interface ConnectionManager {
|
|
22
|
+
/** Send a raw binary packet to the radio. */
|
|
23
|
+
sendRawData(data: Uint8Array): Promise<void>;
|
|
24
|
+
/** Open the connection (e.g. show browser port picker, open port). */
|
|
25
|
+
open(): Promise<void>;
|
|
26
|
+
/** Close the connection. */
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
/** Subscribe to connection lifecycle and data events. */
|
|
29
|
+
on<E extends keyof ConnectionManagerEvents>(event: E, handler: (...args: ConnectionManagerEvents[E]) => void): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
declare const Defaults: {
|
|
2
|
+
/** Base delay between packets within a single queue item (ms). */
|
|
3
|
+
readonly INTER_PACKET_DELAY: 2000;
|
|
4
|
+
/** Maximum additional random jitter added on top of INTER_PACKET_DELAY (ms). */
|
|
5
|
+
readonly INTER_PACKET_JITTER: 500;
|
|
6
|
+
/** Per-peer minimum response interval before the first backoff multiplier kicks in (ms). */
|
|
7
|
+
readonly GM_BACKOFF_BASE: number;
|
|
8
|
+
/** Maximum number of our own events to re-share in response to one GM. */
|
|
9
|
+
readonly GM_MAX_SHARE_EVENTS: 3;
|
|
10
|
+
/** Lower bound of the random pre-share / pre-reply delay (ms). */
|
|
11
|
+
readonly GM_JITTER_MIN: 500;
|
|
12
|
+
/** Upper bound of the random pre-share / pre-reply delay (ms). */
|
|
13
|
+
readonly GM_JITTER_MAX: 1500;
|
|
14
|
+
/** How long to wait for the next chunk before declaring a gap and sending REQUEST (ms). */
|
|
15
|
+
readonly REQUEST_INACTIVITY: 30000;
|
|
16
|
+
/** Number of REQUEST retries before abandoning a partial event. */
|
|
17
|
+
readonly REQUEST_MAX_RETRIES: 3;
|
|
18
|
+
/** How long to keep sent chunk packets around for potential retransmission (ms). */
|
|
19
|
+
readonly SENT_CHUNKS_TTL: number;
|
|
20
|
+
/** How long before we stop deduplicating a previously-seen event ID (ms). */
|
|
21
|
+
readonly DEDUP_TIMEOUT: number;
|
|
22
|
+
/** How long to wait for subsequent chunks of a partial event (ms). */
|
|
23
|
+
readonly EVENT_TIMEOUT: 30000;
|
|
24
|
+
/** TTL assigned to locally-created events (decremented on each re-broadcast hop). */
|
|
25
|
+
readonly INITIAL_TTL: 6;
|
|
26
|
+
};
|
|
27
|
+
export default Defaults;
|
|
28
|
+
/** Widened (plain `number`) version of `Defaults` — use for constructor option types. */
|
|
29
|
+
export type DefaultOptions = {
|
|
30
|
+
[K in keyof typeof Defaults]: number;
|
|
31
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface NostrEvent {
|
|
2
|
+
id: string;
|
|
3
|
+
pubkey: string;
|
|
4
|
+
created_at: number;
|
|
5
|
+
kind: number;
|
|
6
|
+
tags: string[][];
|
|
7
|
+
content: string;
|
|
8
|
+
sig: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SerializedEvent {
|
|
11
|
+
compressed: Uint8Array;
|
|
12
|
+
checksum: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function serializeEvent(event: NostrEvent): Promise<SerializedEvent>;
|
|
15
|
+
export declare function deserializeEvent(chunkPayloads: Uint8Array[], expectedChecksum: number, eventIdHex: string): Promise<NostrEvent>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DecodedGmPacket } from './protocol.js';
|
|
2
|
+
import { EventEmitter, Logger } from './utils.js';
|
|
3
|
+
import { DefaultOptions } from './constants.js';
|
|
4
|
+
import { NostrEvent } from './event-codec.js';
|
|
5
|
+
export interface GmProtocolDeps {
|
|
6
|
+
getNodeId: () => Uint8Array | null;
|
|
7
|
+
getSentEvents: () => Map<string, NostrEvent>;
|
|
8
|
+
sendEvent: (event: NostrEvent) => Promise<void>;
|
|
9
|
+
sendGm: (nodeId: Uint8Array, eventCount: number, recentSince: number) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export type GmProtocolEvents = {
|
|
12
|
+
"gm:receive": [decoded: DecodedGmPacket, byteLength: number];
|
|
13
|
+
};
|
|
14
|
+
export declare class GmProtocol extends EventEmitter<GmProtocolEvents> {
|
|
15
|
+
private getNodeId;
|
|
16
|
+
private getSentEvents;
|
|
17
|
+
private sendEvent;
|
|
18
|
+
private sendGm;
|
|
19
|
+
private backoff;
|
|
20
|
+
private lastResponse;
|
|
21
|
+
private log;
|
|
22
|
+
BACKOFF_BASE: number;
|
|
23
|
+
MAX_SHARE_EVENTS: number;
|
|
24
|
+
JITTER_MIN: number;
|
|
25
|
+
JITTER_MAX: number;
|
|
26
|
+
constructor(deps: GmProtocolDeps, options?: Partial<Pick<DefaultOptions, "GM_BACKOFF_BASE" | "GM_MAX_SHARE_EVENTS" | "GM_JITTER_MIN" | "GM_JITTER_MAX">>, logger?: Logger | null);
|
|
27
|
+
handlePacket(packet: Uint8Array): Promise<void>;
|
|
28
|
+
private jitter;
|
|
29
|
+
private oldestTimestamp;
|
|
30
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { default as Defaults } from './constants.js';
|
|
2
|
+
export { LoRaTransport } from './lora-transport.js';
|
|
3
|
+
export type { LoRaTransportEvents, TransportOptions, PacketReceiveInfo, TimingOptions, } from './lora-transport.js';
|
|
4
|
+
export type { Logger } from './utils.js';
|
|
5
|
+
export { SerialConnectionManager } from './serial-connection.js';
|
|
6
|
+
export type { DeviceInfo, RadioConfig } from './serial-connection.js';
|
|
7
|
+
export type { ConnectionManager, ConnectionManagerEvents } from './connection-manager.js';
|
|
8
|
+
export { Assembler } from './assembler.js';
|
|
9
|
+
export { SendQueue } from './send-queue.js';
|
|
10
|
+
export type { QueueSnapshot, QueueItem } from './send-queue.js';
|
|
11
|
+
export { GmProtocol } from './gm-protocol.js';
|
|
12
|
+
export type { GmProtocolDeps } from './gm-protocol.js';
|
|
13
|
+
export type { NostrEvent } from './event-codec.js';
|
|
14
|
+
export type { DecodedDataChunk, DecodedGmPacket } from './protocol.js';
|
|
15
|
+
export { Defaults };
|
|
16
|
+
export type { DefaultOptions } from './constants.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { A as s, D as o, G as r, L as n, S as t, a as l } from "./serial-connection.js";
|
|
2
|
+
export {
|
|
3
|
+
s as Assembler,
|
|
4
|
+
o as Defaults,
|
|
5
|
+
r as GmProtocol,
|
|
6
|
+
n as LoRaTransport,
|
|
7
|
+
t as SendQueue,
|
|
8
|
+
l as SerialConnectionManager
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { DecodedDataChunk, DecodedGmPacket } from './protocol.js';
|
|
2
|
+
import { NostrEvent } from './event-codec.js';
|
|
3
|
+
import { QueueSnapshot } from './send-queue.js';
|
|
4
|
+
import { EventEmitter, Logger } from './utils.js';
|
|
5
|
+
import { ConnectionManager } from './connection-manager.js';
|
|
6
|
+
export type { ConnectionManager };
|
|
7
|
+
export type { ConnectionManagerEvents } from './connection-manager.js';
|
|
8
|
+
export interface TransportOptions {
|
|
9
|
+
nodeId?: Uint8Array;
|
|
10
|
+
/** Diagnostic logger. Defaults to `null` (silent). */
|
|
11
|
+
logger?: Logger | null;
|
|
12
|
+
interPacketDelay?: number;
|
|
13
|
+
interPacketJitter?: number;
|
|
14
|
+
gmBackoffBase?: number;
|
|
15
|
+
gmMaxShareEvents?: number;
|
|
16
|
+
gmJitterMin?: number;
|
|
17
|
+
gmJitterMax?: number;
|
|
18
|
+
requestInactivity?: number;
|
|
19
|
+
requestMaxRetries?: number;
|
|
20
|
+
sentChunksTtl?: number;
|
|
21
|
+
dedupTimeout?: number;
|
|
22
|
+
eventTimeout?: number;
|
|
23
|
+
initialTtl?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface PacketReceiveInfo {
|
|
26
|
+
snr: number;
|
|
27
|
+
rssi: number;
|
|
28
|
+
size: number;
|
|
29
|
+
}
|
|
30
|
+
export interface TimingOptions {
|
|
31
|
+
delay?: number;
|
|
32
|
+
jitter?: number;
|
|
33
|
+
}
|
|
34
|
+
export type LoRaTransportEvents = {
|
|
35
|
+
"event:receive": [event: NostrEvent];
|
|
36
|
+
"event:send": [event: NostrEvent];
|
|
37
|
+
"chunk:receive": [decoded: DecodedDataChunk, byteLength: number];
|
|
38
|
+
"gm:receive": [decoded: DecodedGmPacket, byteLength: number];
|
|
39
|
+
"packet:receive": [info: PacketReceiveInfo];
|
|
40
|
+
"packet:send": [
|
|
41
|
+
decoded: DecodedDataChunk | DecodedGmPacket,
|
|
42
|
+
byteLength: number
|
|
43
|
+
];
|
|
44
|
+
"request:send": [
|
|
45
|
+
eventIdHex: string,
|
|
46
|
+
missingChunks: number[],
|
|
47
|
+
attempt: number
|
|
48
|
+
];
|
|
49
|
+
"request:receive": [prefixHex: string, missingChunks: number[]];
|
|
50
|
+
"queue:update": [snapshot: QueueSnapshot[]];
|
|
51
|
+
"connect": [portLabel: string];
|
|
52
|
+
"disconnect": [];
|
|
53
|
+
"error": [err: Error];
|
|
54
|
+
};
|
|
55
|
+
export declare class LoRaTransport extends EventEmitter<LoRaTransportEvents> {
|
|
56
|
+
connection: ConnectionManager;
|
|
57
|
+
nodeId: Uint8Array | null;
|
|
58
|
+
private log;
|
|
59
|
+
private eventTimeout;
|
|
60
|
+
private dedupTimeout;
|
|
61
|
+
private initialTtl;
|
|
62
|
+
private requestInactivity;
|
|
63
|
+
private requestMaxRetries;
|
|
64
|
+
private sentChunksTtl;
|
|
65
|
+
private assembler;
|
|
66
|
+
private queue;
|
|
67
|
+
private gm;
|
|
68
|
+
private recentEventIds;
|
|
69
|
+
private inactivityTimers;
|
|
70
|
+
private pendingTimers;
|
|
71
|
+
private sentEvents;
|
|
72
|
+
private sentChunks;
|
|
73
|
+
constructor(connection: ConnectionManager, options?: TransportOptions);
|
|
74
|
+
/**
|
|
75
|
+
* Open the connection and wire up data/lifecycle events.
|
|
76
|
+
* This triggers the browser port picker for `SerialConnectionManager`.
|
|
77
|
+
*/
|
|
78
|
+
begin(): Promise<void>;
|
|
79
|
+
/** Close the connection. */
|
|
80
|
+
end(): Promise<void>;
|
|
81
|
+
sendEvent(event: NostrEvent): Promise<void>;
|
|
82
|
+
sendGm(nodeId: Uint8Array, eventCount?: number, recentSince?: number): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Feed a raw radio packet into the transport.
|
|
85
|
+
* Only needed when implementing a custom `ConnectionManager` that doesn't
|
|
86
|
+
* use the event interface — `begin()` wires this automatically otherwise.
|
|
87
|
+
*/
|
|
88
|
+
handleRawDataPacket(data: {
|
|
89
|
+
data: ArrayBuffer | Uint8Array;
|
|
90
|
+
snr: number;
|
|
91
|
+
rssi: number;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
/** Update the inter-packet timing on the underlying send queue. */
|
|
94
|
+
setTimingOptions({ delay, jitter }?: TimingOptions): void;
|
|
95
|
+
/** Number of events sent by this node (available for GM announcements). */
|
|
96
|
+
get sentEventCount(): number;
|
|
97
|
+
private handleDataPacket;
|
|
98
|
+
private completeSession;
|
|
99
|
+
private handleRequestPacket;
|
|
100
|
+
private handleGmPacket;
|
|
101
|
+
private resetInactivityTimer;
|
|
102
|
+
private clearInactivityTimer;
|
|
103
|
+
private onChunkInactivity;
|
|
104
|
+
private resetPendingTimer;
|
|
105
|
+
private clearPendingTimer;
|
|
106
|
+
private onPendingInactivity;
|
|
107
|
+
private pruneSentChunks;
|
|
108
|
+
private cleanupDeduplication;
|
|
109
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export declare const PacketType: {
|
|
2
|
+
readonly DATA: 1;
|
|
3
|
+
readonly REQUEST: 2;
|
|
4
|
+
readonly GM: 3;
|
|
5
|
+
};
|
|
6
|
+
export type PacketTypeValue = typeof PacketType[keyof typeof PacketType];
|
|
7
|
+
export declare const Limits: {
|
|
8
|
+
readonly MAX_PACKET_SIZE: 168;
|
|
9
|
+
readonly FIRST_CHUNK_HEADER: 44;
|
|
10
|
+
readonly FIRST_CHUNK_PAYLOAD: 124;
|
|
11
|
+
readonly SUBSEQUENT_CHUNK_HEADER: 17;
|
|
12
|
+
readonly SUBSEQUENT_CHUNK_PAYLOAD: 151;
|
|
13
|
+
readonly EVENT_ID_PREFIX_LEN: 14;
|
|
14
|
+
readonly MAX_CHUNKS: 8;
|
|
15
|
+
readonly MAX_COMPRESSED_SIZE: 1152;
|
|
16
|
+
};
|
|
17
|
+
export declare class CRC32 {
|
|
18
|
+
private static table;
|
|
19
|
+
private static makeTable;
|
|
20
|
+
static calculate(data: Uint8Array): number;
|
|
21
|
+
}
|
|
22
|
+
export declare class Compression {
|
|
23
|
+
static compress(data: Uint8Array): Promise<Uint8Array>;
|
|
24
|
+
static decompress(data: Uint8Array): Promise<Uint8Array>;
|
|
25
|
+
}
|
|
26
|
+
export interface FirstChunk {
|
|
27
|
+
type: 'first';
|
|
28
|
+
eventId: Uint8Array;
|
|
29
|
+
eventKind: number;
|
|
30
|
+
totalChunks: number;
|
|
31
|
+
checksum: number;
|
|
32
|
+
ttl: number;
|
|
33
|
+
payload: Uint8Array;
|
|
34
|
+
isLast: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface SubsequentChunk {
|
|
37
|
+
type: 'subsequent';
|
|
38
|
+
eventIdPrefix: Uint8Array;
|
|
39
|
+
chunkIndex: number;
|
|
40
|
+
payload: Uint8Array;
|
|
41
|
+
isLast: boolean;
|
|
42
|
+
}
|
|
43
|
+
export type DecodedDataChunk = FirstChunk | SubsequentChunk;
|
|
44
|
+
export declare class DataPacket {
|
|
45
|
+
static createFromCompressed(eventId: Uint8Array, eventKind: number, compressed: Uint8Array, ttl: number): Uint8Array[];
|
|
46
|
+
static encodeFirst(eventId: Uint8Array, eventKind: number, totalChunks: number, checksum: number, ttl: number, payload: Uint8Array, isLast: boolean): Uint8Array;
|
|
47
|
+
static encodeSubsequent(eventIdPrefix: Uint8Array, chunkIndex: number, payload: Uint8Array, isLast: boolean): Uint8Array;
|
|
48
|
+
/**
|
|
49
|
+
* Decode DATA packet.
|
|
50
|
+
* Distinguishes first vs subsequent by peeking at bytes 1-2:
|
|
51
|
+
* - First chunk: bytes 1-2 are chunk_index == 0x0000, event_id at offset 3
|
|
52
|
+
* - Subsequent chunk: bytes 1-2 are part of event_id_prefix, chunk_index at offset 15
|
|
53
|
+
*
|
|
54
|
+
* For subsequent chunks, the first 2 bytes of event_id_prefix are statistically
|
|
55
|
+
* never 0x0000 for real event IDs, so 0x0000 reliably identifies first chunks.
|
|
56
|
+
*/
|
|
57
|
+
static decode(packet: Uint8Array): DecodedDataChunk;
|
|
58
|
+
}
|
|
59
|
+
export interface DecodedRequestPacket {
|
|
60
|
+
eventIdPrefix: Uint8Array;
|
|
61
|
+
missingChunks: number[];
|
|
62
|
+
}
|
|
63
|
+
export declare class RequestPacket {
|
|
64
|
+
static create(eventIdPrefix: Uint8Array, missingChunks: number[]): Uint8Array;
|
|
65
|
+
static decode(packet: Uint8Array): DecodedRequestPacket;
|
|
66
|
+
}
|
|
67
|
+
export interface DecodedGmPacket {
|
|
68
|
+
nodeId: Uint8Array;
|
|
69
|
+
timestamp: number;
|
|
70
|
+
eventCount: number;
|
|
71
|
+
recentSince: number;
|
|
72
|
+
}
|
|
73
|
+
export declare class GmPacket {
|
|
74
|
+
static create(nodeId: Uint8Array, eventCount: number, recentSince: number, timestamp?: number): Uint8Array;
|
|
75
|
+
static decode(packet: Uint8Array): DecodedGmPacket;
|
|
76
|
+
}
|
|
77
|
+
/** Read the packet type from the flags byte without fully decoding the packet. */
|
|
78
|
+
export declare function parsePacketType(packet: Uint8Array): number;
|
package/dist/react.d.ts
ADDED
package/dist/react.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState as o, useRef as d, useEffect as G, useCallback as s } from "react";
|
|
2
|
+
import { a as I, L as Q } from "./serial-connection.js";
|
|
3
|
+
function D(a = {}) {
|
|
4
|
+
const [g, p] = o(!1), [v, u] = o(!1), [E, l] = o(null), [y, m] = o(null), [L, C] = o(null), [N, R] = o([]), [b, k] = o(null), w = d(null), e = d(null), i = d(a);
|
|
5
|
+
G(() => {
|
|
6
|
+
i.current = a;
|
|
7
|
+
}, [a]);
|
|
8
|
+
const S = s(async () => {
|
|
9
|
+
u(!0), l(null);
|
|
10
|
+
try {
|
|
11
|
+
const n = new I(i.current.logger);
|
|
12
|
+
w.current = n, n.on("deviceInfo", (r) => C(r.name));
|
|
13
|
+
const { onEvent: t, ...f } = i.current, c = new Q(n, f);
|
|
14
|
+
e.current = c, c.on("connect", (r) => {
|
|
15
|
+
p(!0), m(r);
|
|
16
|
+
}), c.on("disconnect", () => {
|
|
17
|
+
p(!1), m(null);
|
|
18
|
+
}), c.on("error", (r) => l(r)), c.on("queue:update", (r) => R(r)), c.on("packet:receive", (r) => k(r)), t && c.on("event:receive", t), await c.begin();
|
|
19
|
+
} catch (n) {
|
|
20
|
+
const t = n instanceof Error ? n : new Error(String(n));
|
|
21
|
+
l(t), u(!1);
|
|
22
|
+
} finally {
|
|
23
|
+
u(!1);
|
|
24
|
+
}
|
|
25
|
+
}, []), h = s(async () => {
|
|
26
|
+
var n;
|
|
27
|
+
await ((n = e.current) == null ? void 0 : n.end()), e.current = null, w.current = null;
|
|
28
|
+
}, []), O = s(async (n) => {
|
|
29
|
+
if (!e.current) throw new Error("Not connected");
|
|
30
|
+
await e.current.sendEvent(n);
|
|
31
|
+
}, []), P = s(async (n, t, f) => {
|
|
32
|
+
if (!e.current) throw new Error("Not connected");
|
|
33
|
+
await e.current.sendGm(n, t, f);
|
|
34
|
+
}, []), T = s((n) => {
|
|
35
|
+
var t;
|
|
36
|
+
(t = e.current) == null || t.setTimingOptions(n);
|
|
37
|
+
}, []);
|
|
38
|
+
return {
|
|
39
|
+
isConnected: g,
|
|
40
|
+
connecting: v,
|
|
41
|
+
error: E,
|
|
42
|
+
portLabel: y,
|
|
43
|
+
deviceName: L,
|
|
44
|
+
sendQueue: N,
|
|
45
|
+
lastPacket: b,
|
|
46
|
+
connect: S,
|
|
47
|
+
disconnect: h,
|
|
48
|
+
sendEvent: O,
|
|
49
|
+
sendGm: P,
|
|
50
|
+
setTimingOptions: T,
|
|
51
|
+
transport: e.current
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
D as useNostrLora
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=react.js.map
|