Merge pull request #15 from AD8IM/revisions

Multiple updates to CI-V and status output aspects
This commit is contained in:
Howard 2024-03-10 20:53:12 -04:00 committed by GitHub
commit 9751b536b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 812 additions and 341 deletions

46
args.go
View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

1
go.mod
View file

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

3
go.sum
View file

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

View file

@ -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,12 +194,14 @@ func handleHotkey(k byte) {
case '\n':
if statusLog.isRealtime() {
statusLog.mutex.Lock()
statusLog.clearInternal()
statusLog.clearStatusLine()
fmt.Println()
statusLog.mutex.Unlock()
statusLog.print()
}
case 'q':
quitChan <- true
default:
log.Error(fmt.Sprintf("INFO: no action mapped to key [%v]\n", string(k)))
}
}

8
log.go
View file

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

10
main.go
View file

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

16
pkt7.go
View file

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

View file

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

View file

@ -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,8 +78,36 @@ type statusLogStruct struct {
data *statusLogData
}
var statusLog statusLogStruct
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
// generate display string for round trip time latency
func (s *statusLogStruct) reportRTTLatency(l time.Duration) {
s.mutex.Lock()
defer s.mutex.Unlock()
@ -88,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
@ -98,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()
@ -109,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()
@ -120,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()
@ -130,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()
@ -140,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()
@ -156,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()
@ -172,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()
@ -182,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()
@ -192,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()
@ -202,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()
@ -212,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()
@ -222,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()
@ -232,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()
@ -242,14 +286,15 @@ func (s *statusLogStruct) reportSWR(swr float64) {
s.data.swr = fmt.Sprintf("%.1f", swr)
}
func (s *statusLogStruct) reportTS(ts uint) {
// generate display string for tuning step value
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)
@ -263,6 +308,7 @@ func (s *statusLogStruct) reportTS(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()
@ -274,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()
@ -329,84 +386,105 @@ func (s *statusLogStruct) reportSplit(mode splitMode, split string) {
}
}
func (s *statusLogStruct) clearInternal() {
fmt.Printf("%c[2K", 27)
// 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()
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)
}
}
// use whitespace padding on left to right-justify the string
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
}
// 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
}
for len(str) < length {
str += " "
if length-len(str) > 0 {
str += strings.Repeat(" ", length-len(str))
}
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()
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 {
nrStr += s.data.nr
nrStr += " " + s.data.nr
} else {
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 +495,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 +525,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,19 +542,27 @@ 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)
}
}
// 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 {
@ -490,24 +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()
@ -527,6 +615,7 @@ func (s *statusLogStruct) startPeriodicPrint() {
go s.loop()
}
// 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
@ -538,15 +627,18 @@ 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()
}
}
}
// 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
@ -558,6 +650,22 @@ func (s *statusLogStruct) initIfNeeded() {
keyboard.init()
}
cols, rows, err := terminal.GetSize(int(os.Stdout.Fd()))
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, termDetail.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 ")

View file

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