feat(udp): custom outbound messages — a trigger, a template, UDP or a URL
The hard-coded emitters each speak one published format at one fixed moment.
This is the escape hatch, and it exists mainly for antenna switches: they are
driven by a URL, and the band change is the trigger they want.
Four triggers, each carrying its own field set: band change (the RADIO's band,
not the entry form — a switch follows the rig, not what is being typed), QSO
logged, rotator command and callsign lookup. The panel prints the fields the
selected trigger can fill, which is the point of the whole thing: a placeholder
the trigger does not carry renders as nothing, and without the list an operator
writes {freq} on a band change and has no way to learn why the switch never
moved.
Three things the panel cannot show, handled here:
- Escaping. A URL needs every value percent-encoded; a UDP payload must not
be touched. The first portable callsign, F4BPO/P, puts a path separator in
the middle of a query string otherwise — it works on the bench and fails on
the air.
- Blocking. An HTTP call to a switch that is unplugged would otherwise sit
for the operating system's timeout, on the path of a band change. It runs
in the background with a 3 s limit.
- Silence. A switch answering 404 after a firmware update fails exactly like
success looks from here, so the status code is logged, throttled per row.
The rotator trigger hooks the COMMAND rather than the SP/LP buttons, so the
compass and a spot click fire it too, and the path ("SP"/"LP") is passed
through because an azimuth alone cannot say which was taken — 137° is short
path to one station and long to another.
Credentials in a URL are stored as typed. That is the operator's call, on the
grounds that this is LAN gear, and the hint in the panel says so.
This commit is contained in:
@@ -41,6 +41,30 @@ const (
|
||||
// Logger32 and others listen on the WSJT-X interface, not for plain text, and
|
||||
// discard a bare ADIF record without a word — so the two cannot be one row.
|
||||
ServiceWSJTLog ServiceType = "wsjt_log"
|
||||
// ServiceCustom is the general case: the operator picks the trigger, writes
|
||||
// the message, and chooses UDP or HTTP. See Trigger and Config.Template.
|
||||
ServiceCustom ServiceType = "custom"
|
||||
)
|
||||
|
||||
// Trigger names the moment a custom outbound row fires.
|
||||
type Trigger string
|
||||
|
||||
const (
|
||||
TriggerQSOLogged Trigger = "qso_logged"
|
||||
TriggerRotatorGo Trigger = "rotator_goto"
|
||||
TriggerLookupDone Trigger = "lookup_done"
|
||||
// TriggerBandChange is the RIG changing band, not the entry form: a switch
|
||||
// follows the radio, not what is being typed. Rare by nature, so unlike a
|
||||
// frequency trigger it needs no damping.
|
||||
TriggerBandChange Trigger = "band_change"
|
||||
)
|
||||
|
||||
// Transport is how a custom row leaves: a UDP datagram or an HTTP GET.
|
||||
type Transport string
|
||||
|
||||
const (
|
||||
TransportUDP Transport = "udp"
|
||||
TransportURL Transport = "url"
|
||||
)
|
||||
|
||||
// Config is one user-defined UDP connection.
|
||||
@@ -55,6 +79,13 @@ type Config struct {
|
||||
DestinationIP string `json:"destination_ip"` // outbound only
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
|
||||
// Custom rows only (ServiceCustom).
|
||||
Trigger Trigger `json:"trigger"`
|
||||
Template string `json:"template"` // message text with {fields}
|
||||
Transport Transport `json:"transport"` // udp | url
|
||||
URL string `json:"url"` // URL template, when transport is url
|
||||
LineEnd string `json:"line_end"` // "" | lf | crlf — UDP only
|
||||
}
|
||||
|
||||
// Repo is the persistence layer for UDP integration rows.
|
||||
@@ -66,7 +97,8 @@ func (r *Repo) List(ctx context.Context) ([]Config, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, direction, name, port, service_type,
|
||||
multicast, multicast_group, destination_ip,
|
||||
enabled, sort_order
|
||||
enabled, sort_order,
|
||||
trig, template, transport, url, line_end
|
||||
FROM integrations_udp
|
||||
ORDER BY direction, sort_order, id`)
|
||||
if err != nil {
|
||||
@@ -78,7 +110,8 @@ func (r *Repo) List(ctx context.Context) ([]Config, error) {
|
||||
var c Config
|
||||
var mc, en int
|
||||
if err := rows.Scan(&c.ID, &c.Direction, &c.Name, &c.Port, &c.ServiceType,
|
||||
&mc, &c.MulticastGroup, &c.DestinationIP, &en, &c.SortOrder); err != nil {
|
||||
&mc, &c.MulticastGroup, &c.DestinationIP, &en, &c.SortOrder,
|
||||
&c.Trigger, &c.Template, &c.Transport, &c.URL, &c.LineEnd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Multicast = mc != 0
|
||||
@@ -105,10 +138,12 @@ func (r *Repo) Save(ctx context.Context, c *Config) error {
|
||||
if c.ID == 0 {
|
||||
res, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO integrations_udp(direction, name, port, service_type,
|
||||
multicast, multicast_group, destination_ip, enabled, sort_order)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
multicast, multicast_group, destination_ip, enabled, sort_order,
|
||||
trig, template, transport, url, line_end)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
c.Direction, c.Name, c.Port, c.ServiceType,
|
||||
mc, c.MulticastGroup, c.DestinationIP, en, c.SortOrder)
|
||||
mc, c.MulticastGroup, c.DestinationIP, en, c.SortOrder,
|
||||
c.Trigger, c.Template, c.Transport, c.URL, c.LineEnd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert udp: %w", err)
|
||||
}
|
||||
@@ -121,10 +156,12 @@ func (r *Repo) Save(ctx context.Context, c *Config) error {
|
||||
direction = ?, name = ?, port = ?, service_type = ?,
|
||||
multicast = ?, multicast_group = ?, destination_ip = ?,
|
||||
enabled = ?, sort_order = ?,
|
||||
trig = ?, template = ?, transport = ?, url = ?, line_end = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?`,
|
||||
c.Direction, c.Name, c.Port, c.ServiceType,
|
||||
mc, c.MulticastGroup, c.DestinationIP, en, c.SortOrder, db.NowISO(), c.ID)
|
||||
mc, c.MulticastGroup, c.DestinationIP, en, c.SortOrder,
|
||||
c.Trigger, c.Template, c.Transport, c.URL, c.LineEnd, db.NowISO(), c.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update udp: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user