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)