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 }