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",
|
"version": "0.25.8",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
+25
-17
@@ -514,7 +514,8 @@ func (b *IcomSerial) SetFrequency(hz int64) error {
|
|||||||
return fmt.Errorf("invalid frequency")
|
return fmt.Errorf("invalid frequency")
|
||||||
}
|
}
|
||||||
b.lastSetFreq, b.lastSetFreqAt = hz, time.Now()
|
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 {
|
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
|
// 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.
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
dataByte := byte(0)
|
dataByte := byte(0)
|
||||||
@@ -532,7 +533,7 @@ func (b *IcomSerial) SetMode(mode string) error {
|
|||||||
dataByte = 1
|
dataByte = 1
|
||||||
}
|
}
|
||||||
// Filter 0x01 (FIL1) is the conventional default for the data-mode set.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,31 +543,38 @@ func (b *IcomSerial) SetMode(mode string) error {
|
|||||||
// a formatted string so callers can tell the two apart.
|
// a formatted string so callers can tell the two apart.
|
||||||
var errIcomAckLost = errors.New("icom: timeout waiting for response")
|
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.
|
// acknowledgement is lost.
|
||||||
//
|
//
|
||||||
// A missing FB is not a missing command — the rig acts on the frame as soon as it
|
// 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
|
// 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
|
// 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
|
// dial and the mode immediately before every key-down, so those acks queue behind
|
||||||
// traffic, and one lost ack was fatal: rigctld answered RPRT -9, JTDX read that
|
// each other, and losing one was fatal: rigctld answers RPRT -9, JTDX reads that
|
||||||
// as losing rig control and tore the connection down mid-over, reopening it a
|
// as losing rig control and tears the connection down mid-over. An operator's log
|
||||||
// moment later (an operator's log shows exactly that, twice, a new rigctld client
|
// shows it happening on set_ptt, on set_freq and on set_mode alike, each failure
|
||||||
// within 300 ms of each failure). The same session over TCI never failed, because
|
// followed within 300 ms by a fresh rigctld client — and shows this resend
|
||||||
// TCI carries no CI-V and needs no Fake It.
|
// 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 {
|
func (b *IcomSerial) SetPTT(on bool) error {
|
||||||
state := byte(0)
|
state := byte(0)
|
||||||
if on {
|
if on {
|
||||||
state = 1
|
state = 1
|
||||||
}
|
}
|
||||||
err := b.exec(civ.CmdPTT, civ.SubPTT, state)
|
return b.execIdempotent(fmt.Sprintf("PTT %v", on), 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPower turns the transceiver on or off (CI-V 0x18). Power-ON is prefixed with
|
// SetPower turns the transceiver on or off (CI-V 0x18). Power-ON is prefixed with
|
||||||
|
|||||||
Reference in New Issue
Block a user