ax25termws

package
v0.13.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 7, 2026 License: GPL-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package ax25termws bridges a per-WebSocket connection to a pkg/ax25conn session: inbound JSON envelopes from the browser drive the session's event loop; outbound observer events become envelopes pushed back to the browser.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Bridge

type Bridge struct {
	// contains filtered or unexported fields
}

Bridge maps inbound envelopes to ax25conn.Event submissions and outbound observer events to envelopes.

The bridge runs one internal pump goroutine that translates OutEvent -> Envelope and writes into cfg.Out. observe() is invoked directly from the session goroutine and MUST stay non-blocking; it enqueues into an internal channel that the pump drains.

The bridge owns an internal context derived from cfg.Ctx so Close() can stop the pump even when the parent ctx is still alive (e.g. a test that wants to verify Close behavior without tearing down the whole http handler).

func New

func New(cfg BridgeConfig) *Bridge

New constructs a Bridge and starts its pump goroutine. The session is opened on the first KindConnect envelope. Callers MUST invoke Close exactly once when the WebSocket terminates so any active LAPB session receives a clean DISC frame on the wire.

func (*Bridge) Close

func (b *Bridge) Close()

Close requests a clean LAPB DISC on any active session and waits for the pump goroutine to exit. Safe to call multiple times.

Why DISC and not Abort: when an operator closes their browser tab, the session WAS in CONNECTED -- LAPB requires a proper disconnect handshake (DISC -> UA) so the peer's state machine drops the link instead of waiting for N2 retries to time out. The session's AWAITING_RELEASE timer guarantees the goroutine still exits even if the peer never UAs.

func (*Bridge) Handle

func (b *Bridge) Handle(ctx context.Context, env Envelope) error

Handle dispatches one inbound envelope to the appropriate side effect. Returns an error if the message cannot be processed.

Handle is not safe for concurrent use; the caller (the WebSocket reader goroutine) is the only goroutine touching b.session.

func (*Bridge) SessionID

func (b *Bridge) SessionID() uint64

SessionID returns the manager-assigned session id, or 0 before a successful Connect.

type BridgeConfig

type BridgeConfig struct {
	// Manager opens and tracks ax25conn sessions. Required.
	Manager *ax25conn.Manager
	// Logger receives bridge-side warnings (e.g. dropped envelopes).
	// Required.
	Logger *slog.Logger
	// Operator is the authenticated user identity, used by the
	// manager for per-operator session caps.
	Operator string
	// Ctx scopes the bridge's lifetime. The pump goroutine that
	// drains the observer inbox into Out exits when Ctx is done so
	// no goroutine leaks after the WebSocket closes.
	Ctx context.Context
	// Out is the channel the bridge fills with outbound envelopes;
	// the WebSocket handler drains it. The bridge sends from three
	// goroutines: the pump (observer events), rawTailPump
	// (packetlog subscriber entries), and the reader's
	// emitErrorEnvelope path. Each sender selects on ctx.Done() so
	// teardown drains them without closing the channel.
	Out chan<- Envelope
	// OnFirstConnected, if set, is invoked once per session the first
	// time the link reaches CONNECTED. Wiring uses it to upsert a
	// recent profile so the pre-connect form's recents list reflects
	// the new connection. The callback runs on a fresh goroutine so
	// it cannot stall the session loop.
	OnFirstConnected func(args ConnectArgs)
	// Transcripts persists per-session recordings when the operator
	// toggles transcript on. Optional; nil disables transcript support
	// entirely (the bridge surfaces a typed error envelope on toggle).
	Transcripts TranscriptRecorder
	// RawPacketLog drives the raw-tail mode (Plan §3f). Optional; nil
	// disables raw-tail support so KindRawTailSubscribe surfaces a
	// typed error envelope.
	RawPacketLog *packetlog.Log
}

BridgeConfig configures one per-WebSocket bridge instance.

type ConnectArgs

type ConnectArgs struct {
	ChannelID uint32   `json:"channel_id"`
	LocalCall string   `json:"local_call"`
	LocalSSID uint8    `json:"local_ssid"`
	DestCall  string   `json:"dest_call"`
	DestSSID  uint8    `json:"dest_ssid"`
	Via       []string `json:"via,omitempty"`
	Mod128    bool     `json:"mod128,omitempty"`
	Paclen    int      `json:"paclen,omitempty"`
	// Maxframe is the LAPB window k. Defaults: 2 (mod-8), 32 (mod-128).
	Maxframe int    `json:"maxframe,omitempty"`
	T1MS     int    `json:"t1_ms,omitempty"`
	T2MS     int    `json:"t2_ms,omitempty"`
	T3MS     int    `json:"t3_ms,omitempty"`
	N2       int    `json:"n2,omitempty"`
	Backoff  string `json:"backoff,omitempty"` // "none"|"linear"|"exponential"; default linear
}

