chore: release v0.27.12

This commit is contained in:
2026-09-05 19:07:21 +02:00
parent f93e1c5898
commit be889681a9
40 changed files with 4971 additions and 453 deletions
+65
View File
@@ -594,6 +594,15 @@ func (b *IcomSerial) SetMode(mode string) error {
if err != nil {
return err
}
return b.setModeBytes(mode, code, data)
}
// setModeBytes is SetMode once the mode is already a CI-V byte and a data flag.
// Split out for the band-stacking recall, which gets both FROM the radio and
// must not go back through an ADIF name to reach them: a register holding CW-R
// or LSB would come back as plain CW or as whatever the band convention says,
// i.e. not the mode the operator left there.
func (b *IcomSerial) setModeBytes(mode string, code byte, data bool) 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.execIdempotent("set mode "+mode, civ.CmdSetMode, code); err != nil {
@@ -2188,3 +2197,59 @@ func (b *IcomSerial) TXAudioSender() (func([]byte) error, error) {
}
return nil, fmt.Errorf("this rig takes transmit audio through its USB sound card, not the CAT link")
}
// ── Band stacking registers ───────────────────────────────────────────────
// RecallBandStack puts the VFO where the operator last was on a band, by asking
// the radio rather than by holding an opinion about it.
//
// The console's band buttons used to send a frequency chosen in software — a
// reasonable middle-of-the-band number, and never where anybody actually
// operates. The radio already knows better: every band key press it has ever
// had is remembered in that band's stacking registers, so register 1 is the
// last place used on that band, and cycling through 2 and 3 walks back through
// the ones before it — CW where CW was worked, and the FT8 frequency where FT8
// was worked, without either being written down anywhere.
//
// Reads the register, then sets frequency and mode from it. Returns the
// frequency it landed on, so the caller can say where it went; a register the
// rig will not read leaves the radio untouched and returns an error, which is
// what makes the caller's fallback to a plain frequency safe.
func (b *IcomSerial) RecallBandStack(band, reg int) (int64, error) {
if b.port == nil {
return 0, fmt.Errorf("not connected")
}
if band <= 0 || reg < 1 || reg > 3 {
return 0, fmt.Errorf("icom: band stack %d/%d is not a register", band, reg)
}
bb, rb := civ.ByteToBCD(band), civ.ByteToBCD(reg)
if err := b.write(civ.CmdExtra, civ.SubBandStack, bb, rb); err != nil {
return 0, err
}
f, err := b.recv(icomReadTimeout, func(d civ.Decoded) bool {
return d.Cmd == civ.CmdExtra && len(d.Data) >= 2 && d.Data[0] == civ.SubBandStack
})
if err != nil {
return 0, err
}
bs, ok := civ.DecodeBandStack(f.Data, bb, rb)
if !ok {
// Logged with the raw frame: the register layout has a tail that differs
// between models, and a rig that answers something we cannot read is the
// one thing worth seeing here.
applog.Printf("icom: band stack %d/%d — cannot read the register from % X", band, reg, f.Data)
return 0, fmt.Errorf("icom: band stacking register %d/%d not understood", band, reg)
}
if err := b.SetFrequency(bs.FreqHz); err != nil {
return 0, err
}
// The mode is best-effort. Landing on the right frequency in the wrong mode
// is a nuisance; refusing the whole recall over it would send the operator
// back to a button that does less.
if bs.Mode != 0 {
if err := b.setModeBytes(civ.ModeToADIF(bs.Mode, bs.Data), bs.Mode, bs.Data); err != nil {
applog.Printf("icom: band stack %d/%d — frequency set, mode 0x%02X refused: %v", band, reg, bs.Mode, err)
}
}
return bs.FreqHz, nil
}