fix(tci server): announce transmit permission, and log what the client sends
MSHV's PTT test does nothing against the TCI sharing server. The initialisation block never carried TX_ENABLE. The document files it under unidirectional control rather than initialisation, so it was missed when the block was written from §4.1 — but its own note says it is "sent to the client when connected", and that is the point: a client that models transmit permission starts out assuming it may NOT transmit. Without it MSHV never even tries, so nothing arrives to relay and there is nothing to see at either end. Sent as true always. OpsLog is not what decides — the radio behind whichever backend is connected does, and its refusal already travels back through SetPTT into the log. TX_FREQUENCY goes with it, at connect and whenever the transmit frequency moves. It is the command a client showing "TX 14.200" reads; channel B alone left that stale. And every command a client sends is now logged. This is the only evidence there will ever be about a program on someone else's machine: "the PTT test does nothing" cannot be answered without knowing whether MSHV sent trx at all, and in what form. Cheap — TCI is event-driven, a client speaks when the operator does something — and capped at 200 lines per connection so one that does poll cannot quietly fill the log. If this was not the cause, the next report answers it in one line rather than another round of guessing.
This commit is contained in:
+4
-2
@@ -6,13 +6,15 @@
|
|||||||
"An entity that is a single island group now fills the IOTA reference on its own — no callbook subscription needed.",
|
"An entity that is a single island group now fills the IOTA reference on its own — no callbook subscription needed.",
|
||||||
"CAT sharing can now speak TCI instead of Hamlib, so a TCI-only program reaches whatever radio OpsLog is on.",
|
"CAT sharing can now speak TCI instead of Hamlib, so a TCI-only program reaches whatever radio OpsLog is on.",
|
||||||
"Lookup cache: a TTL of 0 switches it off, so a callbook record you are correcting is re-read every time.",
|
"Lookup cache: a TTL of 0 switches it off, so a callbook record you are correcting is re-read every time.",
|
||||||
"TCI: when the radio forbids transmitting, PTT now says so instead of doing nothing silently."
|
"TCI: when the radio forbids transmitting, PTT now says so instead of doing nothing silently.",
|
||||||
|
"TCI sharing: the server now announces transmit permission, without which a client such as MSHV never keys at all."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Une entité qui est un seul groupe d’îles remplit désormais la référence IOTA toute seule, sans abonnement callbook.",
|
"Une entité qui est un seul groupe d’îles remplit désormais la référence IOTA toute seule, sans abonnement callbook.",
|
||||||
"Le partage CAT peut désormais parler TCI au lieu de Hamlib : un logiciel TCI atteint la radio, quelle qu’elle soit.",
|
"Le partage CAT peut désormais parler TCI au lieu de Hamlib : un logiciel TCI atteint la radio, quelle qu’elle soit.",
|
||||||
"Cache des recherches : un TTL à 0 le désactive, pour relire à chaque fois une fiche callbook en cours de correction.",
|
"Cache des recherches : un TTL à 0 le désactive, pour relire à chaque fois une fiche callbook en cours de correction.",
|
||||||
"TCI : quand la radio interdit l’émission, le PTT le dit désormais au lieu de ne rien faire en silence."
|
"TCI : quand la radio interdit l’émission, le PTT le dit désormais au lieu de ne rien faire en silence.",
|
||||||
|
"Partage TCI : le serveur annonce désormais l’autorisation d’émettre, sans laquelle un client comme MSHV ne passe jamais en émission."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -91,10 +91,16 @@ type state struct {
|
|||||||
valid bool
|
valid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientLogCap bounds how many of one client's commands reach the log.
|
||||||
|
const clientLogCap = 200
|
||||||
|
|
||||||
// client is one connected program.
|
// client is one connected program.
|
||||||
type client struct {
|
type client struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
mu sync.Mutex // one writer at a time: gorilla panics on concurrent writes
|
mu sync.Mutex // one writer at a time: gorilla panics on concurrent writes
|
||||||
|
// logged counts what has been written to the log for this connection. Only
|
||||||
|
// the reader goroutine touches it, so it needs no lock of its own.
|
||||||
|
logged int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) send(s string) error {
|
func (c *client) send(s string) error {
|
||||||
@@ -211,9 +217,27 @@ func (s *Server) serve(c *client, remote string) {
|
|||||||
}
|
}
|
||||||
// One frame may carry several ";"-terminated commands.
|
// One frame may carry several ";"-terminated commands.
|
||||||
for _, cmd := range strings.Split(string(data), ";") {
|
for _, cmd := range strings.Split(string(data), ";") {
|
||||||
if cmd = strings.TrimSpace(cmd); cmd != "" {
|
if cmd = strings.TrimSpace(cmd); cmd == "" {
|
||||||
s.handle(c, cmd)
|
continue
|
||||||
}
|
}
|
||||||
|
// Every command the client sends, in the log.
|
||||||
|
//
|
||||||
|
// This is the only evidence there will ever be about a program on
|
||||||
|
// someone else's machine: "MSHV's PTT test does nothing" is
|
||||||
|
// unanswerable without knowing whether MSHV sent trx at all, and if
|
||||||
|
// so in what form. Cheap, because TCI is event-driven — a client
|
||||||
|
// speaks when the operator does something, not on a timer.
|
||||||
|
//
|
||||||
|
// Capped so a client that DOES poll cannot quietly fill the
|
||||||
|
// operator's log; the cap says so once and then stays quiet.
|
||||||
|
if c.logged < clientLogCap {
|
||||||
|
c.logged++
|
||||||
|
s.log("tci server: ← %s;", cmd)
|
||||||
|
} else if c.logged == clientLogCap {
|
||||||
|
c.logged++
|
||||||
|
s.log("tci server: (further commands from this client are not logged)")
|
||||||
|
}
|
||||||
|
s.handle(c, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -250,6 +274,20 @@ func (s *Server) initBlock() []string {
|
|||||||
fmt.Sprintf("modulation:0,%s;", mode),
|
fmt.Sprintf("modulation:0,%s;", mode),
|
||||||
fmt.Sprintf("split_enable:0,%t;", split),
|
fmt.Sprintf("split_enable:0,%t;", split),
|
||||||
"trx:0,false;",
|
"trx:0,false;",
|
||||||
|
// TRANSMIT PERMISSION, and it is not optional in practice.
|
||||||
|
//
|
||||||
|
// The document files TX_ENABLE under unidirectional control rather than
|
||||||
|
// initialisation, but its own note says it is "sent to the client when
|
||||||
|
// connected". A client that models permission — and one written for
|
||||||
|
// ExpertSDR users has every reason to — starts out assuming it may NOT
|
||||||
|
// transmit, and without this it never even tries: PTT does nothing and
|
||||||
|
// the server never sees a trx command to refuse.
|
||||||
|
//
|
||||||
|
// Always true. OpsLog is not the thing that decides: the radio behind
|
||||||
|
// whichever backend is connected does, and its refusal comes back through
|
||||||
|
// SetPTT and into the log.
|
||||||
|
"tx_enable:0,true;",
|
||||||
|
fmt.Sprintf("tx_frequency:%d;", tx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +345,9 @@ func (s *Server) publish() []string {
|
|||||||
}
|
}
|
||||||
if !prev.valid || prev.txHz != cur.txHz {
|
if !prev.valid || prev.txHz != cur.txHz {
|
||||||
lines = append(lines, fmt.Sprintf("vfo:0,1,%d;", cur.txHz))
|
lines = append(lines, fmt.Sprintf("vfo:0,1,%d;", cur.txHz))
|
||||||
|
// The transmit frequency has its own command, which is what a client
|
||||||
|
// showing "TX 14.080" reads. Channel B alone leaves that stale.
|
||||||
|
lines = append(lines, fmt.Sprintf("tx_frequency:%d;", cur.txHz))
|
||||||
}
|
}
|
||||||
if (!prev.valid || prev.mode != cur.mode) && cur.mode != "" {
|
if (!prev.valid || prev.mode != cur.mode) && cur.mode != "" {
|
||||||
lines = append(lines, fmt.Sprintf("modulation:0,%s;", cur.mode))
|
lines = append(lines, fmt.Sprintf("modulation:0,%s;", cur.mode))
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ func TestInitBlockCarriesTheDocumentedInitialisationSet(t *testing.T) {
|
|||||||
"protocol:ExpertSDR3,", "device:", "receive_only:false;", "trx_count:1;",
|
"protocol:ExpertSDR3,", "device:", "receive_only:false;", "trx_count:1;",
|
||||||
"channel_count:2;", "vfo_limits:", "if_limits:", "modulations_list:",
|
"channel_count:2;", "vfo_limits:", "if_limits:", "modulations_list:",
|
||||||
"ready;", "start;",
|
"ready;", "start;",
|
||||||
|
// Transmit permission. A client that models it starts out assuming it
|
||||||
|
// may NOT transmit, and without this never even tries — PTT does
|
||||||
|
// nothing and the server never sees a trx command at all.
|
||||||
|
"tx_enable:0,true;",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(block, want) {
|
if !strings.Contains(block, want) {
|
||||||
t.Errorf("the initialisation block is missing %q — a client would not proceed past connect", want)
|
t.Errorf("the initialisation block is missing %q — a client would not proceed past connect", want)
|
||||||
@@ -183,10 +187,14 @@ func TestOnlyChangesAreSent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.freq, r.rxFreq = 14200000, 14200000
|
r.freq, r.rxFreq = 14200000, 14200000
|
||||||
got := s.publish()
|
got := strings.Join(s.publish(), "")
|
||||||
if len(got) != 2 || !strings.Contains(strings.Join(got, ""), "14200000") {
|
// Both channels move together on a simplex rig, and the transmit frequency
|
||||||
// Both channels move together on a simplex rig, and both are reported.
|
// has its own command besides — a client showing "TX 14.200" reads that one,
|
||||||
t.Errorf("after a QSY: %v", got)
|
// and channel B alone leaves it stale.
|
||||||
|
for _, want := range []string{"vfo:0,0,14200000;", "vfo:0,1,14200000;", "tx_frequency:14200000;"} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("after a QSY the clients were not told %q — got %q", want, got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if got := s.publish(); len(got) != 0 {
|
if got := s.publish(); len(got) != 0 {
|
||||||
t.Errorf("the QSY was re-sent: %v", got)
|
t.Errorf("the QSY was re-sent: %v", got)
|
||||||
|
|||||||
Reference in New Issue
Block a user