A transponder does not translate by exactly the published difference — the oscillator on board is decades old on some birds and a kilohertz or two out. So an operator who sounds right to themselves comes back off frequency, corrects it on the transmit VFO, and the tracker put it back one second later, every second, for the rest of the pass. Reported on an IC-9700 against HRD, which keeps the shift the operator sets. The tracker already worked this way for the RECEIVER: it reads the dial back and treats a move as the operator choosing a new station. The transmitter had no equivalent — its comment even said so, "derived from the nominal and never argued with". Now it is read back too, and the difference becomes a standing trim on the nominal uplink. Applied to the nominal rather than the corrected frequency, because a translation error is a fixed offset in the uplink band and not something that scales with the Doppler. Read only while not transmitting: mid-over nobody is turning the knob, and on an Icom this read switches to the SUB band and back, which is the same path TuneSatellite already uses to write the uplink and not something to do under a carrier. Kept per satellite AND per transponder, because that is what it belongs to: the error is a property of the hardware in orbit, stable from one pass to the next. Capped at 20 kHz so a bad stored value cannot put the station outside the passband for ever, and shown in the tune panel with a reset — an offset taken silently from the VFO has to be visible, and the VFO alone cannot bring it back to zero once the operator has drifted somewhere wrong. SatTuner gains SatTransmitHz, implemented for the native Icom satellite mode and for the Flex uplink slice; anything else reports nothing and the uplink is left to the arithmetic, as before.
209 lines
7.5 KiB
Go
209 lines
7.5 KiB
Go
package cat
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"hamlog/internal/applog"
|
|
"hamlog/internal/cat/civ"
|
|
)
|
|
|
|
// Satellite operation on an Icom.
|
|
//
|
|
// Two rigs in the range have a satellite mode of their own — the IC-9700 and
|
|
// the IC-9100 — and on those the right thing to do is ask the radio for it
|
|
// rather than build an imitation out of split. Their satellite mode pairs the
|
|
// MAIN band (the downlink) with the SUB band (the uplink), gives full duplex,
|
|
// and keeps the two dials linked the way the designers meant. Every other Icom
|
|
// has one receiver on one band: it can be tuned to the downlink, and that is
|
|
// the whole truth about what it can do on a cross-band satellite.
|
|
//
|
|
// UNTESTED ON HARDWARE. Built from the IC-9700 CI-V reference: 0x16 0x5A arms
|
|
// satellite mode, 0x07 0xD0 / 0xD1 select MAIN and SUB, and once a band is
|
|
// selected the ordinary 0x05 / 0x06 tune it. If an IC-9700 owner reports it
|
|
// misbehaving, the log lines below name every frame sent.
|
|
|
|
// ErrSatUplinkUnreachable says the downlink was tuned and the uplink was not,
|
|
// because the radio has no second receiver and the two are on different bands.
|
|
//
|
|
// A distinct error rather than a silent half-success: a tracker that quietly
|
|
// stops transmitting where the operator expects it to is worse than one that
|
|
// says it cannot. The caller reports it once, not once per Doppler step.
|
|
var ErrSatUplinkUnreachable = errors.New("cat: this radio has one receiver — the uplink is on another band and cannot be set")
|
|
|
|
// SetSatellite arms the rig's own satellite mode.
|
|
func (b *IcomSerial) SetSatellite(on bool) error {
|
|
if !b.satNative {
|
|
// Nothing to arm and nothing to break: the tuning path below does what
|
|
// this radio can do without any mode change. Refusing here would deny an
|
|
// operator the downlink, which is most of the value on a receive-heavy
|
|
// pass.
|
|
b.satOn = on
|
|
return nil
|
|
}
|
|
if err := b.exec(civ.CmdSwitch, civ.SubSwSatellite, boolByte(on)); err != nil {
|
|
return fmt.Errorf("icom: satellite mode %v refused: %w", on, err)
|
|
}
|
|
b.satOn = on
|
|
applog.Printf("icom: satellite mode %v (%s)", on, b.model)
|
|
if on {
|
|
// Leave the radio pointing at MAIN. Everything else in OpsLog — the poll
|
|
// loop, the logged frequency, the operator's dial — reads the selected
|
|
// band, and on a satellite the band worth reading is the one carrying the
|
|
// downlink.
|
|
_ = b.exec(civ.CmdVFO, civ.SubVFOMain)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TuneSatellite puts the receiver on downHz and the transmitter on upHz.
|
|
func (b *IcomSerial) TuneSatellite(downHz, upHz int64, downMode, upMode string) error {
|
|
if downHz <= 0 {
|
|
return fmt.Errorf("icom: no downlink frequency")
|
|
}
|
|
if !b.satNative {
|
|
return b.tuneSatSingleBand(downHz, upHz, downMode, upMode)
|
|
}
|
|
// MAIN — the downlink.
|
|
if err := b.exec(civ.CmdVFO, civ.SubVFOMain); err != nil {
|
|
return fmt.Errorf("icom: could not select the main band: %w", err)
|
|
}
|
|
if err := b.SetFrequency(downHz); err != nil {
|
|
return err
|
|
}
|
|
if err := b.satSetMode(downMode, downHz); err != nil {
|
|
return err
|
|
}
|
|
// SUB — the uplink.
|
|
if upHz > 0 {
|
|
if err := b.exec(civ.CmdVFO, civ.SubVFOSub); err != nil {
|
|
return fmt.Errorf("icom: could not select the sub band: %w", err)
|
|
}
|
|
uerr := b.execIdempotent(fmt.Sprintf("set uplink %d Hz", upHz),
|
|
append([]byte{civ.CmdSetFreq}, civ.FreqToBCD(upHz)...)...)
|
|
merr := b.satSetMode(upMode, upHz)
|
|
// Back to MAIN whatever happened. A rig left pointing at SUB reports the
|
|
// uplink as its frequency, and every band-dependent thing in OpsLog —
|
|
// the log, the antenna, the amplifier — would follow the transmitter
|
|
// onto the wrong band.
|
|
if err := b.exec(civ.CmdVFO, civ.SubVFOMain); err != nil {
|
|
applog.Printf("icom: could not return to the main band: %v", err)
|
|
}
|
|
if uerr != nil {
|
|
return uerr
|
|
}
|
|
if merr != nil {
|
|
return merr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// satSetMode sets the mode of whichever band is currently selected. An empty
|
|
// mode leaves it alone — a linear transponder is worked in one mode for a whole
|
|
// pass, and re-sending it every second is traffic for nothing.
|
|
func (b *IcomSerial) satSetMode(mode string, freqHz int64) error {
|
|
mode = strings.TrimSpace(mode)
|
|
if mode == "" {
|
|
return nil
|
|
}
|
|
// modeCode resolves "SSB" against the CURRENT dial to pick a sideband, which
|
|
// is wrong here twice over: the dial may still be on the other band, and on
|
|
// satellites USB is the convention on both sides whatever the frequency.
|
|
code, data, err := b.modeCode(satSideband(mode))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.setModeBytes(mode, code, data)
|
|
}
|
|
|
|
// satSideband is the sideband convention above 30 MHz: USB, on both the uplink
|
|
// and the downlink, including the parts of a linear transponder that fall in
|
|
// what would be an LSB band on HF. The exceptions — AO-7's mode A downlink on
|
|
// 29 MHz among them — are still USB by convention, so there is no exception to
|
|
// make.
|
|
func satSideband(mode string) string {
|
|
if strings.EqualFold(strings.TrimSpace(mode), "SSB") {
|
|
return "USB"
|
|
}
|
|
return mode
|
|
}
|
|
|
|
// SatReceiveHz is where the receiver is now.
|
|
func (b *IcomSerial) SatReceiveHz() (int64, error) {
|
|
if b.satNative {
|
|
// The selected band is MAIN — see TuneSatellite, which always returns to
|
|
// it — so the ordinary frequency read is the downlink.
|
|
if err := b.exec(civ.CmdVFO, civ.SubVFOMain); err != nil {
|
|
applog.Printf("icom: sat readback could not select main: %v", err)
|
|
}
|
|
}
|
|
return b.readFreq()
|
|
}
|
|
|
|
// SatTransmitHz is where the transmitter is now.
|
|
//
|
|
// On a rig with native satellite mode the uplink is the SUB band, so this is
|
|
// the same dance TuneSatellite does to write it: select SUB, read, and go
|
|
// back to MAIN whatever happens. Leaving the rig on SUB would have every
|
|
// band-dependent thing in OpsLog — the log, the antenna, the amplifier —
|
|
// follow the transmitter onto the wrong band.
|
|
//
|
|
// Not attempted while transmitting: the operator is not turning the knob
|
|
// mid-over, and switching bands under a carrier is not something to do to
|
|
// somebody else's radio.
|
|
func (b *IcomSerial) SatTransmitHz() (int64, error) {
|
|
if !b.satNative {
|
|
// Split on one band. The rig reports one frequency and it is the
|
|
// receiver's; there is nothing to read.
|
|
return 0, nil
|
|
}
|
|
if err := b.exec(civ.CmdVFO, civ.SubVFOSub); err != nil {
|
|
return 0, fmt.Errorf("icom: could not select the sub band: %w", err)
|
|
}
|
|
hz, err := b.readFreq()
|
|
if merr := b.exec(civ.CmdVFO, civ.SubVFOMain); merr != nil {
|
|
applog.Printf("icom: could not return to the main band: %v", merr)
|
|
}
|
|
return hz, err
|
|
}
|
|
|
|
// tuneSatSingleBand is every other Icom: one receiver, one band.
|
|
//
|
|
// The downlink is set, because that is what the operator is listening to. The
|
|
// uplink is set through split only when it is close enough to be on the same
|
|
// band — QO-100 behind transverters, AO-7's mode A — and otherwise reported as
|
|
// out of reach rather than quietly skipped.
|
|
func (b *IcomSerial) tuneSatSingleBand(downHz, upHz int64, downMode, _ string) error {
|
|
if err := b.SetFrequency(downHz); err != nil {
|
|
return err
|
|
}
|
|
if err := b.satSetMode(downMode, downHz); err != nil {
|
|
return err
|
|
}
|
|
if upHz <= 0 {
|
|
return nil
|
|
}
|
|
// One megahertz apart is the working definition of "the same band" here: it
|
|
// covers a transponder's own passband and any sensible transverter pairing,
|
|
// and excludes every real cross-band satellite (145 / 435 MHz).
|
|
if abs64(upHz-downHz) > 1_000_000 {
|
|
return ErrSatUplinkUnreachable
|
|
}
|
|
if err := b.exec(append([]byte{civ.CmdVfoFreq, civ.SubVfoUnselected}, civ.FreqToBCD(upHz)...)...); err != nil {
|
|
return err
|
|
}
|
|
if !b.satOn {
|
|
return nil
|
|
}
|
|
return b.exec(civ.CmdSplit, boolByte(true))
|
|
}
|
|
|
|
func abs64(v int64) int64 {
|
|
if v < 0 {
|
|
return -v
|
|
}
|
|
return v
|
|
}
|