From 7656d4335b5ceed65388b15f4d7a5b779339af81 Mon Sep 17 00:00:00 2001 From: Birk da Yooper <37058208+birk-da-yooper@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:53:49 -0500 Subject: [PATCH 1/4] Added flag to set alternate controller address Added flag to show CI-V packet contents to support debugging At start-up show virtual soundcard info by both name & filesystem location Updated to always present status lines at bottom of the terminal Removed milliseconds from time shown on status lines Reformatted status line showing packet activity to be easier to follow Adjusted default status update frequency to 150ms to be easier to observe Restored commented out code for working with single VFO Added multiple functions to handle BCD <-> human readable values to improve easy of maintenance Added function to prepare CI-V packets, abstracting this step so that each function no longer needs to create entire packet. This makes the code much easier to follow and maintaint. This will also greatly ease effort needed for extending radio features supported Added/adjusted various comments Updated some variable names to be less terse to facilitate maintenance eg "incTS" to "incTuningStep" Some adjustments to improve readability/ease maintenance in favor of unnecessary optimizations EG - using formatted prints instead of prints using concatenations using strings.Repeat instead of loop to build space buffers abstracted control char strings to variables Hooks for future functionality Removed some commented out code blocks "Trust the repo, Luke!" --- args.go | 46 ++- audio-linux.go | 10 +- audiostream.go | 17 -- civcontrol.go | 728 ++++++++++++++++++++++++++++++++++-------------- go.mod | 1 + go.sum | 3 +- hotkeys.go | 11 +- log.go | 8 +- main.go | 10 +- pkt7.go | 16 +- serialstream.go | 4 + statuslog.go | 138 ++++++--- txseqbuf.go | 9 +- 13 files changed, 698 insertions(+), 303 deletions(-) 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..53f5759 100644 --- a/civcontrol.go +++ b/civcontrol.go @@ -9,8 +9,14 @@ import ( const statusPollInterval = time.Second const commandRetryTimeout = 500 * time.Millisecond -const pttTimeout = 3 * time.Minute -const tuneTimeout = 30 * time.Second +const pttTimeout = 30 * time.Minute + +// const tuneTimeout = 30 * time.Second +const tuneTimeout = 3 * 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 @@ -43,28 +49,50 @@ 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: 1800000, freqTo: 2000000}, // 1.9 - 160m + //{freqFrom: 3400000, freqTo: 4099999}, // 3.5 - 75/80m + {freqFrom: 3500000, freqTo: 4000000}, // 3.5 - 75/80m + //{freqFrom: 6900000, freqTo: 7499999}, // 7 - 40m + {freqFrom: 7000000, freqTo: 7300000}, // 7 - 40m + //{freqFrom: 9900000, freqTo: 10499999}, // 10 + {freqFrom: 10100000, freqTo: 10150000}, // 10 - 30m data modes only in US + //{freqFrom: 13900000, freqTo: 14499999}, // 14 + {freqFrom: 14000000, freqTo: 14350000}, // 14 - 20m + //{freqFrom: 17900000, freqTo: 18499999}, // 18 + {freqFrom: 18068000, freqTo: 18168000}, // 18 -17m + //{freqFrom: 20900000, freqTo: 21499999}, // 21 + {freqFrom: 21000000, freqTo: 21450000}, // 21 - 15m + //{freqFrom: 24400000, freqTo: 25099999}, // 24 + {freqFrom: 24890000, freqTo: 24990000}, // 24 - 12m + //{freqFrom: 28000000, freqTo: 29999999}, // 28 + {freqFrom: 28000000, freqTo: 29700000}, // 28 - 10m + //{freqFrom: 50000000, freqTo: 54000000}, // 50 + {freqFrom: 50000000, freqTo: 54000000}, // 50 - 6m + //{freqFrom: 74800000, freqTo: 107999999}, // WFM - no TX in US + //{freqFrom: 108000000, freqTo: 136999999}, // AIR = no TX in US + //{freqFrom: 144000000, freqTo: 148000000}, // 144 + {freqFrom: 144000000, freqTo: 148000000}, // 144 - 2m + //{freqFrom: 420000000, freqTo: 450000000}, // 430 + {freqFrom: 420000000, freqTo: 450000000}, // 430 - 70cm + //{freqFrom: 0, freqTo: 0}, // GENE // not very useful here + // 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 +112,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 +122,17 @@ type civControlStruct struct { mutex sync.Mutex pendingCmds []*civCmd + getFreq civCmd // NOTE: why was this removed in this (devel) version? is because it doesn't recognize which vfo it's for? 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 +162,7 @@ type civControlStruct struct { setPreamp civCmd setAGC civCmd setNREnabled civCmd - setTS civCmd + setTuningStep civCmd setVFO civCmd setSplit civCmd @@ -167,36 +196,176 @@ 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 inccoming 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? + // hmmmm, but if I drop these then I'm not seeing changes on the status line... does seem the are nmaking it to the radio + /* + 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 +384,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 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) 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 +436,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 +456,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 +478,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 +502,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 +539,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,13 +591,36 @@ 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 wouldbe easier to read if we check size and do value extraction first + // then take actions on appropriate entities in each case + // + + /* + case 0x02: // RF Gain subcmd + if len(d) < 3 { // has at least three bytes of data + return !s.state.getRFGain.pending && !s.state.setRFGain.pending + } + s.state.rfGainPercent = returnedLevel + statusLog.reportRFGain(s.state.rfGainPercent) + if s.state.getRFGain.pending { + s.removePendingCmd(&s.state.getRFGain) + return false + } + if s.state.setRFGain.pending { + s.removePendingCmd(&s.state.setRFGain) + return false + } + */ + 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)) + s.state.rfGainPercent = BCDAsPct(data) statusLog.reportRFGain(s.state.rfGainPercent) if s.state.getRFGain.pending { s.removePendingCmd(&s.state.getRFGain) @@ -449,27 +630,24 @@ 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)) + s.state.sqlPercent = BCDAsPct(data) statusLog.reportSQL(s.state.sqlPercent) 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)) + s.state.nrPercent = BCDAsPct(data) statusLog.reportNR(s.state.nrPercent) if s.state.getNR.pending { s.removePendingCmd(&s.state.getNR) @@ -479,12 +657,11 @@ 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)) + s.state.pwrPercent = BCDAsPct(data) statusLog.reportTxPower(s.state.pwrPercent) if s.state.getPwr.pending { s.removePendingCmd(&s.state.getPwr) @@ -494,6 +671,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 +751,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 +798,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(float64(BCDToSWR(data))) if s.state.getSWR.pending { s.removePendingCmd(&s.state.getSWR) return false @@ -614,7 +807,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(float64(BCDToVd(data))) if s.state.getVd.pending { s.removePendingCmd(&s.state.getVd) return false @@ -624,12 +817,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 +835,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 +858,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 +883,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 +974,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,25 +1002,106 @@ 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 name of a command sent if s.getPendingCmdIndex(cmd) < 0 { + // NOTE: we could simplify all the s.initCmd calls to just the cmd, subcmd, data components if we wrap the icom command packet here + // data := cmd.cmd + // cmd.cmd = []byte{0xfe, 0xfe, civAddress, controllerAddress, data..., 0xfd} s.state.pendingCmds = append(s.state.pendingCmds, cmd) select { case s.newPendingCmdAdded <- true: default: } } + + // noww actually send it to the serial stream return s.st.send(cmd.cmd) } +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 +} + +func pctAsBCD(pct int) (BCD []byte) { + v := uint16(0x0255 * (float64(pct) / 100)) + return []byte{byte(v >> 8), byte(v & 0xff)} +} + +func BCDAsPct(bcd []byte) (pct int) { + hex := uint16(bcd[0])<<8 | uint16(bcd[1]) + pct = int(math.Round((float64(hex) / 0x0255) * 100)) + return +} + +func BCDToSLevel(bcd []byte) (sLevel int) { + sLevel = (int(math.Round(((float64(int(bcd[0])<<8) + float64(bcd[1])) / 0x0241) * 18))) + return +} + +func BCDToSWR(bcd []byte) (SWR int) { + // BCD to SWR + // 0000 => 1.0 + // 0048 => 1.5 + // 0080 => 2.0 + // 0120 => 3.0 + SWR = 1 + (BCDAsPct(bcd)/0x0120)*2 + return +} + +func BCDToVd(bcd []byte) (Vd int) { + // BCD to Vd + // 0000 => 0v + // 0075 => 5v + // 0241 => 16v + // IE - normalize full swing over 0x241, where full swing is 16 volts + Vd = (BCDAsPct(bcd) / 0x241) * 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) +} + +// NOTE: mabye call this BCDtoDecimal? (esp. since the incoming BCD size isn't limited) +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(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}) + s.initCmd(&s.state.setPwr, "setPwr", prepPacket("setPwr", pctAsBCD(percent))) return s.sendCmd(&s.state.setPwr) } @@ -845,8 +1120,7 @@ func (s *civControlStruct) decPwr() error { } 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}) + s.initCmd(&s.state.setRFGain, "setRFGain", prepPacket("setRFGain", pctAsBCD(percent))) return s.sendCmd(&s.state.setRFGain) } @@ -865,8 +1139,7 @@ func (s *civControlStruct) decRFGain() error { } 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}) + s.initCmd(&s.state.setSQL, "setSQL", prepPacket("setSQL", pctAsBCD(percent))) return s.sendCmd(&s.state.setSQL) } @@ -890,8 +1163,7 @@ func (s *civControlStruct) setNR(percent int) error { 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", pctAsBCD(percent))) return s.sendCmd(&s.state.setNR) } @@ -909,15 +1181,6 @@ func (s *civControlStruct) decNR() error { 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 +1190,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 +1211,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 +1259,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 +1267,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 +1298,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 for both though. + 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 +1352,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 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 +1419,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 +1428,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 +1442,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 +1613,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 +1646,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 +1683,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..13bb800 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,7 +194,7 @@ func handleHotkey(k byte) { case '\n': if statusLog.isRealtime() { statusLog.mutex.Lock() - statusLog.clearInternal() + statusLog.clearStatusLine() fmt.Println() statusLog.mutex.Unlock() statusLog.print() 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..ef23ea7 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,7 +78,34 @@ type statusLogStruct struct { data *statusLogData } +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 func (s *statusLogStruct) reportRTTLatency(l time.Duration) { s.mutex.Lock() @@ -242,14 +271,14 @@ func (s *statusLogStruct) reportSWR(swr float64) { s.data.swr = fmt.Sprintf("%.1f", swr) } -func (s *statusLogStruct) reportTS(ts uint) { +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) @@ -329,8 +358,8 @@ func (s *statusLogStruct) reportSplit(mode splitMode, split string) { } } -func (s *statusLogStruct) clearInternal() { - fmt.Printf("%c[2K", 27) +func (s *statusLogStruct) clearStatusLine() { + fmt.Print(termDetail.eraseLine) } func (s *statusLogStruct) print() { @@ -338,12 +367,12 @@ func (s *statusLogStruct) print() { 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) } @@ -353,9 +382,8 @@ 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 } @@ -364,9 +392,8 @@ 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 } @@ -375,19 +402,34 @@ 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 { @@ -396,17 +438,16 @@ func (s *statusLogStruct) update() { 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 +458,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 +488,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,13 +505,17 @@ 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) @@ -497,14 +542,12 @@ func (s *statusLogStruct) isRealtimeInternal() bool { func (s *statusLogStruct) isRealtime() bool { s.mutex.Lock() defer s.mutex.Unlock() - return s.ticker != nil && s.isRealtimeInternal() } func (s *statusLogStruct) isActive() bool { s.mutex.Lock() defer s.mutex.Unlock() - return s.ticker != nil } @@ -527,6 +570,7 @@ func (s *statusLogStruct) startPeriodicPrint() { go s.loop() } +// stop the update timer and clear the status rows... but not any error/info that may have been printed func (s *statusLogStruct) stopPeriodicPrint() { if !s.isActive() { return @@ -538,12 +582,11 @@ 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() + } } } @@ -558,6 +601,17 @@ func (s *statusLogStruct) initIfNeeded() { keyboard.init() } + cols, rows, err := terminal.GetSize(int(os.Stdout.Fd())) + if err == nil { + termDetail.cols = cols + termDetail.rows = rows + } + + // 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, 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:] } From 819a250c7ea68f06ce1b6a5e5fb132c0c211c706 Mon Sep 17 00:00:00 2001 From: Birk da Yooper <37058208+birk-da-yooper@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:49:31 -0500 Subject: [PATCH 2/4] Fix ptt & antenna tune timeouf values --- civcontrol.go | 58 ++++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/civcontrol.go b/civcontrol.go index 53f5759..cd9ceb2 100644 --- a/civcontrol.go +++ b/civcontrol.go @@ -9,17 +9,15 @@ import ( const statusPollInterval = time.Second const commandRetryTimeout = 500 * time.Millisecond -const pttTimeout = 30 * time.Minute +const pttTimeout = 10 * time.Minute // NOTE: US operators must identify at least once every ten minutes -// const tuneTimeout = 30 * time.Second -const tuneTimeout = 3 * time.Second +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 @@ -65,31 +63,35 @@ type civBand struct { // 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: 1800000, freqTo: 2000000}, // 1.9 - 160m - //{freqFrom: 3400000, freqTo: 4099999}, // 3.5 - 75/80m - {freqFrom: 3500000, freqTo: 4000000}, // 3.5 - 75/80m - //{freqFrom: 6900000, freqTo: 7499999}, // 7 - 40m - {freqFrom: 7000000, freqTo: 7300000}, // 7 - 40m - //{freqFrom: 9900000, freqTo: 10499999}, // 10 - {freqFrom: 10100000, freqTo: 10150000}, // 10 - 30m data modes only in US - //{freqFrom: 13900000, freqTo: 14499999}, // 14 - {freqFrom: 14000000, freqTo: 14350000}, // 14 - 20m - //{freqFrom: 17900000, freqTo: 18499999}, // 18 - {freqFrom: 18068000, freqTo: 18168000}, // 18 -17m - //{freqFrom: 20900000, freqTo: 21499999}, // 21 - {freqFrom: 21000000, freqTo: 21450000}, // 21 - 15m - //{freqFrom: 24400000, freqTo: 25099999}, // 24 - {freqFrom: 24890000, freqTo: 24990000}, // 24 - 12m - //{freqFrom: 28000000, freqTo: 29999999}, // 28 - {freqFrom: 28000000, freqTo: 29700000}, // 28 - 10m - //{freqFrom: 50000000, freqTo: 54000000}, // 50 - {freqFrom: 50000000, freqTo: 54000000}, // 50 - 6m - //{freqFrom: 74800000, freqTo: 107999999}, // WFM - no TX in US - //{freqFrom: 108000000, freqTo: 136999999}, // AIR = no TX in US - //{freqFrom: 144000000, freqTo: 148000000}, // 144 + /* + {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 {freqFrom: 420000000, freqTo: 450000000}, // 430 - 70cm //{freqFrom: 0, freqTo: 0}, // GENE // not very useful here // 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 From 7673ffe0a958b767db38eac5e97e05638ea68447 Mon Sep 17 00:00:00 2001 From: Birk da Yooper <37058208+birk-da-yooper@users.noreply.github.com> Date: Sun, 10 Mar 2024 19:58:55 -0400 Subject: [PATCH 3/4] Increased precision of value changes by over double instead of adjusting by percentage, adjust by single steps (IE 255 steps instead of 100) Implement BCD <-> decimal conversions via functions to reduce code duplication Corrected to use actual BCD --- civcontrol.go | 150 +++++++++++++++++++++++++++++++------------------- hotkeys.go | 2 + statuslog.go | 76 +++++++++++++++++++++---- 3 files changed, 160 insertions(+), 68 deletions(-) diff --git a/civcontrol.go b/civcontrol.go index cd9ceb2..888f1e1 100644 --- a/civcontrol.go +++ b/civcontrol.go @@ -175,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 @@ -595,7 +595,7 @@ func (s *civControlStruct) decodeDataModeAndOVF(d []byte) bool { func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { // all of these returns are expected to be three bytes long // subcmd, data msb, data lsb (where data is encoded as BCD) - // code wouldbe easier to read if we check size and do value extraction first + // code would be easier to read if we check size and do value extraction first // then take actions on appropriate entities in each case // @@ -604,8 +604,8 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { if len(d) < 3 { // has at least three bytes of data return !s.state.getRFGain.pending && !s.state.setRFGain.pending } - s.state.rfGainPercent = returnedLevel - statusLog.reportRFGain(s.state.rfGainPercent) + s.state.rfGainLevel = returnedLevel + statusLog.reportRFGain(s.state.rfGainLevel) if s.state.getRFGain.pending { s.removePendingCmd(&s.state.getRFGain) return false @@ -622,8 +622,8 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { if len(data) < 2 { return !s.state.getRFGain.pending && !s.state.setRFGain.pending } - s.state.rfGainPercent = BCDAsPct(data) - 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 @@ -636,8 +636,8 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { if len(data) < 2 { return !s.state.getSQL.pending && !s.state.setSQL.pending } - s.state.sqlPercent = BCDAsPct(data) - 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 @@ -649,8 +649,8 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { if len(data) < 2 { return !s.state.getNR.pending && !s.state.setNR.pending } - s.state.nrPercent = BCDAsPct(data) - 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 @@ -663,8 +663,8 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { if len(data) < 2 { return !s.state.getPwr.pending && !s.state.setPwr.pending } - s.state.pwrPercent = BCDAsPct(data) - 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 @@ -800,7 +800,7 @@ func (s *civControlStruct) decodeVdSWRS(d []byte) bool { return !s.state.getSWR.pending } s.state.lastSWRReceivedAt = time.Now() - statusLog.reportSWR(float64(BCDToSWR(data))) + statusLog.reportSWR(BCDToSWR(data)) if s.state.getSWR.pending { s.removePendingCmd(&s.state.getSWR) return false @@ -809,7 +809,7 @@ func (s *civControlStruct) decodeVdSWRS(d []byte) bool { if len(d) < 3 { return !s.state.getVd.pending } - statusLog.reportVd(float64(BCDToVd(data))) + statusLog.reportVd(BCDToVd(data)) if s.state.getVd.pending { s.removePendingCmd(&s.state.getVd) return false @@ -1025,7 +1025,7 @@ func (s *civControlStruct) sendCmd(cmd *civCmd) error { } } - // noww actually send it to the serial stream + // now actually send it to the serial stream return s.st.send(cmd.cmd) } @@ -1034,47 +1034,84 @@ func prepPacket(command string, data []byte) (pkt []byte) { 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) { - v := uint16(0x0255 * (float64(pct) / 100)) - return []byte{byte(v >> 8), byte(v & 0xff)} + scaled := uint16(255 * (float64(pct) / 100)) + return encodeForSend(scaled) } func BCDAsPct(bcd []byte) (pct int) { - hex := uint16(bcd[0])<<8 | uint16(bcd[1]) - pct = int(math.Round((float64(hex) / 0x0255) * 100)) + pct = int(100 * float64(BCDToDec(bcd)) / 0xff) return } +*/ func BCDToSLevel(bcd []byte) (sLevel int) { - sLevel = (int(math.Round(((float64(int(bcd[0])<<8) + float64(bcd[1])) / 0x0241) * 18))) + // 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 int) { - // BCD to SWR +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 - SWR = 1 + (BCDAsPct(bcd)/0x0120)*2 + fullScale := float64(120) // FWIW, 120 = b01111000 + SWR = 1 + (float64(BCDToDec(bcd))/fullScale)*2 return } -func BCDToVd(bcd []byte) (Vd int) { +func BCDToVd(bcd []byte) (Vd float64) { // BCD to Vd // 0000 => 0v // 0075 => 5v // 0241 => 16v - // IE - normalize full swing over 0x241, where full swing is 16 volts - Vd = (BCDAsPct(bcd) / 0x241) * 16 + // 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 } @@ -1088,7 +1125,6 @@ func (s *civControlStruct) getDigit(v uint, decade int) byte { return byte(uint(asDecimal) % 10) } -// NOTE: mabye call this BCDtoDecimal? (esp. since the incoming BCD size isn't limited) func (s *civControlStruct) decodeFreqData(d []byte) (f uint) { var pos int for _, v := range d { @@ -1102,83 +1138,83 @@ func (s *civControlStruct) decodeFreqData(d []byte) (f uint) { return } -func (s *civControlStruct) setPwr(percent int) error { - s.initCmd(&s.state.setPwr, "setPwr", prepPacket("setPwr", pctAsBCD(percent))) +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 { - s.initCmd(&s.state.setRFGain, "setRFGain", prepPacket("setRFGain", pctAsBCD(percent))) +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 { - s.initCmd(&s.state.setSQL, "setSQL", prepPacket("setSQL", pctAsBCD(percent))) +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 } } - s.initCmd(&s.state.setNR, "setNR", prepPacket("setSNR", pctAsBCD(percent))) + 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 } @@ -1693,7 +1729,7 @@ func debugPacket(command string, pkt []byte) { cmd := pkt[4] pld := pkt[5 : len(pkt)-1] - msg := fmt.Sprintf("'%v' [%x] ", command, pkt) + msg := fmt.Sprintf("'%v' [% x] ", command, pkt) msg += "to " if to == civAddress { diff --git a/hotkeys.go b/hotkeys.go index 13bb800..ecf922a 100644 --- a/hotkeys.go +++ b/hotkeys.go @@ -201,5 +201,7 @@ func handleHotkey(k byte) { } case 'q': quitChan <- true + default: + log.Error(fmt.Sprintf("INFO: no action mapped to key [%v]\n", string(k))) } } diff --git a/statuslog.go b/statuslog.go index ef23ea7..63366e8 100644 --- a/statuslog.go +++ b/statuslog.go @@ -107,6 +107,7 @@ 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() @@ -117,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 @@ -127,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() @@ -138,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() @@ -149,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() @@ -159,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() @@ -169,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() @@ -185,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() @@ -201,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() @@ -211,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() @@ -221,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() @@ -231,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() @@ -241,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() @@ -251,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() @@ -261,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() @@ -271,6 +286,7 @@ func (s *statusLogStruct) reportSWR(swr float64) { s.data.swr = fmt.Sprintf("%.1f", swr) } +// generate display string for tuning step value func (s *statusLogStruct) reportTuningStep(ts uint) { s.mutex.Lock() defer s.mutex.Unlock() @@ -292,6 +308,7 @@ func (s *statusLogStruct) reportTuningStep(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() @@ -303,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() @@ -358,10 +386,16 @@ func (s *statusLogStruct) reportSplit(mode splitMode, split string) { } } +// 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() @@ -378,6 +412,7 @@ func (s *statusLogStruct) print() { } } +// use whitespace padding on left to right-justify the string func (s *statusLogStruct) padLeft(str string, length int) string { if !s.isRealtimeInternal() { return str @@ -388,6 +423,7 @@ func (s *statusLogStruct) padLeft(str string, length int) string { 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 @@ -398,6 +434,7 @@ func (s *statusLogStruct) padRight(str string, length int) string { 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() @@ -433,7 +470,7 @@ func (s *statusLogStruct) update() { if s.data.nr != "" { nrStr = " NR" if s.data.nrEnabled { - nrStr += s.data.nr + nrStr += " " + s.data.nr } else { nrStr += "-" } @@ -522,6 +559,10 @@ func (s *statusLogStruct) update() { } } +// 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 { @@ -535,22 +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() @@ -570,7 +615,7 @@ func (s *statusLogStruct) startPeriodicPrint() { go s.loop() } -// stop the update timer and clear the status rows... but not any error/info that may have been printed +// 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 @@ -590,6 +635,10 @@ func (s *statusLogStruct) stopPeriodicPrint() { } } +// 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 @@ -605,11 +654,16 @@ func (s *statusLogStruct) initIfNeeded() { 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, rows-10) + vertWhitespace := strings.Repeat(termDetail.cursorDown, termDetail.rows-10) fmt.Printf("%v%v", termDetail.eraseScreen, vertWhitespace) c := color.New(color.FgHiWhite) From 8a3a7c4bb05ea7e315eb79f7be7c1816438a58da Mon Sep 17 00:00:00 2001 From: Birk da Yooper <37058208+birk-da-yooper@users.noreply.github.com> Date: Sun, 10 Mar 2024 20:47:02 -0400 Subject: [PATCH 4/4] Comments cleanup --- civcontrol.go | 44 +++++++++++++------------------------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/civcontrol.go b/civcontrol.go index 888f1e1..2b77327 100644 --- a/civcontrol.go +++ b/civcontrol.go @@ -9,7 +9,7 @@ import ( const statusPollInterval = time.Second const commandRetryTimeout = 500 * time.Millisecond -const pttTimeout = 10 * time.Minute // NOTE: US operators must identify at least once every ten minutes +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 @@ -64,8 +64,8 @@ type civBand struct { // (IE help them avoid accidental Tx where not allowed) var civBands = []civBand{ /* - {freqFrom: 1800000, freqTo: 1999999}, // 1.9 - {freqFrom: 3400000, freqTo: 4099999}, // 3.5 - 75/80m + {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 @@ -93,7 +93,7 @@ var civBands = []civBand{ {freqFrom: 50000000, freqTo: 54000000}, // 50 - 6m {freqFrom: 144000000, freqTo: 148000000}, // 144 - 2m {freqFrom: 420000000, freqTo: 450000000}, // 430 - 70cm - //{freqFrom: 0, freqTo: 0}, // GENE // not very useful here + //{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 } @@ -124,7 +124,7 @@ type civControlStruct struct { mutex sync.Mutex pendingCmds []*civCmd - getFreq civCmd // NOTE: why was this removed in this (devel) version? is because it doesn't recognize which vfo it's for? + getFreq civCmd // NOTE: why was this removed in v1.3-devel version? getPwr civCmd getS civCmd // get S-meter reading getOVF civCmd @@ -323,7 +323,7 @@ func (s *civControlStruct) decode(d []byte) bool { debugPacket("decoding", d) } - // minimum valid inccoming packet is six bytes long: 2 start-of-packet, to, from, cmd, end-of-packet + // 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 @@ -334,7 +334,9 @@ func (s *civControlStruct) decode(d []byte) bool { // // 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? - // hmmmm, but if I drop these then I'm not seeing changes on the status line... does seem the are nmaking it to the radio + // 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 @@ -394,7 +396,7 @@ func (s *civControlStruct) decodeFreq(d []byte) bool { 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. + 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 @@ -597,24 +599,7 @@ func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool { // 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 - // - /* - case 0x02: // RF Gain subcmd - if len(d) < 3 { // has at least three bytes of data - return !s.state.getRFGain.pending && !s.state.setRFGain.pending - } - s.state.rfGainLevel = returnedLevel - statusLog.reportRFGain(s.state.rfGainLevel) - if s.state.getRFGain.pending { - s.removePendingCmd(&s.state.getRFGain) - return false - } - if s.state.setRFGain.pending { - s.removePendingCmd(&s.state.setRFGain) - return false - } - */ subcmd := d[0] data := d[1:] switch subcmd { @@ -1013,11 +998,8 @@ func (s *civControlStruct) sendCmd(cmd *civCmd) error { 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 name of a command sent + // 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 { - // NOTE: we could simplify all the s.initCmd calls to just the cmd, subcmd, data components if we wrap the icom command packet here - // data := cmd.cmd - // cmd.cmd = []byte{0xfe, 0xfe, civAddress, controllerAddress, data..., 0xfd} s.state.pendingCmds = append(s.state.pendingCmds, cmd) select { case s.newPendingCmdAdded <- true: @@ -1337,7 +1319,7 @@ func (s *civControlStruct) setTune(enable bool) error { }) } else { // 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 for both though. + // actual behavior appears to be the same even it is set to OFF here b = ON } s.initCmd(&s.state.setTune, "setTune", prepPacket("setTune", []byte{b})) @@ -1423,7 +1405,7 @@ func (s *civControlStruct) toggleNR() error { 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 to the codebase + // 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) }