The hard part of satellite tuning is not the arithmetic, it is deciding who owns the dial. A tracker that forces both frequencies fights the operator every time they turn the knob to follow a station across a linear transponder; one that never touches the receiver leaves them chasing a signal that slides nine kilohertz across a 70 cm pass. So the operator owns the receiver and the tracker follows them. Every second it asks the radio where the receiver actually is. Where it put it, nothing has changed. Further than a dial-turn's tolerance, and the operator has chosen a station: what they landed on is converted back into a nominal frequency, and the transmitter is derived from that. Which is the division of labour on a linear bird — the operator listens, the radio does the sums. Three ways to reach the radio, because a satellite pair is a shape of operating rather than a manufacturer's feature. An IC-9700 or IC-9100 is asked for its OWN satellite mode: it pairs main and sub, gives full duplex, and keeps the dials linked the way its designers meant, which is always better than an imitation built out of split. A Flex gets two slices, A the downlink and B the uplink, created when missing, because "slice B does not exist" is not something to make an operator fix at the start of a ten-minute pass. Everything else gets the downlink, and is told so — half the job announced beats half the job hidden. What goes in the log is the NOMINAL pair. Two stations working each other through a transponder read different numbers off their dials at the same instant; the only figure they can both agree on is the transponder's own. FREQ is the uplink and FREQ_RX the downlink — the one place a satellite QSO differs from every other kind, and the reason FREQ alone cannot describe one.
182 lines
6.5 KiB
Go
182 lines
6.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()
|
|
}
|
|
|
|
// 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
|
|
}
|