Add AGWPE Connected-Mode Support and Configurable Port-to-Channel Mapping for Pat Winlink
Summary
Graywolf's AGWPE server supports APRS/UI traffic, but it cannot currently
replace Direwolf when Pat uses AGWPE for connected-mode AX.25 Winlink sessions.
Two enhancements are needed:
- Implement the AGWPE connected-mode commands used by Pat.
- Allow each AGWPE radio port to map to a selected Graywolf channel.
Use Case
Pat is configured as follows:
"ax25": {
"engine": "agwpe"
},
"agwpe": {
"addr": "graywolf.lan:8000",
"radio_port": 0
}
The desired routing is:
Pat AGWPE port 0 -> Graywolf channel 2 ("VHF Packet")
An AGWPE version request succeeds and reports version 2004.1. Callsign
registration and UI-frame submission also work. However, Pat cannot establish
a connected AX.25 session through Graywolf.
Evidence: Connected Mode Is Explicitly Unsupported
Graywolf describes its AGW implementation as a minimal APRS-only subset:
https://github.com/chrissnell/graywolf/blob/main/pkg/agw/protocol.go#L1-L28
The source explicitly states that connected-mode frame types are accepted and
logged but not implemented.
The AGW dispatcher routes unsupported connected-mode commands to its default
branch, where they are logged and dropped:
https://github.com/chrissnell/graywolf/blob/main/pkg/agw/server.go#L397-L423
Raw AGW frames do not provide a workaround because non-UI AX.25 frames are
also explicitly ignored by that dispatcher.
Graywolf already contains an AX.25 connected-mode state machine under
pkg/ax25conn, including:
- SABM and SABME establishment
- I-frame data transfer
- RR, RNR, REJ, and SREJ handling
- Disconnect handling
- Retransmission timers
- Packet-channel validation
That implementation is not currently connected to the AGWPE server:
https://github.com/chrissnell/graywolf/tree/main/pkg/ax25conn
Evidence: Commands Required by Pat
Pat uses the AGWPE implementation from wl2k-go. Its required command kinds
are defined here:
https://github.com/la5nta/wl2k-go/blob/master/transport/ax25/agwpe/frame_kinds.go#L7-L21
Pat requires these connected-mode operations:
| Command |
Purpose |
C |
Connect without digipeaters |
v |
Connect through digipeaters |
D |
Send and receive connected data |
d |
Disconnect and disconnect notification |
Y |
Query outstanding frames for a connection |
X |
Register a callsign |
x |
Unregister a callsign |
g |
Query port capabilities |
R |
Query AGWPE version |
Pat waits for a C success response or a d failure response after requesting
a connection:
https://github.com/la5nta/wl2k-go/blob/master/transport/ax25/agwpe/conn.go#L211-L251
Pat uses D for net.Conn reads and writes and d during teardown:
https://github.com/la5nta/wl2k-go/blob/master/transport/ax25/agwpe/conn.go#L135-L209
Pat also uses Y to enforce MAXFRAME and flush queued frames before
disconnect. Without a Y response, writes or shutdown can time out:
https://github.com/la5nta/wl2k-go/blob/master/transport/ax25/agwpe/conn.go#L57-L97
Pat initializes its configured AGWPE radio port and registers it as a
connection-oriented transport here:
https://github.com/la5nta/pat/blob/master/app/connect.go#L423-L447
Evidence: Port Mapping Is Hardcoded
Graywolf currently constructs its AGW server with:
PortToChannel: map[uint8]uint32{0: 1},
Source:
https://github.com/chrissnell/graywolf/blob/main/pkg/app/wiring.go#L1216-L1237
The persisted AGW configuration contains only:
listen_addr
callsigns
enabled
It does not contain a channel mapping:
https://github.com/chrissnell/graywolf/blob/main/pkg/configstore/models.go#L259-L277
The AGW API request DTO similarly has no channel-mapping field:
https://github.com/chrissnell/graywolf/blob/main/pkg/webapi/dto/agw.go#L1-L32
Consequently, Pat's radio_port: 0 always routes to Graywolf channel ID 1,
even when the intended packet channel is channel ID 2.
Changing Pat to radio_port: 1 is not a valid workaround because it does not
change Graywolf's hardcoded port-to-channel mapping.
Requested Implementation
Configurable Port Mapping
- Add an explicit mapping from each AGWPE port number to a Graywolf channel ID.
- Persist the mapping in Graywolf's configuration database.
- Expose the mapping through the REST API.
- Add a channel selector for each AGWPE port in the web interface.
- Reject mappings to nonexistent, disabled, or APRS-only channels.
- Preserve backward compatibility for existing single-port configurations.
A possible representation would be:
{
"enabled": true,
"listen_addr": "0.0.0.0:8000",
"ports": [
{
"port": 0,
"callsign": "AI7BQ",
"channel_id": 2
}
]
}
Connected-Mode AGWPE Support
Connect the AGWPE server to the existing pkg/ax25conn implementation.
Implement the following behavior:
- Route
C requests into ax25conn.Manager to initiate a connection.
- Route
v requests into ax25conn.Manager with the requested digipeater path.
- Return a compatible
C notification when the AX.25 connection succeeds.
- Return a compatible
d notification when establishment fails.
- Route client
D frames into the connected AX.25 session.
- Return received connected data to the AGWPE client as
D frames.
- Process client
d requests and close the associated AX.25 session.
- Notify the AGWPE client with
d when the remote station disconnects.
- Implement
Y responses for outstanding frames on a connection.
- Support inbound connections for callsigns registered with
X.
- Associate sessions with the correct AGWPE client, port, local callsign,
remote callsign, and digipeater path.
- Clean up sessions when an AGWPE TCP client disconnects.
Direwolf Compatibility
Responses should match the behavior expected by Pat's wl2k-go AGWPE client.
In particular:
- A successful outbound connection must return a
C frame whose payload begins
with *** CONNECTED With .
- An inbound connection notification must return a
C frame whose payload
begins with *** CONNECTED To .
- Connection failure and teardown must return a
d frame.
- A
Y request must return a four-byte little-endian outstanding-frame count.
D frames must preserve callsigns, radio port, PID, and payload.
Acceptance Criteria
- An AGWPE port can be assigned to any enabled Graywolf packet-capable channel.
- AGWPE port
0 can be mapped to channel ID 2 without modifying Pat.
- Pat can connect to an RMS gateway through Graywolf using
ax25+agwpe.
- Pat can exchange Winlink data bidirectionally over the connection.
- Pat can cancel or close a session without waiting for AGWPE timeouts.
- Remote disconnects are reported promptly to Pat.
- Incoming connected-mode sessions work through Pat's AGWPE listener.
- Connections through one or more digipeaters work using the
v command.
- Integration tests exercise
C, v, D, d, and Y.
- At least one integration test uses
la5nta/wl2k-go as the AGWPE client.
- Existing APRS, monitor, unproto, and raw-frame behavior remains functional.
- Existing AGWPE configurations migrate without losing their settings.
Current Observed Behavior
The AGWPE listener responds successfully to R, G, and M, proving that
the TCP interface and basic protocol framing are operational.
A test M packet submitted on AGWPE port 0 was routed to internal channel
1. Graywolf logged:
tx drop: no backend for channel
component=txbackend channel=1 frame_id=1 len=36
This confirms the current hardcoded mapping:
AGWPE port 0 -> Graywolf channel 1
The desired packet channel is channel ID 2, named VHF Packet.
Even after correcting that mapping, Pat cannot establish a session because
Graywolf's AGWPE dispatcher currently drops the connected-mode commands that
Pat requires.
Add AGWPE Connected-Mode Support and Configurable Port-to-Channel Mapping for Pat Winlink
Summary
Graywolf's AGWPE server supports APRS/UI traffic, but it cannot currently
replace Direwolf when Pat uses AGWPE for connected-mode AX.25 Winlink sessions.
Two enhancements are needed:
Use Case
Pat is configured as follows:
The desired routing is:
An AGWPE version request succeeds and reports version
2004.1. Callsignregistration and UI-frame submission also work. However, Pat cannot establish
a connected AX.25 session through Graywolf.
Evidence: Connected Mode Is Explicitly Unsupported
Graywolf describes its AGW implementation as a minimal APRS-only subset:
https://github.com/chrissnell/graywolf/blob/main/pkg/agw/protocol.go#L1-L28
The source explicitly states that connected-mode frame types are accepted and
logged but not implemented.
The AGW dispatcher routes unsupported connected-mode commands to its default
branch, where they are logged and dropped:
https://github.com/chrissnell/graywolf/blob/main/pkg/agw/server.go#L397-L423
Raw AGW frames do not provide a workaround because non-UI AX.25 frames are
also explicitly ignored by that dispatcher.
Graywolf already contains an AX.25 connected-mode state machine under
pkg/ax25conn, including:That implementation is not currently connected to the AGWPE server:
https://github.com/chrissnell/graywolf/tree/main/pkg/ax25conn
Evidence: Commands Required by Pat
Pat uses the AGWPE implementation from
wl2k-go. Its required command kindsare defined here:
https://github.com/la5nta/wl2k-go/blob/master/transport/ax25/agwpe/frame_kinds.go#L7-L21
Pat requires these connected-mode operations:
CvDdYXxgRPat waits for a
Csuccess response or adfailure response after requestinga connection:
https://github.com/la5nta/wl2k-go/blob/master/transport/ax25/agwpe/conn.go#L211-L251
Pat uses
Dfornet.Connreads and writes anddduring teardown:https://github.com/la5nta/wl2k-go/blob/master/transport/ax25/agwpe/conn.go#L135-L209
Pat also uses
Yto enforceMAXFRAMEand flush queued frames beforedisconnect. Without a
Yresponse, writes or shutdown can time out:https://github.com/la5nta/wl2k-go/blob/master/transport/ax25/agwpe/conn.go#L57-L97
Pat initializes its configured AGWPE radio port and registers it as a
connection-oriented transport here:
https://github.com/la5nta/pat/blob/master/app/connect.go#L423-L447
Evidence: Port Mapping Is Hardcoded
Graywolf currently constructs its AGW server with:
Source:
https://github.com/chrissnell/graywolf/blob/main/pkg/app/wiring.go#L1216-L1237
The persisted AGW configuration contains only:
listen_addrcallsignsenabledIt does not contain a channel mapping:
https://github.com/chrissnell/graywolf/blob/main/pkg/configstore/models.go#L259-L277
The AGW API request DTO similarly has no channel-mapping field:
https://github.com/chrissnell/graywolf/blob/main/pkg/webapi/dto/agw.go#L1-L32
Consequently, Pat's
radio_port: 0always routes to Graywolf channel ID1,even when the intended packet channel is channel ID
2.Changing Pat to
radio_port: 1is not a valid workaround because it does notchange Graywolf's hardcoded port-to-channel mapping.
Requested Implementation
Configurable Port Mapping
A possible representation would be:
{ "enabled": true, "listen_addr": "0.0.0.0:8000", "ports": [ { "port": 0, "callsign": "AI7BQ", "channel_id": 2 } ] }Connected-Mode AGWPE Support
Connect the AGWPE server to the existing
pkg/ax25connimplementation.Implement the following behavior:
Crequests intoax25conn.Managerto initiate a connection.vrequests intoax25conn.Managerwith the requested digipeater path.Cnotification when the AX.25 connection succeeds.dnotification when establishment fails.Dframes into the connected AX.25 session.Dframes.drequests and close the associated AX.25 session.dwhen the remote station disconnects.Yresponses for outstanding frames on a connection.X.remote callsign, and digipeater path.
Direwolf Compatibility
Responses should match the behavior expected by Pat's
wl2k-goAGWPE client.In particular:
Cframe whose payload beginswith
*** CONNECTED With.Cframe whose payloadbegins with
*** CONNECTED To.dframe.Yrequest must return a four-byte little-endian outstanding-frame count.Dframes must preserve callsigns, radio port, PID, and payload.Acceptance Criteria
0can be mapped to channel ID2without modifying Pat.ax25+agwpe.vcommand.C,v,D,d, andY.la5nta/wl2k-goas the AGWPE client.Current Observed Behavior
The AGWPE listener responds successfully to
R,G, andM, proving thatthe TCP interface and basic protocol framing are operational.
A test
Mpacket submitted on AGWPE port0was routed to internal channel1. Graywolf logged:This confirms the current hardcoded mapping:
The desired packet channel is channel ID
2, namedVHF Packet.Even after correcting that mapping, Pat cannot establish a session because
Graywolf's AGWPE dispatcher currently drops the connected-mode commands that
Pat requires.