pax_global_header 0000666 0000000 0000000 00000000064 14003632250 0014505 g ustar 00root root 0000000 0000000 52 comment=4996f10780f5292e1f5b8dec180a9ae4727c92cc
kappanhang-1.3/ 0000775 0000000 0000000 00000000000 14003632250 0013460 5 ustar 00root root 0000000 0000000 kappanhang-1.3/.gitignore 0000664 0000000 0000000 00000000027 14003632250 0015447 0 ustar 00root root 0000000 0000000 kappanhang
__debug_bin
kappanhang-1.3/.vscode/ 0000775 0000000 0000000 00000000000 14003632250 0015021 5 ustar 00root root 0000000 0000000 kappanhang-1.3/.vscode/launch.json 0000664 0000000 0000000 00000000747 14003632250 0017176 0 ustar 00root root 0000000 0000000 {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"args": ["-v", "-i", "1000", "-s"]
}
]
} kappanhang-1.3/.vscode/settings.json 0000664 0000000 0000000 00000000046 14003632250 0017554 0 ustar 00root root 0000000 0000000 {
"go.lintTool": "golangci-lint"
} kappanhang-1.3/.vscode/tasks.json 0000664 0000000 0000000 00000000637 14003632250 0017047 0 ustar 00root root 0000000 0000000 {
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "go build",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
} kappanhang-1.3/LICENSE 0000664 0000000 0000000 00000002211 14003632250 0014461 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2020 Norbert Varga HA2NON, Akos Marton ES1AKOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
/Icom is a registered trademark of Icom Incorporated (Japan)/
kappanhang-1.3/README.md 0000664 0000000 0000000 00000021374 14003632250 0014746 0 ustar 00root root 0000000 0000000 # kappanhang
kappanhang remotely opens audio channels and a serial port to an Icom RS-BA1
server. The app is mainly developed for connecting to the Icom IC-705
transceiver, which has built-in Wi-Fi and RS-BA1 server. All features of the
protocol are implemented including packet retransmission on packet loss.

