@soapbox.pub/nostr-lora
v0.1.1
Published
Reference implementation of [NIP-LR](./NIP.md): Nostr over LoRa.
Readme
@soapbox.pub/nostr-lora
Reference implementation of NIP-LR: Nostr over LoRa.
Handles packet encoding/decoding, chunked transmission, reassembly, retransmission requests, and GM (announce) messages. Designed for use with MeshCore LoRa devices over Web Serial.
Structure
nostr-lora # core — LoRaTransport, SerialConnectionManager, etc.
nostr-lora/react # React hook — useNostrLoraReact is an optional peer dependency. The core entry point has no React dependency.
Installation
npm install @soapbox.pub/nostr-loraUsage
React
import { useNostrLora } from "nostr-lora/react";
function App() {
const { connect, disconnect, isConnected, sendEvent, error } = useNostrLora(
{
onEvent(event) {
console.log("received event", event);
},
},
);
return (
<div>
{isConnected
? <button onClick={disconnect}>Disconnect</button>
: <button onClick={connect}>Connect</button>}
{error && <p>{error.message}</p>}
</div>
);
}connect() triggers the browser's serial port picker. Once connected, incoming
Nostr events are delivered via onEvent. Call sendEvent(event) to broadcast a
signed Nostr event over LoRa.
Full return type:
| Field | Type | Description |
| ------------------ | ------------------------------------------------------ | ----------------------------------------- |
| isConnected | boolean | Whether the serial port is open |
| connecting | boolean | true while the port is being opened |
| error | Error \| null | Last error, if any |
| portLabel | string \| null | USB VID:PID of the connected device |
| deviceName | string \| null | Device name from firmware |
| sendQueue | QueueSnapshot[] | Current outbound packet queue |
| lastPacket | PacketReceiveInfo \| null | SNR/RSSI/size of last received packet |
| connect | () => Promise<void> | Open port and begin transport |
| disconnect | () => Promise<void> | Close port |
| sendEvent | (event: NostrEvent) => Promise<void> | Send a signed event |
| sendGm | (nodeId, eventCount?, recentSince?) => Promise<void> | Broadcast a GM announcement |
| setTimingOptions | ({ delay?, jitter? }) => void | Adjust inter-packet timing live |
| transport | LoRaTransport \| null | Direct access to the underlying transport |
Vanilla (no framework)
import { LoRaTransport, SerialConnectionManager } from "nostr-lora";
const connection = new SerialConnectionManager();
const transport = new LoRaTransport(connection);
transport.on("event:receive", (event) => {
console.log("received event", event);
});
transport.on("connect", (portLabel) => {
console.log("connected to", portLabel);
});
transport.on("error", (err) => {
console.error(err);
});
// Triggers the browser port picker
await transport.begin();
// Send a signed Nostr event
await transport.sendEvent(signedEvent);
// Later
await transport.end();Custom connection
Implement ConnectionManager to use a different transport (e.g. WebSocket,
BLE):
import type { ConnectionManager, ConnectionManagerEvents } from "nostr-lora";
import { LoRaTransport } from "nostr-lora";
class MyConnection implements ConnectionManager {
on<E extends keyof ConnectionManagerEvents>(
event: E,
handler: (...args: ConnectionManagerEvents[E]) => void,
) {/* ... */}
async open() {/* ... */}
async close() {/* ... */}
async sendRawData(data: Uint8Array) {/* ... */}
}
const transport = new LoRaTransport(new MyConnection());Options
All options are optional. Defaults are exported as Defaults:
import { Defaults } from "nostr-lora";| Option | Default | Description |
| ------------------- | ----------- | ----------------------------------------------------------- |
| nodeId | undefined | Your node's identity bytes (used in GM packets) |
| logger | null | Object with log/warn/error/debug methods (e.g. console) |
| interPacketDelay | 2000 ms | Base delay between packets in the send queue |
| interPacketJitter | 500 ms | Max random jitter added on top of the delay |
| requestInactivity | 30000 ms | Idle time before sending a retransmission request |
| requestMaxRetries | 3 | Max retransmission attempts before abandoning |
| eventTimeout | 30000 ms | Max wait for all chunks of a partial event |
| dedupTimeout | 900000 ms | How long to remember received event IDs |
| sentChunksTtl | 300000 ms | How long to keep sent chunks for potential retransmission |
| initialTtl | 6 | Hop TTL assigned to locally-created events |
| gmBackoffBase | 300000 ms | Minimum interval between GM responses to the same peer |
| gmMaxShareEvents | 3 | Max events to re-share in response to a GM |
| gmJitterMin | 500 ms | Min random delay before responding to a GM |
| gmJitterMax | 1500 ms | Max random delay before responding to a GM |
Events
LoRaTransport extends EventEmitter and emits:
| Event | Args | Description |
| ----------------- | ------------------------------------ | ------------------------------------------------ |
| connect | portLabel: string | Connection opened |
| disconnect | — | Connection closed |
| error | err: Error | Error from connection or packet handling |
| event:receive | event: NostrEvent | A complete Nostr event was received and verified |
| event:send | event: NostrEvent | A Nostr event finished sending |
| packet:receive | info: PacketReceiveInfo | Raw packet received (SNR, RSSI, size) |
| chunk:receive | decoded, byteLength | Individual DATA chunk received |
| gm:receive | decoded, byteLength | GM packet received |
| request:receive | prefixHex, missingChunks | Retransmission request received from peer |
| request:send | eventIdHex, missingChunks, attempt | Retransmission request sent |
| queue:update | snapshot: QueueSnapshot[] | Send queue changed |
License
The source code of this library is provided to you under the terms of the MIT License.
