diff --git a/args.go b/args.go index 4742ce4..b0ba8cd 100644 --- a/args.go +++ b/args.go @@ -10,19 +10,23 @@ import ( "github.com/pborman/getopt" ) -var verboseLog bool -var quietLog bool -var connectAddress string -var username string -var password string -var civAddress byte -var serialTCPPort uint16 -var enableSerialDevice bool -var rigctldPort uint16 -var runCmd string -var runCmdOnSerialPortCreated string -var statusLogInterval time.Duration -var setDataModeOnTx bool +var ( + verboseLog bool + quietLog bool + connectAddress string + username string + password string + civAddress byte + controllerAddress byte + serialTCPPort uint16 + enableSerialDevice bool + rigctldPort uint16 + runCmd string + runCmdOnSerialPortCreated string + statusLogInterval time.Duration + setDataModeOnTx bool + debugPackets bool +) func parseArgs() { h := getopt.BoolLong("help", 'h', "display help") @@ -31,14 +35,16 @@ func parseArgs() { a := getopt.StringLong("address", 'a', "IC-705", "Connect to address") u := getopt.StringLong("username", 'u', "beer", "Username") p := getopt.StringLong("password", 'p', "beerbeer", "Password") - c := getopt.StringLong("civ-address", 'c', "0xa4", "CI-V address") + c := getopt.StringLong("civ-address", 'c', "0xa4", "CI-V address for radio") t := getopt.Uint16Long("serial-tcp-port", 't', 4531, "Expose radio's serial port on this TCP port") s := getopt.BoolLong("enable-serial-device", 's', "Expose radio's serial port as a virtual serial port") r := getopt.Uint16Long("rigctld-port", 'r', 4532, "Use this TCP port for the internal rigctld") e := getopt.StringLong("exec", 'e', "", "Exec cmd when connected") o := getopt.StringLong("exec-serial", 'o', "socat /tmp/kappanhang-IC-705.pty /tmp/vmware.pty", "Exec cmd when virtual serial port is created, set to - to disable") - i := getopt.Uint16Long("log-interval", 'i', 100, "Status bar/log interval in milliseconds") + i := getopt.Uint16Long("log-interval", 'i', 150, "Status bar/log interval in milliseconds") d := getopt.BoolLong("set-data-tx", 'd', "Automatically enable data mode on TX") + dp := getopt.BoolLong("debug-packets", 'D', "Show CI-V packets for debugging") + ca := getopt.StringLong("controller-address", 'z', "0xe0", "Controller address") getopt.Parse() @@ -63,6 +69,15 @@ func parseArgs() { } civAddress = byte(civAddressInt) + *ca = strings.Replace(*ca, "0x", "", -1) + *ca = strings.Replace(*ca, "0X", "", -1) + controllerAddressInt, err := strconv.ParseInt(*ca, 16, 64) + if err != nil { + fmt.Println("invalid CI-V address for controller: can't parse", *ca) + os.Exit(1) + } + controllerAddress = byte(controllerAddressInt) + serialTCPPort = *t enableSerialDevice = *s rigctldPort = *r @@ -70,4 +85,5 @@ func parseArgs() { runCmdOnSerialPortCreated = *o statusLogInterval = time.Duration(*i) * time.Millisecond setDataModeOnTx = *d + debugPackets = *dp } diff --git a/audio-linux.go b/audio-linux.go index 5928cb2..38472ab 100644 --- a/audio-linux.go +++ b/audio-linux.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package main @@ -392,6 +393,10 @@ func (a *audioStruct) loop() { // We only init the audio once, with the first device name we acquire, so apps using the virtual sound card // won't have issues with the interface going down while the app is running. +// +// ad8im note: but this seems like it leaves stale virtuals around indefinitely in some cases +// +// so it may be desirable to enable force cleanup and recreate via flags func (a *audioStruct) initIfNeeded(devName string) error { a.devName = devName bufferSizeInBits := (audioSampleRate * audioSampleBytes * 8) / 1000 * pulseAudioBufferLength.Milliseconds() @@ -446,7 +451,10 @@ func (a *audioStruct) initIfNeeded(devName string) error { } if a.virtualSoundcardStream.playBuf == nil { - log.Print("opened device " + a.virtualSoundcardStream.source.Name) + //origina line// log.Print("opened device '" + a.virtualSoundcardStream.source.Name + "'") + log.Print("opened virtual sound card device") + log.Print(" name - source:'" + a.virtualSoundcardStream.source.Name + "' sink:'" + a.virtualSoundcardStream.sink.Name + "'") + log.Print(" location - source:'" + a.virtualSoundcardStream.source.Filename + "' sink:'" + a.virtualSoundcardStream.sink.Filename + "'") a.play = make(chan []byte) a.rec = make(chan []byte) diff --git a/audiostream.go b/audiostream.go index c44a212..e27c63e 100644 --- a/audiostream.go +++ b/audiostream.go @@ -88,25 +88,8 @@ func (s *audioStream) handleRxSeqBufEntry(e seqBufEntry) { audio.play <- e.data } -// var drop int - func (s *audioStream) handleAudioPacket(r []byte) error { gotSeq := binary.LittleEndian.Uint16(r[6:8]) - - // if drop == 0 && time.Now().UnixNano()%10 == 0 { - // log.Print("drop start - ", gotSeq) - // drop = 1 - // return nil - // } else if drop > 0 { - // drop++ - // if drop >= int(time.Now().UnixNano()%10) { - // log.Print("drop stop - ", gotSeq) - // drop = 0 - // } else { - // return nil - // } - // } - if s.timeoutTimer != nil { s.timeoutTimer.Stop() s.timeoutTimer.Reset(audioTimeoutDuration) diff --git a/civcontrol.go b/civcontrol.go index 13d3fa5..2b77327 100644 --- a/civcontrol.go +++ b/civcontrol.go @@ -9,11 +9,15 @@ import ( const statusPollInterval = time.Second const commandRetryTimeout = 500 * time.Millisecond -const pttTimeout = 3 * time.Minute +const pttTimeout = 10 * time.Minute // NOTE: US operators MUST legally identify at least once every ten minutes, most Tx should be much shorter than this + const tuneTimeout = 30 * time.Second +const ON = 1 +const OFF = 0 +const OK = 0xfb +const NG = 0xfa // Commands reference: https://www.icomeurope.com/wp-content/uploads/2020/08/IC-705_ENG_CI-V_1_20200721.pdf - type civOperatingMode struct { name string code byte @@ -43,28 +47,54 @@ var civFilters = []civFilter{ {name: "FIL3", code: 0x03}, } +// NOTE: future enhancement may be to specified allowed TX range w/in the band +// +// definitely needed since it appears this tool will push the PTT at any freq it's tuned to +// question is how does the radio react type civBand struct { freqFrom uint freqTo uint freq uint } +// NOTE: check these against US band assignments +// +// these would be very helpful to load from config file +// even better would be to have a flag/config that will adjust them based on users license class +// (IE help them avoid accidental Tx where not allowed) var civBands = []civBand{ - {freqFrom: 1800000, freqTo: 1999999}, // 1.9 - {freqFrom: 3400000, freqTo: 4099999}, // 3.5 - {freqFrom: 6900000, freqTo: 7499999}, // 7 - {freqFrom: 9900000, freqTo: 10499999}, // 10 - {freqFrom: 13900000, freqTo: 14499999}, // 14 - {freqFrom: 17900000, freqTo: 18499999}, // 18 - {freqFrom: 20900000, freqTo: 21499999}, // 21 - {freqFrom: 24400000, freqTo: 25099999}, // 24 - {freqFrom: 28000000, freqTo: 29999999}, // 28 - {freqFrom: 50000000, freqTo: 54000000}, // 50 - {freqFrom: 74800000, freqTo: 107999999}, // WFM - {freqFrom: 108000000, freqTo: 136999999}, // AIR - {freqFrom: 144000000, freqTo: 148000000}, // 144 - {freqFrom: 420000000, freqTo: 450000000}, // 430 - {freqFrom: 0, freqTo: 0}, // GENE + /* + {freqFrom: 1800000, freqTo: 1999999}, // 1.9 + {freqFrom: 3400000, freqTo: 4099999}, // 3.5 - 75/80m + {freqFrom: 6900000, freqTo: 7499999}, // 7 - 40m + {freqFrom: 9900000, freqTo: 10499999}, // 10 + {freqFrom: 13900000, freqTo: 14499999}, // 14 + {freqFrom: 17900000, freqTo: 18499999}, // 18 + {freqFrom: 20900000, freqTo: 21499999}, // 21 + {freqFrom: 24400000, freqTo: 25099999}, // 24 + {freqFrom: 28000000, freqTo: 29999999}, // 28 + {freqFrom: 50000000, freqTo: 54000000}, // 50 + {freqFrom: 74800000, freqTo: 107999999}, // WFM - no TX in US + {freqFrom: 108000000, freqTo: 136999999}, // AIR = no TX in US + {freqFrom: 144000000, freqTo: 148000000}, // 144 + {freqFrom: 420000000, freqTo: 450000000}, // 430 + {freqFrom: 0, freqTo: 0}, // GENE - general is ok for rx, but tx has statuatory limitations + */ + + {freqFrom: 1800000, freqTo: 2000000}, // 1.9 - 160m + {freqFrom: 3500000, freqTo: 4000000}, // 3.5 - 75/80m + {freqFrom: 7000000, freqTo: 7300000}, // 7 - 40m + {freqFrom: 10100000, freqTo: 10150000}, // 10 - 30m data modes only in US + {freqFrom: 14000000, freqTo: 14350000}, // 14 - 20m + {freqFrom: 18068000, freqTo: 18168000}, // 18 -17m + {freqFrom: 21000000, freqTo: 21450000}, // 21 - 15m + {freqFrom: 24890000, freqTo: 24990000}, // 24 - 12m + {freqFrom: 28000000, freqTo: 29700000}, // 28 - 10m + {freqFrom: 50000000, freqTo: 54000000}, // 50 - 6m + {freqFrom: 144000000, freqTo: 148000000}, // 144 - 2m + {freqFrom: 420000000, freqTo: 450000000}, // 430 - 70cm + //{freqFrom: 0, freqTo: 0}, // GENE // doesn't seem needed or useful + // NOTE: IC-705 doesn't support 33cm or higher, but it's twin the IC-905 does so we may think about that going forward } type splitMode int @@ -84,7 +114,7 @@ type civCmd struct { } type civControlStruct struct { - st *serialStream + st *serialStream // this may be overly terse and unhelpful when troubleshooting issues deinitNeeded chan bool deinitFinished chan bool resetSReadTimer chan bool @@ -94,16 +124,17 @@ type civControlStruct struct { mutex sync.Mutex pendingCmds []*civCmd + getFreq civCmd // NOTE: why was this removed in v1.3-devel version? getPwr civCmd - getS civCmd + getS civCmd // get S-meter reading getOVF civCmd getSWR civCmd getTransmitStatus civCmd getPreamp civCmd getAGC civCmd getTuneStatus civCmd - getVd civCmd - getTS civCmd + getVd civCmd // get Vd meter reading + getTuningStep civCmd getRFGain civCmd getSQL civCmd getNR civCmd @@ -133,7 +164,7 @@ type civControlStruct struct { setPreamp civCmd setAGC civCmd setNREnabled civCmd - setTS civCmd + setTuningStep civCmd setVFO civCmd setSplit civCmd @@ -144,10 +175,10 @@ type civControlStruct struct { subFreq uint ptt bool tune bool - pwrPercent int - rfGainPercent int - sqlPercent int - nrPercent int + pwrLevel int + rfGainLevel int + sqlLevel int + nrLevel int nrEnabled bool operatingModeIdx int dataMode bool @@ -167,36 +198,178 @@ type civControlStruct struct { var civControl civControlStruct -// Returns false if the message should not be forwarded to the serial port TCP server or the virtual serial port. +type CIVCmdSet struct { + // send|read filed name in civControlStruct.state structure + // empty field indicates no traffic in that direction + send string + read string + cmdSeq []byte + //datasize int // how many bytes for data send/return + //statusonly bool // true if return|send is just an OK or NG byte +} + +type CIVCmds map[string]CIVCmdSet + +var CIV = CIVCmds{ + // 0x00 // send frequency data via transceive (to active VFO?) + // 0x01 // send mode data via transceive + // 0x02 // send mode data via transceive + // 0x03 // read operating frequency (of active VFO?) + "getFreq": CIVCmdSet{cmdSeq: []byte{0x03}}, + // 0x04 // read operating mode + "getMode": CIVCmdSet{cmdSeq: []byte{0x04}}, + // 0x05 // set operating frequency (of active VFO?) + // 0x06 // set operating mode + "setMode": CIVCmdSet{cmdSeq: []byte{0x06}}, + // 0x07 // select VFO + "setVFO": CIVCmdSet{cmdSeq: []byte{0x07}}, // switch to operating in VFO mode + // 0x08 // switch to operating in memory mode + // 0x09 + // 0x0a + // 0x0b + // 0x0c + // 0x0d + // 0x0e // scanning related actions + + // 0x0f // split & duplex + "getSplit": CIVCmdSet{cmdSeq: []byte{0x0f}}, // returns split off/on/dup+/dup+ info + "setSplit": CIVCmdSet{cmdSeq: []byte{0x0f}}, // set split to off/on/dup+/dup+ + "disableSplit": CIVCmdSet{cmdSeq: []byte{0x0f, 0x00}}, // directly turn off + + // 0x10 + "getTuningStep": CIVCmdSet{cmdSeq: []byte{0x10}}, + "setTuningStep": CIVCmdSet{cmdSeq: []byte{0x10}}, + // 0x11 + // 0x12 // no command documented + // 0x13 // enable various speech output ( for radio operation by visually impaired) + // 0x14 // gain, sqleuule, noise reduction, + "getRFGain": CIVCmdSet{cmdSeq: []byte{0x14, 0x02}}, + "setRFGain": CIVCmdSet{cmdSeq: []byte{0x14, 0x02}}, + "getSQL": CIVCmdSet{cmdSeq: []byte{0x14, 0x03}}, + "setSQL": CIVCmdSet{cmdSeq: []byte{0x14, 0x03}}, + "getNR": CIVCmdSet{cmdSeq: []byte{0x14, 0x06}}, + "setNR": CIVCmdSet{cmdSeq: []byte{0x14, 0x06}}, + "getPwr": CIVCmdSet{cmdSeq: []byte{0x14, 0x0a}}, // RF Power + "setPwr": CIVCmdSet{cmdSeq: []byte{0x14, 0x0a}}, + // 0x15 + "getS": CIVCmdSet{cmdSeq: []byte{0x15, 0x02}}, //read S-meter level + "getSWR": CIVCmdSet{cmdSeq: []byte{0x15, 0x12}}, + "getVd": CIVCmdSet{cmdSeq: []byte{0x15, 0x15}}, + // 0x16 // misc - preamp, NB, NR, filters, tone squelches, etc + "getPreamp": CIVCmdSet{cmdSeq: []byte{0x16, 0x02}}, + "setPreamp": CIVCmdSet{cmdSeq: []byte{0x16, 0x02}}, + "getAGC": CIVCmdSet{cmdSeq: []byte{0x16, 0x12}}, + "setAGC": CIVCmdSet{cmdSeq: []byte{0x16, 0x12}}, + "getNREnabled": CIVCmdSet{cmdSeq: []byte{0x16, 0x40}}, + "setNREnabled": CIVCmdSet{cmdSeq: []byte{0x16, 0x40}}, + // 0x17 // send CW messages (up to 30 chars) + "sendCWMsg": CIVCmdSet{cmdSeq: []byte{0x17}}, + // 0x18 + // 0x19 + + // 0x1a // a lot of misc settings (VOX, GPS Pos, NTP, share pictures, pwr supply type) + // 0x1a 0x00 // memory contents + // 0x1a 0x01 // stacking register contents + // 0x1a 0x02 // mem keyer contents + // 0x1a 0x03 // IF filter width + // 0x1a 0x04 // AGC time constant + // 0x1a 0x05 // a LOT of subcmcds here.. + /// seems to be most/all of SET menu. EG scope, audio scope, voice TX, Keyer/CW, RTTY, Recording, Scan, GPS + // 0x1a 0x06 // Data mode + // 0x1a 0x07 // NTP + // 0x1a 0x08 // NTP + // 0x1a 0x09 // OVF + // 0x1a 0x0a // share pictures + // 0x1a 0x0b // pwr supply + "getDataMode": CIVCmdSet{cmdSeq: []byte{0x1a, 0x06}}, + "setDataMode": CIVCmdSet{cmdSeq: []byte{0x1a, 0x06}}, + "getOVF": CIVCmdSet{cmdSeq: []byte{0x1a, 0x09}}, + // 0x1b // repeater tone|tsql|dtcs|csql settings + // 0x1c // PTT, ant tuner, XFC on|off + "getTransmitStatus": CIVCmdSet{cmdSeq: []byte{0x1c, 0x00}}, // is radio doing Rx or Tx + "setPTT": CIVCmdSet{cmdSeq: []byte{0x1c, 0x00}}, // current code base does next 2 commands as "data" + "setTune": CIVCmdSet{cmdSeq: []byte{0x1c, 0x01}}, // antenna tuner, NOT frequency tuning + "getTuneStatus": CIVCmdSet{cmdSeq: []byte{0x1c, 0x01}}, // antenna tuner, NOT frequency tuning + // 0x1d // no command documented + // 0x1e // TX band edge settings + // 0x1f // DV (D-Star) my station & UR/R1/R2 settings + // 0x20 // various DV (D-Star) commands + // 0x21 // RIT (recieve increment tuning) settings + // 0x22 // DV (D-Star) settings + // 0x23 // GPS position setting + // 0x24 // TX output power settings + // 0x25 // VFO frequency settings + "getMainVFOFreq": CIVCmdSet{cmdSeq: []byte{0x25, 0x00}}, + "setMainVFOFreq": CIVCmdSet{cmdSeq: []byte{0x25, 0x00}}, + "getSubVFOFreq": CIVCmdSet{cmdSeq: []byte{0x25, 0x01}}, + "setSubVFOFreq": CIVCmdSet{cmdSeq: []byte{0x25, 0x01}}, + // 0x26 // VFO mode & filter settings + "getMainVFOMode": CIVCmdSet{cmdSeq: []byte{0x26, 0x00}}, + "setMainVFOMode": CIVCmdSet{cmdSeq: []byte{0x26, 0x00}}, + "getSubVFOMode": CIVCmdSet{cmdSeq: []byte{0x26, 0x01}}, + "setSubVFOMode": CIVCmdSet{cmdSeq: []byte{0x26, 0x01}}, + // 0x27 // scope settings + // 0x28 // TX voice memory + // nothing documented beyond 0x28 +} + +var noData = []byte{} + +// returns true if packet is 'ok' to be forwared to the (TCP or virtual) serial port +// returns false if the message should not be forwarded to either serial port func (s *civControlStruct) decode(d []byte) bool { + + if debugPackets { + debugPacket("decoding", d) + } + + // minimum valid incoming packet is six bytes long: 2 start-of-packet, to, from, cmd, end-of-packet + // sanity check that incoming packets is of minimal size, and properly wrapped valid header & end bytes if len(d) < 6 || d[0] != 0xfe || d[1] != 0xfe || d[len(d)-1] != 0xfd { return true } + // ignore if it was intended for a different device on the bus, or not from the radio we are controlling + // in theory we *could* support multiple radios concurrently, with enough design updates. + // + // NOTE: looks like I'm seeing all of the commands I'm sending to the radio... + // is this due to CI-V USB Echo Back being enabled? OR are we seeing out own packets? + // if we drop these then any changes sent are not seen on the status line... does seem they are making it to the radio though + // this suggests the code is just decoding what it *asked* the transiever to do, and not actually listening to hear + // what the transciever says it actually did. + /* + if intendedFor, expectedFrom := d[2], d[3]; intendedFor != controllerAddress || expectedFrom != civAddress { + return true + } + */ + + // NOTE: shouldn't payload start after byte 4, not byte 5 payload := d[5 : len(d)-1] s.state.mutex.Lock() defer s.state.mutex.Unlock() switch d[4] { - // case 0x00: - // return s.decodeFreq(payload) - case 0x01: + case 0x00: // send frequency data via transceive (to active VFO?) + return s.decodeFreq(payload) + case 0x01: // send mode data via transceive return s.decodeMode(payload) - // case 0x03: - // return s.decodeFreq(payload) - case 0x04: + case 0x02: // send mode data via transceive + return false // not implemented, return failure indicator + case 0x03: // read operating frequency (of active VFO?) + return s.decodeFreq(payload) + case 0x04: // read operating mode return s.decodeMode(payload) - // case 0x05: - // return s.decodeFreq(payload) - case 0x06: + case 0x05: // set operating frequency (of active VFO?) + return s.decodeFreq(payload) + case 0x06: // set operating mode return s.decodeMode(payload) case 0x07: return s.decodeVFO(payload) case 0x0f: return s.decodeSplit(payload) case 0x10: - return s.decodeTS(payload) + return s.decodeTuningStep(payload) case 0x1a: return s.decodeDataModeAndOVF(payload) case 0x14: @@ -215,47 +388,34 @@ func (s *civControlStruct) decode(d []byte) bool { return true } -func (s *civControlStruct) decodeFreqData(d []byte) (f uint) { - var pos int - for _, v := range d { - s1 := v & 0x0f - s2 := v >> 4 - f += uint(s1) * uint(math.Pow(10, float64(pos))) - pos++ - f += uint(s2) * uint(math.Pow(10, float64(pos))) - pos++ +// NOTE: this was commented out... why? is it bcaus it doesn't know which VFO or always checks VFO A even if B selected? +func (s *civControlStruct) decodeFreq(d []byte) bool { + if len(d) < 2 { + return !s.state.getFreq.pending && !s.state.setMainVFOFreq.pending } - return + s.state.freq = s.decodeFreqData(d) + statusLog.reportFrequency(s.state.freq) + + s.state.bandIdx = len(civBands) - 1 // set the band idx to the last in range for a default (this was the general range) untile band is determined + for i := range civBands { + if s.state.freq >= civBands[i].freqFrom && s.state.freq <= civBands[i].freqTo { + s.state.bandIdx = i + civBands[s.state.bandIdx].freq = s.state.freq + break + } + } + + if s.state.getFreq.pending { + s.removePendingCmd(&s.state.getFreq) + return false + } + if s.state.setMainVFOFreq.pending { + s.removePendingCmd(&s.state.setMainVFOFreq) + return false + } + return true } -// func (s *civControlStruct) decodeFreq(d []byte) bool { -// if len(d) < 2 { -// return !s.state.getFreq.pending && !s.state.setMainVFOFreq.pending -// } - -// s.state.freq = s.decodeFreqData(d) -// statusLog.reportFrequency(s.state.freq) - -// s.state.bandIdx = len(civBands) - 1 // Set the band idx to GENE by default. -// for i := range civBands { -// if s.state.freq >= civBands[i].freqFrom && s.state.freq <= civBands[i].freqTo { -// s.state.bandIdx = i -// civBands[s.state.bandIdx].freq = s.state.freq -// break -// } -// } - -// if s.state.getFreq.pending { -// s.removePendingCmd(&s.state.getFreq) -// return false -// } -// if s.state.setMainVFOFreq.pending { -// s.removePendingCmd(&s.state.setMainVFOFreq) -// return false -// } -// return true -// } - func (s *civControlStruct) decodeFilterValueToFilterIdx(v byte) int { for i := range civFilters { if civFilters[i].code == v { @@ -280,8 +440,11 @@ func (s *civControlStruct) decodeMode(d []byte) bool { if len(d) > 1 { s.state.filterIdx = s.decodeFilterValueToFilterIdx(d[1]) } - statusLog.reportMode(civOperatingModes[s.state.operatingModeIdx].name, s.state.dataMode, - civFilters[s.state.filterIdx].name) + statusLog.reportMode( + civOperatingModes[s.state.operatingModeIdx].name, + s.state.dataMode, + civFilters[s.state.filterIdx].name, + ) if s.state.setMode.pending { s.removePendingCmd(&s.state.setMode) @@ -297,10 +460,8 @@ func (s *civControlStruct) decodeVFO(d []byte) bool { if d[0] == 1 { s.state.vfoBActive = true - log.Print("active vfo: B") } else { s.state.vfoBActive = false - log.Print("active vfo: A") } if s.state.setVFO.pending { @@ -321,15 +482,16 @@ func (s *civControlStruct) decodeSplit(d []byte) bool { switch d[0] { default: s.state.splitMode = splitModeOff + str = " " case 0x01: s.state.splitMode = splitModeOn str = "SPLIT" case 0x11: s.state.splitMode = splitModeDUPMinus - str = "DUP-" + str = " DUP-" case 0x12: s.state.splitMode = splitModeDUPPlus - str = "DUP+" + str = " DUP+" } statusLog.reportSplit(s.state.splitMode, str) @@ -344,9 +506,9 @@ func (s *civControlStruct) decodeSplit(d []byte) bool { return true } -func (s *civControlStruct) decodeTS(d []byte) bool { +func (s *civControlStruct) decodeTuningStep(d []byte) bool { if len(d) < 1 { - return !s.state.getTS.pending && !s.state.setTS.pending + return !s.state.getTuningStep.pending && !s.state.setTuningStep.pending } s.state.tsValue = d[0] @@ -381,14 +543,14 @@ func (s *civControlStruct) decodeTS(d []byte) bool { case 13: s.state.ts = 100000 } - statusLog.reportTS(s.state.ts) + statusLog.reportTuningStep(s.state.ts) - if s.state.getTS.pending { - s.removePendingCmd(&s.state.getTS) + if s.state.getTuningStep.pending { + s.removePendingCmd(&s.state.getTuningStep) return false } - if s.state.setTS.pending { - s.removePendingCmd(&s.state.setTS) + if s.state.setTuningStep.pending { + s.removePendingCmd(&s.state.setTuningStep) return false } return true @@ -433,14 +595,20 @@ func (s *civControlStruct) decodeDataModeAndOVF(d []byte) bool { } func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { - switch d[0] { - case 0x02: - if len(d) < 3 { + // all of these returns are expected to be three bytes long + // subcmd, data msb, data lsb (where data is encoded as BCD) + // code would be easier to read if we check size and do value extraction first + // then take actions on appropriate entities in each case + + subcmd := d[0] + data := d[1:] + switch subcmd { + case 0x02: // RF Gain subcmd + if len(data) < 2 { return !s.state.getRFGain.pending && !s.state.setRFGain.pending } - hex := uint16(d[1])<<8 | uint16(d[2]) - s.state.rfGainPercent = int(math.Round((float64(hex) / 0x0255) * 100)) - statusLog.reportRFGain(s.state.rfGainPercent) + s.state.rfGainLevel = BCDToDec(data) + statusLog.reportRFGain(s.state.rfGainLevel) if s.state.getRFGain.pending { s.removePendingCmd(&s.state.getRFGain) return false @@ -449,28 +617,25 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { s.removePendingCmd(&s.state.setRFGain) return false } - case 0x03: - if len(d) < 3 { + case 0x03: // Squelch level subcmd + if len(data) < 2 { return !s.state.getSQL.pending && !s.state.setSQL.pending } - hex := uint16(d[1])<<8 | uint16(d[2]) - s.state.sqlPercent = int(math.Round((float64(hex) / 0x0255) * 100)) - statusLog.reportSQL(s.state.sqlPercent) + s.state.sqlLevel = BCDToDec(data) + statusLog.reportSQL(s.state.sqlLevel) if s.state.getSQL.pending { s.removePendingCmd(&s.state.getSQL) return false } if s.state.setSQL.pending { - s.removePendingCmd(&s.state.setSQL) return false } - case 0x06: - if len(d) < 3 { + case 0x06: // Noise Reduction level subcmd + if len(data) < 2 { return !s.state.getNR.pending && !s.state.setNR.pending } - hex := uint16(d[1])<<8 | uint16(d[2]) - s.state.nrPercent = int(math.Round((float64(hex) / 0x0255) * 100)) - statusLog.reportNR(s.state.nrPercent) + s.state.nrLevel = BCDToDec(data) + statusLog.reportNR(s.state.nrLevel) if s.state.getNR.pending { s.removePendingCmd(&s.state.getNR) return false @@ -479,13 +644,12 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { s.removePendingCmd(&s.state.setNR) return false } - case 0x0a: - if len(d) < 3 { + case 0x0a: // RF Power Level subcmd + if len(data) < 2 { return !s.state.getPwr.pending && !s.state.setPwr.pending } - hex := uint16(d[1])<<8 | uint16(d[2]) - s.state.pwrPercent = int(math.Round((float64(hex) / 0x0255) * 100)) - statusLog.reportTxPower(s.state.pwrPercent) + s.state.pwrLevel = BCDToDec(data) + statusLog.reportTxPower(s.state.pwrLevel) if s.state.getPwr.pending { s.removePendingCmd(&s.state.getPwr) return false @@ -494,6 +658,21 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { s.removePendingCmd(&s.state.setPwr) return false } + // hooks for future functionality extension + case 0x01: // AF level (aka volume) subcmd + case 0x07: // PassBandTuning1 position + case 0x08: // PassBandTuning2 position + case 0x09: // CW pitch, 0000 = 300Hz, 0255 = 900Hz each step is 5Hz + case 0x0b: // mic gain + case 0x0c: // keying speed, 0000 = 6wpm, 0255 = 48wpm + case 0x0d: // notch filter setting, 0000 = max widdershins rotation, 0255 = max clockwise rotation + case 0x0e: // COMP level + case 0x0f: // break-in delay, 0000 = 2.0 d, 0255 = 13.0d + case 0x12: // Noise Blanker level + case 0x15: // Monitor audio level + case 0x16: // VOX gain + case 0x17: // anti-VOX gain + case 0x19: // LCD backlight brightness } return true } @@ -559,18 +738,19 @@ func (s *civControlStruct) decodeTransmitStatus(d []byte) bool { } func (s *civControlStruct) decodeVdSWRS(d []byte) bool { - switch d[0] { + subcmd := d[0] + data := d[1:] + switch subcmd { case 0x02: - if len(d) < 3 { + if len(data) < 2 { return !s.state.getS.pending } - sValue := (int(math.Round(((float64(int(d[1])<<8) + float64(d[2])) / 0x0241) * 18))) + sValue := BCDToSLevel(data) sStr := "S" if sValue <= 9 { sStr += fmt.Sprint(sValue) } else { sStr += "9+" - switch sValue { case 10: sStr += "10" @@ -605,7 +785,7 @@ func (s *civControlStruct) decodeVdSWRS(d []byte) bool { return !s.state.getSWR.pending } s.state.lastSWRReceivedAt = time.Now() - statusLog.reportSWR(((float64(int(d[1])<<8)+float64(d[2]))/0x0120)*2 + 1) + statusLog.reportSWR(BCDToSWR(data)) if s.state.getSWR.pending { s.removePendingCmd(&s.state.getSWR) return false @@ -614,7 +794,7 @@ func (s *civControlStruct) decodeVdSWRS(d []byte) bool { if len(d) < 3 { return !s.state.getVd.pending } - statusLog.reportVd(((float64(int(d[1])<<8) + float64(d[2])) / 0x0241) * 16) + statusLog.reportVd(BCDToVd(data)) if s.state.getVd.pending { s.removePendingCmd(&s.state.getVd) return false @@ -624,12 +804,14 @@ func (s *civControlStruct) decodeVdSWRS(d []byte) bool { } func (s *civControlStruct) decodePreampAGCNREnabled(d []byte) bool { - switch d[0] { + subcmd := d[0] + data := d[1:] + switch subcmd { case 0x02: - if len(d) < 2 { + if len(data) < 1 { return !s.state.getPreamp.pending && !s.state.setPreamp.pending } - s.state.preamp = int(d[1]) + s.state.preamp = int(data[0]) statusLog.reportPreamp(s.state.preamp) if s.state.getPreamp.pending { s.removePendingCmd(&s.state.getPreamp) @@ -640,10 +822,10 @@ func (s *civControlStruct) decodePreampAGCNREnabled(d []byte) bool { return false } case 0x12: - if len(d) < 2 { + if len(data) < 1 { return !s.state.getAGC.pending && !s.state.setAGC.pending } - s.state.agc = int(d[1]) + s.state.agc = int(data[0]) var agc string switch s.state.agc { case 1: @@ -663,10 +845,10 @@ func (s *civControlStruct) decodePreampAGCNREnabled(d []byte) bool { return false } case 0x40: - if len(d) < 2 { + if len(data) < 1 { return !s.state.getNREnabled.pending && !s.state.setNREnabled.pending } - if d[1] == 1 { + if data[0] == 1 { s.state.nrEnabled = true } else { s.state.nrEnabled = false @@ -688,13 +870,11 @@ func (s *civControlStruct) decodeVFOFreq(d []byte) bool { if len(d) < 2 { return !s.state.getMainVFOFreq.pending && !s.state.getSubVFOFreq.pending && !s.state.setSubVFOFreq.pending } - f := s.decodeFreqData(d[1:]) switch d[0] { default: s.state.freq = f statusLog.reportFrequency(s.state.freq) - s.state.bandIdx = len(civBands) - 1 // Set the band idx to GENE by default. for i := range civBands { if s.state.freq >= civBands[i].freqFrom && s.state.freq <= civBands[i].freqTo { @@ -781,10 +961,11 @@ func (s *civControlStruct) decodeVFOMode(d []byte) bool { return true } +// better name might be prepCmd, loadCmd, or newCmd... or at least expand to initializeCmd func (s *civControlStruct) initCmd(cmd *civCmd, name string, data []byte) { *cmd = civCmd{} cmd.name = name - cmd.cmd = data + cmd.cmd = data // this is the cmd + subcmd + data to send } func (s *civControlStruct) getPendingCmdIndex(cmd *civCmd) int { @@ -808,12 +989,16 @@ func (s *civControlStruct) removePendingCmd(cmd *civCmd) { } func (s *civControlStruct) sendCmd(cmd *civCmd) error { + // if serial stream isn't established there's nowhere to send the command to if s.st == nil { return nil } cmd.pending = true cmd.sentAt = time.Now() + + // add this cmd request to the list of pending commands we'll need to process returned data for + // each cmd request is a pointer to a civCmd object, so this is check of a specfic request rather than just name of a command sent if s.getPendingCmdIndex(cmd) < 0 { s.state.pendingCmds = append(s.state.pendingCmds, cmd) select { @@ -821,103 +1006,201 @@ func (s *civControlStruct) sendCmd(cmd *civCmd) error { default: } } + + // now actually send it to the serial stream return s.st.send(cmd.cmd) } -func (s *civControlStruct) setPwr(percent int) error { - v := uint16(0x0255 * (float64(percent) / 100)) - s.initCmd(&s.state.setPwr, "setPwr", []byte{254, 254, civAddress, 224, 0x14, 0x0a, byte(v >> 8), byte(v & 0xff), 253}) +func prepPacket(command string, data []byte) (pkt []byte) { + pkt = append([]byte{0xfe, 0xfe}, []byte{civAddress, controllerAddress}...) + pkt = append(pkt, CIV[command].cmdSeq...) + pkt = append(pkt, data...) + pkt = append(pkt, []byte{0xfd}...) + if debugPackets { + debugPacket(command, pkt) + } + return +} + +// encode to BCD using double dabble algorithm +func encodeForSend(decimal int) (bcd []byte) { + + v := uint32(decimal) + v <<= 3 + for shifts := 3; shifts < 8; shifts++ { + // when ONEs or TENs places are 5 or more, add 3 to that place prior to the shift left + if v&0x00f00 > 0x00400 { + v += 0x00300 + } + // is TENs place >= 5, if so add 3 to it and shift left one bit + if v&0x0f000 > 0x04000 { + v += 0x03000 + } + v <<= 1 + } + + hundreds := (v & 0xf0000) >> 16 + tens := (v & 0x0f000) >> 12 + ones := (v & 0x00f00) >> 8 + lo := ((tens << 3) + (tens << 1)) + ones + bcd = append(bcd, byte(hundreds)) + bcd = append(bcd, byte(lo)) + return +} + +func BCDToDec(bcd []byte) int { + return int(bcd[0]*100 + bcd[1]) +} + +/* +func pctAsBCD(pct int) (BCD []byte) { + scaled := uint16(255 * (float64(pct) / 100)) + return encodeForSend(scaled) +} + +func BCDAsPct(bcd []byte) (pct int) { + pct = int(100 * float64(BCDToDec(bcd)) / 0xff) + return +} +*/ + +func BCDToSLevel(bcd []byte) (sLevel int) { + // BCD to S-level + // 0000 => S0 + // 0120 => S9 + // 0241 => S9 + 60dB + // we want 17 S-levels, 10 for S0-9, plus 7 for each +10dB, number them 1 - 18 + fullScale := float64(241) // FWIW, 241 = b11110001 + sLevel = int(float64(BCDToDec(bcd))/fullScale) * 18 + return +} + +func BCDToSWR(bcd []byte) (SWR float64) { + // BCD to SWR - note that this isn't linear + // 0000 => 1.0 + // 0048 => 1.5 + // 0080 => 2.0 + // 0120 => 3.0 + fullScale := float64(120) // FWIW, 120 = b01111000 + SWR = 1 + (float64(BCDToDec(bcd))/fullScale)*2 + return +} + +func BCDToVd(bcd []byte) (Vd float64) { + // BCD to Vd + // 0000 => 0v + // 0075 => 5v + // 0241 => 16v + // IE - normalize full swing over 0-241, where full swing is 16 volts + fullScale := float64(241) // FWIW, 241 = b11110001 + Vd = (float64(BCDToDec(bcd)) / fullScale) * 16 + return +} + +// NOTE: maybe call this decToBCDByDecade? or BCDDigit? +func (s *civControlStruct) getDigit(v uint, decade int) byte { + asDecimal := float64(v) + for decade > 0 { + asDecimal /= 10 + decade-- + } + return byte(uint(asDecimal) % 10) +} + +func (s *civControlStruct) decodeFreqData(d []byte) (f uint) { + var pos int + for _, v := range d { + s1 := v & 0x0f + s2 := v >> 4 + f += uint(s1) * uint(math.Pow(10, float64(pos))) + pos++ + f += uint(s2) * uint(math.Pow(10, float64(pos))) + pos++ + } + return +} + +func (s *civControlStruct) setPwr(level int) error { + s.initCmd(&s.state.setPwr, "setPwr", prepPacket("setPwr", encodeForSend(level))) return s.sendCmd(&s.state.setPwr) } func (s *civControlStruct) incPwr() error { - if s.state.pwrPercent < 100 { - return s.setPwr(s.state.pwrPercent + 1) + if s.state.pwrLevel < 255 { + return s.setPwr(s.state.pwrLevel + 1) } return nil } func (s *civControlStruct) decPwr() error { - if s.state.pwrPercent > 0 { - return s.setPwr(s.state.pwrPercent - 1) + if s.state.pwrLevel > 0 { + return s.setPwr(s.state.pwrLevel - 1) } return nil } -func (s *civControlStruct) setRFGain(percent int) error { - v := uint16(0x0255 * (float64(percent) / 100)) - s.initCmd(&s.state.setRFGain, "setRFGain", []byte{254, 254, civAddress, 224, 0x14, 0x02, byte(v >> 8), byte(v & 0xff), 253}) +func (s *civControlStruct) setRFGain(level int) error { + s.initCmd(&s.state.setRFGain, "setRFGain", prepPacket("setRFGain", encodeForSend(level))) return s.sendCmd(&s.state.setRFGain) } func (s *civControlStruct) incRFGain() error { - if s.state.rfGainPercent < 100 { - return s.setRFGain(s.state.rfGainPercent + 1) + if s.state.rfGainLevel < 255 { + return s.setRFGain(s.state.rfGainLevel + 1) } return nil } func (s *civControlStruct) decRFGain() error { - if s.state.rfGainPercent > 0 { - return s.setRFGain(s.state.rfGainPercent - 1) + if s.state.rfGainLevel > 0 { + return s.setRFGain(s.state.rfGainLevel - 1) } return nil } -func (s *civControlStruct) setSQL(percent int) error { - v := uint16(0x0255 * (float64(percent) / 100)) - s.initCmd(&s.state.setSQL, "setSQL", []byte{254, 254, civAddress, 224, 0x14, 0x03, byte(v >> 8), byte(v & 0xff), 253}) +func (s *civControlStruct) setSQL(level int) error { + s.initCmd(&s.state.setSQL, "setSQL", prepPacket("setSQL", encodeForSend(level))) return s.sendCmd(&s.state.setSQL) } func (s *civControlStruct) incSQL() error { - if s.state.sqlPercent < 100 { - return s.setSQL(s.state.sqlPercent + 1) + if s.state.sqlLevel < 255 { + return s.setSQL(s.state.sqlLevel + 1) } return nil } func (s *civControlStruct) decSQL() error { - if s.state.sqlPercent > 0 { - return s.setSQL(s.state.sqlPercent - 1) + if s.state.sqlLevel > 0 { + return s.setSQL(s.state.sqlLevel - 1) } return nil } -func (s *civControlStruct) setNR(percent int) error { +func (s *civControlStruct) setNR(level int) error { if !s.state.nrEnabled { if err := s.toggleNR(); err != nil { return err } } - v := uint16(0x0255 * (float64(percent) / 100)) - s.initCmd(&s.state.setNR, "setNR", []byte{254, 254, civAddress, 224, 0x14, 0x06, byte(v >> 8), byte(v & 0xff), 253}) + s.initCmd(&s.state.setNR, "setNR", prepPacket("setSNR", encodeForSend(level))) return s.sendCmd(&s.state.setNR) } func (s *civControlStruct) incNR() error { - if s.state.nrPercent < 100 { - return s.setNR(s.state.nrPercent + 1) + if s.state.nrLevel < 255 { + return s.setNR(s.state.nrLevel + 1) } return nil } func (s *civControlStruct) decNR() error { - if s.state.nrPercent > 0 { - return s.setNR(s.state.nrPercent - 1) + if s.state.nrLevel > 0 { + return s.setNR(s.state.nrLevel - 1) } return nil } -func (s *civControlStruct) getDigit(v uint, n int) byte { - f := float64(v) - for n > 0 { - f /= 10 - n-- - } - return byte(uint(f) % 10) -} - func (s *civControlStruct) incFreq() error { return s.setMainVFOFreq(s.state.freq + s.state.ts) } @@ -927,6 +1210,8 @@ func (s *civControlStruct) decFreq() error { } func (s *civControlStruct) encodeFreqData(f uint) (b [5]byte) { + // min/max valid frequency: 30kHZ, 470MHz + // NOTE: there are no software sanity checks on the value. TODO: add them here v0 := s.getDigit(f, 9) v1 := s.getDigit(f, 8) b[4] = v0<<4 | v1 @@ -946,14 +1231,14 @@ func (s *civControlStruct) encodeFreqData(f uint) (b [5]byte) { } func (s *civControlStruct) setMainVFOFreq(f uint) error { - b := s.encodeFreqData(f) - s.initCmd(&s.state.setMainVFOFreq, "setMainVFOFreq", []byte{254, 254, civAddress, 224, 0x25, 0x00, b[0], b[1], b[2], b[3], b[4], 253}) + asBCD := s.encodeFreqData(f) // encodes to [5]byte to ensure leading zero's aren't lost + s.initCmd(&s.state.setMainVFOFreq, "setMainVFOFreq", prepPacket("setMainVFOFreq", asBCD[:])) return s.sendCmd(&s.state.setMainVFOFreq) } func (s *civControlStruct) setSubVFOFreq(f uint) error { - b := s.encodeFreqData(f) - s.initCmd(&s.state.setSubVFOFreq, "setSubVFOFreq", []byte{254, 254, civAddress, 224, 0x25, 0x01, b[0], b[1], b[2], b[3], b[4], 253}) + asBCD := s.encodeFreqData(f) // encodes to [5]byte to ensure leading zero's aren't lost + s.initCmd(&s.state.setSubVFOFreq, "setSubVFOFreq", prepPacket("setSubVFOFreq", asBCD[:])) return s.sendCmd(&s.state.setSubVFOFreq) } @@ -994,7 +1279,7 @@ func (s *civControlStruct) decFilter() error { } func (s *civControlStruct) setOperatingModeAndFilter(modeCode, filterCode byte) error { - s.initCmd(&s.state.setMode, "setMode", []byte{254, 254, civAddress, 224, 0x06, modeCode, filterCode, 253}) + s.initCmd(&s.state.setMode, "setMode", prepPacket("setMode", []byte{modeCode, filterCode})) if err := s.sendCmd(&s.state.setMode); err != nil { return err } @@ -1002,28 +1287,30 @@ func (s *civControlStruct) setOperatingModeAndFilter(modeCode, filterCode byte) } func (s *civControlStruct) setSubVFOMode(modeCode, dataMode, filterCode byte) error { - s.initCmd(&s.state.setSubVFOMode, "setSubVFOMode", []byte{254, 254, civAddress, 224, 0x26, 0x01, modeCode, dataMode, filterCode, 253}) + s.initCmd(&s.state.setSubVFOMode, "setSubVFOMode", prepPacket("setSubVFOMode", []byte{modeCode, dataMode, filterCode})) return s.sendCmd(&s.state.setSubVFOMode) } +// TODO: add controls to prevent pushing PTT if outside licensed allocations func (s *civControlStruct) setPTT(enable bool) error { var b byte if enable { - b = 1 + b = ON s.state.pttTimeoutTimer = time.AfterFunc(pttTimeout, func() { _ = s.setPTT(false) }) } - s.initCmd(&s.state.setPTT, "setPTT", []byte{254, 254, civAddress, 224, 0x1c, 0, b, 253}) + s.initCmd(&s.state.setPTT, "setPTT", prepPacket("setPTT", []byte{b})) return s.sendCmd(&s.state.setPTT) } +// enable/disable antenna tuner func (s *civControlStruct) setTune(enable bool) error { if s.state.ptt { return nil } - var b byte + var b byte // per CI-V guide: 0=off, 1=on, 2=tune if enable { b = 2 s.state.tuneTimeoutTimer = time.AfterFunc(tuneTimeout, func() { @@ -1031,27 +1318,29 @@ func (s *civControlStruct) setTune(enable bool) error { _ = s.setTune(false) }) } else { - b = 1 + // BUG? this was value in codebase, but shouldn't it be to OFF (and we only see ON when asking about it's state?) + // actual behavior appears to be the same even it is set to OFF here + b = ON } - s.initCmd(&s.state.setTune, "setTune", []byte{254, 254, civAddress, 224, 0x1c, 1, b, 253}) + s.initCmd(&s.state.setTune, "setTune", prepPacket("setTune", []byte{b})) return s.sendCmd(&s.state.setTune) } -func (s *civControlStruct) toggleTune() error { +func (s *civControlStruct) toggleAntennaTuner() error { return s.setTune(!s.state.tune) } func (s *civControlStruct) setDataMode(enable bool) error { - var b byte - var f byte + var dataMode byte + var filter byte if enable { - b = 1 - f = 1 + dataMode = ON + filter = 0x01 // TODO: update to pick by name AND switch to prefered filter (typically FIL2) } else { - b = 0 - f = 0 + dataMode = OFF + filter = OFF } - s.initCmd(&s.state.setDataMode, "setDataMode", []byte{254, 254, civAddress, 224, 0x1a, 0x06, b, f, 253}) + s.initCmd(&s.state.setDataMode, "setDataMode", prepPacket("setDataMode", []byte{dataMode, filter})) return s.sendCmd(&s.state.setDataMode) } @@ -1083,60 +1372,66 @@ func (s *civControlStruct) decBand() error { return s.setMainVFOFreq(f) } +// NOTE: better name might be rotatePreamp func (s *civControlStruct) togglePreamp() error { + // NOTE: in HF there is PAMP1 & PAMP2, in VHF just "on" (same as PAMP1) b := byte(s.state.preamp + 1) if b > 2 { - b = 0 + b = OFF } - s.initCmd(&s.state.setPreamp, "setPreamp", []byte{254, 254, civAddress, 224, 0x16, 0x02, b, 253}) + s.initCmd(&s.state.setPreamp, "setPreamp", prepPacket("setPreamp", []byte{b})) return s.sendCmd(&s.state.setPreamp) } +// NOTE: again, rotateAGC may be a better name func (s *civControlStruct) toggleAGC() error { + // NOTE: values are fast/mid/slow => 1/2/3 b := byte(s.state.agc + 1) if b > 3 { b = 1 } - s.initCmd(&s.state.setAGC, "setAGC", []byte{254, 254, civAddress, 224, 0x16, 0x12, b, 253}) + s.initCmd(&s.state.setAGC, "setAGC", prepPacket("setAGC", []byte{b})) return s.sendCmd(&s.state.setAGC) } func (s *civControlStruct) toggleNR() error { var b byte if !s.state.nrEnabled { - b = 1 + b = ON } - s.initCmd(&s.state.setNREnabled, "setNREnabled", []byte{254, 254, civAddress, 224, 0x16, 0x40, b, 253}) + s.initCmd(&s.state.setNREnabled, "setNREnabled", prepPacket("setNREnabled", []byte{b})) return s.sendCmd(&s.state.setNREnabled) } -func (s *civControlStruct) setTS(b byte) error { - s.initCmd(&s.state.setTS, "setTS", []byte{254, 254, civAddress, 224, 0x10, b, 253}) - return s.sendCmd(&s.state.setTS) +func (s *civControlStruct) setTuningStep(b byte) error { + // NOTE: only values 00 - 13 are valid (enforced in the (inc|dec)TuningStep functions) + // we may want to enforce here if adding a direct selection method to the codebase + s.initCmd(&s.state.setTuningStep, "setTuningStep", prepPacket("setTuningStep", []byte{b})) + return s.sendCmd(&s.state.setTuningStep) } -func (s *civControlStruct) incTS() error { +func (s *civControlStruct) incTuningStep() error { var b byte if s.state.tsValue == 13 { b = 0 } else { b = s.state.tsValue + 1 } - return s.setTS(b) + return s.setTuningStep(b) } -func (s *civControlStruct) decTS() error { +func (s *civControlStruct) decTuningStep() error { var b byte if s.state.tsValue == 0 { b = 13 } else { b = s.state.tsValue - 1 } - return s.setTS(b) + return s.setTuningStep(b) } func (s *civControlStruct) setVFO(nr byte) error { - s.initCmd(&s.state.setVFO, "setVFO", []byte{254, 254, civAddress, 224, 0x07, nr, 253}) + s.initCmd(&s.state.setVFO, "setVFO", prepPacket("setVFO", []byte{nr})) if err := s.sendCmd(&s.state.setVFO); err != nil { return err } @@ -1144,6 +1439,7 @@ func (s *civControlStruct) setVFO(nr byte) error { } func (s *civControlStruct) toggleVFO() error { + // NOTE: I believe we could also use the exchangeVFO command, and make sure we update s.state to reflect which is active: var b byte if !s.state.vfoBActive { b = 1 @@ -1152,10 +1448,13 @@ func (s *civControlStruct) toggleVFO() error { } func (s *civControlStruct) setSplit(mode splitMode) error { + // NOTE: desired is to also call equalizeVFOs when enabling split mode on HF bands.. at least for the first time of a session var b byte switch mode { default: b = 0x10 + case splitModeOff: + b = 0x10 case splitModeOn: b = 0x01 case splitModeDUPMinus: @@ -1163,129 +1462,119 @@ func (s *civControlStruct) setSplit(mode splitMode) error { case splitModeDUPPlus: b = 0x12 } - s.initCmd(&s.state.setSplit, "setSplit", []byte{254, 254, civAddress, 224, 0x0f, b, 253}) + s.initCmd(&s.state.setSplit, "setSplit", prepPacket("setSplit", []byte{b})) return s.sendCmd(&s.state.setSplit) } func (s *civControlStruct) toggleSplit() error { var mode splitMode switch s.state.splitMode { - case splitModeOff: + case splitModeOff: // 0 mode = splitModeOn - case splitModeOn: - mode = splitModeDUPMinus + case splitModeOn: // 1 + mode = splitModeDUPMinus // 2 case splitModeDUPMinus: - mode = splitModeDUPPlus - default: + mode = splitModeDUPPlus // 3 + default: // anything else mode = splitModeOff } return s.setSplit(mode) } -// func (s *civControlStruct) getFreq() error { -// s.initCmd(&s.state.getFreq, "getFreq", []byte{254, 254, civAddress, 224, 3, 253}) -// return s.sendCmd(&s.state.getFreq) -// } - -// func (s *civControlStruct) getMode() error { -// s.initCmd(&s.state.getMode, "getMode", []byte{254, 254, civAddress, 224, 4, 253}) -// return s.sendCmd(&s.state.getMode) -// } - -// func (s *civControlStruct) getDataMode() error { -// s.initCmd(&s.state.getDataMode, "getDataMode", []byte{254, 254, civAddress, 224, 0x1a, 0x06, 253}) -// return s.sendCmd(&s.state.getDataMode) -// } +func (s *civControlStruct) getFreq() error { + s.initCmd(&s.state.getFreq, "getFreq", prepPacket("getFreq", noData)) + return s.sendCmd(&s.state.getFreq) +} func (s *civControlStruct) getPwr() error { - s.initCmd(&s.state.getPwr, "getPwr", []byte{254, 254, civAddress, 224, 0x14, 0x0a, 253}) + s.initCmd(&s.state.getPwr, "getPwr", prepPacket("getPwr", noData)) return s.sendCmd(&s.state.getPwr) } func (s *civControlStruct) getTransmitStatus() error { - s.initCmd(&s.state.getTransmitStatus, "getTransmitStatus", []byte{254, 254, civAddress, 224, 0x1c, 0, 253}) + s.initCmd(&s.state.getTransmitStatus, "getTransmitStatus", prepPacket("getTransmitStatus", noData)) if err := s.sendCmd(&s.state.getTransmitStatus); err != nil { return err } - s.initCmd(&s.state.getTuneStatus, "getTuneStatus", []byte{254, 254, civAddress, 224, 0x1c, 1, 253}) + s.initCmd(&s.state.getTuneStatus, "getTuneStatus", prepPacket("getTuneStatus", noData)) return s.sendCmd(&s.state.getTuneStatus) } func (s *civControlStruct) getPreamp() error { - s.initCmd(&s.state.getPreamp, "getPreamp", []byte{254, 254, civAddress, 224, 0x16, 0x02, 253}) + s.initCmd(&s.state.getPreamp, "getPreamp", prepPacket("getPreamp", noData)) return s.sendCmd(&s.state.getPreamp) } func (s *civControlStruct) getAGC() error { - s.initCmd(&s.state.getAGC, "getAGC", []byte{254, 254, civAddress, 224, 0x16, 0x12, 253}) + s.initCmd(&s.state.getAGC, "getAGC", prepPacket("getAGC", noData)) return s.sendCmd(&s.state.getAGC) } func (s *civControlStruct) getVd() error { - s.initCmd(&s.state.getVd, "getVd", []byte{254, 254, civAddress, 224, 0x15, 0x15, 253}) + s.initCmd(&s.state.getVd, "getVd", prepPacket("getVd", noData)) return s.sendCmd(&s.state.getVd) } func (s *civControlStruct) getS() error { - s.initCmd(&s.state.getS, "getS", []byte{254, 254, civAddress, 224, 0x15, 0x02, 253}) + s.initCmd(&s.state.getS, "getS", prepPacket("getS", noData)) return s.sendCmd(&s.state.getS) } func (s *civControlStruct) getOVF() error { - s.initCmd(&s.state.getOVF, "getOVF", []byte{254, 254, civAddress, 224, 0x1a, 0x09, 253}) + s.initCmd(&s.state.getOVF, "getOVF", prepPacket("getOVF", noData)) return s.sendCmd(&s.state.getOVF) } func (s *civControlStruct) getSWR() error { - s.initCmd(&s.state.getSWR, "getSWR", []byte{254, 254, civAddress, 224, 0x15, 0x12, 253}) + s.initCmd(&s.state.getSWR, "getSWR", prepPacket("getSWR", noData)) return s.sendCmd(&s.state.getSWR) } -func (s *civControlStruct) getTS() error { - s.initCmd(&s.state.getTS, "getTS", []byte{254, 254, civAddress, 224, 0x10, 253}) - return s.sendCmd(&s.state.getTS) +func (s *civControlStruct) getTuningStep() error { + s.initCmd(&s.state.getTuningStep, "getTuningStep", prepPacket("getTuningStep", noData)) + return s.sendCmd(&s.state.getTuningStep) } func (s *civControlStruct) getRFGain() error { - s.initCmd(&s.state.getRFGain, "getRFGain", []byte{254, 254, civAddress, 224, 0x14, 0x02, 253}) + s.initCmd(&s.state.getRFGain, "getRFGain", prepPacket("getRFGain", noData)) return s.sendCmd(&s.state.getRFGain) } func (s *civControlStruct) getSQL() error { - s.initCmd(&s.state.getSQL, "getSQL", []byte{254, 254, civAddress, 224, 0x14, 0x03, 253}) + s.initCmd(&s.state.getSQL, "getSQL", prepPacket("getSQL", noData)) return s.sendCmd(&s.state.getSQL) } func (s *civControlStruct) getNR() error { - s.initCmd(&s.state.getNR, "getNR", []byte{254, 254, civAddress, 224, 0x14, 0x06, 253}) + s.initCmd(&s.state.getNR, "getNR", prepPacket("getNR", noData)) return s.sendCmd(&s.state.getNR) } func (s *civControlStruct) getNREnabled() error { - s.initCmd(&s.state.getNREnabled, "getNREnabled", []byte{254, 254, civAddress, 224, 0x16, 0x40, 253}) + s.initCmd(&s.state.getNREnabled, "getNREnabled", prepPacket("getNREnabled", noData)) return s.sendCmd(&s.state.getNREnabled) } func (s *civControlStruct) getSplit() error { - s.initCmd(&s.state.getSplit, "getSplit", []byte{254, 254, civAddress, 224, 0x0f, 253}) + s.initCmd(&s.state.getSplit, "getSplit", prepPacket("getSplit", noData)) return s.sendCmd(&s.state.getSplit) } func (s *civControlStruct) getBothVFOFreq() error { - s.initCmd(&s.state.getMainVFOFreq, "getMainVFOFreq", []byte{254, 254, civAddress, 224, 0x25, 0, 253}) + s.initCmd(&s.state.getMainVFOFreq, "getMainVFOFreq", prepPacket("getMainVFOFreq", noData)) if err := s.sendCmd(&s.state.getMainVFOFreq); err != nil { return err } - s.initCmd(&s.state.getSubVFOFreq, "getSubVFOFreq", []byte{254, 254, civAddress, 224, 0x25, 1, 253}) + s.initCmd(&s.state.getSubVFOFreq, "getSubVFOFreq", prepPacket("getSubVFOFreq", noData)) return s.sendCmd(&s.state.getSubVFOFreq) } func (s *civControlStruct) getBothVFOMode() error { - s.initCmd(&s.state.getMainVFOMode, "getMainVFOMode", []byte{254, 254, civAddress, 224, 0x26, 0, 253}) + s.initCmd(&s.state.getMainVFOMode, "getMainVFOMode", prepPacket("getMainVFOMode", noData)) if err := s.sendCmd(&s.state.getMainVFOMode); err != nil { return err } - s.initCmd(&s.state.getSubVFOMode, "getSubVFOMode", []byte{254, 254, civAddress, 224, 0x26, 1, 253}) + s.initCmd(&s.state.getSubVFOMode, "getSubVFOMode", prepPacket("getSubVFOMode", noData)) return s.sendCmd(&s.state.getSubVFOMode) } @@ -1344,6 +1633,9 @@ func (s *civControlStruct) loop() { func (s *civControlStruct) init(st *serialStream) error { s.st = st + if err := s.getFreq(); err != nil { + return err + } if err := s.getBothVFOFreq(); err != nil { return err } @@ -1374,7 +1666,7 @@ func (s *civControlStruct) init(st *serialStream) error { if err := s.getSWR(); err != nil { return err } - if err := s.getTS(); err != nil { + if err := s.getTuningStep(); err != nil { return err } if err := s.getRFGain(); err != nil { @@ -1411,3 +1703,41 @@ func (s *civControlStruct) deinit() { s.deinitNeeded = nil s.st = nil } + +func debugPacket(command string, pkt []byte) { + + to := pkt[2] + frm := pkt[3] + cmd := pkt[4] + pld := pkt[5 : len(pkt)-1] + + msg := fmt.Sprintf("'%v' [% x] ", command, pkt) + msg += "to " + + if to == civAddress { + msg += "[RADIO] " + } else if to == controllerAddress { + msg += "[CONTROLLER] " + } else { + msg += fmt.Sprintf("[UNKNOWN DEVICE: %02x] ", to) + } + msg += "<= from " + if frm == civAddress { + msg += "[RADIO] " + } else if frm == controllerAddress { + msg += "[CONTROLLER] " + } else { + msg += fmt.Sprintf("[UNKNOWN DEVICE - BUG!: %02x] ", frm) + } + + msg += fmt.Sprintf("cmd: [%02x] ", cmd) + + x := " " + for _, b := range pld { + x += fmt.Sprintf("%02x ", b) + } + + msg += fmt.Sprintf("payload [%v]", x) + log.Print(msg) + return +} diff --git a/go.mod b/go.mod index c23636d..8c0b00d 100644 --- a/go.mod +++ b/go.mod @@ -11,4 +11,5 @@ require ( github.com/pborman/getopt v1.1.0 go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.16.0 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 ) diff --git a/go.sum b/go.sum index 897b0bc..9804389 100644 --- a/go.sum +++ b/go.sum @@ -34,11 +34,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -46,6 +44,7 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/hotkeys.go b/hotkeys.go index b9eccaf..ecf922a 100644 --- a/hotkeys.go +++ b/hotkeys.go @@ -4,12 +4,15 @@ import "fmt" func handleHotkey(k byte) { switch k { + case 'c': + // provide a way to clear the screen since sometimes the stack of errors gets to be rather distracting + fmt.Printf("%v", termDetail.eraseScreen) case 'l': audio.togglePlaybackToDefaultSoundcard() case ' ': audio.toggleRecFromDefaultSoundcard() case 't': - if err := civControl.toggleTune(); err != nil { + if err := civControl.toggleAntennaTuner(); err != nil { log.Error("can't toggle tune: ", err) } case '+': @@ -137,11 +140,11 @@ func handleHotkey(k byte) { log.Error("can't decrease freq: ", err) } case '}': - if err := civControl.incTS(); err != nil { + if err := civControl.incTuningStep(); err != nil { log.Error("can't increase ts: ", err) } case '{': - if err := civControl.decTS(); err != nil { + if err := civControl.decTuningStep(); err != nil { log.Error("can't decrease ts: ", err) } case 'm': @@ -191,12 +194,14 @@ func handleHotkey(k byte) { case '\n': if statusLog.isRealtime() { statusLog.mutex.Lock() - statusLog.clearInternal() + statusLog.clearStatusLine() fmt.Println() statusLog.mutex.Unlock() statusLog.print() } case 'q': quitChan <- true + default: + log.Error(fmt.Sprintf("INFO: no action mapped to key [%v]\n", string(k))) } } diff --git a/log.go b/log.go index b2b6033..bccef01 100644 --- a/log.go +++ b/log.go @@ -29,7 +29,7 @@ func (l *logger) GetCallerFileName(withLine bool) string { func (l *logger) Print(a ...interface{}) { if statusLog.isRealtime() { statusLog.mutex.Lock() - statusLog.clearInternal() + statusLog.clearStatusLine() defer func() { statusLog.mutex.Unlock() statusLog.print() @@ -45,7 +45,7 @@ func (l *logger) PrintStatusLog(a ...interface{}) { func (l *logger) Debug(a ...interface{}) { if statusLog.isRealtime() { statusLog.mutex.Lock() - statusLog.clearInternal() + statusLog.clearStatusLine() defer func() { statusLog.mutex.Unlock() statusLog.print() @@ -57,7 +57,7 @@ func (l *logger) Debug(a ...interface{}) { func (l *logger) Error(a ...interface{}) { if statusLog.isRealtime() { statusLog.mutex.Lock() - statusLog.clearInternal() + statusLog.clearStatusLine() defer func() { statusLog.mutex.Unlock() statusLog.print() @@ -69,7 +69,7 @@ func (l *logger) Error(a ...interface{}) { func (l *logger) ErrorC(a ...interface{}) { if statusLog.isRealtime() { statusLog.mutex.Lock() - statusLog.clearInternal() + statusLog.clearStatusLine() defer func() { statusLog.mutex.Unlock() statusLog.print() diff --git a/main.go b/main.go index 131b96a..22da284 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,10 @@ func getAboutStr() string { } else { v = "(devel)" } - return "kappanhang " + v + " by Norbert Varga HA2NON and Akos Marton ES1AKOS https://github.com/nonoo/kappanhang" + about_msg := "orignal kappanhang " + v + " by Norbert Varga HA2NON and Akos Marton ES1AKOS https://github.com/nonoo/kappanhang" + about_msg += "\n\tthis version produced by AD8IM and can be fuond at http://github.com/AD8IM/kappanhang" + + return about_msg } func wait(d time.Duration, osSignal chan os.Signal) (shouldExit bool) { @@ -54,7 +57,6 @@ func runControlStream(osSignal chan os.Signal) (requireWait, shouldExit bool, ex } ctrl := &controlStream{} - if err := ctrl.init(); err != nil { log.Error(err) ctrl.deinit() @@ -65,8 +67,8 @@ func runControlStream(osSignal chan os.Signal) (requireWait, shouldExit bool, ex } select { - // Need to wait before reinit because the IC-705 will disconnect our audio stream eventually if we relogin - // in a too short interval without a deauth... + // Need to wait before reinit because the IC-705 will disconnect our audio stream eventually + // if we relogin in a too short interval without a deauth... case requireWait = <-gotErrChan: ctrl.deinit() return diff --git a/pkt7.go b/pkt7.go index 4ae0eb9..44e6b13 100644 --- a/pkt7.go +++ b/pkt7.go @@ -30,16 +30,24 @@ type pkt7Type struct { var controlStreamLatency time.Duration func (p *pkt7Type) isPkt7(r []byte) bool { - return len(r) == 21 && bytes.Equal(r[1:6], []byte{0x00, 0x00, 0x00, 0x07, 0x00}) // Note that the first byte can be 0x15 or 0x00, so we ignore that. + // Note that the first byte can be 0x15 or 0x00, so we ignore that. + return len(r) == 21 && bytes.Equal(r[1:6], []byte{0x00, 0x00, 0x00, 0x07, 0x00}) } func (p *pkt7Type) handle(s *streamCommon, r []byte) error { gotSeq := binary.LittleEndian.Uint16(r[6:8]) if r[16] == 0x00 { // This is a pkt7 request from the radio. // Replying to the radio. - // Example request from radio: 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x1c, 0x0e, 0xe4, 0x35, 0xdd, 0x72, 0xbe, 0xd9, 0xf2, 0x63, 0x00, 0x57, 0x2b, 0x12, 0x00 - // Example answer from PC: 0x15, 0x00, 0x00, 0x00, 0x07, 0x00, 0x1c, 0x0e, 0xbe, 0xd9, 0xf2, 0x63, 0xe4, 0x35, 0xdd, 0x72, 0x01, 0x57, 0x2b, 0x12, 0x00 - if p.sendTicker != nil { // Only replying if the auth is already done. + // Example request from radio: + // 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x1c, 0x0e, 0xe4, 0x35, 0xdd, + // 0x72, 0xbe, 0xd9, 0xf2, 0x63, 0x00, 0x57, 0x2b, 0x12, 0x00 + // + // Example answer from PC: + // 0x15, 0x00, 0x00, 0x00, 0x07, 0x00, 0x1c, 0x0e, 0xbe, 0xd9, 0xf2, + // 0x63, 0xe4, 0x35, 0xdd, 0x72, 0x01, 0x57, 0x2b, 0x12, 0x00 + + // Only replying if the auth is already done. + if p.sendTicker != nil { if err := p.sendReply(s, r[17:21], gotSeq); err != nil { return err } diff --git a/serialstream.go b/serialstream.go index 81ef399..2c68d9c 100644 --- a/serialstream.go +++ b/serialstream.go @@ -30,6 +30,7 @@ type serialStream struct { deinitFinishedChan chan bool } +// load the data into a full packet and send it func (s *serialStream) send(d []byte) error { l := byte(len(d)) p := append([]byte{0x15 + l, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -92,6 +93,9 @@ func (s *serialStream) handleRxSeqBufEntry(e seqBufEntry) { e.data = e.data[21:] + // decode the received CI-V data packet + // if it fails return directly to the main polling loop, without sending it on to the serial &/or network channels + if !civControl.decode(e.data) { return } diff --git a/statuslog.go b/statuslog.go index cf9ce7e..63366e8 100644 --- a/statuslog.go +++ b/statuslog.go @@ -3,11 +3,13 @@ package main import ( "fmt" "os" + "strings" "sync" "time" "github.com/fatih/color" "github.com/mattn/go-isatty" + "golang.org/x/crypto/ssh/terminal" ) type statusLogData struct { @@ -76,8 +78,36 @@ type statusLogStruct struct { data *statusLogData } -var statusLog statusLogStruct +type termAspects struct { + cols int + rows int + cursorLeft string + cursorRight string + cursorUp string + cursorDown string + eraseLine string + eraseScreen string +} +var statusLog statusLogStruct +var termDetail = termAspects{ + cols: 0, + rows: 0, + cursorUp: fmt.Sprintf("%c[1A", 0x1b), + cursorDown: fmt.Sprintf("%c[1B", 0x1b), + cursorRight: fmt.Sprintf("%c[1C", 0x1b), + cursorLeft: fmt.Sprintf("%c[1D", 0x1b), + eraseLine: fmt.Sprintf("%c[2K", 0x1b), + eraseScreen: fmt.Sprintf("%c[2J", 0x1b), +} + +var upArrow = "\u21d1" +var downArrow = "\u21d3" + +// var roundTripArrow = "\u2b6f\u200a" // widdershin circle w/arrow +var roundTripArrow = "\u2b8c\u200a" // out and back arrow + +// generate display string for round trip time latency func (s *statusLogStruct) reportRTTLatency(l time.Duration) { s.mutex.Lock() defer s.mutex.Unlock() @@ -88,6 +118,7 @@ func (s *statusLogStruct) reportRTTLatency(l time.Duration) { s.data.rttStr = fmt.Sprint(l.Milliseconds()) } +// update string that displays current audio status func (s *statusLogStruct) updateAudioStateStr() { if s.data.audioRecOn { s.data.audioStateStr = s.preGenerated.audioStateStr.rec @@ -98,6 +129,7 @@ func (s *statusLogStruct) updateAudioStateStr() { } } +// set audio monitoring status to off/on and make call to update string to display func (s *statusLogStruct) reportAudioMon(enabled bool) { s.mutex.Lock() defer s.mutex.Unlock() @@ -109,6 +141,7 @@ func (s *statusLogStruct) reportAudioMon(enabled bool) { s.updateAudioStateStr() } +// set audio recording status to off/on and make call to update string to display func (s *statusLogStruct) reportAudioRec(enabled bool) { s.mutex.Lock() defer s.mutex.Unlock() @@ -120,6 +153,7 @@ func (s *statusLogStruct) reportAudioRec(enabled bool) { s.updateAudioStateStr() } +// update main VFO frequency value held in status log data structure func (s *statusLogStruct) reportFrequency(f uint) { s.mutex.Lock() defer s.mutex.Unlock() @@ -130,6 +164,7 @@ func (s *statusLogStruct) reportFrequency(f uint) { s.data.frequency = f } +// update sub-VFO frequency value held status log data structure func (s *statusLogStruct) reportSubFrequency(f uint) { s.mutex.Lock() defer s.mutex.Unlock() @@ -140,6 +175,7 @@ func (s *statusLogStruct) reportSubFrequency(f uint) { s.data.subFrequency = f } +// update main VFO mode & predefined filter data held instatus log data structure func (s *statusLogStruct) reportMode(mode string, dataMode bool, filter string) { s.mutex.Lock() defer s.mutex.Unlock() @@ -156,6 +192,7 @@ func (s *statusLogStruct) reportMode(mode string, dataMode bool, filter string) s.data.filter = filter } +// update sub-VFO mode & predefined filter data held instatus log data structure func (s *statusLogStruct) reportSubMode(mode string, dataMode bool, filter string) { s.mutex.Lock() defer s.mutex.Unlock() @@ -172,6 +209,7 @@ func (s *statusLogStruct) reportSubMode(mode string, dataMode bool, filter strin s.data.subFilter = filter } +// generate display string for preamp status func (s *statusLogStruct) reportPreamp(preamp int) { s.mutex.Lock() defer s.mutex.Unlock() @@ -182,6 +220,7 @@ func (s *statusLogStruct) reportPreamp(preamp int) { s.data.preamp = fmt.Sprint("PAMP", preamp) } +// generate display string for AGC status func (s *statusLogStruct) reportAGC(agc string) { s.mutex.Lock() defer s.mutex.Unlock() @@ -192,6 +231,7 @@ func (s *statusLogStruct) reportAGC(agc string) { s.data.agc = "AGC" + agc } +// set noise reduction status to off/on in status log data structure func (s *statusLogStruct) reportNREnabled(enabled bool) { s.mutex.Lock() defer s.mutex.Unlock() @@ -202,6 +242,7 @@ func (s *statusLogStruct) reportNREnabled(enabled bool) { s.data.nrEnabled = enabled } +// generate display string for (battery) voltage func (s *statusLogStruct) reportVd(voltage float64) { s.mutex.Lock() defer s.mutex.Unlock() @@ -212,6 +253,7 @@ func (s *statusLogStruct) reportVd(voltage float64) { s.data.vd = fmt.Sprintf("%.1fV", voltage) } +// set S-level value in status log data structure func (s *statusLogStruct) reportS(sValue string) { s.mutex.Lock() defer s.mutex.Unlock() @@ -222,6 +264,7 @@ func (s *statusLogStruct) reportS(sValue string) { s.data.s = sValue } +// set over-volt fault true/fault status in status log data structure func (s *statusLogStruct) reportOVF(ovf bool) { s.mutex.Lock() defer s.mutex.Unlock() @@ -232,6 +275,7 @@ func (s *statusLogStruct) reportOVF(ovf bool) { s.data.ovf = ovf } +// generate display string for SWR status func (s *statusLogStruct) reportSWR(swr float64) { s.mutex.Lock() defer s.mutex.Unlock() @@ -242,14 +286,15 @@ func (s *statusLogStruct) reportSWR(swr float64) { s.data.swr = fmt.Sprintf("%.1f", swr) } -func (s *statusLogStruct) reportTS(ts uint) { +// generate display string for tuning step value +func (s *statusLogStruct) reportTuningStep(ts uint) { s.mutex.Lock() defer s.mutex.Unlock() if s.data == nil { return } - s.data.ts = "TS" + s.data.ts = "TS: " if ts >= 1000 { if ts%1000 == 0 { s.data.ts += fmt.Sprintf("%.0fk", float64(ts)/1000) @@ -263,6 +308,7 @@ func (s *statusLogStruct) reportTS(ts uint) { } } +// set push-to-talk (aka Tx) status in status log data structure func (s *statusLogStruct) reportPTT(ptt, tune bool) { s.mutex.Lock() defer s.mutex.Unlock() @@ -274,46 +320,57 @@ func (s *statusLogStruct) reportPTT(ptt, tune bool) { s.data.ptt = ptt } -func (s *statusLogStruct) reportTxPower(percent int) { +// convert int value 0 - 255 to a floating point percentage +func asPercentage(level int) (pct float64) { + pct = 100.00 * (float64(level) / 0xff) + return +} + +// generate the display string for transmit power value +func (s *statusLogStruct) reportTxPower(level int) { s.mutex.Lock() defer s.mutex.Unlock() if s.data == nil { return } - s.data.txPower = fmt.Sprint(percent, "%") + s.data.txPower = fmt.Sprintf("%3.1f%%", asPercentage(level)) } -func (s *statusLogStruct) reportRFGain(percent int) { +// generate the display string for RF Gain value +func (s *statusLogStruct) reportRFGain(level int) { s.mutex.Lock() defer s.mutex.Unlock() if s.data == nil { return } - s.data.rfGain = fmt.Sprint(percent, "%") + s.data.rfGain = fmt.Sprintf("%3.1f%%", asPercentage(level)) } -func (s *statusLogStruct) reportSQL(percent int) { +// generate the display string for squelch value +func (s *statusLogStruct) reportSQL(level int) { s.mutex.Lock() defer s.mutex.Unlock() if s.data == nil { return } - s.data.sql = fmt.Sprint(percent, "%") + s.data.sql = fmt.Sprintf("%3.1f%%", asPercentage(level)) } -func (s *statusLogStruct) reportNR(percent int) { +// generate the display string for noise reduction level +func (s *statusLogStruct) reportNR(level int) { s.mutex.Lock() defer s.mutex.Unlock() if s.data == nil { return } - s.data.nr = fmt.Sprint(percent, "%") + s.data.nr = fmt.Sprintf("%3.1f%%", asPercentage(level)) } +// generate the display string for split frequency operating mode func (s *statusLogStruct) reportSplit(mode splitMode, split string) { s.mutex.Lock() defer s.mutex.Unlock() @@ -329,84 +386,105 @@ func (s *statusLogStruct) reportSplit(mode splitMode, split string) { } } -func (s *statusLogStruct) clearInternal() { - fmt.Printf("%c[2K", 27) +// clears the entire line the cursor is located on +func (s *statusLogStruct) clearStatusLine() { + fmt.Print(termDetail.eraseLine) } +// displays the current status information +// +// (NOTE: s.isRealtimeInternal merely returns true/false for if in terminal, this should be cleaned up for clarity) +// if running in a terminal, print the current status to the console and reposition the cursor to the first line of output) +// if not, send just the last line (packet rate info) to the debugging log func (s *statusLogStruct) print() { s.mutex.Lock() defer s.mutex.Unlock() if s.isRealtimeInternal() { - s.clearInternal() + s.clearStatusLine() fmt.Println(s.data.line1) - s.clearInternal() + s.clearStatusLine() fmt.Println(s.data.line2) - s.clearInternal() - fmt.Printf(s.data.line3+"%c[1A%c[1A", 27, 27) + s.clearStatusLine() + fmt.Printf(s.data.line3+"%v%v", termDetail.cursorUp, termDetail.cursorUp) } else { log.PrintStatusLog(s.data.line3) } } +// use whitespace padding on left to right-justify the string func (s *statusLogStruct) padLeft(str string, length int) string { if !s.isRealtimeInternal() { return str } - - for len(str) < length { - str = " " + str + if length-len(str) > 0 { + str = strings.Repeat(" ", length-len(str)) + str } return str } +// use whitespace paddind on the right-hand side of the string for consisting formatting func (s *statusLogStruct) padRight(str string, length int) string { if !s.isRealtimeInternal() { return str } - - for len(str) < length { - str += " " + if length-len(str) > 0 { + str += strings.Repeat(" ", length-len(str)) } return str } +// update variables used for status output using current values to regenerate the strings to display func (s *statusLogStruct) update() { s.mutex.Lock() defer s.mutex.Unlock() - var filterStr string + var ( + filterStr string + preampStr string + agcStr string + nrStr string + rfGainStr string + sqlStr string + stateStr string + tsStr string + modeStr string + vdStr string + txPowerStr string + splitStr string + swrStr string + ) + if s.data.filter != "" { filterStr = " " + s.data.filter } - var preampStr string + if s.data.preamp != "" { preampStr = " " + s.data.preamp } - var agcStr string + if s.data.agc != "" { agcStr = " " + s.data.agc } - var nrStr string + if s.data.nr != "" { nrStr = " NR" if s.data.nrEnabled { - nrStr += s.data.nr + nrStr += " " + s.data.nr } else { nrStr += "-" } } - var rfGainStr string + if s.data.rfGain != "" { rfGainStr = " rfg " + s.data.rfGain } - var sqlStr string + if s.data.sql != "" { sqlStr = " sql " + s.data.sql } s.data.line1 = fmt.Sprint(s.data.audioStateStr, filterStr, preampStr, agcStr, nrStr, rfGainStr, sqlStr) - var stateStr string if s.data.tune { stateStr = s.preGenerated.stateStr.tune } else if s.data.ptt { @@ -417,29 +495,29 @@ func (s *statusLogStruct) update() { ovfStr = s.preGenerated.ovf } if len(s.data.s) <= 2 { - stateStr = s.preGenerated.rxColor.Sprint(" " + s.padRight(s.data.s, 4) + " ") + stateStr = s.preGenerated.rxColor.Sprintf(" %v ", s.padRight(s.data.s, 4)) } else { - stateStr = s.preGenerated.rxColor.Sprint(" " + s.padRight(s.data.s, 5) + " ") + stateStr = s.preGenerated.rxColor.Sprintf(" %v ", s.padRight(s.data.s, 5)) } stateStr += ovfStr } - var tsStr string + if s.data.ts != "" { tsStr = " " + s.data.ts } - var modeStr string + if s.data.mode != "" { modeStr = " " + s.data.mode + s.data.dataMode } - var vdStr string + if s.data.vd != "" { vdStr = " " + s.data.vd } - var txPowerStr string + if s.data.txPower != "" { txPowerStr = " txpwr " + s.data.txPower } - var splitStr string + if s.data.split != "" { splitStr = " " + s.data.split if s.data.splitMode == splitModeOn { @@ -447,7 +525,7 @@ func (s *statusLogStruct) update() { s.data.subMode, s.data.subDataMode, s.data.subFilter) } } - var swrStr string + if (s.data.tune || s.data.ptt) && s.data.swr != "" { swrStr = " SWR" + s.data.swr } @@ -464,19 +542,27 @@ func (s *statusLogStruct) update() { retransmitsStr = s.preGenerated.retransmitsColor.Sprint(" ", retransmits, " ") } - s.data.line3 = fmt.Sprint("up ", s.padLeft(fmt.Sprint(time.Since(s.data.startTime).Round(time.Second)), 6), - " rtt ", s.padLeft(s.data.rttStr, 3), "ms up ", - s.padLeft(netstat.formatByteCount(up), 8), "/s down ", - s.padLeft(netstat.formatByteCount(down), 8), "/s retx ", retransmitsStr, "/1m lost ", lostStr, "/1m\r") + s.data.line3 = fmt.Sprint( + " [", s.padLeft(netstat.formatByteCount(up), 8), "/s "+upArrow+"] ", + " [", s.padLeft(netstat.formatByteCount(down), 8), "/s "+downArrow+"] ", + " [", s.padLeft(s.data.rttStr, 3), "ms "+roundTripArrow+"] ", + " re-Tx ", retransmitsStr, "/1m lost ", lostStr, "/1m", + " - uptime: ", s.padLeft(fmt.Sprint(time.Since(s.data.startTime).Round(time.Second)), 6), + "\r") if s.isRealtimeInternal() { - t := time.Now().Format("2006-01-02T15:04:05.000Z0700") + //t := time.Now().Format("2006-01-02T15:04:05.000Z0700") // this is visually busy with no real benefit + t := time.Now().Format("2006-01-02T15:04:05 Z0700") s.data.line1 = fmt.Sprint(t, " ", s.data.line1) s.data.line2 = fmt.Sprint(t, " ", s.data.line2) s.data.line3 = fmt.Sprint(t, " ", s.data.line3) } } +// status logging loop +// +// listen to ticker channel for data which indicates an recalculate and display status should be done +// listen to stop channel for indication to terminate logging func (s *statusLogStruct) loop() { for { select { @@ -490,24 +576,26 @@ func (s *statusLogStruct) loop() { } } +// poorly named, this actually indicates if we're running in an interactive terminal or not func (s *statusLogStruct) isRealtimeInternal() bool { return keyboard.initialized } +// check if the ticker timer is being used, and if running in an interactive terminal func (s *statusLogStruct) isRealtime() bool { s.mutex.Lock() defer s.mutex.Unlock() - return s.ticker != nil && s.isRealtimeInternal() } +// check if the ticker timer is active func (s *statusLogStruct) isActive() bool { s.mutex.Lock() defer s.mutex.Unlock() - return s.ticker != nil } +// set initial values, start ticker timer, and start main loop in a goroutine func (s *statusLogStruct) startPeriodicPrint() { s.mutex.Lock() defer s.mutex.Unlock() @@ -527,6 +615,7 @@ func (s *statusLogStruct) startPeriodicPrint() { go s.loop() } +// stop the ticker timer, send flag on stop channel, wait for stop finished channel data, then clear the status lines on the terminal func (s *statusLogStruct) stopPeriodicPrint() { if !s.isActive() { return @@ -538,15 +627,18 @@ func (s *statusLogStruct) stopPeriodicPrint() { <-s.stopFinishedChan if s.isRealtimeInternal() { - s.clearInternal() - fmt.Println() - s.clearInternal() - fmt.Println() - s.clearInternal() - fmt.Println() + statusRows := 3 // AD8IM NOTE: I intend to adjust this in the future to be dynamic, eg more rows when terminal is narrow + for i := 0; i < statusRows; i++ { + s.clearStatusLine() + fmt.Println() + } } } +// initialization tasks +// +// initialize keyboard/set log timer depending on if running in terminal or not +// update display related variables in status log structure based on terminal characteristics func (s *statusLogStruct) initIfNeeded() { if s.data != nil { // Already initialized? return @@ -558,6 +650,22 @@ func (s *statusLogStruct) initIfNeeded() { keyboard.init() } + cols, rows, err := terminal.GetSize(int(os.Stdout.Fd())) + if err == nil { + termDetail.cols = cols + termDetail.rows = rows + } else { + // if redirecting to a file these are zeros, and that's a problem + // yes, needs actual error check too + termDetail.cols = 120 + termDetail.rows = 20 + } + + // consider doing this with a nice looking start up screen too + // what'd be kinda useful would be a nice map of the hotkeys + vertWhitespace := strings.Repeat(termDetail.cursorDown, termDetail.rows-10) + fmt.Printf("%v%v", termDetail.eraseScreen, vertWhitespace) + c := color.New(color.FgHiWhite) c.Add(color.BgWhite) s.preGenerated.audioStateStr.off = c.Sprint(" MON ") diff --git a/txseqbuf.go b/txseqbuf.go index 458bbb3..e0e006e 100644 --- a/txseqbuf.go +++ b/txseqbuf.go @@ -4,7 +4,7 @@ import "time" // This value is sent to the transceiver and - according to my observations - it will use // this as it's RX buf length. Note that if it is set to larger than 500-600ms then audio TX -// won't work (small radio memory?) +// won't work (small radio memory?) - HA2NON const txSeqBufLength = 300 * time.Millisecond type txSeqBufEntry struct { @@ -27,8 +27,15 @@ func (s *txSeqBufStruct) add(seq seqNum, p []byte) { } func (s *txSeqBufStruct) purgeOldEntries() { + // previous comment: // We keep much more entries than the specified length of the TX seqbuf, so we can serve // any requests coming from the server. + // + // NOTE: need to wade deeper through the entire code base and see if the following is a more useful comment, or we need a better "why" here: + // + // Prune the oldest item in the txSeqBuf if it's older than ten time the txSeqBuf length... so "about 1 second" + // this enables serving any requests from the server, even if older than the max... up to a point + for len(s.entries) > 0 && time.Since(s.entries[0].addedAt) > txSeqBufLength*10 { s.entries = s.entries[1:] }