ConnectArgs is the payload of a KindConnect envelope.

type Envelope

type Envelope struct {
	Kind       MsgKind               `json:"kind"`
	Connect    *ConnectArgs          `json:"connect,omitempty"`
	Data       []byte                `json:"data,omitempty"`
	State      *StatePayload         `json:"state,omitempty"`
	Stats      *StatsPayload         `json:"stats,omitempty"`
	Error      *ErrorPayload         `json:"error,omitempty"`
	Transcript *TranscriptSetPayload `json:"transcript,omitempty"`
	RawTailSub *RawTailSubscribeArgs `json:"raw_tail_sub,omitempty"`
	RawTail    *RawTailEntry         `json:"raw_tail,omitempty"`
}

Envelope is the on-wire JSON shape. Data is base64-encoded by encoding/json when the field type is []byte; the JS side decodes via atob.

type ErrorPayload

type ErrorPayload struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

ErrorPayload is a typed error surfaced back to the operator.

type MsgKind

type MsgKind string

MsgKind enumerates wire-level message types. The wire is JSON; the kind value drives a switch in both directions.

const (
	// Client -> Server
	KindConnect          MsgKind = "connect"
	KindData             MsgKind = "data"
	KindDisconnect       MsgKind = "disconnect"
	KindAbort            MsgKind = "abort"
	KindTranscriptSet    MsgKind = "transcript_set"
	KindRawTailSubscribe MsgKind = "raw_tail_subscribe"
	KindRawTailUnsub     MsgKind = "raw_tail_unsubscribe"

	// Server -> Client
	KindState     MsgKind = "state"
	KindDataRX    MsgKind = "data_rx"
	KindLinkStats MsgKind = "link_stats"
	KindError     MsgKind = "error"
	KindRawTail   MsgKind = "raw_tail"
)

type RawTailEntry

type RawTailEntry struct {
	TS        time.Time `json:"ts"`
	Source    string    `json:"source"`
	Type      string    `json:"type,omitempty"`
	Direction string    `json:"direction,omitempty"`
	ChannelID uint32    `json:"channel_id,omitempty"`
	From      string    `json:"from,omitempty"`
	Raw       string    `json:"raw,omitempty"` // TNC2-formatted packet
}

RawTailEntry is the wire-form of a packetlog Entry, slimmed down to what RawPacketView renders. Server-only payload.

type RawTailSubscribeArgs

type RawTailSubscribeArgs struct {
	ChannelID uint32 `json:"channel_id"`
	Source    string `json:"source,omitempty"`
	Type      string `json:"type,omitempty"`
	Direction string `json:"direction,omitempty"`
	// SubstringMatch narrows by matching against the Entry's Decoded.Source
	// (callsign) -- common operator-friendly filter.
	SubstringMatch string `json:"substring,omitempty"`
}

RawTailSubscribeArgs scopes a raw-packet tail to one channel and optional Filter fields. Empty fields match anything.

type StatePayload

type StatePayload struct {
	Name   string `json:"name"`             // DISCONNECTED, AWAITING_CONNECTION, CONNECTED, ...
	Reason string `json:"reason,omitempty"` // human-readable transition cause
}

StatePayload reports a state transition.

type StatsPayload

type StatsPayload struct {
	State    string `json:"state"`
	VS       uint8  `json:"vs"`
	VR       uint8  `json:"vr"`
	VA       uint8  `json:"va"`
	RC       int    `json:"rc"`
	PeerBusy bool   `json:"peer_busy"`
	OwnBusy  bool   `json:"own_busy"`
	FramesTX uint64 `json:"frames_tx"`
	FramesRX uint64 `json:"frames_rx"`
	BytesTX  uint64 `json:"bytes_tx"`
	BytesRX  uint64 `json:"bytes_rx"`
	RTTMS    int    `json:"rtt_ms"`
}

StatsPayload is the LinkStats snapshot the bridge ships to the telemetry side panel.

type TranscriptRecorder

type TranscriptRecorder interface {
	// Begin opens a new transcript session keyed to the connect args.
	// Returns the persistent session id.
	Begin(ctx context.Context, channelID uint32, peerCall string, peerSSID uint8, viaPath string) (uint32, error)
	// Append persists one transcript entry.
	Append(ctx context.Context, sessionID uint32, ts time.Time, direction, kind string, payload []byte) error
	// End stamps the wrap-up fields on a transcript session.
	End(ctx context.Context, sessionID uint32, reason string, bytes, frames uint64) error
}

TranscriptRecorder is the persistence interface the bridge calls to record one session's transcript. Implementations adapt the configstore (see pkg/webapi/ax25_terminal.go for the wiring).

type TranscriptSetPayload

type TranscriptSetPayload struct {
	Enabled bool `json:"enabled"`
}

TranscriptSetPayload toggles transcript recording on/off for the current session. Sent client -> server only.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL