Creates a serial RNode interface.
Protected_Whether a terminal closed event has already been dispatched for the
current connection episode (dedupe guard).
Outbound queue. Each entry is the already-serialized raw RNS payload plus a flag distinguishing ordinary packets from the raw id-callsign beacon (which, like Python, is framed as a CMD_DATA payload without being a real Packet).
Protected_AbortController for the current reconnect wait, so disconnect() can
cancel an in-flight backoff immediately.
Protected_Reconnect attempt counter for the current drop episode. Reset to 0 at the start of each Interface._runReconnectLoop run.
Protected_Single-flight guard: only one reconnect loop runs at a time.
Times an announce burst has latched on this interface.
Whether automatic reconnection is enabled for this initiator interface.
Nominal physical bitrate of this interface in bits per second
(self.bitrate on RNS.Interfaces.Interface in the Python reference,
default 62500). Each interface overrides this with its medium's rate.
Used by TransportCore.prioritizeInterfaces() to order the interface set
highest-bitrate-first (mirrors the Python reference's
Transport.prioritize_interfaces); the per-bitrate link-timeout and
announce-rate-limit behaviours that also build on it are tracked as
Phase 2 of work doc #20. Configured bitrates below
Reticulum.MINIMUM_BITRATE are ignored (matching Python).
Per-dial connect timeout in seconds.
Epoch milliseconds when the interface was constructed (self.created in
the Python reference, which uses time.time()).
Per-interface default IFAC size (bytes) when IFAC is enabled but no
explicit ifacSize was given. Mirrors DEFAULT_IFAC_SIZE on each
Python interface (16 for Auto/Backbone, 8 for AX.25). Subclasses
override; the base default of 16 matches the common case.
Permanent stop signal read by the reconnect loop. Set by disconnect().
True once a display-capable device (ESP32/NRF52) has been detected.
Per-interface path preference weight (Interface.gravity in the Python
reference, DEFAULT_GRAVITY = 0). When the same announce reaches this
node over multiple interfaces, the path table prefers the entry learned
via the higher-gravity interface (e.g. a wired backbone over a slow radio
link). null means "no preference" — import("../core/reticulum.js").Reticulum
substitutes its defaultGravity at addInterface time.
Announces dropped (not held) because the held table was at its cap.
Held announces released back into the inbound pipeline.
Announces held while an ingress burst is latched, keyed by destination
hash hex (Python held_announces). Drained by
processHeldAnnounces on the transport sweep.
Incoming-announce timestamp ring (seconds).
Earliest held-announce release time (seconds); set on burst activation.
Remaining quiet evaluations before a latched PR burst unlatches.
Derived IFAC Ed25519 identity (only its signing ability is used).
Populated lazily by _ensureIfacMaterial; null while IFAC is
disabled or before first use.
Derived 64-byte IFAC key (HKDF over import("../core/ifac.js").IFAC_SALT).
Shared passphrase enabling IFAC (ifac_netkey). See ifacNetname.
Shared network name enabling IFAC (ifac_netname). When set together
with ifacNetkey (or alone), packets on this interface are
authenticated and obfuscated. Both endpoints must share the same value.
IFAC signature of fullHash(ifacKey), published in the discovery
announce.
IFAC-specific violations: missing/invalid/short IFAC fields.
Whether ingress burst control is enabled on this interface (Python
ingress_control). Disabling makes shouldIngressLimit and
shouldIngressLimitPr always return false.
The initiator flag is always true for an RNode (it dials the radio).
Incoming path-request timestamp ring (seconds).
Maximum reconnection attempts per drop. Infinity retries forever.
Outgoing-announce timestamp ring (seconds).
Outgoing path-request timestamp ring (seconds).
Inbound packet-filter (dedup) hits.
Times a path? burst has latched on this interface.
Unique-tag path requests dropped while a PR burst was latched.
Generic protocol violations: malformed packets, bad signatures, etc.
Seconds to wait between reconnection attempts.
The underlying socket, when this interface is backed by a Node.js stream.
StaticANNOUNCE_Seconds after which an unanswered announce sample decays out of the
deque (AR_FREQ_DECAY = 1/AR_MINFREQ_HZ = 10 s).
StaticCALLSIGN_Maximum encoded ID callsign beacon length in bytes (Python parity).
StaticDEFAULT_Default IFAC size, matching the Python DEFAULT_IFAC_SIZE = 8.
StaticDISPLAY_Default display-read poll interval in seconds (Python parity).
StaticDISPLAY_Display snapshot size in bytes (CMD_DISP_READ, Python parity).
StaticFB_Framebuffer bits per pixel (Python FB_BITS_PER_PIXEL).
StaticFB_Bytes per framebuffer line (Python FB_BYTES_PER_LINE).
StaticFB_Framebuffer width in pixels (Python FB_PIXEL_WIDTH).
StaticFB_Pixels packed per framebuffer byte (Python FB_PIXELS_PER_BYTE).
StaticFB_Full framebuffer size in bytes.
StaticFREQ_Maximum supported frequency in Hz.
StaticFREQ_Minimum supported frequency in Hz.
StaticFREQ_Rolling-sample cap for the announce/PR frequency deques
(IA_FREQ_SAMPLES / IP_FREQ_SAMPLES / OP_FREQ_SAMPLES in the Python
reference — all 48; Python reuses IA_FREQ_SAMPLES for the PR deque).
StaticHW_Hardware MTU for the LoRa path, matching the Python HW_MTU = 508.
StaticIC_Announce burst threshold for established interfaces, Hz (IC_BURST_FREQ).
StaticIC_Announce burst threshold for new interfaces, Hz (IC_BURST_FREQ_NEW).
StaticIC_Seconds a burst stays latched after activation (IC_BURST_HOLD).
StaticIC_Seconds before held announces may release after a burst (IC_BURST_PENALTY).
StaticIC_Minimum deque samples before a frequency is reported
(IC_DEQUE_MIN_SAMPLE = 2 — i.e. > 2 samples).
StaticIC_Seconds between held-announce releases once draining
(IC_HELD_RELEASE_INTERVAL).
StaticIC_Interface age in seconds below which the stricter "new interface"
burst thresholds apply (IC_NEW_TIME = 2 h).
StaticIC_Quiet evaluations required to unlatch a PR burst after the hold
(ic_pr_burst_cooldown = 3; any above-threshold evaluation resets it).
Anti-flapping hysteresis added upstream in "Improved PR ingress
limiter" — the announce limiter has no cooldown.
StaticIC_Path-request burst threshold for established interfaces, Hz (IC_PR_BURST_FREQ).
StaticIC_Path-request burst threshold for new interfaces, Hz (IC_PR_BURST_FREQ_NEW).
StaticMAX_Maximum held announces buffered per interface while an announce burst
is latched (MAX_HELD_ANNOUNCES). A held table at this size silently
drops further announces for destinations not already held.
StaticPR_Seconds after which a PR sample decays (PR_FREQ_DECAY = 10 s).
StaticREQUIRED_Minimum required firmware major version.
StaticREQUIRED_Minimum required firmware minor version.
StaticRSSI_RSSI offset applied to raw radio RSSI readings, matching the Python ref.
Whether IFAC is enabled on this interface (a shared secret is configured).
Not used: RNode inbound is event-driven (the internal read loop dispatches
packet events directly). Returns null.
The outbound Packet stream. Transport acquires a writer in addInterface;
each written packet is KISS-framed as a data frame and sent once the radio
is online and ready (see RNodeInterface#send).
Protected_Protected
Signals the reconnect loop to stop and cancels any in-flight backoff.
Client subclasses call this at the top of their disconnect().
Protected_Protected
Dispatches a terminal closed event exactly once per connection episode.
Protected_Protected
Counts an inbound packet against rxb and dispatches the "packet"
event, the single inbound chokepoint each interface's read loop funnels
through. Mirrors the self.rxb += len(data) + self.owner.inbound(...)
pairing in each Python interface's process_incoming.
Uses the deserialized packet's cached raw bytes when available (set by
Packet.deserialize), avoiding a re-serialize. RNodeInterface dispatches
its own packets (it counts the IFAC-inclusive payload) and does not call
this.
Protected_Protected
Derives and caches the IFAC key/identity/signature from the configured
ifacNetname / ifacNetkey, mirroring the per-interface
setup in RNS/Reticulum.py (~l.975). No-op (resolves false) when IFAC
is disabled. Memoised so the HKDF + Ed25519 key load runs at most once.
true if IFAC material is available.
Protected_Protected
Opens the transport and configures the radio. Used both for the initial connection and for each reconnect attempt.
Resolves once the radio is online.
Protected_Protected
Called when the underlying connection drops (the inbound stream ends or
errors). For an initiator with auto-reconnect enabled and not deliberately
detached, dispatches disconnected and kicks off the reconnect loop;
otherwise dispatches a terminal closed event.
Matches the Python reference read_loop, which reconnects the initiator
on any termination and tears down (non-reconnecting) everyone else.
Protected_Protected
Initializes shared reconnect state from constructor options. Called by client interface subclasses (TCP, WebSocket) that support reconnection.
Subclasses must also set Interface.initiator: true for an
outbound dialer, false for an adopted/server-spawned socket.
Protected_Protected
Verifies and unseals inbound raw wire bytes (RNS.Transport.inbound).
Enforces the flag-presence rules: an IFAC-enabled interface drops a
flag-clear packet, and a plain interface drops a flag-set packet — both
return null (silent drop). For an IFAC interface it then unmasks,
strips the IFAC and verifies it by re-signing; a mismatch also yields
null. Subclasses/interfaces call this at the chokepoint where a frame
has been unframed to bytes, just before Packet.deserialize.
Sealed or plain wire bytes straight off the medium.
The unsealed bytes, or null to drop.
Protected_Protected
Opens the serial device (non-blocking), configures the line discipline via
inherited-fd stty, and returns the transport handles for the base class.
Protected_Protected
Records an outbound packet against txb. Subclasses (or the
interface's outbound stream write callback) call this at the point a
packet is handed to the medium — the single chokepoint where every
transmitted packet passes, whether sent via send, the transport
router, or a broadcast. Mirrors the self.txb += len(data) line in each
Python interface's process_outgoing.
RNodeInterface overrides its own counting (it measures the IFAC-inclusive wire payload) and does not call this.
Protected_Protected
Runs the single-flight reconnect loop. Repeatedly waits reconnectWait
seconds then attempts to re-establish the connection via the subclass
_establishConnection() hook, until it succeeds, the interface is
detached, or maxReconnectTries is exceeded (terminal closed).
Each attempt fires a reconnecting event with the upcoming attempt
number, the wait, and the cap, for observability. A successful reconnect
fires connected (via _establishConnection).
Protected_Protected
Seals raw (un-IFACed) wire bytes for transmit (RNS.Transport.transmit).
No-op passthrough when IFAC is disabled; otherwise derives the IFAC
material on first use, then signs, sets the ifac_flag, inserts the IFAC
field and XOR-masks the packet. Subclasses/interfaces call this at the
chokepoint where a packet is serialised to bytes, just before framing.
Serialised, unsealed wire bytes.
The bytes to put on the medium.
Protected_Protected
Resolves after ms, or immediately if signal aborts. Used so
disconnect() can cancel an in-flight reconnect backoff at once.
Recomputes the nominal on-air bitrate from the echoed LoRa params.
Derives a 0–100 link-quality figure from the latest SNR reading.
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
Optionaloptions: boolean | AddEventListenerOptionsAge of this interface in seconds (Python age()).
Applies node-global ingress-control overrides to this interface
(mirrors Python, where every interface reads the [reticulum]-section
ic_* defaults via RNS.Reticulum.get_instance()._default_ic_*() — there
is no per-interface config for these). Only keys present in overrides
are assigned; absent keys keep the class constants. Called by
import("../core/reticulum.js").Reticulum#addInterface when the
node was constructed with an ingressControl config block. These are
deliberately not constructor options / interface schema properties:
they scope to the whole node, like the Python reference.
Optional hook invoked by import("../transport/transport.js").TransportCore#addInterface with the transport that owns this interface, right after the interface is attached.
The base implementation is a no-op. Interfaces that spawn sub-interfaces
dynamically — notably AutoInterface, which discovers peers and
spawns one per peer — override it to remember the transport so the spawned
peers can be auto-registered without a separate Reticulum global (the
Python reference uses the global RNS.Transport.add_interface for this).
Overriders should also register any peers spawned before the transport was
attached, so the addInterface/connect call order doesn't matter.
Opens the transport, runs the detect → configure → validate handshake, and brings the radio online.
On a first-attempt failure with auto-reconnect enabled, the promise rejects (so the caller knows) but the reconnect loop keeps retrying in the background — matching the Python reference, which spawns a reconnect thread on the first failure.
Sends the detect + firmware/platform/MCU query sequence, matching the
Python detect() byte-for-byte (four frames sharing FEND boundaries).
Returns control of the display to the device firmware. Mirrors Python
disable_external_framebuffer. No-op on headless devices.
Powers the radio down, sends the host-leave command, closes the transport,
and cancels any pending reconnect. Dispatches disconnected then a
terminal closed.
The dispatchEvent() method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent().
Writes a full image to the framebuffer, one
RNodeInterface.FB_BYTES_PER_LINE-byte line at a time. Trailing
bytes that do not fill a complete line are ignored. Mirrors Python
display_image.
The number of complete lines written.
Enables host control of the on-device display (external framebuffer mode)
so that RNodeInterface#displayImage output is shown. Mirrors Python
enable_external_framebuffer. No-op on headless devices.
Returns a stats snapshot for this RNode, extending the base traffic
counters with the LoRa radio telemetry the firmware reports over KISS:
on-air bitrate, active radio parameters, signal quality (RSSI/SNR/Q),
channel airtime & load, and battery/temperature. Fields are null until
the radio has reported them.
Forces a hardware reset of the RNode. Sends CMD_RESET with the 0xF8 reset
code, then waits for the device to reboot (Python hard_reset, which
sleeps 2.25s). A rebooting ESP32 reports CMD_RESET 0xF8 once it is back,
which the read loop treats as a connection loss (→ reconnect). No-op before
connect.
Buffers an announce for delayed processing while an ingress burst is
latched (Python hold_announce). Announces at or beyond
PATHFINDER_M - 1 (127) hops are dropped rather than held; a destination
already in the table always replaces its entry (newest emission wins);
beyond icMaxHeldAnnounces distinct destinations, new ones are
silently dropped.
Validated announce.
Records an IFAC (interface authentication code) violation on this
interface (Python ifac_violation): missing IFAC flag, insufficient
packet size for the IFAC field, or an IFAC that fails re-verification.
Increments ifacViolations, logs at DEBUG, returns null.
Optionaldescription: string | null = nullIncoming announce rate in Hz over the current sample window (Python
incoming_announce_frequency). Returns 0 with fewer than
Interface.IC_DEQUE_MIN_SAMPLE+1 samples; a sample older than
arFreqDecay decays out of the window.
Incoming path? request rate in Hz (Python incoming_pr_frequency).
Same sampling rules as incomingAnnounceFrequency, with the PR
decay window.
Sends the host-leave command (Python leave()).
Outgoing announce rate in Hz (Python outgoing_announce_frequency).
Needs more than one sample.
Outgoing path? request rate in Hz (Python outgoing_pr_frequency).
Needs more than one sample.
Records a packet-filter (dedup) hit on this interface (Python
packet_filter_hit): an inbound non-announce packet whose hash is
already in the dedup ring. Increments packetFilterHits, returns
null.
Releases one held announce if conditions allow (the selection half of
Python process_held_announces): at most one announce per
icHeldReleaseInterval, never before icHeldRelease, and
only while the incoming announce frequency is back below the burst
threshold. Selection prefers the lowest hop count (nearest destinations
converge first). The caller re-injects the returned packet into the
normal inbound pipeline (Python spawns a thread calling
Transport.inbound(raw, receiving_interface)).
The announce to
re-inject, or null when nothing is releasable.
Records a generic protocol violation on this interface (Python
protocol_violation): malformed packets, invalid announce signatures,
tagless / oversized path requests, undecodable MTU signalling, inbound
processing exceptions. Increments protocolViolations, logs at
DEBUG, and returns null so it chains as the return value at every
drop site.
Optionaldescription: string | null = null
Optional human-readable detail.
Requests the current 1024-byte on-device display snapshot and resolves once
the device echoes it back (or after timeoutMs). The image is kept on
RNodeInterface#rDisp; the round-trip latency is on
RNodeInterface#rDispLatency. Mirrors Python read_display. This is
distinct from RNodeInterface#readFramebuffer (the host-writable
512-byte framebuffer). No-op on headless devices.
OptionaltimeoutMs: number = 2000The display image, or null on timeout or on a headless device.
Requests the current 512-byte framebuffer contents and resolves once the
device has echoed them back (or after timeoutMs). The image is also kept
on RNodeInterface#rFrameBuffer; the measured round-trip latency is
on RNodeInterface#rFrameBufferLatency. Mirrors Python
read_framebuffer.
OptionaltimeoutMs: number = 2000The framebuffer, or null on timeout or on a headless device.
Records an inbound announce into iaFreqDeque (Python
received_announce). Spawned interfaces propagate the sample to their
parent so bursts are detected at the medium level.
OptionalfromSpawned: boolean = false
Internal: true when called on a parent.
Records an inbound path? request into ipFreqDeque (Python
received_path_request). Spawned interfaces propagate to their parent.
OptionalfromSpawned: boolean = false
Internal: true when called on a parent.
The removeEventListener() method of the EventTarget interface removes an event listener previously registered with EventTarget.addEventListener() from the target. The event listener to be removed is identified using a combination of the event type, the event listener function itself, and various optional options that may affect the matching process; see Matching event listeners for removal.
Optionaloptions: boolean | EventListenerOptionsSends a packet, honouring flow control. If the radio is online and ready
the packet is transmitted immediately (and, with flow control, the next one
is gated on CMD_READY); otherwise it is queued for later. Mirrors the
Python reference process_outgoing. Sending a packet also arms the ID
beacon timer (see RNodeInterface#_armIdBeacon).
Records an outbound announce into oaFreqDeque (Python
sent_announce); counted by TransportCore.broadcast at the transmit
chokepoint, and surfaced as outgoingAnnounceFrequency for the
future announce-rate-table work (#31 step 6).
OptionalfromSpawned: boolean = false
Internal: true when called on a parent.
Records an outbound path? request into opFreqDeque (Python
sent_path_request); consumed by egress PR limiting (work doc #31 step 4).
OptionalfromSpawned: boolean = false
Internal: true when called on a parent.
Whether announce ingress should be limited right now (Python
should_ingress_limit). Latches a burst when the incoming announce
frequency exceeds the threshold for the interface's age — stricter
(icBurstFreqNew) during the first icNewTime seconds. Once
latched, stays limiting for at least icBurstHold seconds and
until the frequency drops back below the threshold; the call that
unlatches still reports true (mirroring the Python reference, the
next packet after it flows normally).
Consumers: held-announce buffering for unknown destinations (work doc #31 step 3). The announce frequency side effects (latching plus arming icHeldRelease with the icBurstPenalty) match Python so the state is already correct when that lands.
Whether path? request ingress should be limited right now (Python
should_ingress_limit_pr, incl. the upstream cooldown hysteresis).
Latches when the incoming PR frequency exceeds the age-dependent
threshold (icPrBurstFreqNew during the first icNewTime
seconds, icPrBurstFreq after). Once latched, stays limiting for
at least icBurstHold seconds; after the hold, unlatching takes
Interface.IC_PR_BURST_COOLDOWN+1 consecutive below-threshold
evaluations — any above-threshold evaluation resets the cooldown
(anti-flapping at the boundary). Consumers: TransportCore drops
unique-tag path requests while a burst is latched (work doc #31 step 2 —
our inline processing equivalent of the Python reference's
TC_INGRESS_LIMITED traffic-class demotion).
Begins periodically polling the on-device display, refreshing
RNodeInterface#rDisp every intervalSeconds. Mirrors Python
start_display_updates. No-op on headless devices. Call
RNodeInterface#stopDisplayUpdates to stop.
OptionalintervalSeconds: number = RNodeInterface.DISPLAY_READ_INTERVAL
Poll interval in seconds; defaults to RNodeInterface.DISPLAY_READ_INTERVAL (1.0).
Stops the periodic display-update poll started by startDisplayUpdates.
Writes one RNodeInterface.FB_BYTES_PER_LINE-byte line to the
framebuffer at the given line index (0-based). The payload
[line, ...lineData] is KISS-escaped, matching Python write_framebuffer.
StaticgetReturns the JSON Schema for the serial RNode backend (the base radio
options plus the serial port/baudRate).
A JSON Schema object.
RNode interface over a POSIX serial device.
Subclasses the transport-agnostic RNodeInterface and supplies the byte transport via RNodeSerialInterface#_openTransport using a non-blocking fd + inherited-fd
stty+ polledreadSync/writeSync.Registered in the Node interface registry as
"rnode-serial".