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
This commit is contained in:
Birk da Yooper 2024-03-10 19:58:55 -04:00
parent 819a250c7e
commit 7673ffe0a9
3 changed files with 160 additions and 68 deletions

View file

@ -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 {

View file

@ -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)))
}
}

View file

@ -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)