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] 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:] }