Add displaying PTT/freq/mode info to the status bar

This commit is contained in:
Nonoo 2020-10-29 09:39:13 +01:00
parent 9957abbb46
commit dfb5dbd948
10 changed files with 266 additions and 35 deletions

View file

@ -78,17 +78,25 @@ If the `-s` command line argument is specified, then kappanhang will create a
the transceiver directly. Look at the app log to find out the name of the
virtual serial port.
### Status log
### Status bar
kappanhang displays a "realtime" status log (when the audio/serial connection
kappanhang displays a "realtime" status bar (when the audio/serial connection
is up) with the following info:
- `up`: how long the audio/serial connection is active
- `rtt`: roundtrip communication latency with the server
- `up/down`: currently used upload/download bandwidth (only considering UDP
payload to/from the server)
- `retx`: audio/serial retransmit request count to/from the server
- `lost`: lost audio/serial packet count from the server
- First status bar line:
- `state`: RX/TX/TUNE depending on the PTT status
- `freq`: operating frequency in MHz, mode (LSB/USB/FM...), active filter
- Second status bar line:
- `up`: how long the audio/serial connection is active
- `rtt`: roundtrip communication latency with the server
- `up/down`: currently used upload/download bandwidth (only considering UDP
payload to/from the server)
- `retx`: audio/serial retransmit request count to/from the server
- `lost`: lost audio/serial packet count from the server
Data for the first status bar line is acquired by monitoring CiV traffic in
the serial stream.
`retx` and `lost` are displayed in a 1 minute window, which means they will be
reset to 0 if they don't increase for 1 minute. A `retx` value other than 0
@ -97,9 +105,11 @@ if `loss` stays 0 then the issues were fixed using packet retransmission.
`loss` indicates failed retransmit sequences, so packet loss. This can cause
audio and serial communication disruptions.
If the status log interval (can be changed with the `-i` command line
argument) is equal to or above 1 second, then the realtime status log will be
disabled and all status log lines will be written as new console lines.
If status bar interval (can be changed with the `-i` command line
argument) is equal to or above 1 second, then the realtime status bar will be
disabled and the contents of the second line of the status bar will be written
as new console log lines. This is also the case if a Unix/VT100 terminal is
not available.
## Authors

View file

@ -20,7 +20,7 @@ func parseArgs() {
a := getopt.StringLong("address", 'a', "IC-705", "Connect to address")
t := getopt.Uint16Long("serial-tcp-port", 'p', 4533, "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")
i := getopt.Uint16Long("log-interval", 'i', 100, "Status log interval in milliseconds")
i := getopt.Uint16Long("log-interval", 'i', 100, "Status bar/log interval in milliseconds")
getopt.Parse()

122
civdecoder.go Normal file
View file

@ -0,0 +1,122 @@
package main
import "math"
const civAddress = 0xa4
type civDecoderStruct struct {
}
func (s *civDecoderStruct) decode(d []byte) {
if len(d) < 6 || d[0] != 0xfe || d[1] != 0xfe || d[len(d)-1] != 0xfd {
return
}
payload := d[5 : len(d)-1]
switch d[4] {
case 0x00:
s.decodeFreq(payload)
case 0x01:
s.decodeMode(payload)
case 0x03:
s.decodeFreq(payload)
case 0x04:
s.decodeMode(payload)
case 0x1c:
s.decodePTT(payload)
}
}
func (s *civDecoderStruct) decodeFreq(d []byte) {
var f float64
var pos int
for _, v := range d {
s1 := v & 0x0f
s2 := v >> 4
f += float64(s1) * math.Pow(10, float64(pos))
pos++
f += float64(s2) * math.Pow(10, float64(pos))
pos++
}
statusLog.reportFrequency(f)
}
func (s *civDecoderStruct) decodeMode(d []byte) {
if len(d) < 1 {
return
}
var mode string
switch d[0] {
case 0x00:
mode = "LSB"
case 0x01:
mode = "USB"
case 0x02:
mode = "AM"
case 0x03:
mode = "CW"
case 0x04:
mode = "RTTY"
case 0x05:
mode = "FM"
case 0x06:
mode = "WFM"
case 0x07:
mode = "CW-R"
case 0x08:
mode = "RTTY-R"
case 0x17:
mode = "DV"
}
var filter string
if len(d) > 1 {
switch d[1] {
case 0x01:
filter = "FIL1"
case 0x02:
filter = "FIL2"
case 0x03:
filter = "FIL3"
}
}
statusLog.reportMode(mode, filter)
}
func (s *civDecoderStruct) decodePTT(d []byte) {
if len(d) < 2 {
return
}
var ptt bool
var tune bool
switch d[0] {
case 0:
if d[1] == 1 {
ptt = true
}
case 1:
if d[1] == 2 {
tune = true
}
}
statusLog.reportPTT(ptt, tune)
}
func (s *civDecoderStruct) query(st *serialStream) error {
// Querying frequency.
if err := st.send([]byte{254, 254, civAddress, 224, 3, 253}); err != nil {
return err
}
// Querying mode.
if err := st.send([]byte{254, 254, civAddress, 224, 4, 253}); err != nil {
return err
}
// Querying PTT.
if err := st.send([]byte{254, 254, civAddress, 224, 0x1c, 0, 253}); err != nil {
return err
}
return nil
}

View file

@ -338,8 +338,6 @@ func (s *controlStream) deinit() {
log.Debug("sending deauth")
_ = s.sendPktAuth(0x01)
_ = s.sendPktAuth(0x01)
_ = s.sendPktAuth(0x01)
_ = s.sendPktAuth(0x01)
// Waiting a little bit to make sure the radio can send retransmit requests.
time.Sleep(500 * time.Millisecond)
}

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 8.5 MiB

2
go.mod
View file

@ -4,7 +4,9 @@ go 1.14
require (
github.com/akosmarton/papipes v0.0.0-20201027113853-3c63b4919c76
github.com/fatih/color v1.9.0
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2
github.com/mattn/go-isatty v0.0.11
github.com/pborman/getopt v1.1.0
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0

10
go.sum
View file

@ -5,6 +5,8 @@ github.com/akosmarton/papipes v0.0.0-20201027113853-3c63b4919c76/go.mod h1:mdvQ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4=
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
@ -14,6 +16,11 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@ -46,7 +53,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=

22
log.go
View file

@ -27,13 +27,9 @@ func (l *logger) GetCallerFileName(withLine bool) string {
}
}
func (l *logger) printLineClear() {
fmt.Printf("%c[2K", 27)
}
func (l *logger) Printf(a string, b ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Infof(l.GetCallerFileName(false)+": "+a, b...)
@ -41,7 +37,7 @@ func (l *logger) Printf(a string, b ...interface{}) {
func (l *logger) Print(a ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Info(append([]interface{}{l.GetCallerFileName(false) + ": "}, a...)...)
@ -53,7 +49,7 @@ func (l *logger) PrintStatusLog(a ...interface{}) {
func (l *logger) Debugf(a string, b ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Debugf(l.GetCallerFileName(true)+": "+a, b...)
@ -61,7 +57,7 @@ func (l *logger) Debugf(a string, b ...interface{}) {
func (l *logger) Debug(a ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Debug(append([]interface{}{l.GetCallerFileName(true) + ": "}, a...)...)
@ -69,7 +65,7 @@ func (l *logger) Debug(a ...interface{}) {
func (l *logger) Errorf(a string, b ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Errorf(l.GetCallerFileName(true)+": "+a, b...)
@ -77,7 +73,7 @@ func (l *logger) Errorf(a string, b ...interface{}) {
func (l *logger) Error(a ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Error(append([]interface{}{l.GetCallerFileName(true) + ": "}, a...)...)
@ -85,7 +81,7 @@ func (l *logger) Error(a ...interface{}) {
func (l *logger) ErrorC(a ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Error(a...)
@ -93,7 +89,7 @@ func (l *logger) ErrorC(a ...interface{}) {
func (l *logger) Fatalf(a string, b ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Fatalf(l.GetCallerFileName(true)+": "+a, b...)
@ -101,7 +97,7 @@ func (l *logger) Fatalf(a string, b ...interface{}) {
func (l *logger) Fatal(a ...interface{}) {
if statusLog.isRealtime() {
l.printLineClear()
statusLog.clear()
defer statusLog.print()
}
l.logger.Fatal(append([]interface{}{l.GetCallerFileName(true) + ": "}, a...)...)

View file

@ -19,6 +19,7 @@ type serialStream struct {
receivedSerialData bool
lastReceivedSeq uint16
civDecoder civDecoderStruct
readFromSerialPort struct {
buf bytes.Buffer
@ -92,6 +93,8 @@ func (s *serialStream) handleRxSeqBufEntry(e seqBufEntry) {
e.data = e.data[21:]
s.civDecoder.decode(e.data)
if serialPort.write != nil {
serialPort.write <- e.data
}
@ -250,6 +253,10 @@ func (s *serialStream) init(devName string) error {
s.readFromSerialPort.frameTimeout = time.NewTimer(0)
<-s.readFromSerialPort.frameTimeout.C
if err := s.civDecoder.query(s); err != nil {
return err
}
go s.loop()
return nil
}

View file

@ -2,8 +2,12 @@ package main
import (
"fmt"
"os"
"sync"
"time"
"github.com/fatih/color"
"github.com/mattn/go-isatty"
)
type statusLogStruct struct {
@ -12,7 +16,18 @@ type statusLogStruct struct {
stopFinishedChan chan bool
mutex sync.Mutex
line string
line1 string
line2 string
state struct {
rxStr string
txStr string
tuneStr string
}
stateStr string
frequency float64
mode string
filter string
startTime time.Time
rttLatency time.Duration
@ -27,15 +42,56 @@ func (s *statusLogStruct) reportRTTLatency(l time.Duration) {
s.rttLatency = l
}
func (s *statusLogStruct) reportFrequency(f float64) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.frequency = f
}
func (s *statusLogStruct) reportMode(mode, filter string) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.mode = mode
s.filter = filter
}
func (s *statusLogStruct) reportPTT(ptt, tune bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if tune {
s.stateStr = s.state.tuneStr
} else if ptt {
s.stateStr = s.state.txStr
} else {
s.stateStr = s.state.rxStr
}
}
func (s *statusLogStruct) clearInternal() {
fmt.Printf("%c[2K", 27)
}
func (s *statusLogStruct) clear() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.clearInternal()
}
func (s *statusLogStruct) print() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.isRealtimeInternal() {
log.printLineClear()
fmt.Print(s.line)
s.clearInternal()
fmt.Println(s.line1)
s.clearInternal()
fmt.Printf(s.line2+"%c[1A", 27)
} else {
log.PrintStatusLog(s.line)
log.PrintStatusLog(s.line2)
}
}
@ -43,15 +99,26 @@ func (s *statusLogStruct) update() {
s.mutex.Lock()
defer s.mutex.Unlock()
up, down, lost, retransmits := netstat.get()
var modeStr string
if s.mode != "" {
modeStr = " " + s.mode
}
var filterStr string
if s.filter != "" {
filterStr = " " + s.filter
}
s.line1 = fmt.Sprint("state ", s.stateStr, " freq: ", fmt.Sprintf("%f", s.frequency/1000000), modeStr, filterStr)
s.line = fmt.Sprint("up ", time.Since(s.startTime).Round(time.Second),
up, down, lost, retransmits := netstat.get()
s.line2 = fmt.Sprint("up ", time.Since(s.startTime).Round(time.Second),
" rtt ", s.rttLatency.Milliseconds(), "ms up ",
netstat.formatByteCount(up), "/s down ",
netstat.formatByteCount(down), "/s retx ", retransmits, " /1m lost ", lost, " /1m")
netstat.formatByteCount(down), "/s retx ", retransmits, " /1m lost ", lost, " /1m\r")
if s.isRealtimeInternal() {
s.line = fmt.Sprint(time.Now().Format("2006-01-02T15:04:05.000Z0700"), " ", s.line, "\r")
t := time.Now().Format("2006-01-02T15:04:05.000Z0700")
s.line1 = fmt.Sprint(t, " ", s.line1)
s.line2 = fmt.Sprint(t, " ", s.line2)
}
}
@ -90,6 +157,21 @@ func (s *statusLogStruct) startPeriodicPrint() {
s.mutex.Lock()
defer s.mutex.Unlock()
if !isatty.IsTerminal(os.Stdout.Fd()) && statusLogInterval < time.Second {
statusLogInterval = time.Second
}
c := color.New(color.FgHiWhite)
c.Add(color.BgWhite)
s.stateStr = c.Sprint(" ?? ")
c = color.New(color.FgHiWhite)
c.Add(color.BgGreen)
s.state.rxStr = c.Sprint(" RX ")
c = color.New(color.FgHiWhite, color.BlinkRapid)
c.Add(color.BgRed)
s.state.txStr = c.Sprint(" TX ")
s.state.tuneStr = c.Sprint(" TUNE ")
s.startTime = time.Now()
s.stopChan = make(chan bool)
s.stopFinishedChan = make(chan bool)
@ -106,4 +188,8 @@ func (s *statusLogStruct) stopPeriodicPrint() {
s.stopChan <- true
<-s.stopFinishedChan
fmt.Println()
fmt.Println()
fmt.Println()
}