mirror of
https://github.com/nonoo/kappanhang.git
synced 2026-01-20 07:40:14 +01:00
Merge pull request #15 from AD8IM/revisions
Multiple updates to CI-V and status output aspects
This commit is contained in:
commit
9751b536b3
46
args.go
46
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
804
civcontrol.go
804
civcontrol.go
File diff suppressed because it is too large
Load diff
1
go.mod
1
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
|
||||
)
|
||||
|
|
|
|||
3
go.sum
3
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=
|
||||
|
|
|
|||
13
hotkeys.go
13
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,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
8
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()
|
||||
|
|
|
|||
10
main.go
10
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
|
||||
|
|
|
|||
16
pkt7.go
16
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
212
statuslog.go
212
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,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 ")
|
||||
|
|
|
|||
|
|
@ -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:]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue