diff --git a/changelog.json b/changelog.json index 84c4458..373abe4 100644 --- a/changelog.json +++ b/changelog.json @@ -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": "", diff --git a/internal/cat/icomserial.go b/internal/cat/icomserial.go index ae29a47..d240791 100644 --- a/internal/cat/icomserial.go +++ b/internal/cat/icomserial.go @@ -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