kappanhang currently only supports Linux, but support for other platforms can
be easily added if anyone is interested and has the skills (volunteers
needed, as I'm only developing the Linux version).
## Compatible hardware/software
- Icom RS-BA1 server software
- Icom IC-705
- Icom IC-9700
Send me an [email](mailto:nonoo@nonoo.hu) if you've tested a new hardware or
software and it is working with kappanhang.
## Compiling
You'll need:
- Go installed on your computer. On Ubuntu-like systems, install a recent
package `golang`.
- The development package for libpulse installed. On Ubuntu-like systems,
this is package `libpulse-dev`.
Then:
```
go get github.com/nonoo/kappanhang
go install github.com/nonoo/kappanhang
```
This will typically install `kappanhang` into `$HOME/go/bin`.
## Required settings on the RS-BA1 server (the transceiver)
- Make sure network settings (on the Icom IC-705 in: `Menu -> Set ->
WLAN set -> Remote settings`) are the following:
- **Network control** is turned on.
- **UDP ports** are on their default values:
- Control port: `50001`
- Serial port: `50002`
- Audio port: `50003`
- **Internet access line** is on the default `FTTH` value.
- Make sure the following settings are set:
- **DATA MOD** is set to `WLAN` (on the Icom IC-705 in: `Menu -> Set ->
Connectors -> MOD Input -> DATA MOD`)
- **CI-V Address** is on the default `A4h` value (on the Icom IC-705 in:
`Menu -> Set -> Connectors -> CI-V`.
## Running
You can get the available command line parameters with the `-h` command line
argument.
If no command line arguments are set, then the app will try to connect to the
host **ic-705** (ic-705.local or ic-705.localdomain) with the username `beer`
and password `beerbeer`. You can set the username with the `-u` and the
password with the `-p` command line arguments.
Here's a quick video tutorial on how to run kappanhang on a Raspberry Pi:
[](https://www.youtube.com/watch?v=93hYhXHCVeU)
After it is connected and logged in:
- Creates a virtual PulseAudio **sound card** (48kHz, s16le, mono). This can be
used to record/play audio from/to the server (the transceiver). You can also
set this sound card in [WSJT-X](https://physics.princeton.edu/pulsar/K1JT/wsjtx.html).
- Starts an **internal rigctld** server. This can be used for controlling the
server (the transceiver) with [Hamlib](https://hamlib.github.io/) (`rigctl`)
clients. This internal rigctld is needed for more reliable rigctl
communication, as the original rigctld is very sensitive to timeouts.
To use this with for example [WSJT-X](https://physics.princeton.edu/pulsar/K1JT/wsjtx.html),
open WSJT-X settings, go to the *Radio* tab, set the *rig type* to `Hamlib
NET rigctl`, and the *Network server* to `localhost`.
- Starts a **TCP server** on port `4531` for exposing the **serial port**.
This can be used for an externally launched `rigctld` for example.
### Virtual serial port
If the `-s` command line argument is specified, then kappanhang will create a
**virtual serial port**, so other apps which don't support Hamlib can access
the transceiver directly. Look at the app log to find out the name of the
virtual serial port. It will be something like `/tmp/kappanhang-IC-705.pty`
(the server's name appended to the string *kappanhang*). After the virtual
serial port is created, the command specified with `-o` will be ran, which is
`socat /tmp/kappanhang-IC-705.pty /tmp/vmware.pty` by default. Running the
command can be disabled with `-o -`. The command is only executed once, as the
virtual serial port will stay opened even if the RS-BA1 server disconnects.
I use this command to link a COM port in a Windows OS running in VMware to
the virtual serial port, so I can use the original RS-BA1 software remote
control GUI.
### Status bar
kappanhang displays a "realtime" status bar (when the audio/serial connection
is up) with the following info:
- First status bar line:
- `MON/REC`: current status of the audio monitor (see the *Hotkeys* section
in this README for more information about this feature)
- `filter`: active filter (FIL1, FIL2 etc.)
- `preamp`: PAMP0 means the preamp is off
- `AGC`: AGC state (F - fast, M - middle, S - slow)
- `rfg`: RF gain in percent
- `sql`: squelch level in percent
- `nr`: noise reduction level in percent
- Second status bar line:
- `S meter`: periodically refreshed S meter value, OVF is displayed on
overflow, displays TX on transmit (or TUNE)
- `freq`: operating frequency in MHz
- `TS`: tuning step
- `mode`: LSB/USB/FM etc. *-D* indicates data mode
- `SPLIT/DUP-/DUP+`: displayed when split/DUP operation is active, the TX
frequency is also displayed in split mode
- `voltage`: drain voltage of the final amplifier MOS-FETs, updated when a
TX/TUNE is over
- `txpwr`: current transmit power setting in percent
- `swr`: reported SWR (only displayed during TX)
- Third 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 2 status bar lines are acquired by monitoring CiV traffic
in the serial stream. S value and OVF are queried periodically, but these
queries/replies are filtered from the serial data stream sent to the TCP
serial port server and to the virtual serial port.
`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
indicates issues with the connection (probably a poor Wi-Fi connection), but
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 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 last 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.
### Hotkeys
- `q` (quit): closes the app
- `l` (listen): toggles audio stream playback to the default sound device.
This is useful for quickly listening into the audio stream coming from the
server (the transceiver).
- `space`: toggles PTT and audio stream recording from the default sound
device. You can transmit your own voice using a mic attached to your
computer for example.
Some basic CAT control hotkeys are also supported:
- `t`: toggles the tune process
- `+`: increases TX power
- `-`: decreases TX power
- `0` to `9`: set TX power in 10% steps
- `)`: set TX power to 100%
- `[`, `]`: decreases, increases frequency
- `{`, `}`: decreases, increases tuning step
- `;`, `'`: decreases, increases RF gain
- `!` to `(` (shift + numbers): set RF gain in 10% steps
- `:`, `"`: decreases, increases squelch level
- `,`, `.`: decreases, increases noise reduction level
- `/`: toggles noise reduction
- `n`, `m`: cycles through operating modes
- `d`, `f`: cycles through filters
- `D`: toggles data mode
- `v`, `b`: cycles through bands
- `p`: toggles preamp
- `a`: toggles AGC
- `o`: toggles VFO A/B
- `s`: toggles split/DUP+- operation
## Icom IC-705 Wi-Fi notes
Note that the built-in Wi-Fi in the Icom IC-705 has **very limited range**,
and **sensitive to interference**. If you see a lot of retransmits in the log,
or packet loss, then:
- Place the IC-705 close to your Wi-Fi AP/router, or use a Wi-Fi range
extender device
- Make sure the Wi-Fi bandwith is set to max. 20Mhz in the Wi-Fi router (see
explanation [here](https://superuser.com/questions/542191/does-moving-my-router-from-20mhz-to-40-mhz-increase-my-wireless-speed))
- Try switching Wi-Fi channel on your Wi-Fi router. Only channels 1, 6 or 11
should be used (see explanation [here](https://www.metageek.com/training/resources/why-channels-1-6-11.html))
Sometimes rebooting the transceiver helps, as the network stack in the IC-705
is not quite free of bugs. :)
## Contributors
- Norbert Varga HA2NON [nonoo@nonoo.hu](mailto:nonoo@nonoo.hu)
- Akos Marton ES1AKOS
- W6EL (passcode algorithm)
## Donations
If you find this app useful then [buy me a beer](https://paypal.me/ha2non). :)
kappanhang-1.3/args.go 0000664 0000000 0000000 00000003671 14003632250 0014752 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"os"
"time"
"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
func parseArgs() {
h := getopt.BoolLong("help", 'h', "display help")
v := getopt.BoolLong("verbose", 'v', "Enable verbose (debug) logging")
q := getopt.BoolLong("quiet", 'q', "Disable logging")
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.UintLong("civ-address", 'c', 0xa4, "CI-V address")
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")
d := getopt.BoolLong("set-data-tx", 'd', "Automatically enable data mode on TX")
getopt.Parse()
if *h || *a == "" || (*q && *v) {
fmt.Println(getAboutStr())
getopt.Usage()
os.Exit(1)
}
verboseLog = *v
quietLog = *q
connectAddress = *a
username = *u
password = *p
civAddress = byte(*c)
serialTCPPort = *t
enableSerialDevice = *s
rigctldPort = *r
runCmd = *e
runCmdOnSerialPortCreated = *o
statusLogInterval = time.Duration(*i) * time.Millisecond
setDataModeOnTx = *d
}
kappanhang-1.3/audio-linux.go 0000664 0000000 0000000 00000032016 14003632250 0016247 0 ustar 00root root 0000000 0000000 // +build linux
package main
import (
"bytes"
"errors"
"io"
"os"
"sync"
"time"
"github.com/akosmarton/papipes"
"github.com/mesilliac/pulse-simple"
)
const audioSampleRate = 48000
const audioSampleBytes = 2
const pulseAudioBufferLength = 100 * time.Millisecond
const audioFrameLength = 20 * time.Millisecond
const audioFrameSize = int((audioSampleRate * audioSampleBytes * audioFrameLength) / time.Second)
const maxPlayBufferSize = audioFrameSize*5 + int((audioSampleRate*audioSampleBytes*audioRxSeqBufLength)/time.Second)
type audioStruct struct {
devName string
deinitNeededChan chan bool
deinitFinishedChan chan bool
// Send to this channel to play audio.
play chan []byte
// Read from this channel for audio.
rec chan []byte
virtualSoundcardStream struct {
source papipes.Source
sink papipes.Sink
mutex sync.Mutex
playBuf *bytes.Buffer
canPlay chan bool
}
defaultSoundcardStream struct {
togglePlaybackChan chan bool
playStream *pulse.Stream
recStream *pulse.Stream
recLoopDeinitNeededChan chan bool
recLoopDeinitFinishedChan chan bool
mutex sync.Mutex
playBuf *bytes.Buffer
canPlay chan bool
}
}
var audio audioStruct
func (a *audioStruct) defaultSoundCardPlayStreamDeinit() {
_ = a.defaultSoundcardStream.playStream.Drain()
a.defaultSoundcardStream.playStream.Free()
a.defaultSoundcardStream.playStream = nil
}
func (a *audioStruct) defaultSoundCardRecStreamDeinit() {
if a.defaultSoundcardStream.recStream == nil {
return
}
a.defaultSoundcardStream.recLoopDeinitNeededChan <- true
<-a.defaultSoundcardStream.recLoopDeinitFinishedChan
a.defaultSoundcardStream.recStream.Free()
a.defaultSoundcardStream.recStream = nil
}
func (a *audioStruct) togglePlaybackToDefaultSoundcard() {
if a.defaultSoundcardStream.togglePlaybackChan == nil {
return
}
// Non-blocking send to channel.
select {
case a.defaultSoundcardStream.togglePlaybackChan <- true:
default:
}
}
func (a *audioStruct) toggleRecFromDefaultSoundcard() {
if a.defaultSoundcardStream.recStream == nil {
ss := pulse.SampleSpec{Format: pulse.SAMPLE_S16LE, Rate: audioSampleRate, Channels: 1}
battr := pulse.NewBufferAttr()
battr.Fragsize = uint32(audioFrameSize)
var err error
a.defaultSoundcardStream.recStream, err = pulse.NewStream("", "kappanhang", pulse.STREAM_RECORD, "", a.devName,
&ss, nil, battr)
if err == nil {
a.defaultSoundcardStream.recLoopDeinitNeededChan = make(chan bool)
a.defaultSoundcardStream.recLoopDeinitFinishedChan = make(chan bool)
go a.recLoopFromDefaultSoundcard()
log.Print("turned on audio rec")
statusLog.reportAudioRec(true)
if setDataModeOnTx {
if err := civControl.setDataMode(true); err != nil {
log.Error("can't enable data mode: ", err)
}
}
if err := civControl.setPTT(true); err != nil {
log.Error("can't turn on ptt: ", err)
}
} else {
log.Error("can't turn on rec: ", err)
a.defaultSoundcardStream.recStream = nil
}
} else {
a.defaultSoundCardRecStreamDeinit()
statusLog.reportAudioRec(false)
log.Print("turned off audio rec")
if err := civControl.setPTT(false); err != nil {
log.Error("can't turn off ptt: ", err)
}
}
}
func (a *audioStruct) doTogglePlaybackToDefaultSoundcard() {
if a.defaultSoundcardStream.playStream == nil {
log.Print("turned on audio playback")
statusLog.reportAudioMon(true)
ss := pulse.SampleSpec{Format: pulse.SAMPLE_S16LE, Rate: audioSampleRate, Channels: 1}
a.defaultSoundcardStream.playStream, _ = pulse.Playback("kappanhang", a.devName, &ss)
} else {
a.defaultSoundCardPlayStreamDeinit()
log.Print("turned off audio playback")
statusLog.reportAudioMon(false)
}
}
func (a *audioStruct) playLoopToDefaultSoundcard(deinitNeededChan, deinitFinishedChan chan bool) {
for {
select {
case <-a.defaultSoundcardStream.canPlay:
case <-a.defaultSoundcardStream.togglePlaybackChan:
a.doTogglePlaybackToDefaultSoundcard()
case <-deinitNeededChan:
deinitFinishedChan <- true
return
}
for {
a.defaultSoundcardStream.mutex.Lock()
if a.defaultSoundcardStream.playBuf.Len() < audioFrameSize {
a.defaultSoundcardStream.mutex.Unlock()
break
}
d := make([]byte, audioFrameSize)
bytesToWrite, err := a.defaultSoundcardStream.playBuf.Read(d)
a.defaultSoundcardStream.mutex.Unlock()
if err != nil {
log.Error(err)
break
}
if bytesToWrite != len(d) {
log.Error("buffer underread")
break
}
for len(d) > 0 && a.defaultSoundcardStream.playStream != nil {
written, err := a.defaultSoundcardStream.playStream.Write(d)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
reportError(err)
}
break
}
d = d[written:]
}
}
}
}
func (a *audioStruct) recLoopFromDefaultSoundcard() {
defer func() {
a.defaultSoundcardStream.recLoopDeinitFinishedChan <- true
}()
frameBuf := make([]byte, audioFrameSize)
buf := bytes.NewBuffer([]byte{})
for {
select {
case <-a.defaultSoundcardStream.recLoopDeinitNeededChan:
return
default:
}
n, err := a.defaultSoundcardStream.recStream.Read(frameBuf)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
reportError(err)
}
}
// Do not send silence frames to the radio unnecessarily
if isAllZero(frameBuf[:n]) {
continue
}
buf.Write(frameBuf[:n])
for buf.Len() >= len(frameBuf) {
// We need to create a new []byte slice for each chunk to be able to send it through the rec chan.
b := make([]byte, len(frameBuf))
n, err = buf.Read(b)
if err != nil {
reportError(err)
}
if n != len(frameBuf) {
reportError(errors.New("audio buffer read error"))
}
select {
case a.rec <- b:
case <-a.defaultSoundcardStream.recLoopDeinitNeededChan:
return
}
}
}
}
func (a *audioStruct) playLoopToVirtualSoundcard(deinitNeededChan, deinitFinishedChan chan bool) {
for {
select {
case <-a.virtualSoundcardStream.canPlay:
case <-deinitNeededChan:
deinitFinishedChan <- true
return
}
for {
a.virtualSoundcardStream.mutex.Lock()
if a.virtualSoundcardStream.playBuf.Len() < audioFrameSize {
a.virtualSoundcardStream.mutex.Unlock()
break
}
d := make([]byte, audioFrameSize)
bytesToWrite, err := a.virtualSoundcardStream.playBuf.Read(d)
a.virtualSoundcardStream.mutex.Unlock()
if err != nil {
log.Error(err)
break
}
if bytesToWrite != len(d) {
log.Error("buffer underread")
break
}
for len(d) > 0 {
written, err := a.virtualSoundcardStream.source.Write(d)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
reportError(err)
}
break
}
d = d[written:]
}
}
}
}
func (a *audioStruct) recLoopFromVirtualSoundcard(deinitNeededChan, deinitFinishedChan chan bool) {
defer func() {
deinitFinishedChan <- true
}()
frameBuf := make([]byte, audioFrameSize)
buf := bytes.NewBuffer([]byte{})
for {
select {
case <-deinitNeededChan:
return
default:
}
n, err := a.virtualSoundcardStream.sink.Read(frameBuf)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
reportError(err)
if err == io.EOF {
<-deinitNeededChan
return
}
}
}
// Do not send silence frames to the radio unnecessarily
if isAllZero(frameBuf[:n]) {
continue
}
buf.Write(frameBuf[:n])
for buf.Len() >= len(frameBuf) {
// We need to create a new []byte slice for each chunk to be able to send it through the rec chan.
b := make([]byte, len(frameBuf))
n, err = buf.Read(b)
if err != nil {
reportError(err)
}
if n != len(frameBuf) {
reportError(errors.New("audio buffer read error"))
}
select {
case a.rec <- b:
case <-deinitNeededChan:
return
}
}
}
}
func (a *audioStruct) loop() {
playLoopToVirtualSoundcardDeinitNeededChan := make(chan bool)
playLoopToVirtualSoundcardDeinitFinishedChan := make(chan bool)
go a.playLoopToVirtualSoundcard(playLoopToVirtualSoundcardDeinitNeededChan, playLoopToVirtualSoundcardDeinitFinishedChan)
playLoopToDefaultSoundcardDeinitNeededChan := make(chan bool)
playLoopToDefaultSoundcardDeinitFinishedChan := make(chan bool)
go a.playLoopToDefaultSoundcard(playLoopToDefaultSoundcardDeinitNeededChan, playLoopToDefaultSoundcardDeinitFinishedChan)
recLoopFromVirtualSoundcardDeinitNeededChan := make(chan bool)
recLoopFromVirtualSoundcardDeinitFinishedChan := make(chan bool)
go a.recLoopFromVirtualSoundcard(recLoopFromVirtualSoundcardDeinitNeededChan, recLoopFromVirtualSoundcardDeinitFinishedChan)
var d []byte
for {
select {
case d = <-a.play:
case <-a.deinitNeededChan:
a.closeIfNeeded()
recLoopFromVirtualSoundcardDeinitNeededChan <- true
<-recLoopFromVirtualSoundcardDeinitFinishedChan
playLoopToVirtualSoundcardDeinitNeededChan <- true
<-playLoopToVirtualSoundcardDeinitFinishedChan
if a.defaultSoundcardStream.playStream != nil {
a.defaultSoundCardPlayStreamDeinit()
}
playLoopToDefaultSoundcardDeinitNeededChan <- true
<-playLoopToDefaultSoundcardDeinitFinishedChan
a.deinitFinishedChan <- true
return
}
a.virtualSoundcardStream.mutex.Lock()
free := maxPlayBufferSize - a.virtualSoundcardStream.playBuf.Len()
if free < len(d) {
b := make([]byte, len(d)-free)
_, _ = a.virtualSoundcardStream.playBuf.Read(b)
}
a.virtualSoundcardStream.playBuf.Write(d)
a.virtualSoundcardStream.mutex.Unlock()
// Non-blocking notify.
select {
case a.virtualSoundcardStream.canPlay <- true:
default:
}
if a.defaultSoundcardStream.playStream != nil {
a.defaultSoundcardStream.mutex.Lock()
free := maxPlayBufferSize - a.defaultSoundcardStream.playBuf.Len()
if free < len(d) {
b := make([]byte, len(d)-free)
_, _ = a.defaultSoundcardStream.playBuf.Read(b)
}
a.defaultSoundcardStream.playBuf.Write(d)
a.defaultSoundcardStream.mutex.Unlock()
// Non-blocking notify.
select {
case a.defaultSoundcardStream.canPlay <- true:
default:
}
}
}
}
// 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.
func (a *audioStruct) initIfNeeded(devName string) error {
a.devName = devName
bufferSizeInBits := (audioSampleRate * audioSampleBytes * 8) / 1000 * pulseAudioBufferLength.Milliseconds()
if !a.virtualSoundcardStream.source.IsOpen() {
a.virtualSoundcardStream.source.Name = "kappanhang-" + a.devName
a.virtualSoundcardStream.source.Filename = "/tmp/kappanhang-" + a.devName + ".source"
a.virtualSoundcardStream.source.Rate = audioSampleRate
a.virtualSoundcardStream.source.Format = "s16le"
a.virtualSoundcardStream.source.Channels = 1
a.virtualSoundcardStream.source.SetProperty("device.buffering.buffer_size", bufferSizeInBits)
a.virtualSoundcardStream.source.SetProperty("device.description", "kappanhang: "+a.devName)
// Cleanup previous pipes.
sources, err := papipes.GetActiveSources()
if err == nil {
for _, i := range sources {
if i.Filename == a.virtualSoundcardStream.source.Filename {
i.Close()
}
}
}
if err := a.virtualSoundcardStream.source.Open(); err != nil {
return err
}
}
if !a.virtualSoundcardStream.sink.IsOpen() {
a.virtualSoundcardStream.sink.Name = "kappanhang-" + a.devName
a.virtualSoundcardStream.sink.Filename = "/tmp/kappanhang-" + a.devName + ".sink"
a.virtualSoundcardStream.sink.Rate = audioSampleRate
a.virtualSoundcardStream.sink.Format = "s16le"
a.virtualSoundcardStream.sink.Channels = 1
a.virtualSoundcardStream.sink.UseSystemClockForTiming = true
a.virtualSoundcardStream.sink.SetProperty("device.buffering.buffer_size", bufferSizeInBits)
a.virtualSoundcardStream.sink.SetProperty("device.description", "kappanhang: "+a.devName)
// Cleanup previous pipes.
sinks, err := papipes.GetActiveSinks()
if err == nil {
for _, i := range sinks {
if i.Filename == a.virtualSoundcardStream.sink.Filename {
i.Close()
}
}
}
if err := a.virtualSoundcardStream.sink.Open(); err != nil {
return err
}
}
if a.virtualSoundcardStream.playBuf == nil {
log.Print("opened device " + a.virtualSoundcardStream.source.Name)
a.play = make(chan []byte)
a.rec = make(chan []byte)
a.virtualSoundcardStream.playBuf = bytes.NewBuffer([]byte{})
a.defaultSoundcardStream.playBuf = bytes.NewBuffer([]byte{})
a.virtualSoundcardStream.canPlay = make(chan bool)
a.defaultSoundcardStream.canPlay = make(chan bool)
a.defaultSoundcardStream.togglePlaybackChan = make(chan bool)
a.deinitNeededChan = make(chan bool)
a.deinitFinishedChan = make(chan bool)
go a.loop()
}
return nil
}
func (a *audioStruct) closeIfNeeded() {
if a.virtualSoundcardStream.source.IsOpen() {
if err := a.virtualSoundcardStream.source.Close(); err != nil {
if _, ok := err.(*os.PathError); !ok {
log.Error(err)
}
}
}
if a.virtualSoundcardStream.sink.IsOpen() {
if err := a.virtualSoundcardStream.sink.Close(); err != nil {
if _, ok := err.(*os.PathError); !ok {
log.Error(err)
}
}
}
}
func (a *audioStruct) deinit() {
a.defaultSoundCardRecStreamDeinit()
a.closeIfNeeded()
if a.deinitNeededChan != nil {
a.deinitNeededChan <- true
<-a.deinitFinishedChan
}
}
kappanhang-1.3/audiostream.go 0000664 0000000 0000000 00000012114 14003632250 0016323 0 ustar 00root root 0000000 0000000 package main
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"time"
)
const audioTimeoutDuration = 5 * time.Second
const audioRxSeqBufLength = 100 * time.Millisecond
type audioStream struct {
common streamCommon
deinitNeededChan chan bool
deinitFinishedChan chan bool
timeoutTimer *time.Timer
receivedAudio bool
lastReceivedSeq uint16
serverAudioTime time.Time
rxSeqBuf seqBuf
rxSeqBufEntryChan chan seqBufEntry
audioSendSeq uint16
}
// sendPart1 expects 1364 bytes of PCM data.
func (s *audioStream) sendPart1(pcmData []byte) error {
err := s.common.pkt0.sendTrackedPacket(&s.common,
append([]byte{0x6c, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
byte(s.common.localSID >> 24), byte(s.common.localSID >> 16), byte(s.common.localSID >> 8), byte(s.common.localSID),
byte(s.common.remoteSID >> 24), byte(s.common.remoteSID >> 16), byte(s.common.remoteSID >> 8), byte(s.common.remoteSID),
0x80, 0x00, byte((s.audioSendSeq - 1) >> 8), byte(s.audioSendSeq - 1), 0x00, 0x00, byte(len(pcmData) >> 8), byte(len(pcmData))},
pcmData...))
if err != nil {
return err
}
s.audioSendSeq++
return nil
}
// sendPart2 expects 556 bytes of PCM data.
func (s *audioStream) sendPart2(pcmData []byte) error {
err := s.common.pkt0.sendTrackedPacket(&s.common, append([]byte{0x44, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
byte(s.common.localSID >> 24), byte(s.common.localSID >> 16), byte(s.common.localSID >> 8), byte(s.common.localSID),
byte(s.common.remoteSID >> 24), byte(s.common.remoteSID >> 16), byte(s.common.remoteSID >> 8), byte(s.common.remoteSID),
0x80, 0x00, byte((s.audioSendSeq - 1) >> 8), byte(s.audioSendSeq - 1), 0x00, 0x00, byte(len(pcmData) >> 8), byte(len(pcmData))},
pcmData...))
if err != nil {
return err
}
s.audioSendSeq++
return nil
}
func (s *audioStream) handleRxSeqBufEntry(e seqBufEntry) {
gotSeq := uint16(e.seq)
if s.receivedAudio {
// Out of order packets can happen if we receive a retransmitted packet, but too late.
if s.rxSeqBuf.compareSeq(e.seq, seqNum(s.lastReceivedSeq)) != larger {
log.Debug("got out of order pkt seq #", e.seq)
return
}
expectedSeq := s.lastReceivedSeq + 1
if expectedSeq != gotSeq {
var missingPkts int
if gotSeq > expectedSeq {
missingPkts = int(gotSeq) - int(expectedSeq)
} else {
missingPkts = int(gotSeq) + 65536 - int(expectedSeq)
}
netstat.reportLoss(missingPkts)
log.Error("lost ", missingPkts, " audio packets")
s.serverAudioTime = s.serverAudioTime.Add(time.Duration(10*missingPkts) * time.Millisecond)
}
s.serverAudioTime = s.serverAudioTime.Add(10 * time.Millisecond)
} else {
s.serverAudioTime = time.Now()
}
s.lastReceivedSeq = gotSeq
s.receivedAudio = true
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)
}
return s.rxSeqBuf.add(seqNum(gotSeq), r[24:])
}
func (s *audioStream) handleRead(r []byte) error {
if len(r) >= 580 && (bytes.Equal(r[:6], []byte{0x6c, 0x05, 0x00, 0x00, 0x00, 0x00}) || bytes.Equal(r[:6], []byte{0x44, 0x02, 0x00, 0x00, 0x00, 0x00})) {
return s.handleAudioPacket(r)
}
return nil
}
func (s *audioStream) loop() {
for {
select {
case r := <-s.common.readChan:
if err := s.handleRead(r); err != nil {
reportError(err)
}
case <-s.timeoutTimer.C:
reportError(errors.New(fmt.Sprint("audio stream timeout after ",
time.Since(statusLog.data.startTime), ", try rebooting the radio")))
case e := <-s.rxSeqBufEntryChan:
s.handleRxSeqBufEntry(e)
case d := <-audio.rec:
if err := s.sendPart1(d[:1364]); err != nil {
reportError(err)
}
if err := s.sendPart2(d[1364:1920]); err != nil {
reportError(err)
}
case <-s.deinitNeededChan:
s.deinitFinishedChan <- true
return
}
}
}
func (s *audioStream) init(devName string) error {
if err := s.common.init("audio", audioStreamPort); err != nil {
return err
}
if err := audio.initIfNeeded(devName); err != nil {
return err
}
if err := s.common.start(); err != nil {
return err
}
s.common.pkt7.startPeriodicSend(&s.common, 1, false)
// This stream does not use periodic pkt0 idle packets.
s.audioSendSeq = 1
log.Print("stream started")
s.rxSeqBufEntryChan = make(chan seqBufEntry)
s.rxSeqBuf.init(audioRxSeqBufLength, 0xffff, 0, s.rxSeqBufEntryChan, s.common.requestRetransmit)
s.timeoutTimer = time.NewTimer(audioTimeoutDuration)
s.deinitNeededChan = make(chan bool)
s.deinitFinishedChan = make(chan bool)
go s.loop()
return nil
}
func (s *audioStream) deinit() {
if s.deinitNeededChan != nil {
s.deinitNeededChan <- true
<-s.deinitFinishedChan
}
if s.timeoutTimer != nil {
s.timeoutTimer.Stop()
}
s.common.deinit()
s.rxSeqBuf.deinit()
}
kappanhang-1.3/civcontrol.go 0000664 0000000 0000000 00000103211 14003632250 0016167 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"math"
"sync"
"time"
)
const statusPollInterval = time.Second
const commandRetryTimeout = 500 * time.Millisecond
const pttTimeout = 3 * time.Minute
const tuneTimeout = 30 * time.Second
// Commands reference: https://www.icomeurope.com/wp-content/uploads/2020/08/IC-705_ENG_CI-V_1_20200721.pdf
type civOperatingMode struct {
name string
code byte
}
var civOperatingModes = []civOperatingMode{
{name: "LSB", code: 0x00},
{name: "USB", code: 0x01},
{name: "AM", code: 0x02},
{name: "CW", code: 0x03},
{name: "RTTY", code: 0x04},
{name: "FM", code: 0x05},
{name: "WFM", code: 0x06},
{name: "CW-R", code: 0x07},
{name: "RTTY-R", code: 0x08},
{name: "DV", code: 0x17},
}
type civFilter struct {
name string
code byte
}
var civFilters = []civFilter{
{name: "FIL1", code: 0x01},
{name: "FIL2", code: 0x02},
{name: "FIL3", code: 0x03},
}
type civBand struct {
freqFrom uint
freqTo uint
freq uint
}
var civBands = []civBand{
{freqFrom: 1800000, freqTo: 1999999}, // 1.9
{freqFrom: 3400000, freqTo: 4099999}, // 3.5
{freqFrom: 6900000, freqTo: 7499999}, // 7
{freqFrom: 9900000, freqTo: 10499999}, // 10
{freqFrom: 13900000, freqTo: 14499999}, // 14
{freqFrom: 17900000, freqTo: 18499999}, // 18
{freqFrom: 20900000, freqTo: 21499999}, // 21
{freqFrom: 24400000, freqTo: 25099999}, // 24
{freqFrom: 28000000, freqTo: 29999999}, // 28
{freqFrom: 50000000, freqTo: 54000000}, // 50
{freqFrom: 74800000, freqTo: 107999999}, // WFM
{freqFrom: 108000000, freqTo: 136999999}, // AIR
{freqFrom: 144000000, freqTo: 148000000}, // 144
{freqFrom: 420000000, freqTo: 450000000}, // 430
{freqFrom: 0, freqTo: 0}, // GENE
}
type splitMode int
const (
splitModeOff = iota
splitModeOn
splitModeDUPMinus
splitModeDUPPlus
)
type civCmd struct {
pending bool
sentAt time.Time
name string
cmd []byte
}
type civControlStruct struct {
st *serialStream
deinitNeeded chan bool
deinitFinished chan bool
resetSReadTimer chan bool
newPendingCmdAdded chan bool
state struct {
mutex sync.Mutex
pendingCmds []*civCmd
getPwr civCmd
getS civCmd
getOVF civCmd
getSWR civCmd
getTransmitStatus civCmd
getPreamp civCmd
getAGC civCmd
getTuneStatus civCmd
getVd civCmd
getTS civCmd
getRFGain civCmd
getSQL civCmd
getNR civCmd
getNREnabled civCmd
getSplit civCmd
getMainVFOFreq civCmd
getSubVFOFreq civCmd
getMainVFOMode civCmd
getSubVFOMode civCmd
lastSReceivedAt time.Time
lastOVFReceivedAt time.Time
lastSWRReceivedAt time.Time
lastVFOFreqReceivedAt time.Time
setPwr civCmd
setRFGain civCmd
setSQL civCmd
setNR civCmd
setMainVFOFreq civCmd
setSubVFOFreq civCmd
setMode civCmd
setSubVFOMode civCmd
setPTT civCmd
setTune civCmd
setDataMode civCmd
setPreamp civCmd
setAGC civCmd
setNREnabled civCmd
setTS civCmd
setVFO civCmd
setSplit civCmd
pttTimeoutTimer *time.Timer
tuneTimeoutTimer *time.Timer
freq uint
subFreq uint
ptt bool
tune bool
pwrPercent int
rfGainPercent int
sqlPercent int
nrPercent int
nrEnabled bool
operatingModeIdx int
dataMode bool
filterIdx int
subOperatingModeIdx int
subDataMode bool
subFilterIdx int
bandIdx int
preamp int
agc int
tsValue byte
ts uint
vfoBActive bool
splitMode splitMode
}
}
var civControl civControlStruct
// Returns false if the message should not be forwarded to the serial port TCP server or the virtual serial port.
func (s *civControlStruct) decode(d []byte) bool {
if len(d) < 6 || d[0] != 0xfe || d[1] != 0xfe || d[len(d)-1] != 0xfd {
return true
}
payload := d[5 : len(d)-1]
s.state.mutex.Lock()
defer s.state.mutex.Unlock()
switch d[4] {
// case 0x00:
// return s.decodeFreq(payload)
case 0x01:
return s.decodeMode(payload)
// case 0x03:
// return s.decodeFreq(payload)
case 0x04:
return s.decodeMode(payload)
// case 0x05:
// return s.decodeFreq(payload)
case 0x06:
return s.decodeMode(payload)
case 0x07:
return s.decodeVFO(payload)
case 0x0f:
return s.decodeSplit(payload)
case 0x10:
return s.decodeTS(payload)
case 0x1a:
return s.decodeDataModeAndOVF(payload)
case 0x14:
return s.decodePowerRFGainSQLNRPwr(payload)
case 0x1c:
return s.decodeTransmitStatus(payload)
case 0x15:
return s.decodeVdSWRS(payload)
case 0x16:
return s.decodePreampAGCNREnabled(payload)
case 0x25:
return s.decodeVFOFreq(payload)
case 0x26:
return s.decodeVFOMode(payload)
}
return true
}
func (s *civControlStruct) decodeFreqData(d []byte) (f uint) {
var pos int
for _, v := range d {
s1 := v & 0x0f
s2 := v >> 4
f += uint(s1) * uint(math.Pow(10, float64(pos)))
pos++
f += uint(s2) * uint(math.Pow(10, float64(pos)))
pos++
}
return
}
// func (s *civControlStruct) decodeFreq(d []byte) bool {
// if len(d) < 2 {
// return !s.state.getFreq.pending && !s.state.setMainVFOFreq.pending
// }
// s.state.freq = s.decodeFreqData(d)
// statusLog.reportFrequency(s.state.freq)
// s.state.bandIdx = len(civBands) - 1 // Set the band idx to GENE by default.
// for i := range civBands {
// if s.state.freq >= civBands[i].freqFrom && s.state.freq <= civBands[i].freqTo {
// s.state.bandIdx = i
// civBands[s.state.bandIdx].freq = s.state.freq
// break
// }
// }
// if s.state.getFreq.pending {
// s.removePendingCmd(&s.state.getFreq)
// return false
// }
// if s.state.setMainVFOFreq.pending {
// s.removePendingCmd(&s.state.setMainVFOFreq)
// return false
// }
// return true
// }
func (s *civControlStruct) decodeFilterValueToFilterIdx(v byte) int {
for i := range civFilters {
if civFilters[i].code == v {
return i
}
}
return 0
}
func (s *civControlStruct) decodeMode(d []byte) bool {
if len(d) < 1 {
return !s.state.setMode.pending
}
for i := range civOperatingModes {
if civOperatingModes[i].code == d[0] {
s.state.operatingModeIdx = i
break
}
}
if len(d) > 1 {
s.state.filterIdx = s.decodeFilterValueToFilterIdx(d[1])
}
statusLog.reportMode(civOperatingModes[s.state.operatingModeIdx].name, s.state.dataMode,
civFilters[s.state.filterIdx].name)
if s.state.setMode.pending {
s.removePendingCmd(&s.state.setMode)
return false
}
return true
}
func (s *civControlStruct) decodeVFO(d []byte) bool {
if len(d) < 1 {
return !s.state.setVFO.pending
}
if d[0] == 1 {
s.state.vfoBActive = true
log.Print("active vfo: B")
} else {
s.state.vfoBActive = false
log.Print("active vfo: A")
}
if s.state.setVFO.pending {
// The radio does not send frequencies automatically.
_ = s.getBothVFOFreq()
s.removePendingCmd(&s.state.setVFO)
return false
}
return true
}
func (s *civControlStruct) decodeSplit(d []byte) bool {
if len(d) < 1 {
return !s.state.getSplit.pending && !s.state.setSplit.pending
}
var str string
switch d[0] {
default:
s.state.splitMode = splitModeOff
case 0x01:
s.state.splitMode = splitModeOn
str = "SPLIT"
case 0x11:
s.state.splitMode = splitModeDUPMinus
str = "DUP-"
case 0x12:
s.state.splitMode = splitModeDUPPlus
str = "DUP+"
}
statusLog.reportSplit(s.state.splitMode, str)
if s.state.getSplit.pending {
s.removePendingCmd(&s.state.getSplit)
return false
}
if s.state.setSplit.pending {
s.removePendingCmd(&s.state.setSplit)
return false
}
return true
}
func (s *civControlStruct) decodeTS(d []byte) bool {
if len(d) < 1 {
return !s.state.getTS.pending && !s.state.setTS.pending
}
s.state.tsValue = d[0]
switch s.state.tsValue {
default:
s.state.ts = 1
case 1:
s.state.ts = 100
case 2:
s.state.ts = 500
case 3:
s.state.ts = 1000
case 4:
s.state.ts = 5000
case 5:
s.state.ts = 6250
case 6:
s.state.ts = 8330
case 7:
s.state.ts = 9000
case 8:
s.state.ts = 10000
case 9:
s.state.ts = 12500
case 10:
s.state.ts = 20000
case 11:
s.state.ts = 25000
case 12:
s.state.ts = 50000
case 13:
s.state.ts = 100000
}
statusLog.reportTS(s.state.ts)
if s.state.getTS.pending {
s.removePendingCmd(&s.state.getTS)
return false
}
if s.state.setTS.pending {
s.removePendingCmd(&s.state.setTS)
return false
}
return true
}
func (s *civControlStruct) decodeDataModeAndOVF(d []byte) bool {
switch d[0] {
case 0x06:
if len(d) < 3 {
return !s.state.setDataMode.pending
}
if d[1] == 1 {
s.state.dataMode = true
s.state.filterIdx = s.decodeFilterValueToFilterIdx(d[2])
} else {
s.state.dataMode = false
}
statusLog.reportMode(civOperatingModes[s.state.operatingModeIdx].name, s.state.dataMode,
civFilters[s.state.filterIdx].name)
if s.state.setDataMode.pending {
s.removePendingCmd(&s.state.setDataMode)
return false
}
case 0x09:
if len(d) < 2 {
return !s.state.getOVF.pending
}
if d[1] != 0 {
statusLog.reportOVF(true)
} else {
statusLog.reportOVF(false)
}
s.state.lastOVFReceivedAt = time.Now()
if s.state.getOVF.pending {
s.removePendingCmd(&s.state.getOVF)
return false
}
}
return true
}
func (s *civControlStruct) decodePowerRFGainSQLNRPwr(d []byte) bool {
switch d[0] {
case 0x02:
if len(d) < 3 {
return !s.state.getRFGain.pending && !s.state.setRFGain.pending
}
hex := uint16(d[1])<<8 | uint16(d[2])
s.state.rfGainPercent = int(math.Round((float64(hex) / 0x0255) * 100))
statusLog.reportRFGain(s.state.rfGainPercent)
if s.state.getRFGain.pending {
s.removePendingCmd(&s.state.getRFGain)
return false
}
if s.state.setRFGain.pending {
s.removePendingCmd(&s.state.setRFGain)
return false
}
case 0x03:
if len(d) < 3 {
return !s.state.getSQL.pending && !s.state.setSQL.pending
}
hex := uint16(d[1])<<8 | uint16(d[2])
s.state.sqlPercent = int(math.Round((float64(hex) / 0x0255) * 100))
statusLog.reportSQL(s.state.sqlPercent)
if s.state.getSQL.pending {
s.removePendingCmd(&s.state.getSQL)
return false
}
if s.state.setSQL.pending {
s.removePendingCmd(&s.state.setSQL)
return false
}
case 0x06:
if len(d) < 3 {
return !s.state.getNR.pending && !s.state.setNR.pending
}
hex := uint16(d[1])<<8 | uint16(d[2])
s.state.nrPercent = int(math.Round((float64(hex) / 0x0255) * 100))
statusLog.reportNR(s.state.nrPercent)
if s.state.getNR.pending {
s.removePendingCmd(&s.state.getNR)
return false
}
if s.state.setNR.pending {
s.removePendingCmd(&s.state.setNR)
return false
}
case 0x0a:
if len(d) < 3 {
return !s.state.getPwr.pending && !s.state.setPwr.pending
}
hex := uint16(d[1])<<8 | uint16(d[2])
s.state.pwrPercent = int(math.Round((float64(hex) / 0x0255) * 100))
statusLog.reportTxPower(s.state.pwrPercent)
if s.state.getPwr.pending {
s.removePendingCmd(&s.state.getPwr)
return false
}
if s.state.setPwr.pending {
s.removePendingCmd(&s.state.setPwr)
return false
}
}
return true
}
func (s *civControlStruct) decodeTransmitStatus(d []byte) bool {
if len(d) < 2 {
return !s.state.getTuneStatus.pending && !s.state.getTransmitStatus.pending && !s.state.setPTT.pending
}
switch d[0] {
case 0:
if d[1] == 1 {
s.state.ptt = true
} else {
if s.state.ptt { // PTT released?
s.state.ptt = false
if s.state.pttTimeoutTimer != nil {
s.state.pttTimeoutTimer.Stop()
}
_ = s.getVd()
}
}
statusLog.reportPTT(s.state.ptt, s.state.tune)
if s.state.setPTT.pending {
s.removePendingCmd(&s.state.setPTT)
return false
}
case 1:
if d[1] == 2 {
s.state.tune = true
// The transceiver does not send the tune state after it's finished.
time.AfterFunc(time.Second, func() {
_ = s.getTransmitStatus()
})
} else {
if s.state.tune { // Tune finished?
s.state.tune = false
s.state.tuneTimeoutTimer.Stop()
_ = s.getVd()
}
}
statusLog.reportPTT(s.state.ptt, s.state.tune)
if s.state.setTune.pending {
s.removePendingCmd(&s.state.setTune)
return false
}
}
if s.state.getTuneStatus.pending {
s.removePendingCmd(&s.state.getTuneStatus)
return false
}
if s.state.getTransmitStatus.pending {
s.removePendingCmd(&s.state.getTransmitStatus)
return false
}
return true
}
func (s *civControlStruct) decodeVdSWRS(d []byte) bool {
switch d[0] {
case 0x02:
if len(d) < 3 {
return !s.state.getS.pending
}
sValue := (int(math.Round(((float64(int(d[1])<<8) + float64(d[2])) / 0x0241) * 18)))
sStr := "S"
if sValue <= 9 {
sStr += fmt.Sprint(sValue)
} else {
sStr += "9+"
switch sValue {
case 10:
sStr += "10"
case 11:
sStr += "20"
case 12:
sStr += "30"
case 13:
sStr += "40"
case 14:
sStr += "40"
case 15:
sStr += "40"
case 16:
sStr += "40"
case 17:
sStr += "50"
case 18:
sStr += "50"
default:
sStr += "60"
}
}
s.state.lastSReceivedAt = time.Now()
statusLog.reportS(sStr)
if s.state.getS.pending {
s.removePendingCmd(&s.state.getS)
return false
}
case 0x12:
if len(d) < 3 {
return !s.state.getSWR.pending
}
s.state.lastSWRReceivedAt = time.Now()
statusLog.reportSWR(((float64(int(d[1])<<8)+float64(d[2]))/0x0120)*2 + 1)
if s.state.getSWR.pending {
s.removePendingCmd(&s.state.getSWR)
return false
}
case 0x15:
if len(d) < 3 {
return !s.state.getVd.pending
}
statusLog.reportVd(((float64(int(d[1])<<8) + float64(d[2])) / 0x0241) * 16)
if s.state.getVd.pending {
s.removePendingCmd(&s.state.getVd)
return false
}
}
return true
}
func (s *civControlStruct) decodePreampAGCNREnabled(d []byte) bool {
switch d[0] {
case 0x02:
if len(d) < 2 {
return !s.state.getPreamp.pending && !s.state.setPreamp.pending
}
s.state.preamp = int(d[1])
statusLog.reportPreamp(s.state.preamp)
if s.state.getPreamp.pending {
s.removePendingCmd(&s.state.getPreamp)
return false
}
if s.state.setPreamp.pending {
s.removePendingCmd(&s.state.setPreamp)
return false
}
case 0x12:
if len(d) < 2 {
return !s.state.getAGC.pending && !s.state.setAGC.pending
}
s.state.agc = int(d[1])
var agc string
switch s.state.agc {
case 1:
agc = "F"
case 2:
agc = "M"
case 3:
agc = "S"
}
statusLog.reportAGC(agc)
if s.state.getAGC.pending {
s.removePendingCmd(&s.state.getAGC)
return false
}
if s.state.setAGC.pending {
s.removePendingCmd(&s.state.setAGC)
return false
}
case 0x40:
if len(d) < 2 {
return !s.state.getNREnabled.pending && !s.state.setNREnabled.pending
}
if d[1] == 1 {
s.state.nrEnabled = true
} else {
s.state.nrEnabled = false
}
statusLog.reportNREnabled(s.state.nrEnabled)
if s.state.getNREnabled.pending {
s.removePendingCmd(&s.state.getNREnabled)
return false
}
if s.state.setNREnabled.pending {
s.removePendingCmd(&s.state.setNREnabled)
return false
}
}
return true
}
func (s *civControlStruct) decodeVFOFreq(d []byte) bool {
if len(d) < 2 {
return !s.state.getMainVFOFreq.pending && !s.state.getSubVFOFreq.pending && !s.state.setSubVFOFreq.pending
}
f := s.decodeFreqData(d[1:])
switch d[0] {
default:
s.state.freq = f
statusLog.reportFrequency(s.state.freq)
s.state.bandIdx = len(civBands) - 1 // Set the band idx to GENE by default.
for i := range civBands {
if s.state.freq >= civBands[i].freqFrom && s.state.freq <= civBands[i].freqTo {
s.state.bandIdx = i
civBands[s.state.bandIdx].freq = s.state.freq
break
}
}
if s.state.getMainVFOFreq.pending {
s.removePendingCmd(&s.state.getMainVFOFreq)
return false
}
if s.state.setMainVFOFreq.pending {
s.removePendingCmd(&s.state.setMainVFOFreq)
return false
}
case 0x01:
s.state.subFreq = f
statusLog.reportSubFrequency(s.state.subFreq)
if s.state.getSubVFOFreq.pending {
s.removePendingCmd(&s.state.getSubVFOFreq)
return false
}
if s.state.setSubVFOFreq.pending {
s.removePendingCmd(&s.state.setSubVFOFreq)
return false
}
}
return true
}
func (s *civControlStruct) decodeVFOMode(d []byte) bool {
if len(d) < 2 {
return !s.state.getMainVFOMode.pending && !s.state.getSubVFOMode.pending && !s.state.setSubVFOMode.pending
}
operatingModeIdx := -1
for i := range civOperatingModes {
if civOperatingModes[i].code == d[1] {
operatingModeIdx = i
break
}
}
var dataMode bool
if len(d) > 2 && d[2] != 0 {
dataMode = true
}
filterIdx := -1
if len(d) > 3 {
filterIdx = s.decodeFilterValueToFilterIdx(d[3])
}
switch d[0] {
default:
s.state.operatingModeIdx = operatingModeIdx
s.state.dataMode = dataMode
if filterIdx >= 0 {
s.state.filterIdx = filterIdx
}
statusLog.reportMode(civOperatingModes[s.state.operatingModeIdx].name, s.state.dataMode,
civFilters[s.state.filterIdx].name)
if s.state.getMainVFOMode.pending {
s.removePendingCmd(&s.state.getMainVFOMode)
return false
}
case 0x01:
s.state.subOperatingModeIdx = operatingModeIdx
s.state.subDataMode = dataMode
s.state.subFilterIdx = filterIdx
statusLog.reportSubMode(civOperatingModes[s.state.subOperatingModeIdx].name, s.state.subDataMode,
civFilters[s.state.subFilterIdx].name)
if s.state.getSubVFOMode.pending {
s.removePendingCmd(&s.state.getSubVFOMode)
return false
}
if s.state.setSubVFOMode.pending {
s.removePendingCmd(&s.state.setSubVFOMode)
return false
}
}
return true
}
func (s *civControlStruct) initCmd(cmd *civCmd, name string, data []byte) {
*cmd = civCmd{}
cmd.name = name
cmd.cmd = data
}
func (s *civControlStruct) getPendingCmdIndex(cmd *civCmd) int {
for i := range s.state.pendingCmds {
if cmd == s.state.pendingCmds[i] {
return i
}
}
return -1
}
func (s *civControlStruct) removePendingCmd(cmd *civCmd) {
cmd.pending = false
index := s.getPendingCmdIndex(cmd)
if index < 0 {
return
}
s.state.pendingCmds[index] = s.state.pendingCmds[len(s.state.pendingCmds)-1]
s.state.pendingCmds[len(s.state.pendingCmds)-1] = nil
s.state.pendingCmds = s.state.pendingCmds[:len(s.state.pendingCmds)-1]
}
func (s *civControlStruct) sendCmd(cmd *civCmd) error {
if s.st == nil {
return nil
}
cmd.pending = true
cmd.sentAt = time.Now()
if s.getPendingCmdIndex(cmd) < 0 {
s.state.pendingCmds = append(s.state.pendingCmds, cmd)
select {
case s.newPendingCmdAdded <- true:
default:
}
}
return s.st.send(cmd.cmd)
}
func (s *civControlStruct) setPwr(percent int) error {
v := uint16(0x0255 * (float64(percent) / 100))
s.initCmd(&s.state.setPwr, "setPwr", []byte{254, 254, civAddress, 224, 0x14, 0x0a, byte(v >> 8), byte(v & 0xff), 253})
return s.sendCmd(&s.state.setPwr)
}
func (s *civControlStruct) incPwr() error {
if s.state.pwrPercent < 100 {
return s.setPwr(s.state.pwrPercent + 1)
}
return nil
}
func (s *civControlStruct) decPwr() error {
if s.state.pwrPercent > 0 {
return s.setPwr(s.state.pwrPercent - 1)
}
return nil
}
func (s *civControlStruct) setRFGain(percent int) error {
v := uint16(0x0255 * (float64(percent) / 100))
s.initCmd(&s.state.setRFGain, "setRFGain", []byte{254, 254, civAddress, 224, 0x14, 0x02, byte(v >> 8), byte(v & 0xff), 253})
return s.sendCmd(&s.state.setRFGain)
}
func (s *civControlStruct) incRFGain() error {
if s.state.rfGainPercent < 100 {
return s.setRFGain(s.state.rfGainPercent + 1)
}
return nil
}
func (s *civControlStruct) decRFGain() error {
if s.state.rfGainPercent > 0 {
return s.setRFGain(s.state.rfGainPercent - 1)
}
return nil
}
func (s *civControlStruct) setSQL(percent int) error {
v := uint16(0x0255 * (float64(percent) / 100))
s.initCmd(&s.state.setSQL, "setSQL", []byte{254, 254, civAddress, 224, 0x14, 0x03, byte(v >> 8), byte(v & 0xff), 253})
return s.sendCmd(&s.state.setSQL)
}
func (s *civControlStruct) incSQL() error {
if s.state.sqlPercent < 100 {
return s.setSQL(s.state.sqlPercent + 1)
}
return nil
}
func (s *civControlStruct) decSQL() error {
if s.state.sqlPercent > 0 {
return s.setSQL(s.state.sqlPercent - 1)
}
return nil
}
func (s *civControlStruct) setNR(percent int) error {
if !s.state.nrEnabled {
if err := s.toggleNR(); err != nil {
return err
}
}
v := uint16(0x0255 * (float64(percent) / 100))
s.initCmd(&s.state.setNR, "setNR", []byte{254, 254, civAddress, 224, 0x14, 0x06, byte(v >> 8), byte(v & 0xff), 253})
return s.sendCmd(&s.state.setNR)
}
func (s *civControlStruct) incNR() error {
if s.state.nrPercent < 100 {
return s.setNR(s.state.nrPercent + 1)
}
return nil
}
func (s *civControlStruct) decNR() error {
if s.state.nrPercent > 0 {
return s.setNR(s.state.nrPercent - 1)
}
return nil
}
func (s *civControlStruct) getDigit(v uint, n int) byte {
f := float64(v)
for n > 0 {
f /= 10
n--
}
return byte(uint(f) % 10)
}
func (s *civControlStruct) incFreq() error {
return s.setMainVFOFreq(s.state.freq + s.state.ts)
}
func (s *civControlStruct) decFreq() error {
return s.setMainVFOFreq(s.state.freq - s.state.ts)
}
func (s *civControlStruct) encodeFreqData(f uint) (b [5]byte) {
v0 := s.getDigit(f, 9)
v1 := s.getDigit(f, 8)
b[4] = v0<<4 | v1
v0 = s.getDigit(f, 7)
v1 = s.getDigit(f, 6)
b[3] = v0<<4 | v1
v0 = s.getDigit(f, 5)
v1 = s.getDigit(f, 4)
b[2] = v0<<4 | v1
v0 = s.getDigit(f, 3)
v1 = s.getDigit(f, 2)
b[1] = v0<<4 | v1
v0 = s.getDigit(f, 1)
v1 = s.getDigit(f, 0)
b[0] = v0<<4 | v1
return
}
func (s *civControlStruct) setMainVFOFreq(f uint) error {
b := s.encodeFreqData(f)
s.initCmd(&s.state.setMainVFOFreq, "setMainVFOFreq", []byte{254, 254, civAddress, 224, 0x25, 0x00, b[0], b[1], b[2], b[3], b[4], 253})
return s.sendCmd(&s.state.setMainVFOFreq)
}
func (s *civControlStruct) setSubVFOFreq(f uint) error {
b := s.encodeFreqData(f)
s.initCmd(&s.state.setSubVFOFreq, "setSubVFOFreq", []byte{254, 254, civAddress, 224, 0x25, 0x01, b[0], b[1], b[2], b[3], b[4], 253})
return s.sendCmd(&s.state.setSubVFOFreq)
}
func (s *civControlStruct) incOperatingMode() error {
s.state.operatingModeIdx++
if s.state.operatingModeIdx >= len(civOperatingModes) {
s.state.operatingModeIdx = 0
}
return civControl.setOperatingModeAndFilter(civOperatingModes[s.state.operatingModeIdx].code,
civFilters[s.state.filterIdx].code)
}
func (s *civControlStruct) decOperatingMode() error {
s.state.operatingModeIdx--
if s.state.operatingModeIdx < 0 {
s.state.operatingModeIdx = len(civOperatingModes) - 1
}
return civControl.setOperatingModeAndFilter(civOperatingModes[s.state.operatingModeIdx].code,
civFilters[s.state.filterIdx].code)
}
func (s *civControlStruct) incFilter() error {
s.state.filterIdx++
if s.state.filterIdx >= len(civFilters) {
s.state.filterIdx = 0
}
return civControl.setOperatingModeAndFilter(civOperatingModes[s.state.operatingModeIdx].code,
civFilters[s.state.filterIdx].code)
}
func (s *civControlStruct) decFilter() error {
s.state.filterIdx--
if s.state.filterIdx < 0 {
s.state.filterIdx = len(civFilters) - 1
}
return civControl.setOperatingModeAndFilter(civOperatingModes[s.state.operatingModeIdx].code,
civFilters[s.state.filterIdx].code)
}
func (s *civControlStruct) setOperatingModeAndFilter(modeCode, filterCode byte) error {
s.initCmd(&s.state.setMode, "setMode", []byte{254, 254, civAddress, 224, 0x06, modeCode, filterCode, 253})
if err := s.sendCmd(&s.state.setMode); err != nil {
return err
}
return s.getBothVFOMode()
}
func (s *civControlStruct) setSubVFOMode(modeCode, dataMode, filterCode byte) error {
s.initCmd(&s.state.setSubVFOMode, "setSubVFOMode", []byte{254, 254, civAddress, 224, 0x26, 0x01, modeCode, dataMode, filterCode, 253})
return s.sendCmd(&s.state.setSubVFOMode)
}
func (s *civControlStruct) setPTT(enable bool) error {
var b byte
if enable {
b = 1
s.state.pttTimeoutTimer = time.AfterFunc(pttTimeout, func() {
_ = s.setPTT(false)
})
}
s.initCmd(&s.state.setPTT, "setPTT", []byte{254, 254, civAddress, 224, 0x1c, 0, b, 253})
return s.sendCmd(&s.state.setPTT)
}
func (s *civControlStruct) setTune(enable bool) error {
if s.state.ptt {
return nil
}
var b byte
if enable {
b = 2
s.state.tuneTimeoutTimer = time.AfterFunc(tuneTimeout, func() {
_ = s.setTune(false)
})
} else {
b = 1
}
s.initCmd(&s.state.setTune, "setTune", []byte{254, 254, civAddress, 224, 0x1c, 1, b, 253})
return s.sendCmd(&s.state.setTune)
}
func (s *civControlStruct) toggleTune() error {
return s.setTune(!s.state.tune)
}
func (s *civControlStruct) setDataMode(enable bool) error {
var b byte
var f byte
if enable {
b = 1
f = 1
} else {
b = 0
f = 0
}
s.initCmd(&s.state.setDataMode, "setDataMode", []byte{254, 254, civAddress, 224, 0x1a, 0x06, b, f, 253})
return s.sendCmd(&s.state.setDataMode)
}
func (s *civControlStruct) toggleDataMode() error {
return s.setDataMode(!s.state.dataMode)
}
func (s *civControlStruct) incBand() error {
i := s.state.bandIdx + 1
if i >= len(civBands) {
i = 0
}
f := civBands[i].freq
if f == 0 {
f = (civBands[i].freqFrom + civBands[i].freqTo) / 2
}
return s.setMainVFOFreq(f)
}
func (s *civControlStruct) decBand() error {
i := s.state.bandIdx - 1
if i < 0 {
i = len(civBands) - 1
}
f := civBands[i].freq
if f == 0 {
f = civBands[i].freqFrom
}
return s.setMainVFOFreq(f)
}
func (s *civControlStruct) togglePreamp() error {
b := byte(s.state.preamp + 1)
if b > 2 {
b = 0
}
s.initCmd(&s.state.setPreamp, "setPreamp", []byte{254, 254, civAddress, 224, 0x16, 0x02, b, 253})
return s.sendCmd(&s.state.setPreamp)
}
func (s *civControlStruct) toggleAGC() error {
b := byte(s.state.agc + 1)
if b > 3 {
b = 1
}
s.initCmd(&s.state.setAGC, "setAGC", []byte{254, 254, civAddress, 224, 0x16, 0x12, b, 253})
return s.sendCmd(&s.state.setAGC)
}
func (s *civControlStruct) toggleNR() error {
var b byte
if !s.state.nrEnabled {
b = 1
}
s.initCmd(&s.state.setNREnabled, "setNREnabled", []byte{254, 254, civAddress, 224, 0x16, 0x40, b, 253})
return s.sendCmd(&s.state.setNREnabled)
}
func (s *civControlStruct) setTS(b byte) error {
s.initCmd(&s.state.setTS, "setTS", []byte{254, 254, civAddress, 224, 0x10, b, 253})
return s.sendCmd(&s.state.setTS)
}
func (s *civControlStruct) incTS() error {
var b byte
if s.state.tsValue == 13 {
b = 0
} else {
b = s.state.tsValue + 1
}
return s.setTS(b)
}
func (s *civControlStruct) decTS() error {
var b byte
if s.state.tsValue == 0 {
b = 13
} else {
b = s.state.tsValue - 1
}
return s.setTS(b)
}
func (s *civControlStruct) setVFO(nr byte) error {
s.initCmd(&s.state.setVFO, "setVFO", []byte{254, 254, civAddress, 224, 0x07, nr, 253})
if err := s.sendCmd(&s.state.setVFO); err != nil {
return err
}
return s.getBothVFOMode()
}
func (s *civControlStruct) toggleVFO() error {
var b byte
if !s.state.vfoBActive {
b = 1
}
return s.setVFO(b)
}
func (s *civControlStruct) setSplit(mode splitMode) error {
var b byte
switch mode {
default:
b = 0x10
case splitModeOn:
b = 0x01
case splitModeDUPMinus:
b = 0x11
case splitModeDUPPlus:
b = 0x12
}
s.initCmd(&s.state.setSplit, "setSplit", []byte{254, 254, civAddress, 224, 0x0f, b, 253})
return s.sendCmd(&s.state.setSplit)
}
func (s *civControlStruct) toggleSplit() error {
var mode splitMode
switch s.state.splitMode {
case splitModeOff:
mode = splitModeOn
case splitModeOn:
mode = splitModeDUPMinus
case splitModeDUPMinus:
mode = splitModeDUPPlus
default:
mode = splitModeOff
}
return s.setSplit(mode)
}
// func (s *civControlStruct) getFreq() error {
// s.initCmd(&s.state.getFreq, "getFreq", []byte{254, 254, civAddress, 224, 3, 253})
// return s.sendCmd(&s.state.getFreq)
// }
// func (s *civControlStruct) getMode() error {
// s.initCmd(&s.state.getMode, "getMode", []byte{254, 254, civAddress, 224, 4, 253})
// return s.sendCmd(&s.state.getMode)
// }
// func (s *civControlStruct) getDataMode() error {
// s.initCmd(&s.state.getDataMode, "getDataMode", []byte{254, 254, civAddress, 224, 0x1a, 0x06, 253})
// return s.sendCmd(&s.state.getDataMode)
// }
func (s *civControlStruct) getPwr() error {
s.initCmd(&s.state.getPwr, "getPwr", []byte{254, 254, civAddress, 224, 0x14, 0x0a, 253})
return s.sendCmd(&s.state.getPwr)
}
func (s *civControlStruct) getTransmitStatus() error {
s.initCmd(&s.state.getTransmitStatus, "getTransmitStatus", []byte{254, 254, civAddress, 224, 0x1c, 0, 253})
if err := s.sendCmd(&s.state.getTransmitStatus); err != nil {
return err
}
s.initCmd(&s.state.getTuneStatus, "getTuneStatus", []byte{254, 254, civAddress, 224, 0x1c, 1, 253})
return s.sendCmd(&s.state.getTuneStatus)
}
func (s *civControlStruct) getPreamp() error {
s.initCmd(&s.state.getPreamp, "getPreamp", []byte{254, 254, civAddress, 224, 0x16, 0x02, 253})
return s.sendCmd(&s.state.getPreamp)
}
func (s *civControlStruct) getAGC() error {
s.initCmd(&s.state.getAGC, "getAGC", []byte{254, 254, civAddress, 224, 0x16, 0x12, 253})
return s.sendCmd(&s.state.getAGC)
}
func (s *civControlStruct) getVd() error {
s.initCmd(&s.state.getVd, "getVd", []byte{254, 254, civAddress, 224, 0x15, 0x15, 253})
return s.sendCmd(&s.state.getVd)
}
func (s *civControlStruct) getS() error {
s.initCmd(&s.state.getS, "getS", []byte{254, 254, civAddress, 224, 0x15, 0x02, 253})
return s.sendCmd(&s.state.getS)
}
func (s *civControlStruct) getOVF() error {
s.initCmd(&s.state.getOVF, "getOVF", []byte{254, 254, civAddress, 224, 0x1a, 0x09, 253})
return s.sendCmd(&s.state.getOVF)
}
func (s *civControlStruct) getSWR() error {
s.initCmd(&s.state.getSWR, "getSWR", []byte{254, 254, civAddress, 224, 0x15, 0x12, 253})
return s.sendCmd(&s.state.getSWR)
}
func (s *civControlStruct) getTS() error {
s.initCmd(&s.state.getTS, "getTS", []byte{254, 254, civAddress, 224, 0x10, 253})
return s.sendCmd(&s.state.getTS)
}
func (s *civControlStruct) getRFGain() error {
s.initCmd(&s.state.getRFGain, "getRFGain", []byte{254, 254, civAddress, 224, 0x14, 0x02, 253})
return s.sendCmd(&s.state.getRFGain)
}
func (s *civControlStruct) getSQL() error {
s.initCmd(&s.state.getSQL, "getSQL", []byte{254, 254, civAddress, 224, 0x14, 0x03, 253})
return s.sendCmd(&s.state.getSQL)
}
func (s *civControlStruct) getNR() error {
s.initCmd(&s.state.getNR, "getNR", []byte{254, 254, civAddress, 224, 0x14, 0x06, 253})
return s.sendCmd(&s.state.getNR)
}
func (s *civControlStruct) getNREnabled() error {
s.initCmd(&s.state.getNREnabled, "getNREnabled", []byte{254, 254, civAddress, 224, 0x16, 0x40, 253})
return s.sendCmd(&s.state.getNREnabled)
}
func (s *civControlStruct) getSplit() error {
s.initCmd(&s.state.getSplit, "getSplit", []byte{254, 254, civAddress, 224, 0x0f, 253})
return s.sendCmd(&s.state.getSplit)
}
func (s *civControlStruct) getBothVFOFreq() error {
s.initCmd(&s.state.getMainVFOFreq, "getMainVFOFreq", []byte{254, 254, civAddress, 224, 0x25, 0, 253})
if err := s.sendCmd(&s.state.getMainVFOFreq); err != nil {
return err
}
s.initCmd(&s.state.getSubVFOFreq, "getSubVFOFreq", []byte{254, 254, civAddress, 224, 0x25, 1, 253})
return s.sendCmd(&s.state.getSubVFOFreq)
}
func (s *civControlStruct) getBothVFOMode() error {
s.initCmd(&s.state.getMainVFOMode, "getMainVFOMode", []byte{254, 254, civAddress, 224, 0x26, 0, 253})
if err := s.sendCmd(&s.state.getMainVFOMode); err != nil {
return err
}
s.initCmd(&s.state.getSubVFOMode, "getSubVFOMode", []byte{254, 254, civAddress, 224, 0x26, 1, 253})
return s.sendCmd(&s.state.getSubVFOMode)
}
func (s *civControlStruct) loop() {
for {
s.state.mutex.Lock()
nextPendingCmdTimeout := time.Hour
for i := range s.state.pendingCmds {
diff := time.Since(s.state.pendingCmds[i].sentAt)
if diff >= commandRetryTimeout {
nextPendingCmdTimeout = 0
break
}
if diff < nextPendingCmdTimeout {
nextPendingCmdTimeout = diff
}
}
s.state.mutex.Unlock()
select {
case <-s.deinitNeeded:
s.deinitFinished <- true
return
case <-time.After(statusPollInterval):
if s.state.ptt || s.state.tune {
if !s.state.getSWR.pending && time.Since(s.state.lastSWRReceivedAt) >= statusPollInterval {
_ = s.getSWR()
}
} else {
if !s.state.getS.pending && time.Since(s.state.lastSReceivedAt) >= statusPollInterval {
_ = s.getS()
}
if !s.state.getOVF.pending && time.Since(s.state.lastOVFReceivedAt) >= statusPollInterval {
_ = s.getOVF()
}
}
if !s.state.getMainVFOFreq.pending && !s.state.getSubVFOFreq.pending &&
time.Since(s.state.lastVFOFreqReceivedAt) >= statusPollInterval {
_ = s.getBothVFOFreq()
}
case <-s.resetSReadTimer:
case <-s.newPendingCmdAdded:
case <-time.After(nextPendingCmdTimeout):
s.state.mutex.Lock()
for _, cmd := range s.state.pendingCmds {
if time.Since(cmd.sentAt) >= commandRetryTimeout {
log.Debug("retrying cmd send ", cmd.name)
_ = s.sendCmd(cmd)
}
}
s.state.mutex.Unlock()
}
}
}
func (s *civControlStruct) init(st *serialStream) error {
s.st = st
if err := s.getBothVFOFreq(); err != nil {
return err
}
if err := s.getBothVFOMode(); err != nil {
return err
}
if err := s.getPwr(); err != nil {
return err
}
if err := s.getTransmitStatus(); err != nil {
return err
}
if err := s.getPreamp(); err != nil {
return err
}
if err := s.getAGC(); err != nil {
return err
}
if err := s.getVd(); err != nil {
return err
}
if err := s.getS(); err != nil {
return err
}
if err := s.getOVF(); err != nil {
return err
}
if err := s.getSWR(); err != nil {
return err
}
if err := s.getTS(); err != nil {
return err
}
if err := s.getRFGain(); err != nil {
return err
}
if err := s.getSQL(); err != nil {
return err
}
if err := s.getNR(); err != nil {
return err
}
if err := s.getNREnabled(); err != nil {
return err
}
if err := s.getSplit(); err != nil {
return err
}
s.deinitNeeded = make(chan bool)
s.deinitFinished = make(chan bool)
s.resetSReadTimer = make(chan bool)
s.newPendingCmdAdded = make(chan bool)
go s.loop()
return nil
}
func (s *civControlStruct) deinit() {
if s.deinitNeeded == nil {
return
}
s.deinitNeeded <- true
<-s.deinitFinished
s.deinitNeeded = nil
s.st = nil
}
kappanhang-1.3/cmdrunner.go 0000664 0000000 0000000 00000003142 14003632250 0016004 0 ustar 00root root 0000000 0000000 package main
import (
"os/exec"
"strings"
"syscall"
"time"
)
const startCmdDelay = time.Second
type cmdRunner struct {
restartNeeded chan bool
runEndNeeded chan bool
runEndFinished chan bool
}
var runCmdRunner cmdRunner
var serialCmdRunner cmdRunner
func (c *cmdRunner) kill(cmd *exec.Cmd) {
err := cmd.Process.Kill()
if err != nil {
_ = cmd.Process.Signal(syscall.SIGKILL)
}
}
func (c *cmdRunner) run(cmdLine string) {
var cmd *exec.Cmd
defer func() {
if cmd != nil {
c.kill(cmd)
}
c.runEndFinished <- true
}()
s := strings.Split(cmdLine, " ")
for {
select {
case <-c.runEndNeeded:
return
case <-time.After(startCmdDelay):
}
cmd = exec.Command(s[0], s[1:]...)
err := cmd.Start()
if err != nil {
log.Error("error starting ", cmd)
continue
}
log.Print("started: ", cmd)
finishedChan := make(chan error)
go func() {
finishedChan <- cmd.Wait()
}()
select {
case <-c.restartNeeded:
log.Debug("restarting ", cmd)
c.kill(cmd)
case err := <-finishedChan:
if err != nil {
log.Error(cmd, " error: ", err)
}
case <-c.runEndNeeded:
return
}
}
}
func (c *cmdRunner) startIfNeeded(cmdLine string) {
if c.runEndNeeded != nil || cmdLine == "" || cmdLine == "-" {
return
}
c.restartNeeded = make(chan bool)
c.runEndNeeded = make(chan bool)
c.runEndFinished = make(chan bool)
go c.run(cmdLine)
}
// func (c *cmdRunner) restart() {
// if c.restartNeeded == nil {
// return
// }
// c.restartNeeded <- true
// }
func (c *cmdRunner) stop() {
if c.runEndNeeded == nil {
return
}
c.runEndNeeded <- true
<-c.runEndFinished
}
kappanhang-1.3/controlstream.go 0000664 0000000 0000000 00000037026 14003632250 0016713 0 ustar 00root root 0000000 0000000 package main
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"time"
)
const controlStreamPort = 50001
const serialStreamPort = 50002
const audioStreamPort = 50003
const reauthInterval = time.Minute
const reauthTimeout = 3 * time.Second
type controlStream struct {
common streamCommon
serial serialStream
audio audioStream
deinitNeededChan chan bool
deinitFinishedChan chan bool
authInnerSendSeq uint16
authID [6]byte
gotAuthID bool
authOk bool
a8replyID [16]byte
gotA8ReplyID bool
serialAndAudioStreamOpened bool
deinitializing bool
requestSerialAndAudioTimeout *time.Timer
reauthTimeoutTimer *time.Timer
}
func (s *controlStream) sendPktLogin() error {
// The reply to the auth packet will contain a 6 bytes long auth ID with the first 2 bytes set to our ID.
var authStartID [2]byte
if _, err := rand.Read(authStartID[:]); err != nil {
return err
}
usernameEncoded := passcode(username)
passwordEncoded := passcode(password)
p := []byte{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
byte(s.common.localSID >> 24), byte(s.common.localSID >> 16), byte(s.common.localSID >> 8), byte(s.common.localSID),
byte(s.common.remoteSID >> 24), byte(s.common.remoteSID >> 16), byte(s.common.remoteSID >> 8), byte(s.common.remoteSID),
0x00, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, byte(s.authInnerSendSeq),
byte(s.authInnerSendSeq >> 8), 0x00, authStartID[0], authStartID[1], 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
usernameEncoded[0], usernameEncoded[1], usernameEncoded[2], usernameEncoded[3],
usernameEncoded[4], usernameEncoded[5], usernameEncoded[6], usernameEncoded[7],
usernameEncoded[8], usernameEncoded[9], usernameEncoded[10], usernameEncoded[11],
usernameEncoded[12], usernameEncoded[13], usernameEncoded[14], usernameEncoded[15],
passwordEncoded[0], passwordEncoded[1], passwordEncoded[2], passwordEncoded[3],
passwordEncoded[4], passwordEncoded[5], passwordEncoded[6], passwordEncoded[7],
passwordEncoded[8], passwordEncoded[9], passwordEncoded[10], passwordEncoded[11],
passwordEncoded[12], passwordEncoded[13], passwordEncoded[14], passwordEncoded[15],
0x69, 0x63, 0x6f, 0x6d, 0x2d, 0x70, 0x63, 0x00, // icom-pc in plain text
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
if err := s.common.pkt0.sendTrackedPacket(&s.common, p); err != nil {
return err
}
s.authInnerSendSeq++
return nil
}
func (s *controlStream) sendPktAuth(magic byte) error {
// Example request from PC: 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00,
// 0xbb, 0x41, 0x3f, 0x2b, 0xe6, 0xb2, 0x7b, 0x7b,
// 0x00, 0x00, 0x00, 0x30, 0x01, 0x05, 0x00, 0x02,
// 0x00, 0x00, 0x5d, 0x37, 0x12, 0x82, 0x3b, 0xde,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
// Example reply from radio: 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00,
// 0xe6, 0xb2, 0x7b, 0x7b, 0xbb, 0x41, 0x3f, 0x2b,
// 0x00, 0x00, 0x00, 0x30, 0x02, 0x05, 0x00, 0x02,
// 0x00, 0x00, 0x5d, 0x37, 0x12, 0x82, 0x3b, 0xde,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
p := []byte{0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
byte(s.common.localSID >> 24), byte(s.common.localSID >> 16), byte(s.common.localSID >> 8), byte(s.common.localSID),
byte(s.common.remoteSID >> 24), byte(s.common.remoteSID >> 16), byte(s.common.remoteSID >> 8), byte(s.common.remoteSID),
0x00, 0x00, 0x00, 0x30, 0x01, magic, 0x00, byte(s.authInnerSendSeq),
byte(s.authInnerSendSeq >> 8), 0x00, s.authID[0], s.authID[1], s.authID[2], s.authID[3], s.authID[4], s.authID[5],
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
if err := s.common.pkt0.sendTrackedPacket(&s.common, p); err != nil {
return err
}
s.authInnerSendSeq++
return nil
}
func (s *controlStream) sendRequestSerialAndAudio() error {
log.Debug("requesting serial and audio stream")
txSeqBufLengthMs := uint16(txSeqBufLength.Milliseconds())
usernameEncoded := passcode(username)
p := []byte{0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
byte(s.common.localSID >> 24), byte(s.common.localSID >> 16), byte(s.common.localSID >> 8), byte(s.common.localSID),
byte(s.common.remoteSID >> 24), byte(s.common.remoteSID >> 16), byte(s.common.remoteSID >> 8), byte(s.common.remoteSID),
0x00, 0x00, 0x00, 0x80, 0x01, 0x03, 0x00, byte(s.authInnerSendSeq),
byte(s.authInnerSendSeq >> 8), 0x00, s.authID[0], s.authID[1], s.authID[2], s.authID[3], s.authID[4], s.authID[5],
s.a8replyID[0], s.a8replyID[1], s.a8replyID[2], s.a8replyID[3], s.a8replyID[4], s.a8replyID[5], s.a8replyID[6], s.a8replyID[7],
s.a8replyID[8], s.a8replyID[9], s.a8replyID[10], s.a8replyID[11], s.a8replyID[12], s.a8replyID[13], s.a8replyID[14], s.a8replyID[15],
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x49, 0x43, 0x2d, 0x37, 0x30, 0x35, 0x00, 0x00, // IC-705 in plain text
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
usernameEncoded[0], usernameEncoded[1], usernameEncoded[2], usernameEncoded[3],
usernameEncoded[4], usernameEncoded[5], usernameEncoded[6], usernameEncoded[7],
usernameEncoded[8], usernameEncoded[9], usernameEncoded[10], usernameEncoded[11],
usernameEncoded[12], usernameEncoded[13], usernameEncoded[14], usernameEncoded[15],
0x01, 0x01, 0x04, 0x04, 0x00, 0x00, byte(audioSampleRate >> 8), byte(audioSampleRate & 0xff),
0x00, 0x00, byte(audioSampleRate >> 8), byte(audioSampleRate & 0xff),
0x00, 0x00, byte(serialStreamPort >> 8), byte(serialStreamPort & 0xff),
0x00, 0x00, byte(audioStreamPort >> 8), byte(audioStreamPort & 0xff), 0x00, 0x00,
byte(txSeqBufLengthMs >> 8), byte(txSeqBufLengthMs & 0xff), 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
if err := s.common.pkt0.sendTrackedPacket(&s.common, p); err != nil {
return err
}
s.authInnerSendSeq++
return nil
}
func (s *controlStream) sendRequestSerialAndAudioIfPossible() {
if !s.serialAndAudioStreamOpened && s.authOk && s.gotA8ReplyID {
if err := s.sendRequestSerialAndAudio(); err != nil {
reportError(err)
}
}
}
func (s *controlStream) handleRead(r []byte) error {
switch len(r) {
case 168:
if bytes.Equal(r[:6], []byte{0xa8, 0x00, 0x00, 0x00, 0x00, 0x00}) {
// Example answer from radio:
// 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00,
// 0x01, 0x13, 0x11, 0x18, 0x38, 0xff, 0x55, 0x7d,
// 0x00, 0x00, 0x00, 0x98, 0x02, 0x02, 0x00, 0x07,
// 0x00, 0x00, 0x7f, 0x91, 0x00, 0x00, 0x4f, 0x0d,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x01, 0x93, 0x8a, 0x01, 0x24, 0x17, 0x64,
// 0xbc, 0x4b, 0xa3, 0xa0, 0x13, 0x58, 0x41, 0x04,
// 0x58, 0x2d, 0x49, 0x43, 0x2d, 0x37, 0x30, 0x35,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x49, 0x43, 0x4f, 0x4d, 0x5f, 0x56,
// 0x41, 0x55, 0x44, 0x49, 0x4f, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x3f, 0x3f, 0xa4, 0x01, 0xff, 0x01,
// 0xff, 0x01, 0x01, 0x01, 0x00, 0x00, 0x4b, 0x00,
// 0x01, 0x50, 0x00, 0xb8, 0x0b, 0x00, 0x00, 0x00
copy(s.a8replyID[:], r[66:82])
s.gotA8ReplyID = true
}
case 64:
if bytes.Equal(r[:6], []byte{0x40, 0x00, 0x00, 0x00, 0x00, 0x00}) {
// Example answer from radio: 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
// 0x33, 0x60, 0xd4, 0xe5, 0xf4, 0x67, 0x86, 0xe1,
// 0x00, 0x00, 0x00, 0x30, 0x02, 0x05, 0x00, 0x02,
// 0x00, 0x00, 0x35, 0x34, 0x76, 0x11, 0xb9, 0xd0,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
s.reauthTimeoutTimer.Stop()
log.Debug("auth ok")
if r[21] == 0x05 { // Answer for our second auth?
s.authOk = true
s.sendRequestSerialAndAudioIfPossible()
}
}
case 80:
if bytes.Equal(r[:6], []byte{0x50, 0x00, 0x00, 0x00, 0x00, 0x00}) {
// Example answer from radio: 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00,
// 0x86, 0x1f, 0x2f, 0xcc, 0x03, 0x03, 0x89, 0x29,
// 0x00, 0x00, 0x00, 0x40, 0x02, 0x03, 0x00, 0x52,
// 0x00, 0x00, 0xf8, 0xad, 0x06, 0x8d, 0xda, 0x7b,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
// 0x80, 0x00, 0x00, 0x90, 0xc7, 0x0e, 0x86, 0x01,
// 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
if bytes.Equal(r[48:51], []byte{0xff, 0xff, 0xff}) {
if !s.serialAndAudioStreamOpened {
return errors.New("auth failed, try rebooting the radio")
}
return errors.New("auth failed")
}
if bytes.Equal(r[48:51], []byte{0x00, 0x00, 0x00}) && r[64] == 0x01 {
return errors.New("got radio disconnected")
}
}
case 144:
if !s.serialAndAudioStreamOpened && bytes.Equal(r[:6], []byte{0x90, 0x00, 0x00, 0x00, 0x00, 0x00}) && r[96] == 1 {
// Example answer:
// 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00,
// 0xc6, 0x5f, 0x6f, 0x0c, 0x5f, 0x8b, 0x1e, 0x89,
// 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x31, 0x30, 0x31, 0x47, 0x39, 0x07,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
// 0x80, 0x00, 0x00, 0x90, 0xc7, 0x0e, 0x86, 0x01,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x49, 0x43, 0x2d, 0x37, 0x30, 0x35, 0x00, 0x00, // IC-705 in plain text
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x01, 0x00, 0x00, 0x00, 0x69, 0x63, 0x6f, 0x6d,
// 0x2d, 0x70, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x03, 0x03,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
s.requestSerialAndAudioTimeout.Stop()
devName := parseNullTerminatedString(r[64:])
log.Print("got serial and audio request success, device name: ", devName)
// Stuff can change in the meantime because of a previous login...
s.common.remoteSID = binary.BigEndian.Uint32(r[8:12])
s.common.localSID = binary.BigEndian.Uint32(r[12:16])
copy(s.authID[:], r[26:32])
s.gotAuthID = true
statusLog.startPeriodicPrint()
if err := s.serial.init(devName); err != nil {
return errors.New("serial/" + err.Error())
}
if err := s.audio.init(devName); err != nil {
return errors.New("audio/" + err.Error())
}
s.serialAndAudioStreamOpened = true
runCmdRunner.startIfNeeded(runCmd)
if enableSerialDevice {
serialCmdRunner.startIfNeeded(runCmdOnSerialPortCreated)
}
if err := rigctld.initIfNeeded(); err != nil {
return err
}
}
}
return nil
}
func (s *controlStream) loop() {
netstat.reset()
s.reauthTimeoutTimer = time.NewTimer(0)
<-s.reauthTimeoutTimer.C
reauthTicker := time.NewTicker(reauthInterval)
for {
select {
case r := <-s.common.readChan:
if !s.deinitializing {
if err := s.handleRead(r); err != nil {
reportError(err)
}
}
case <-reauthTicker.C:
log.Debug("sending auth")
s.reauthTimeoutTimer.Reset(reauthTimeout)
if err := s.sendPktAuth(0x05); err != nil {
reportError(err)
}
case <-s.reauthTimeoutTimer.C:
log.Error("auth timeout, audio/serial stream may stop")
case <-s.deinitNeededChan:
s.deinitFinishedChan <- true
return
}
}
}
func (s *controlStream) init() error {
log.Debug("init")
if err := s.common.init("control", controlStreamPort); err != nil {
return err
}
if err := s.common.start(); err != nil {
return err
}
s.common.pkt0.init(&s.common)
if err := s.sendPktLogin(); err != nil {
return err
}
log.Debug("expecting login answer")
// Example success auth packet: 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
// 0xe6, 0xb2, 0x7b, 0x7b, 0xbb, 0x41, 0x3f, 0x2b,
// 0x00, 0x00, 0x00, 0x50, 0x02, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x5d, 0x37, 0x12, 0x82, 0x3b, 0xde,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x46, 0x54, 0x54, 0x48, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
r, err := s.common.expect(96, []byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00})
if err != nil {
return err
}
if bytes.Equal(r[48:52], []byte{0xff, 0xff, 0xff, 0xfe}) {
return errors.New("invalid username/password")
}
s.common.pkt7.startPeriodicSend(&s.common, 2, false)
copy(s.authID[:], r[26:32])
s.gotAuthID = true
if err := s.sendPktAuth(0x02); err != nil {
return err
}
log.Debug("login ok, first auth sent...")
s.common.pkt0.startPeriodicSend(&s.common)
if err := s.sendPktAuth(0x05); err != nil {
return err
}
log.Debug("second auth sent...")
s.requestSerialAndAudioTimeout = time.AfterFunc(5*time.Second, func() {
reportError(errors.New("login/serial/audio request timeout"))
})
s.deinitNeededChan = make(chan bool)
s.deinitFinishedChan = make(chan bool)
go s.loop()
return nil
}
func (s *controlStream) deinit() {
s.deinitializing = true
s.serialAndAudioStreamOpened = false
statusLog.stopPeriodicPrint()
if s.deinitNeededChan != nil {
s.deinitNeededChan <- true
<-s.deinitFinishedChan
}
if s.requestSerialAndAudioTimeout != nil {
s.requestSerialAndAudioTimeout.Stop()
s.requestSerialAndAudioTimeout = nil
}
if s.gotAuthID && s.common.gotRemoteSID && s.common.conn != nil {
log.Debug("sending deauth")
_ = s.sendPktAuth(0x01)
// Waiting a little bit to make sure the radio can send retransmit requests.
time.Sleep(500 * time.Millisecond)
}
s.common.deinit()
s.serial.deinit()
s.audio.deinit()
}
kappanhang-1.3/demo.gif 0000664 0000000 0000000 00004575437 14003632250 0015124 0 ustar 00root root 0000000 0000000 GIF89adZ÷ ¢¢¢ ŽŽŽQQQ”””mmm£££---)))˜˜˜¤¤¤›››BBB‹‹‹,,,NNN666———œœœ………///%%%fffššš~~~^^^§§§<<< ƒƒ„>>>™™™YYXWWW†††[[[LLLoooŠŠŠ''&¨¨¨’’’CCCDDDcccvvv*** [ª†ââá}}}pppžžždddJJJ888998555231FFF…††çççääãÈÜ—ÕÕÐ```uuuHHHOOO111]]]hhhsss\\\€ŒI¤zKŸŸŸeee@@@###w«$$$¡¡¡o{Q'''tttô^XqqqÈÉÆ222èWW¦¦¦XXX+++ÄÚg???332äääàààŒŒŒ²mP&&%Âån„……nnnÛZW»Þ>¨K«Ì=»ßdû[[ÄÏ?µ×>ááߺPÒeVˆhe`³ã;ÕÕÑððï898ÌÓDÉévÏÕbÓë“ÑݱîÂIÞÞÞôôôéíÛîîîòòòÚÚÚêêéîñãÈͺ÷¿J¡Ù=Èã†áááááàóóóÖ>ÔÎFÇÔ§Ãa]Òä£ñññêêêÍÍ͹^\ÃÛCÍë„õõõÂÂÂííí˜Ð@l³}Ï?ˆ°øªLööö·äHùùù÷÷÷øøøðððûµL ÉUÂÁ>þ½Kû’RÝì´Ùí¢”O8lL³··×ÆË¥·zê©¿-„C¸sq“‘¸Ör¤†€üC„ì•L•ž©~åÛÞ˜G¬|‡ÄººÍÛÝ⫽ʰ¹ëµÆâÓØÏÁÆç„MÏTj«^º½¾¯zs³ÍwI†[ƒyãèÓÑ·¿ØºÃXe\8¢¦£¨¹‹Q—_®Àƒä³ÃÚ¥¶¼®³ó£¸¢kô¦¢ë†©Êfýýýüüüûûûúúúþþþ©’’“ž\©__œ‘Gýcoã±Kž°kh‘MÀPJ²‹D΀IóŸMü~€˜ƒ{éz ÑßhJuŸ“…ø©”!ÿNETSCAPE2.0 !ù ÿ , dZ ÿ Ù H° Áƒè*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—cÊœI¦Í›8sêÜɳ§ÏŸ@ƒ
J´hÍ£FÓ)]Ê´©Ó§P£JJµªÕ«X³jÝʵ«×¯`ÊK¶¬Ù³hÓª]˶۷HãÂUG·®Ý»xóêÝË·¯ß¿€L¸°áÈ+^̸±ãÇ#KžL¹²å˘3ÏMº®³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀƒ—K\óªãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»÷ïàËÿO¾¼ùóèÓ«_Ï~øfVðãËŸO¿¾ýûøóëßÏ¿¿ÿÿ (à€hà&¨à‚6èàƒF(á„VØÊ…ím”ʆvèᇠ†(âˆ$–hâ‰(¦¨âŠ,¶èâ‹0Æ(ãŒ4Öhã8æ¨ãŽ<öèã@iœ…ˆiä‘H&©ä’L6éä“PF)å”TViå•Xf©å–\véå—`†)æ˜d–iæ™h¦©¦Å]µæ"pÆ)çœtÖiçxæ©çž|öé矀*è „j衈&ªè¢Œ6êè£F*餔VÊf›Óm¢é¦®têé§ †*ꨤ–jꩨ¦ªêª¬¶êê«°Æÿ*무Öjë¸æªë®¼öêë¯À+¬¯
Wé(È&«ì²Ì6ëì³ÐF+í´ÔVkíµØf«í¶Üvëí·à†+î¸ä–kî¹è¦«îºì¶›çoœ*"¯»œÔkï½øæ«ï¾üöëï¿ ,ðÀlðÁ'¬ðÂ7ìðÃG,ñÄWlñÅg|1ž»ÅÛ® ‡,òÈ$—lòÉ(§¬òÊ,·ìòË0Ç,óÌ4×lóÍ8ç¬óÎ<÷ìóÏ@-ôÐD=¯Çú·¤Æh4íôÓPG-õÔTWmõÕXgõÖ\wíõ×`‡-öØd—möÙh§öÚl·íöÛpÇ-·¾•0 VÝ¢=ÉÞ|÷ÿí÷߀.øà„nøáˆ'®øâŒ7îøãG.ùä”Wnùå˜g®ùæœwîùçŸ7B·Ýâ}´è'¢úꬷîúë°Ç.ûì´×nûí¸ç®ûî¼÷îûïÀ/üðÄoüñÈ'¯üòÌ7ïüóÆ—,*x¡–=#Øg¯ýöÜwïý÷à‡/þøä—oþù觯þúì·ïþûðÇ/ÿüô×oÿýøç¯ÿþüÛ?„Ô§RB‘$ÑI ? €ÈÀ:ðŒ 'HÁ
Zð‚Ì 7ÈÁzðƒ ¡GHšð„(L¡
WȺð…0ü¤@²:!&^~ wÈÃúð‡@¢ÿ‡HÄ"ñˆHL¢%H ? îñ*ÖV<–‚%ZñŠXÌ¢·ÈÅ.zñ‹`cS0²(ÂTR£×ÈÆ6ºñpŒ£çØ@âkI1½tHÇ>úñ€¤ IH. Œ¦ÓÎéXÈF:ò‘Œ¤$'IÇ<íh"š ’PGÉNzò“ ¥(GÉÁK†
;P¼)WÉÊVºò•°£
L™ÈÖœn±Ì¥.wÉË^ú²”´$ «2§e`Hf2AA @á²x€@ÁC°ƒ1ùËnzó›àg
Óð¿'ÒI0Ÿr“Y‡vºJˆà3£IOY@a4,öÉÿÏlÆSœ
¨@
PÖÝKIÄL¥È.øÎ†¶óŸ
„f='ŠOöó¢û„(A7*G)à"ƒì0 üŽš” ]Ö€P·7²Ó¡ïtà<'ZÏ{6PŸí§1NÊSfá íø DŠÁ Ìì©R½™ÒT
Ó:·