diff --git a/README.md b/README.md index 0276927..5c61c96 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/args.go b/args.go index cb0fa19..a8d6e8d 100644 --- a/args.go +++ b/args.go @@ -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() diff --git a/civdecoder.go b/civdecoder.go new file mode 100644 index 0000000..f3e7726 --- /dev/null +++ b/civdecoder.go @@ -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 +} diff --git a/controlstream.go b/controlstream.go index 20b5608..ba13a31 100644 --- a/controlstream.go +++ b/controlstream.go @@ -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) } diff --git a/demo.gif b/demo.gif index 1a7a479..6d88323 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/go.mod b/go.mod index 7d4fd54..b1ab969 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e94c122..b8b1d0e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/log.go b/log.go index 698ebcf..02d8ea9 100644 --- a/log.go +++ b/log.go @@ -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...)...) diff --git a/serialstream.go b/serialstream.go index 741d40c..d00d247 100644 --- a/serialstream.go +++ b/serialstream.go @@ -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 } diff --git a/statuslog.go b/statuslog.go index 72bb3be..50a703e 100644 --- a/statuslog.go +++ b/statuslog.go @@ -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() }