fix(icom): a set whose acknowledgement is lost is sent once more
Extends to frequency and mode what PTT already had. A missing FB is not a missing command: the rig acts on the frame as it decodes it, and what expires is our wait for the answer, on a bus shared with the rig's own transceive updates. JTDX in "Split: Fake It" moves the dial and the mode immediately before every key-down, so those acks queue behind each other. Losing one is fatal to the client rather than merely untidy: rigctld answers RPRT -9, JTDX reads that as losing rig control and tears the connection down mid-over. An operator's log shows three set_freq failures and one set_mode, each followed within 300 ms by a fresh rigctld client -- and shows the PTT resend rescuing an over that would otherwise have ended there. Opt-in per caller rather than folded into exec: only a command that says "be in this state" can be repeated safely, and a relative one must never come through here. The acknowledgement loss itself is still unexplained. Every failure in that log is preceded by a state read reporting SSB on a rig in DATA, which points at CI-V frame desync rather than a slow rig, and needs a trace to pin down.
This commit is contained in:
@@ -1,4 +1,18 @@
|
||||
[
|
||||
{
|
||||
"version": "0.25.9",
|
||||
"date": "",
|
||||
"en": [
|
||||
"The band/mode matrix colours can be chosen in Appearance, starting from the ones your theme already paints. Its legend is translated too.",
|
||||
"TCI sharing: a refused un-key no longer leaves the rig stuck transmitting, and PTT is dropped if the client dies mid-over.",
|
||||
"Icom: a frequency or mode change whose acknowledgement is lost is sent again, like PTT — losing one made JTDX drop the radio."
|
||||
],
|
||||
"fr": [
|
||||
"Les couleurs de la matrice bandes/modes se choisissent dans Apparence, à partir de celles du thème. Sa légende est traduite aussi.",
|
||||
"Partage TCI : un retour en réception refusé ne laisse plus le poste bloqué en émission, et le PTT retombe si le logiciel meurt.",
|
||||
"Icom : un changement de fréquence ou de mode dont l’accusé se perd est renvoyé, comme le PTT — en perdre un faisait lâcher JTDX."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.25.8",
|
||||
"date": "",
|
||||
|
||||
+25
-17
@@ -514,7 +514,8 @@ func (b *IcomSerial) SetFrequency(hz int64) error {
|
||||
return fmt.Errorf("invalid frequency")
|
||||
}
|
||||
b.lastSetFreq, b.lastSetFreqAt = hz, time.Now()
|
||||
return b.exec(append([]byte{civ.CmdSetFreq}, civ.FreqToBCD(hz)...)...)
|
||||
return b.execIdempotent(fmt.Sprintf("set frequency %d Hz", hz),
|
||||
append([]byte{civ.CmdSetFreq}, civ.FreqToBCD(hz)...)...)
|
||||
}
|
||||
|
||||
func (b *IcomSerial) SetMode(mode string) error {
|
||||
@@ -524,7 +525,7 @@ func (b *IcomSerial) SetMode(mode string) error {
|
||||
}
|
||||
// Set the base mode (keeping the rig's current filter by sending only the
|
||||
// mode byte), then set the data-mode flag for digital modes.
|
||||
if err := b.exec(civ.CmdSetMode, code); err != nil {
|
||||
if err := b.execIdempotent("set mode "+mode, civ.CmdSetMode, code); err != nil {
|
||||
return err
|
||||
}
|
||||
dataByte := byte(0)
|
||||
@@ -532,7 +533,7 @@ func (b *IcomSerial) SetMode(mode string) error {
|
||||
dataByte = 1
|
||||
}
|
||||
// Filter 0x01 (FIL1) is the conventional default for the data-mode set.
|
||||
_ = b.exec(civ.CmdExtra, civ.SubDataMode, dataByte, 0x01)
|
||||
_ = b.execIdempotent("set data mode", civ.CmdExtra, civ.SubDataMode, dataByte, 0x01)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -542,31 +543,38 @@ func (b *IcomSerial) SetMode(mode string) error {
|
||||
// a formatted string so callers can tell the two apart.
|
||||
var errIcomAckLost = errors.New("icom: timeout waiting for response")
|
||||
|
||||
// SetPTT keys or unkeys the transmitter (CI-V 0x1C 0x00), retrying ONCE when the
|
||||
// execIdempotent runs a SET command and sends it ONCE MORE if the
|
||||
// acknowledgement is lost.
|
||||
//
|
||||
// A missing FB is not a missing command — the rig acts on the frame as soon as it
|
||||
// decodes it, and what expires is our wait for the answer on a bus shared with
|
||||
// the rig's own transceive updates. JTDX in "Split Operating: Fake It" moves the
|
||||
// dial immediately before every key-down, so the PTT ack queues behind that
|
||||
// traffic, and one lost ack was fatal: rigctld answered RPRT -9, JTDX read that
|
||||
// as losing rig control and tore the connection down mid-over, reopening it a
|
||||
// moment later (an operator's log shows exactly that, twice, a new rigctld client
|
||||
// within 300 ms of each failure). The same session over TCI never failed, because
|
||||
// TCI carries no CI-V and needs no Fake It.
|
||||
// dial and the mode immediately before every key-down, so those acks queue behind
|
||||
// each other, and losing one was fatal: rigctld answers RPRT -9, JTDX reads that
|
||||
// as losing rig control and tears the connection down mid-over. An operator's log
|
||||
// shows it happening on set_ptt, on set_freq and on set_mode alike, each failure
|
||||
// followed within 300 ms by a fresh rigctld client — and shows this resend
|
||||
// rescuing a PTT that would otherwise have ended the over.
|
||||
//
|
||||
// Re-sending is safe: asking for a state the rig is already in changes nothing.
|
||||
// Only for commands that say "be in this state": re-sending one changes nothing
|
||||
// if the first arrived. A relative or incremental command must not come through
|
||||
// here, which is why this is opt-in per caller rather than folded into exec.
|
||||
func (b *IcomSerial) execIdempotent(what string, payload ...byte) error {
|
||||
err := b.exec(payload...)
|
||||
if err == nil || !errors.Is(err, errIcomAckLost) {
|
||||
return err
|
||||
}
|
||||
applog.Printf("icom: %s — no acknowledgement in %s, sending it once more", what, icomCmdTimeout)
|
||||
return b.exec(payload...)
|
||||
}
|
||||
|
||||
// SetPTT keys or unkeys the transmitter (CI-V 0x1C 0x00).
|
||||
func (b *IcomSerial) SetPTT(on bool) error {
|
||||
state := byte(0)
|
||||
if on {
|
||||
state = 1
|
||||
}
|
||||
err := b.exec(civ.CmdPTT, civ.SubPTT, state)
|
||||
if err == nil || !errors.Is(err, errIcomAckLost) {
|
||||
return err
|
||||
}
|
||||
applog.Printf("icom: PTT %v — no acknowledgement in %s, sending it once more", on, icomCmdTimeout)
|
||||
return b.exec(civ.CmdPTT, civ.SubPTT, state)
|
||||
return b.execIdempotent(fmt.Sprintf("PTT %v", on), civ.CmdPTT, civ.SubPTT, state)
|
||||
}
|
||||
|
||||
// SetPower turns the transceiver on or off (CI-V 0x18). Power-ON is prefixed with
|
||||
|
||||
Reference in New Issue
Block a user