kappanhang/statuslog.go
Birk da Yooper 7656d4335b 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!"
2024-02-24 20:53:49 -05:00

640 lines
13 KiB
Go

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 {
line1 string
line2 string
line3 string
ptt bool
tune bool
frequency uint
subFrequency uint
mode string
dataMode string
filter string
subMode string
subDataMode string
subFilter string
preamp string
agc string
vd string
txPower string
rfGain string
sql string
nr string
nrEnabled bool
s string
ovf bool
swr string
ts string
split string
splitMode splitMode
startTime time.Time
rttStr string
audioMonOn bool
audioRecOn bool
audioStateStr string
}
type statusLogStruct struct {
ticker *time.Ticker
stopChan chan bool
stopFinishedChan chan bool
mutex sync.Mutex
preGenerated struct {
rxColor *color.Color
retransmitsColor *color.Color
lostColor *color.Color
splitColor *color.Color
stateStr struct {
tx string
tune string
}
audioStateStr struct {
off string
monOn string
rec string
}
ovf string
}
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()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.rttStr = fmt.Sprint(l.Milliseconds())
}
func (s *statusLogStruct) updateAudioStateStr() {
if s.data.audioRecOn {
s.data.audioStateStr = s.preGenerated.audioStateStr.rec
} else if s.data.audioMonOn {
s.data.audioStateStr = s.preGenerated.audioStateStr.monOn
} else {
s.data.audioStateStr = s.preGenerated.audioStateStr.off
}
}
func (s *statusLogStruct) reportAudioMon(enabled bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.audioMonOn = enabled
s.updateAudioStateStr()
}
func (s *statusLogStruct) reportAudioRec(enabled bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.audioRecOn = enabled
s.updateAudioStateStr()
}
func (s *statusLogStruct) reportFrequency(f uint) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.frequency = f
}
func (s *statusLogStruct) reportSubFrequency(f uint) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.subFrequency = f
}
func (s *statusLogStruct) reportMode(mode string, dataMode bool, filter string) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.mode = mode
if dataMode {
s.data.dataMode = "-D"
} else {
s.data.dataMode = ""
}
s.data.filter = filter
}
func (s *statusLogStruct) reportSubMode(mode string, dataMode bool, filter string) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.subMode = mode
if dataMode {
s.data.subDataMode = "-D"
} else {
s.data.subDataMode = ""
}
s.data.subFilter = filter
}
func (s *statusLogStruct) reportPreamp(preamp int) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.preamp = fmt.Sprint("PAMP", preamp)
}
func (s *statusLogStruct) reportAGC(agc string) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.agc = "AGC" + agc
}
func (s *statusLogStruct) reportNREnabled(enabled bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.nrEnabled = enabled
}
func (s *statusLogStruct) reportVd(voltage float64) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.vd = fmt.Sprintf("%.1fV", voltage)
}
func (s *statusLogStruct) reportS(sValue string) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.s = sValue
}
func (s *statusLogStruct) reportOVF(ovf bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.ovf = ovf
}
func (s *statusLogStruct) reportSWR(swr float64) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.swr = fmt.Sprintf("%.1f", swr)
}
func (s *statusLogStruct) reportTuningStep(ts uint) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.ts = "TS: "
if ts >= 1000 {
if ts%1000 == 0 {
s.data.ts += fmt.Sprintf("%.0fk", float64(ts)/1000)
} else if ts%100 == 0 {
s.data.ts += fmt.Sprintf("%.1fk", float64(ts)/1000)
} else {
s.data.ts += fmt.Sprintf("%.2fk", float64(ts)/1000)
}
} else {
s.data.ts += fmt.Sprint(ts)
}
}
func (s *statusLogStruct) reportPTT(ptt, tune bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.tune = tune
s.data.ptt = ptt
}
func (s *statusLogStruct) reportTxPower(percent int) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.txPower = fmt.Sprint(percent, "%")
}
func (s *statusLogStruct) reportRFGain(percent int) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.rfGain = fmt.Sprint(percent, "%")
}
func (s *statusLogStruct) reportSQL(percent int) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.sql = fmt.Sprint(percent, "%")
}
func (s *statusLogStruct) reportNR(percent int) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.nr = fmt.Sprint(percent, "%")
}
func (s *statusLogStruct) reportSplit(mode splitMode, split string) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.data == nil {
return
}
s.data.splitMode = mode
if split == "" {
s.data.split = ""
} else {
s.data.split = s.preGenerated.splitColor.Sprint(split)
}
}
func (s *statusLogStruct) clearStatusLine() {
fmt.Print(termDetail.eraseLine)
}
func (s *statusLogStruct) print() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.isRealtimeInternal() {
s.clearStatusLine()
fmt.Println(s.data.line1)
s.clearStatusLine()
fmt.Println(s.data.line2)
s.clearStatusLine()
fmt.Printf(s.data.line3+"%v%v", termDetail.cursorUp, termDetail.cursorUp)
} else {
log.PrintStatusLog(s.data.line3)
}
}
func (s *statusLogStruct) padLeft(str string, length int) string {
if !s.isRealtimeInternal() {
return str
}
if length-len(str) > 0 {
str = strings.Repeat(" ", length-len(str)) + str
}
return str
}
func (s *statusLogStruct) padRight(str string, length int) string {
if !s.isRealtimeInternal() {
return str
}
if length-len(str) > 0 {
str += strings.Repeat(" ", length-len(str))
}
return str
}
func (s *statusLogStruct) update() {
s.mutex.Lock()
defer s.mutex.Unlock()
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
}
if s.data.preamp != "" {
preampStr = " " + s.data.preamp
}
if s.data.agc != "" {
agcStr = " " + s.data.agc
}
if s.data.nr != "" {
nrStr = " NR"
if s.data.nrEnabled {
nrStr += s.data.nr
} else {
nrStr += "-"
}
}
if s.data.rfGain != "" {
rfGainStr = " rfg " + s.data.rfGain
}
if s.data.sql != "" {
sqlStr = " sql " + s.data.sql
}
s.data.line1 = fmt.Sprint(s.data.audioStateStr, filterStr, preampStr, agcStr, nrStr, rfGainStr, sqlStr)
if s.data.tune {
stateStr = s.preGenerated.stateStr.tune
} else if s.data.ptt {
stateStr = s.preGenerated.stateStr.tx
} else {
var ovfStr string
if s.data.ovf {
ovfStr = s.preGenerated.ovf
}
if len(s.data.s) <= 2 {
stateStr = s.preGenerated.rxColor.Sprintf(" %v ", s.padRight(s.data.s, 4))
} else {
stateStr = s.preGenerated.rxColor.Sprintf(" %v ", s.padRight(s.data.s, 5))
}
stateStr += ovfStr
}
if s.data.ts != "" {
tsStr = " " + s.data.ts
}
if s.data.mode != "" {
modeStr = " " + s.data.mode + s.data.dataMode
}
if s.data.vd != "" {
vdStr = " " + s.data.vd
}
if s.data.txPower != "" {
txPowerStr = " txpwr " + s.data.txPower
}
if s.data.split != "" {
splitStr = " " + s.data.split
if s.data.splitMode == splitModeOn {
splitStr += fmt.Sprintf("/%.6f/%s%s/%s", float64(s.data.subFrequency)/1000000,
s.data.subMode, s.data.subDataMode, s.data.subFilter)
}
}
if (s.data.tune || s.data.ptt) && s.data.swr != "" {
swrStr = " SWR" + s.data.swr
}
s.data.line2 = fmt.Sprint(stateStr, " ", fmt.Sprintf("%.6f", float64(s.data.frequency)/1000000),
tsStr, modeStr, splitStr, vdStr, txPowerStr, swrStr)
up, down, lost, retransmits := netstat.get()
lostStr := "0"
if lost > 0 {
lostStr = s.preGenerated.lostColor.Sprint(" ", lost, " ")
}
retransmitsStr := "0"
if retransmits > 0 {
retransmitsStr = s.preGenerated.retransmitsColor.Sprint(" ", retransmits, " ")
}
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") // 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)
}
}
func (s *statusLogStruct) loop() {
for {
select {
case <-s.ticker.C:
s.update()
s.print()
case <-s.stopChan:
s.stopFinishedChan <- true
return
}
}
}
func (s *statusLogStruct) isRealtimeInternal() bool {
return keyboard.initialized
}
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
}
func (s *statusLogStruct) startPeriodicPrint() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.initIfNeeded()
s.data = &statusLogData{
s: "S0",
startTime: time.Now(),
rttStr: "?",
audioStateStr: s.preGenerated.audioStateStr.off,
}
s.stopChan = make(chan bool)
s.stopFinishedChan = make(chan bool)
s.ticker = time.NewTicker(statusLogInterval)
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
}
s.ticker.Stop()
s.ticker = nil
s.stopChan <- true
<-s.stopFinishedChan
if s.isRealtimeInternal() {
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()
}
}
}
func (s *statusLogStruct) initIfNeeded() {
if s.data != nil { // Already initialized?
return
}
if quietLog || (!isatty.IsTerminal(os.Stdout.Fd()) && statusLogInterval < time.Second) {
statusLogInterval = time.Second
} else {
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 ")
s.preGenerated.rxColor = color.New(color.FgHiWhite)
s.preGenerated.rxColor.Add(color.BgGreen)
s.preGenerated.audioStateStr.monOn = s.preGenerated.rxColor.Sprint(" MON ")
c = color.New(color.FgHiWhite, color.BlinkRapid)
c.Add(color.BgRed)
s.preGenerated.stateStr.tx = c.Sprint(" TX ")
s.preGenerated.stateStr.tune = c.Sprint(" TUNE ")
s.preGenerated.audioStateStr.rec = c.Sprint(" REC ")
c = color.New(color.FgHiWhite)
c.Add(color.BgRed)
s.preGenerated.ovf = c.Sprint(" OVF ")
s.preGenerated.retransmitsColor = color.New(color.FgHiWhite)
s.preGenerated.retransmitsColor.Add(color.BgYellow)
s.preGenerated.lostColor = color.New(color.FgHiWhite)
s.preGenerated.lostColor.Add(color.BgRed)
s.preGenerated.splitColor = color.New(color.FgHiMagenta)
}