first commit in new repo

This commit is contained in:
Torsten Harenberg
2025-02-02 19:19:16 +01:00
commit 255137345f
10 changed files with 2009 additions and 0 deletions

124
README.md Normal file
View File

@@ -0,0 +1,124 @@
# The PACTOR-TCP-BRIDGE
<img src="pics/ptb.png" alt="logo" width="200" style="float: right">
by Torsten Harenberg (DL1THM)
## What is the PACTOR-TCP-BRIDGE (ptb)
It's a tool that
- talks via a serial line (USB or Bluetooth) to [PACTOR modems](https://scs-ptc.com/modems.html) made by [SCS](https://scs-ptc.com) using the extended WA8DED hostmode [(see the manual, chapter 10)](https://www.p4dragon.com/download/SCS_Manual_PTC-IIIusb_4.1.pdf) protocol these modems offer
- offers two TCP sockets which can be used by [Pat](https://getpat.io/) (and other tools)
## What it is used for
The main purpose is to build a connection between Pat and PACTOR modems. In order to establish a "listen" mode for PACTOR in Pat,
the modem needs to be kept in the WA8DED hostmode. I found it easiest to create a separate program
for that - in the same way, the VARA modem is a separate program.
On the long run, this tool will replace the current PACTOR driver in Pat.
## How to use it
In order to use this tool, you'll need to configure it.
If you start it for the first time, it will create a default configuration file and will tell you its location:
```bash
% ./ptb
new Config file /Users/harenber/.config/dl1thm.pactortcpbridge/Config.yaml created. Please edit the Config file and restart the application
```
Now edit the file and configure it accordingly:
```yaml
device: /tmp/ttyUSB0
baudrate: 9600
mycall: N0CALL
server_address: 127.0.0.1:8300
data_address: 127.0.0.1:8301
cmdline_init: ""
vara_mode: false
```
| Variable | Meaning |
|-------------------|-----------------------------------------------------------------------------------------------------------------------------|
| `device` | Path to the serial device where the modem is connected |
| `baudrate` | baud rate of the modem |
| `mycall` | The callsign to be used on HF |
| `server_address` | server socket address for the **commands** |
| `data_address` | server socket address for the **data** |
| `cmdline_init` | extra commands sent to the modem before going into hostmode, separated by semicolons, Ex: `DISP BR 1;DISP A 1;DISP DIMMOFF` |
| `vara_mode` | see the chapter about the VARA mode |
If you plan to you Pat with the VARA driver, data `data_address` you **must** use a port number one number higher than the `server_address`.
A matching Pat config using VARA for the example above would look like:
```yaml
"varahf": {
"addr": "localhost:8300",
"bandwidth": 2300,
"rig": "",
"ptt_ctrl": false
}
```
Using PACTOR, the PTT is triggered through the modem, so you need to set `ptt_ctrl` to `false`! If you configure a `rig`, it
needs to be connected in a way that `rigctl` can configure it. Configure your rig through the PACTOR modem
is **not** supported in VARA mode.
## VARA mode
The VARA mode translate WA8DED commands into VARA commands and vice versa. See the VARA homepage for the "VARA TNC Commands"
doumentation.
If enabledm software which is written to interact with the VARA software TNC to work
with PACTOR as well. It was used by the author to use the [VARA driver for Pat](https://github.com/n8jja/Pat-Vara)
with the PACTOR-TCP-Bridge.
## How to run
As a rule of thumb, you have to start the PACTOR-TCP-bridge before you want to use
Pat and stop it after you finished using Pat.
So a typical session works like this
1. start the PACTOR-TCP-Bridge
2. wait for it to finish configure your modem
3. use Pat (or any other software)
4. quit Pat
5. stop the PACTOR-TCP-Bridge
The software supports command line options:
```bash
% ./ptb --help
Usage of ptb - the PACTOR-IP-Bridge:
-c, --configfile string Name of config file (default "Config.yaml")
-d, --daemon Daemon mode (no TUI)
-l, --logfile string Path to the log file (default "/tmp/pactortcpbridge.log")
```
which should be rather self-explaining.
A typical session (without using the daemon mode, see below) looks like this:
![Screenshot showing a terminal with a session](pics/Screenshot.png)
- on the upper left you will see all **payload** from and to the PACTOR modem
- on the lower left the **commands** (with VARA translations, if switched on) to and **answers** from the modem
- on the right hand side you'll see a clock, then a mode indicator (showing `VARA` or `TCP`). Furthermore, you'll see two indicator (`TCP CMD` and `TCP DATA`) which will turn green when a client is connected. The 4 counters below show the usage of the buffers.
## "Daemon"-mode
The "daemon"-mode will omit the terminal UI ("TUI") and might be useful if you have no full terminal (or to run the tool with systemd).
Instead of the UI, you will only see a message saying that you may quit with CTRL-C.
## Keys
- **CTRL-C**<br />Quits the program
- **CTRL-V**<br />swtiches between the VARA and normal mode

15
TODO.txt Normal file
View File

@@ -0,0 +1,15 @@
--Kein leeren Befehl senden--
-- Keine leere Antwort durch parsen --
-- VARA Tests --
-- CLI --
-- Debug handling --
-- Muell vom Input ausschliessen? --
-- kommen error Mmeldungen durch? -> ja --
stophostmodem(): Timeouts beim lesen vom Modem... (scheint weg zu sein?)
-- Was mache ich mit dem initfile --
Log sollte auch auf stdout schreiben
-- VARA Mode beim starten --
-- Goconcurrentqueue los werden --
-- "daemon" mode: Achtung.. Daten fuer den Screen werden wahrscheinlich noch geschickt. ERLEDIGT --
-- Vielleicht flags umstellen auf Standard-Flags mit short forms wie hier beschrieben https://www.antoniojgutierrez.com/posts/2021-05-14-short-and-long-options-in-go-flags-pkg/ --

241
fifo.go Normal file
View File

@@ -0,0 +1,241 @@
package main
import (
"context"
"errors"
"sync"
)
type ByteFIFO struct {
buffer []byte // The underlying byte slice
mutex sync.Mutex // Mutex for thread safety
cond *sync.Cond // Condition variable for signaling
cap int // Capacity of the buffer
}
// NewByteFIFO creates a new ByteFIFO with the given capacity.
func NewByteFIFO(capacity int) *ByteFIFO {
fifo := &ByteFIFO{
buffer: make([]byte, 0, capacity),
cap: capacity,
}
fifo.cond = sync.NewCond(&fifo.mutex) // Initialize condition variable
return fifo
}
// Enqueue adds bytes to the end of the buffer. Returns an error if the buffer is full.
func (f *ByteFIFO) Enqueue(data []byte) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.buffer)+len(data) > f.cap {
return errors.New("buffer overflow")
}
f.buffer = append(f.buffer, data...)
f.cond.Signal() // Notify waiting goroutines that data is available
return nil
}
// Dequeue removes and returns the first `n` bytes from the buffer (or what's left if n>len(f.buffer)).
// Returns an error if nothing is in the queue
func (f *ByteFIFO) Dequeue(n int) ([]byte, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.buffer) == 0 {
return nil, errors.New("buffer is empty")
}
if n > len(f.buffer) {
n = len(f.buffer)
}
data := f.buffer[:n]
f.buffer = f.buffer[n:]
return data, nil
}
// DequeueOrWait removes and returns the first `n` bytes, waiting if the buffer is empty until data is available.
func (f *ByteFIFO) DequeueOrWait(n int) ([]byte, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
// Wait until enough data is available
for len(f.buffer) < n {
f.cond.Wait()
}
data := f.buffer[:n]
f.buffer = f.buffer[n:]
return data, nil
}
// DequeueOrWaitContext removes and returns the first `n` bytes, waiting if necessary until data is available or the context is canceled.
func (f *ByteFIFO) DequeueOrWaitContext(ctx context.Context, n int) ([]byte, error) {
done := make(chan struct{})
var data []byte
var err error
go func() {
f.mutex.Lock()
defer f.mutex.Unlock()
defer close(done)
// Wait until enough data is available
for len(f.buffer) < n {
select {
case <-ctx.Done():
err = ctx.Err()
return
default:
f.cond.Wait()
}
}
data = f.buffer[:n]
f.buffer = f.buffer[n:]
}()
select {
case <-done:
return data, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
// Peek returns the first `n` bytes without removing them from the buffer. Returns an error if there are not enough bytes.
func (f *ByteFIFO) Peek(n int) ([]byte, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if n > len(f.buffer) {
return nil, errors.New("not enough data in buffer")
}
return f.buffer[:n], nil
}
// Size returns the current number of bytes in the buffer.
func (f *ByteFIFO) GetLen() int {
f.mutex.Lock()
defer f.mutex.Unlock()
return len(f.buffer)
}
// Capacity returns the maximum capacity of the buffer.
func (f *ByteFIFO) Capacity() int {
return f.cap
}
// *****
type StringFIFO struct {
buffer []string // The underlying byte slice
mutex sync.Mutex // Mutex for thread safety
cond *sync.Cond // Condition variable for signaling
}
// NewByteFIFO creates a new ByteFIFO with the given capacity.
func NewStringFIFO() *StringFIFO {
fifo := &StringFIFO{
buffer: make([]string, 0),
}
fifo.cond = sync.NewCond(&fifo.mutex) // Initialize condition variable
return fifo
}
// Enqueue adds a string to the end of the buffer. Returns an error if the buffer is full.
func (f *StringFIFO) Enqueue(data string) error {
f.mutex.Lock()
defer f.mutex.Unlock()
f.buffer = append(f.buffer, data)
f.cond.Signal() // Notify waiting goroutines that data is available
return nil
}
// Dequeue removes and returns the first `n` bytes from the buffer (or what's left if n>len(f.buffer)).
// Returns an error if nothing is in the queue
func (f *StringFIFO) Dequeue() (string, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.buffer) == 0 {
return "", errors.New("buffer is empty")
}
data := f.buffer[0]
f.buffer = f.buffer[1:]
return data, nil
}
// DequeueOrWait removes and returns the first `n` bytes, waiting if the buffer is empty until data is available.
func (f *StringFIFO) DequeueOrWait() (string, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
// Wait until enough data is available
for len(f.buffer) == 0 {
f.cond.Wait()
}
data := f.buffer[0]
f.buffer = f.buffer[1:]
return data, nil
}
// DequeueOrWaitContext removes and returns the first `n` bytes, waiting if necessary until data is available or the context is canceled.
func (f *StringFIFO) DequeueOrWaitContext(ctx context.Context) (string, error) {
done := make(chan struct{})
var data string
var err error
go func() {
f.mutex.Lock()
defer f.mutex.Unlock()
defer close(done)
// Wait until enough data is available
for len(f.buffer) == 0 {
select {
case <-ctx.Done():
err = ctx.Err()
return
default:
f.cond.Wait()
}
}
data = f.buffer[0]
f.buffer = f.buffer[1:]
}()
select {
case <-done:
return data, err
case <-ctx.Done():
return "", ctx.Err()
}
}
// Peek returns the first `n` bytes without removing them from the buffer. Returns an error if there are not enough bytes.
func (f *StringFIFO) Peek() (string, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.buffer) == 0 {
return "", errors.New("not enough data in buffer")
}
return f.buffer[0], nil
}
// GetLen returns the current number of bytes in the buffer.
func (f *StringFIFO) GetLen() int {
f.mutex.Lock()
defer f.mutex.Unlock()
return len(f.buffer)
}

23
go.mod Normal file
View File

@@ -0,0 +1,23 @@
module gotests/pactorjarabridge
go 1.23
require (
github.com/TwiN/go-color v1.4.1
github.com/albenik/go-serial/v2 v2.6.1
github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6
github.com/jroimartin/gocui v0.5.0
github.com/karanveersp/store v0.0.0-20230714152426-8bd691c2bad8
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/nsf/termbox-go v1.1.1 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/sys v0.20.0 // indirect
)

192
main.go Normal file
View File

@@ -0,0 +1,192 @@
package main
import (
"errors"
"flag"
"fmt"
"path/filepath"
//log "github.com/fangdingjun/go-log"
"log"
"os"
"os/signal"
"syscall"
"github.com/jroimartin/gocui"
"github.com/karanveersp/store"
"gopkg.in/yaml.v2"
)
const usage = `Usage of ptb - the PACTOR-IP-Bridge:
-c, --configfile string Name of config file (default "Config.yaml")
-d, --daemon Daemon mode (no TUI)
-l, --logfile string Path to the log file
`
const (
StatusTCPCmdActive uint8 = 1 << iota //00000001
StatusTCPDataActive //00000010
)
type TCPServer struct {
Protocol chan string
ToPactor *ByteFIFO
FromPactor *ByteFIFO
VARAMode bool
DaemonMode bool
Status uint8
Command struct {
Cmd *StringFIFO
Response *StringFIFO
}
Data struct {
Data *ByteFIFO
Response *ByteFIFO
}
}
func NewTCPServer(varamode bool, daemonmode bool) *TCPServer {
return &TCPServer{
Protocol: make(chan string, 1024),
ToPactor: NewByteFIFO(1024),
FromPactor: NewByteFIFO(1024),
VARAMode: varamode,
DaemonMode: daemonmode,
Status: 0,
Command: struct {
Cmd *StringFIFO
Response *StringFIFO
}{Cmd: NewStringFIFO(), Response: NewStringFIFO()},
Data: struct {
Data *ByteFIFO
Response *ByteFIFO
}{Data: NewByteFIFO(10240), Response: NewByteFIFO(10240)},
}
}
type Userconfig struct {
Device string `yaml:"device"`
Baudrate int `yaml:"baudrate"`
Mycall string `yaml:"mycall"`
ServerAddress string `yaml:"server_address"`
DataAddress string `yaml:"data_address"`
CmdLineInit string `yaml:"cmdline_init"`
StartwithVaraMode bool `yaml:"vara_mode"`
}
func configmanage(Config *Userconfig, path string) error {
cf := store.GetApplicationDirPath() + string(os.PathSeparator) + path
_, err := os.Stat(cf)
if err != nil {
log.Println("loadConfig error:", err)
log.Println("Config file not found. Creating new one")
Config = &Userconfig{Device: "/tmp/ttyUSB0",
Baudrate: 9600,
Mycall: "N0CALL",
ServerAddress: "127.0.0.1:8300",
DataAddress: "127.0.0.1:8301",
CmdLineInit: "",
StartwithVaraMode: false}
if err := store.Save("Config.yaml", Config); err != nil {
log.Println("failed to save the Config file: ", err)
return err
}
return errors.New(fmt.Sprintf("new Config file %s created. Please edit the Config file and restart the application", cf))
}
if err := store.Load(path, Config); err != nil {
return err
}
if Config.Device != "" {
//log.Println("Config loaded:", Config)
return nil
}
return errors.New(fmt.Sprintf("Empty device name in Config file. Please edit the Config file %s and restart the application", cf))
}
func init() {
store.Init("dl1thm.pactortcpbridge")
store.Register("ini", yaml.Marshal, yaml.Unmarshal)
}
var s *TCPServer // s is a pointer to TCPServer, managing communication via channels and FIFO queues for command and data handling.
func main() {
tmpfile := filepath.FromSlash(os.TempDir() + "/ptb.log")
// read command line arguments -- the standard flag module unfortunately doesn't know shorthands
var configfile string
flag.StringVar(&configfile, "configfile", "Config.yaml", "Name of config file")
flag.StringVar(&configfile, "c", "Config.yaml", "Name of config file")
var logfile string
flag.StringVar(&logfile, "logfile", tmpfile, "Name of log file")
flag.StringVar(&logfile, "l", tmpfile, "Name of log file")
var daemonMode bool
flag.BoolVar(&daemonMode, "daemon", false, "Daemon mode (no TUI)")
flag.BoolVar(&daemonMode, "d", false, "Daemon mode (no TUI)")
flag.Usage = func() { fmt.Print(usage) }
flag.Parse()
f, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
defer f.Close()
log.SetOutput(f)
fmt.Println("logging to " + logfile)
// read config
var Config Userconfig
//c := store.GetApplicationDirPath()
err = configmanage(&Config, configfile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
s = NewTCPServer(Config.StartwithVaraMode, daemonMode)
fmt.Println("Initializing PACTOR modem, please wait...")
m, err := OpenModem(Config.Device, Config.Baudrate, Config.Mycall, "", Config.CmdLineInit)
if err != nil {
log.Panicln(err)
}
defer m.Close()
go tcpCmdServer(&Config)
go tcpDataServer(&Config)
if !daemonMode {
// Initialize the gocui GUI
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
g.SetManagerFunc(layout)
go protocolUpdate(g)
go pactorUpdate(g)
go statusUpdate(g)
// Set keybindings
if err := keybindings(g); err != nil {
log.Panicln(err)
}
// Start the main TUI loop
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
} else {
// daemon mode: just print a message and wait for CTRL-C
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("pactortcpbridge running, press ctrl+c to end the process...")
<-done
}
}

BIN
pics/Screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
pics/ptb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

937
ptc.go Normal file
View File

@@ -0,0 +1,937 @@
package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"github.com/TwiN/go-color"
"github.com/albenik/go-serial/v2"
"log"
"math/bits"
"os"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/howeyc/crc16"
)
var SCSversion = map[string]string{
"A": "PTC-II",
"B": "PTC-IIpro",
"C": "PTC-IIe",
"D": "PTC-IIex",
"E": "PTC-IIusb",
"F": "PTC-IInet",
"H": "DR-7800",
"I": "DR-7400",
"K": "DR-7000",
"L": "PTC-IIIusb",
"T": "PTC-IIItrx",
}
type pflags struct {
exit bool
stopmodem bool
//disconnected chan struct{}
connected chan struct{}
closeWaiting chan struct{}
}
type pmux struct {
device sync.Mutex
pactor sync.Mutex
write sync.Mutex
read sync.Mutex
close sync.Mutex
bufLen sync.Mutex
// sendbuf sync.Mutex
// recvbuf sync.Mutex
}
type Modem struct {
devicePath string
localAddr string
remoteAddr string
state State
device *serial.Port
mux pmux
wg sync.WaitGroup
flags pflags
goodChunks int
// recvBuf bytes.Buffer
// cmdBuf chan string
// sendBuf bytes.Buffer
packetcounter bool
chanbusy bool
cmdlineinit string
initfile string
closeOnce sync.Once
}
const (
SerialTimeout = 1
PactorChannel = 4
MaxSendData = 255
MaxFrameNotTX = 2
)
// ToPactor states
const (
Closed State = iota
Ready
Connecting
Conntected
)
type State uint8
//var debugMux sync.Mutex
func debugEnabled() int {
if value, ok := os.LookupEnv("PACTOR_DEBUG"); ok {
level, err := strconv.Atoi(value)
if err == nil {
return level
}
}
return 0
}
func writeDebug(message string, level int) {
//debugMux.Lock()
// defer debugMux.Unlock()
if debugEnabled() >= level {
_, file, no, ok := runtime.Caller(1)
if ok {
log.Println(file + "#" + strconv.Itoa(no) + ": " + message)
} else {
log.Println(message)
}
}
return
}
// OpenModem: Initialise the pactor modem and all variables. Switch the modem into hostmode.
//
// Will abort if modem reports failed link setup, Close() is called or timeout
// has occured (90 seconds)
func OpenModem(path string, baudRate int, myCall string, initfile string, cmdlineinit string) (p *Modem, err error) {
writeDebug("Trying to open "+path+" with "+strconv.Itoa(baudRate)+" baud.", 0)
p = &Modem{
// Initialise variables
devicePath: path,
localAddr: myCall,
remoteAddr: "",
state: Closed,
device: nil,
flags: pflags{
exit: false,
stopmodem: false,
connected: make(chan struct{}, 1),
closeWaiting: make(chan struct{}, 1)},
goodChunks: 0,
// cmdBuf: make(chan string, 0),
cmdlineinit: cmdlineinit,
initfile: initfile,
}
writeDebug("Initialising pactor modem", 1)
if err := p.checkSerialDevice(); err != nil {
writeDebug(err.Error(), 1)
return nil, err
}
//Setup serial device
if p.device, err = serial.Open(p.devicePath, serial.WithBaudrate(baudRate), serial.WithReadTimeout(SerialTimeout)); err != nil {
writeDebug(err.Error(), 1)
return nil, err
}
err = p.init()
if err != nil {
return nil, err
}
// run modem thread
time.Sleep(3 * time.Second) // give the device a moment to settle, so no commands get lost
return p, nil
}
func (p *Modem) setState(state State) {
writeDebug("Setting state to: "+strconv.FormatUint(uint64(state), 10), 1)
p.state = state
}
func (p *Modem) IsClosed() bool {
if p == nil {
return true
}
return p.state == Closed
}
func (p *Modem) init() (err error) {
writeDebug("Entering PACTOR init", 1)
// Get modem out of CRC hostmode if it is still in it while starting.
//p.stophostmode()
//p._write(string([]byte{0xaa, 0xaa, 0x00, 0x01, 0x05, 0x4a, 0x48, 0x4f, 0x53, 0x54, 0x30, 0xfb, 0x3d}))
//time.Sleep(100 * time.Millisecond)
//_, _, _ = p._read(100)
if _, _, err = p.writeAndGetResponse("", -1, false, 10240); err != nil {
return err
}
// Make sure, modem is in main menu. Will respose with "ERROR:" when already in it -> Just discard answer!
_, ans, err := p.writeAndGetResponse("Quit", -1, false, 1024)
if err != nil {
return err
}
if len(ans) < 2 {
return errors.New("Modem does not react to Quit command. Please re-power your modem")
}
// Check if we can determine the modem's type
_, ver, err := p.writeAndGetResponse("ver ##", -1, false, 1024)
if err != nil {
return err
}
re, err := regexp.Compile(`\w#1`)
if err != nil {
return errors.New("Cannot read SCS modem version string, did you configure the correct serial device? Error: " + err.Error())
}
version := strings.ReplaceAll(string(re.Find([]byte(ver))), "#1", "")
modem, exists := SCSversion[version]
if !exists {
return errors.New("Found a modem type: " + ver + " which this driver doesn't support. Please contact the author.")
}
writeDebug("Found a "+modem+" modem at "+p.devicePath, 0)
writeDebug("Running init commands", 1)
ct := time.Now()
commands := []string{"DD", "RESTART", "MYcall " + p.localAddr, "PTCH " + strconv.Itoa(PactorChannel),
"MAXE 35", "REM 0", "CHOB 0", "PD 1",
"ADDLF 0", "ARX 0", "BELL 0", "BC 0", "BKCHR 25", "CMSG 0", "LFIGNORE 0", "LISTEN 0", "MAIL 0", "REMOTE 0",
"PDTIMER 5", "STATUS 1",
"TONES 4", "MARK 1600", "SPACE 1400", "CWID 0", "CONType 3", "MODE 0",
"DATE " + ct.Format("020106"), "TIME " + ct.Format("150405")}
//run additional commands provided on the command line with "init"
if p.cmdlineinit != "" {
for _, command := range strings.Split(strings.TrimSuffix(p.cmdlineinit, "\n"), ";") {
commands = append(commands, command)
}
}
for _, cmd := range commands {
var res string
writeDebug("Sending command to modem: "+cmd, 0)
_, res, err = p.writeAndGetResponse(cmd, -1, false, 1024)
if err != nil {
return err
}
if strings.Contains(res, "ERROR") {
log.Print(`Command "` + cmd + `" not accepted: ` + res)
return fmt.Errorf(`Command "` + cmd + `" not accepted: ` + res)
} else {
l := fmt.Sprintf("%s --> "+color.InGreen("OK\n"), cmd) // print some output
log.Println(l)
fmt.Println(l)
}
}
// run additional commands stored in the init script file
if p.initfile != "" {
if err = p.runInitScript(p.initfile); err != nil {
return err
}
}
p.flags.stopmodem = false
p.setState(Ready)
go p.modemThread()
//p.flags.exit = false
return nil
}
// Send additional initialisation command stored in the InitScript
//
// Each command has to be on a new line
func (p *Modem) runInitScript(initScript string) error {
if _, err := os.Stat(initScript); os.IsNotExist(err) {
return fmt.Errorf("ERROR: PTC init script defined but not existent: %s", initScript)
}
file, err := os.Open(initScript)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
writeDebug("Sending command to modem: "+scanner.Text(), 0)
if _, _, err = p.writeAndGetResponse(scanner.Text(), -1, false, 1024); err != nil {
return err
}
}
if err := scanner.Err(); err != nil {
return err
}
return nil
}
func (p *Modem) modemThread() {
writeDebug("start modem thread", 1)
_, _, _ = p.read(1024)
_, _, _ = p.writeAndGetResponse("JHOST4", -1, false, 1024)
_, _, _ = p.read(1024)
// need to wait a second for WA8DED mode to "settle"
time.Sleep(time.Second)
const chunkSize = 1024
p.wg.Add(1)
for !p.flags.stopmodem {
// TX data
if p.getNumFramesNotTransmitted() < MaxFrameNotTX {
// data := p.getSendData()
data, err := s.Data.Data.Dequeue(256)
if err == nil {
//data := []byte(d.(string))
writeDebug("Write data ("+strconv.Itoa(len(data))+"): "+hex.EncodeToString(data), 2)
// TODO: Catch errors!
_, _, _ = p.writeAndGetResponse(string(data), PactorChannel, false, chunkSize)
time.Sleep(1000 * time.Millisecond) // wait for the data to be filled into the buffer
if s.VARAMode {
s.Command.Response.Enqueue(fmt.Sprintf("BUFFER %d", s.Data.Data.GetLen()))
}
}
}
// TX commands
cmd, err := s.Command.Cmd.Dequeue()
if err == nil {
writeDebug("Write command ("+strconv.Itoa(len(cmd))+"): "+cmd, 1)
_, ans, err := p.writeAndGetResponse(cmd, PactorChannel, true, chunkSize)
if err != nil {
writeDebug("Error when sending Command: "+err.Error(), 0)
}
if strings.HasPrefix(cmd, "C ") || strings.HasPrefix(cmd, "c ") {
// in VARA mode we need to know if the connection is outgoing or incoming so mark outgoing connects
p.setState(Connecting)
}
if len(ans) > 2 {
if !s.VARAMode {
s.Command.Response.Enqueue(ans[2:])
}
}
writeDebug("Answer from modem: "+ans, 1)
// TODO: Catch errors!
}
// RX
var res []byte
if channels, err := p.getChannelsWithOutput(); err == nil {
for _, c := range channels {
_, data, _ := p.writeAndGetResponse("G", c, true, chunkSize)
if _, res, err = p.checkResponse(data, c); err != nil {
writeDebug("checkResponse returned: "+err.Error(), 1)
} else {
if res != nil {
writeDebug("response: "+string(res)+"\n"+hex.Dump(res), 3)
switch c {
case PactorChannel:
s.Data.Response.Enqueue(res)
writeDebug("Written to sendbuf", 3)
case 254: //Status update
p.chanbusy = int(res[0])&112 == 112 // See PTC-IIIusb manual "STATUS" command
writeDebug("PACTOR state: "+strconv.FormatInt(int64(res[0]), 2), 2)
default:
writeDebug("Channel "+strconv.Itoa(c)+": "+string(res), 1)
}
}
}
}
}
}
time.Sleep(100 * time.Millisecond)
_, _, err := p.writeAndGetResponse("JHOST0", PactorChannel, true, chunkSize)
if err != nil {
writeDebug("PACTOR ERROR Could not get modem out of the WA8DED mode: "+err.Error(), 0)
}
writeDebug("Modemthread is about to end", 1)
time.Sleep(100 * time.Millisecond)
p.wg.Done()
//p.ModemClose()
writeDebug("exit modem thread", 1)
}
func (p *Modem) Close() error {
_, file, no, ok := runtime.Caller(1)
if ok {
writeDebug("PACTOR Close called from "+file+"#"+strconv.Itoa(no), 1)
} else {
writeDebug("PACTOR Close called", 1)
}
var err error
p.closeOnce.Do(func() { err = p.close() })
return err
}
func (p *Modem) stophostmode() {
writeDebug("Stopping WA8DED hostmode", 0)
var ok bool
var err error
// Send JHOST0 in CRC-enhanced WA8DED mode syntax
for ok == false {
buff := make([]byte, 100)
p.device.Write([]byte{0xaa, 0xaa, 0x00, 0x01, 0x05, 0x4a, 0x48, 0x4f, 0x53, 0x54, 0x30, 0xfb, 0x3d})
time.Sleep(100 * time.Millisecond)
for {
n, err := p.device.Read(buff)
if err != nil {
log.Fatal(err)
break
}
if n == 0 {
break
}
//fmt.Printf("%v", string(buff[:n]))
}
p.device.Write([]byte("\rRESTART\r"))
time.Sleep(1000 * time.Millisecond)
for {
n, err := p.device.Read(buff)
if err != nil {
log.Fatal(err)
break
}
if n == 0 {
break
}
//fmt.Printf("%v", string(buff[:n]))
}
ok, err = regexp.Match("cmd:", buff)
if err != nil {
writeDebug("Error in stophostmode: "+err.Error(), 0)
}
}
}
// Wait for transmission to be finished
//
// BLOCKING until either all frames are transmitted and acknowledged or timeouts
// occurs
func (p *Modem) waitTransmissionFinish(t time.Duration) error {
timeout := time.After(t)
tick := time.Tick(500 * time.Millisecond)
for {
notAck := p.getNumFrameNotAck()
notTrans := p.getNumFramesNotTransmitted()
select {
case <-timeout:
if notAck != 0 && notTrans != 0 {
return errors.New("Timeout: " + strconv.Itoa(notAck) + "frames not ack and " + strconv.Itoa(notTrans) + " frames not transmitted")
} else if notAck != 0 {
return errors.New("Timeout: " + strconv.Itoa(notAck) + "frames not ack")
} else {
return errors.New("Timeout: " + strconv.Itoa(notTrans) + " frames not transmitted")
}
case <-tick:
if notAck == 0 && notTrans == 0 {
return nil
}
}
}
}
// Close current serial connection. Stop all threads and close all channels.
func (p *Modem) close() (err error) {
_, file, no, ok := runtime.Caller(1)
if ok {
writeDebug("PACTOR close called from "+file+"#"+strconv.Itoa(no), 1)
} else {
writeDebug("PACTOR close called", 1)
}
//p.disconnect()
writeDebug("signal modem thread to stop", 1)
p.flags.stopmodem = true
writeDebug("waiting for all threads to exit", 1)
p.wg.Wait()
writeDebug("modem thread stopped", 1)
//p.hostmodeQuit()
// will not close the serial port as we reuse it
//p.device.Close()
p.stophostmode()
p.setState(Closed)
writeDebug("PACTOR close() finished", 1)
return nil
}
// Set specified flag (channel)
func setFlag(flag chan struct{}) {
flag <- struct{}{}
}
// Get number of frames not yet transmitted by the modem
func (p *Modem) getNumFramesNotTransmitted() int {
//return p.channelState.c
return 0
}
// Get number of frames not acknowledged by the remote station
func (p *Modem) getNumFrameNotAck() int {
//return p.channelState.d
return 0
}
// Check if serial device is still available (e.g still connected)
func (p *Modem) checkSerialDevice() (err error) {
if _, err := os.Stat(p.devicePath); os.IsNotExist(err) {
return fmt.Errorf("ERROR: Device %s does not exist", p.devicePath)
}
return nil
}
// Poll channel 255 with "G" command to find what channels have output.
// Write them into the channels list.
func (p *Modem) getChannelsWithOutput() (channels []int, err error) {
//Poll Channel 255 to find what channels have output
n, chs, err := p.writeAndGetResponse("G", 255, true, 1024)
if err != nil {
return nil, err
}
channels = make([]int, 0)
for i, ch := range []byte(chs)[:n-1] {
if (i == 0 && ch == 255) || (i == 1 && ch == 1) {
continue
}
channels = append(channels, int(ch)-1)
}
writeDebug("Channels with output: "+fmt.Sprintf("%#v", channels), 2)
return
}
// Do some checks on the returned data.
//
// Currently, only connection information (payload) messages are passed on
func (p *Modem) checkResponse(resp string, ch int) (n int, data []byte, err error) {
if len(resp) < 3 {
return 0, nil, fmt.Errorf("No data")
}
head := []byte(resp[:3])
payload := []byte(resp[3:])
length := int(head[2]) + 1
pl := len(payload)
if int(head[0]) != ch {
writeDebug("WARNING: Returned data does not match polled channel\n"+hex.Dump([]byte(resp)), 1)
return 0, nil, fmt.Errorf("Channel missmatch")
}
if int(head[1]) == 1 { //sucess,message follows
writeDebug("*** SUCCESS: "+string(payload), 0)
}
if int(head[1]) == 2 {
writeDebug("*** ERROR: "+string(payload), 0)
}
if int(head[1]) != 7 && int(head[1]) != 3 {
if !s.VARAMode {
s.Command.Response.Enqueue(fmt.Sprintf("%s\n", payload))
}
writeDebug("Message from Modem: "+string(payload), 0)
return 0, nil, fmt.Errorf("Not a data response")
}
if int(head[1]) == 3 { //Link status
if !s.VARAMode {
s.Command.Response.Enqueue(fmt.Sprintf("%s\n", payload))
}
writeDebug("*** LINK STATUS: "+string(payload), 0)
// there is no way to get the remote callsign from the WA8DED data, so we have to parse the link status :(
re, err := regexp.Compile(` CONNECTED to \w{2,16}`)
if err != nil {
writeDebug("Cannot convert connect message to callsign: "+string(payload), 1)
} else {
ans := strings.ReplaceAll(string(re.Find(payload)), " CONNECTED to ", "")
if len(ans) > 2 { //callsign consists of 3+ characters
p.remoteAddr = ans
writeDebug("PACTOR connection to: "+ans, 0)
if s.VARAMode {
if p.state == Connecting { //outgoing connection
s.Command.Response.Enqueue(fmt.Sprintf("CONNECTED %s %s BW", p.localAddr, ans))
} else { //incoming connection
s.Command.Response.Enqueue(fmt.Sprintf("CONNECTED %s %s BW", ans, p.localAddr))
}
}
p.setState(Conntected)
return 0, nil, nil
}
}
if strings.Contains(string(payload), "DISCONNECTED") {
writeDebug("PACTOR DISCONNECTED", 0)
p.setState(Ready)
if s.VARAMode {
s.Command.Response.Enqueue("DISCONNECTED")
}
// }
//p.channelState.f = 0
return 0, nil, nil
}
return 0, nil, fmt.Errorf("Link data")
}
if length != pl {
writeDebug("WARNING: Data length "+strconv.Itoa(pl)+" does not match stated amount "+strconv.Itoa(length)+". After "+strconv.Itoa(p.goodChunks)+" good chunks.", 1)
p.goodChunks = 0
if pl < length {
// TODO: search for propper go function
for i := 0; i < (length - pl); i++ {
payload = append(payload, 0x00)
}
} else {
payload = payload[:length]
}
} else {
p.goodChunks += 1
writeDebug("Good chunk", 2)
}
return length, payload, nil
}
// Write and read response from pactor modem
//
// Can be used in both, normal and hostmode. If used in hostmode, provide
// channel (>=0). If used in normal mode, set channel to -1, isCommand is
// ignored.
func (p *Modem) writeAndGetResponse(msg string, ch int, isCommand bool, chunkSize int) (int, string, error) {
writeDebug("wagr: Channel: "+strconv.Itoa(ch)+"; isCommand: "+strconv.FormatBool(isCommand)+"\n"+hex.Dump([]byte(msg)), 3)
var err error
var n int
var str string
if ch < 0 {
err = p._write(msg + "\r")
if err != nil {
return 0, "", err
}
time.Sleep(500 * time.Millisecond)
i := 0
var tmp []byte
for {
n, tmp, err = p._read(chunkSize)
str = string(tmp)
if err == nil {
break
}
i += 1
if i > 9 {
writeDebug("No successful read after 10 times!", 1)
return 0, "", err
}
}
writeDebug(fmt.Sprintf("response: %s\n%s", str, hex.Dump(tmp)), 2)
return n, str, err
} else {
err = p.writeChannel(msg, ch, isCommand)
if err != nil {
return 0, "", err
}
if msg == "JHOST0" { //looks like the SCS modems do not answer to JHOST0, although the standard defines it
return 0, "", nil
}
time.Sleep(50 * time.Millisecond)
writeDebug("Decode WA8DED", 4)
buf := []byte{}
valid := false
for valid == false {
if bytes.Compare(buf, []byte{170, 170, 170, 85}) == 0 { // check for re-request #170#170#170#85
// packet was re-requested!!
writeDebug("Re-Request magic received", 3)
buf = []byte{} //delete the re-request packet
err = p.writeChannel(msg, ch, isCommand) // write command again
if err != nil {
return 0, "", err
}
}
br, b, err := p._read(1)
if err != nil {
writeDebug("ERROR at _read: "+error.Error(err), 1)
}
writeDebug("Len: "+strconv.Itoa(len(buf))+"State: "+string(hex.Dump(buf)+"\n"), 4)
if br > 0 {
//we got some data
buf = append(buf, b...)
if len(buf) > 5 { // otherwise it's no enh. WA8DED: 2 magic bytes, 2 header bytes, 2 CRC bytes
//unstuff (from 3rd byte on)
t := bytes.Replace(buf[2:], []byte{170, 0}, []byte{170}, -1)
buf = append([]byte{0xaa, 0xaa}, t...)
if checkcrc(buf) {
n = len(t) - 2
str = string(t[:n])
valid = true
} else {
writeDebug("(still) Invalid checksum", 4)
}
}
}
}
}
p.packetcounter = !(p.packetcounter)
writeDebug("wagr: returning \n"+hex.Dump([]byte(str)), 3)
return n, str, err
}
// Flush waits for the last frames to be transmitted.
//
// Will throw error if remaining frames could not bet sent within 120s
func (p *Modem) Flush() (err error) {
writeDebug("Flush called", 2)
if err = p.waitTransmissionFinish(120 * time.Second); err != nil {
writeDebug(err.Error(), 2)
}
return
}
// TxBufferLen returns the number of bytes in the out sendbuf queue.
func (p *Modem) TxBufferLen() int {
writeDebug("TxBufferLen called ("+strconv.Itoa(s.Data.Data.GetLen())+" bytes remaining in sendbuf)", 2)
return s.Data.Data.GetLen() + (p.getNumFramesNotTransmitted() * MaxSendData)
}
func (p *Modem) Busy() bool {
return p.chanbusy
}
// Write to serial connection (thread safe)
//
// No other read/write operation allowed during this time
func (p *Modem) write(cmd string) error {
p.mux.pactor.Lock()
defer p.mux.pactor.Unlock()
return p._write(cmd)
}
// Write to serial connection (NOT thread safe)
//
// If used, make shure to lock/unlock p.mux.pactor mutex!
func (p *Modem) _write(cmd string) error {
if err := p.checkSerialDevice(); err != nil {
writeDebug(err.Error(), 1)
return err
}
writeDebug("write: \n"+hex.Dump([]byte(cmd)), 3)
p.mux.device.Lock()
defer p.mux.device.Unlock()
out := cmd + "\r"
for {
status, err := p.device.GetModemStatusBits()
if err != nil {
writeDebug("GetModemStatusBits failed. cmd: "+cmd+" Error: "+err.Error(), 1)
return err
}
if status.CTS {
for {
sent, err := p.device.Write([]byte(out))
if err == nil {
break
} else {
// log.Errorf("ERROR while sending serial command: %s\n", out)
writeDebug(err.Error(), 2)
out = out[sent:]
}
}
break
}
}
return nil
}
// *** Helper functions for the CRC hostmode
// Although these functions are small, I prefer to keep their functionality
// separate. They follow the steps in the SCS documentation
// helper function: de-hexlify and write to debug channel
func printhex(s string) {
t, _ := hex.DecodeString(s)
writeDebug(string(t), 3)
}
// helper function: "unstuff" a string
func unstuff(s string) string {
//Expect: the string contains aa aa at the beginning, that should NOT be
//stuffed
n, _ := hex.DecodeString(s[4:])
n = bytes.Replace(n, []byte{170, 0}, []byte{170}, -1)
var r []byte
r = append([]byte{0xaa, 0xaa}, n...)
re := hex.EncodeToString(r)
return re
}
// helper function: "stuff" a string: replaces every #170 with #170#0
func stuff(s string) string {
//Expect: the string contains aa aa at the beginning, that should NOT be
//stuffed
n, err := hex.DecodeString(s[4:])
if err != nil {
writeDebug("ERROR in Stuff: "+err.Error()+"\n"+string(hex.Dump([]byte(s))), 1)
}
n = bytes.Replace(n, []byte{170}, []byte{170, 0}, -1)
var r []byte
r = append([]byte{0xaa, 0xaa}, n...)
re := hex.EncodeToString(r)
return re
}
// helper function: calculates the CCITT-CRC16 checksum
func checksum(s string) uint16 {
tochecksum, _ := hex.DecodeString(s[4:])
chksum := bits.ReverseBytes16(crc16.ChecksumCCITT([]byte(tochecksum)))
return chksum
}
// helper fuction: check the checksum by comparing
func checkcrc(s []byte) bool {
tochecksum := s[2 : len(s)-2]
chksum := bits.ReverseBytes16(crc16.ChecksumCCITT(tochecksum))
pksum := s[len(s)-2:]
return (binary.BigEndian.Uint16(pksum) == chksum)
}
// super helper fuction: convert an ordinary WA8DED message into a CRC-Hostmode message
func docrc(msg string) string {
// step 1: add a #170170
msg = fmt.Sprintf("%02x%02x%s", 170, 170, msg)
// step 2: calculate and add the checksum
chksum := checksum(msg)
msg = fmt.Sprintf("%s%04x", msg, chksum)
// step 3: add "stuff" bytes
msg = stuff(msg)
return msg
}
// Write channel to serial connection (NOT thread safe)
//
// If used, make shure to lock/unlock p.mux.pactor mutex!
func (p *Modem) writeChannel(msg string, ch int, isCommand bool) error {
if err := p.checkSerialDevice(); err != nil {
writeDebug(err.Error(), 1)
return err
}
var d byte
switch {
case !p.packetcounter && !isCommand:
d = byte(0x00)
case !p.packetcounter && isCommand:
d = byte(0x01)
case p.packetcounter && !isCommand:
d = byte(0x80)
case p.packetcounter && isCommand:
d = byte(0x81)
}
var o []byte = []byte{170, 170, byte(ch), byte(d), byte((len(msg) - 1))}
o = append(o, []byte(msg)...)
cksum := bits.ReverseBytes16(crc16.ChecksumCCITT(o[2:]))
cksumb := make([]byte, 2)
binary.BigEndian.PutUint16(cksumb, cksum)
o = append(o, cksumb...)
tostuff := o[2:]
tostuff = bytes.Replace(tostuff, []byte{170}, []byte{170, 0}, -1)
o = append([]byte{170, 170}, tostuff...)
/*
msg = fmt.Sprintf("%02x%02x%s", 170, 170, msg)
// step 2: calculate and add the checksum
chksum := checksum(msg)
msg = fmt.Sprintf("%s%04x", msg, chksum)
// step 3: add "stuff" bytes
msg = stuff(msg)
*/
/*c := fmt.Sprintf("%02x", ch)
l := fmt.Sprintf("%02x", (len(msg) - 1))
s := hex.EncodeToString([]byte(msg))
// add crc hostmode addons
bs, _ := hex.DecodeString(docrc(fmt.Sprintf("%s%s%s%s", c, d, l, s)))*/
writeDebug("write channel: \n"+hex.Dump(o), 2)
if err := p.write(string(o)); err != nil {
writeDebug(err.Error(), 2)
return err
}
/*if !isCommand {
p.cmdBuf <- "%Q"
}*/
return nil
}
// Read from serial connection (thread safe)
//
// No other read/write operation allowed during this time
func (p *Modem) read(chunkSize int) (int, []byte, error) {
p.mux.pactor.Lock()
defer p.mux.pactor.Unlock()
return p._read(chunkSize)
}
// Read from serial connection (NOT thread safe)
//
// If used, make shure to lock/unlock p.mux.pactor mutex!
func (p *Modem) _read(chunkSize int) (int, []byte, error) {
if err := p.checkSerialDevice(); err != nil {
writeDebug(err.Error(), 1)
return 0, []byte{}, err
}
buf := make([]byte, chunkSize)
p.mux.device.Lock()
defer p.mux.device.Unlock()
n, err := p.device.Read(buf)
if err != nil {
writeDebug("Error received during read: "+err.Error(), 1)
return 0, []byte{}, err
}
return n, buf[0:n], nil
}

320
tcpserver.go Normal file
View File

@@ -0,0 +1,320 @@
package main
import (
"bufio"
"context"
"errors"
"fmt"
"github.com/TwiN/go-color"
"log"
"net"
"regexp"
"strings"
"time"
)
// Chunks splits a string into chunks of chunkSize length
// by topskip taken from https://stackoverflow.com/questions/18556693/slice-string-into-letters
func Chunks(s string, chunkSize int) []string {
if len(s) == 0 {
return nil
}
if chunkSize >= len(s) {
return []string{s}
}
var chunks []string = make([]string, 0, (len(s)-1)/chunkSize+1)
currentLen := 0
currentStart := 0
for i := range s {
if currentLen == chunkSize {
chunks = append(chunks, s[currentStart:i])
currentLen = 0
currentStart = i
}
currentLen++
}
chunks = append(chunks, s[currentStart:])
return chunks
}
// HandleConnection processes incoming connections
func handleTCPCmdConnection(conn net.Conn) {
defer func() {
s.Status &^= StatusTCPCmdActive
conn.Close()
}()
s.Status |= StatusTCPCmdActive
// Sending "IAMLIVE" every 60 seconds
alivectx, alivecancel := context.WithCancel(context.Background())
go func(conn net.Conn, ctx context.Context) {
for ctx.Err() == nil {
if s.VARAMode {
s.Command.Response.Enqueue("IAMALIVE")
}
time.Sleep(60 * time.Second)
}
}(conn, alivectx)
defer alivecancel()
/*
bufferctx, buffercancel := context.WithCancel(context.Background())
go func(conn net.Conn, ctx context.Context) {
for ctx.Err() == nil {
if s.VARAMode {
s.Command.Response.Enqueue(fmt.Sprintf("BUFFER %d", s.Data.Data.GetLen()))
}
time.Sleep(10 * time.Second)
}
}(conn, bufferctx)
defer buffercancel()
*/
s.Protocol <- fmt.Sprintf(color.InGreen("TCP Cmd Connection established with %s\n"), conn.RemoteAddr())
s.Status |= StatusTCPCmdActive
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func(ctx context.Context) {
for ctx.Err() == nil {
msg, err := s.Command.Response.DequeueOrWaitContext(ctx)
if err != nil {
s.Protocol <- fmt.Sprintf(color.InRed("End of Cmd Data Connection %s\n"), conn.RemoteAddr())
return
} else {
s.Protocol <- fmt.Sprintf(color.InCyan("Response: %s\n"), msg)
conn.Write([]byte(fmt.Sprintf("%s\r", msg)))
}
}
}(ctx)
reader := bufio.NewReader(conn)
for {
// Read incoming message
message, err := reader.ReadString('\r')
if err != nil {
//s.Protocol <- fmt.Sprintf(color.InRed("Connection closed by %s\n"), conn.RemoteAddr())
s.Status &^= StatusTCPCmdActive
alivecancel()
//buffercancel()
cancel()
conn.Close()
break
}
message = strings.TrimSpace(message)
re := regexp.MustCompile("[[:^ascii:]]")
message = re.ReplaceAllLiteralString(message, "") // prevent any non-ASCII from being sent
if message == "" { // do not send an empty message to the controller
continue
}
// Process the VARA TNC protocol commands
if s.VARAMode {
translatedmsg, predefanswer, err := processVARACommand(message)
if err != nil {
s.Protocol <- color.InRed(fmt.Sprintf("Error translating VARA command %s: %s\n", message, err.Error()))
log.Println(fmt.Sprintf("Error translating VARA command %s: %s\n", message, err.Error()))
s.Command.Response.Enqueue("WRONG")
} else {
if translatedmsg != "" {
s.Protocol <- fmt.Sprintf(color.InPurple("(V) Got: %s Sending: %s\n"), message, translatedmsg)
s.Command.Cmd.Enqueue(translatedmsg)
}
if predefanswer != "" {
s.Command.Response.Enqueue(predefanswer)
} else {
s.Command.Response.Enqueue("OK")
}
}
} else { // TCP Mode == passthrough
s.Protocol <- fmt.Sprintf(color.InPurple("(T) Sending: %s\n"), message)
s.Command.Cmd.Enqueue(message)
}
}
}
func handleTCPDataConnection(conn net.Conn) {
defer func() {
s.Status &^= StatusTCPDataActive
conn.Close()
}()
s.Protocol <- fmt.Sprintf(color.InGreen("TCP Data Connection established with %s\n"), conn.RemoteAddr())
s.Status |= StatusTCPDataActive
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
msg, err := s.Data.Response.DequeueOrWaitContext(ctx, 1)
if err != nil {
s.Protocol <- fmt.Sprintf(color.InRed("End of TCP Data Connection %s\n"), conn.RemoteAddr())
return
} else {
//TODO: VARA
if !s.DaemonMode {
s.FromPactor.Enqueue(msg)
}
conn.Write(msg)
}
}
}()
reader := bufio.NewReader(conn)
for {
var temp []byte
buf := make([]byte, 1024)
n, err := reader.Read(buf)
if n > 0 {
temp = append(temp, buf[:n]...)
}
if err != nil {
s.Status &^= StatusTCPDataActive
cancel()
conn.Close()
return
}
//TODO: muss das nicht weg?
/* if temp == 0x04 {
break
}
*/
if !s.DaemonMode {
s.ToPactor.Enqueue(temp)
}
if n > 255 {
// we can dump at max 255 bytes into
for _, ck := range Chunks(string(temp), 255) {
s.Data.Data.Enqueue([]byte(ck))
}
} else {
s.Data.Data.Enqueue(temp)
}
}
}
// Process VARA commands and return appropriate WA8DED command and a pre-defined answer
func processVARACommand(command string) (string, string, error) {
switch {
case strings.HasPrefix(command, "CONNECT"):
re, err := regexp.MatchString(`CONNECT \w+ \w+`, command)
if err != nil || !re {
return "", "", errors.New("Error matching regex")
}
c := strings.Split(command, " ")
return fmt.Sprintf("C %s", c[2]), "", nil
case strings.HasPrefix(command, "DISCONNECT"):
return "D", "", nil
case strings.HasPrefix(command, "ABORT"):
return "DD", "", nil
// case strings.HasPrefix(command, "VERSION"):
// return "", nil
case strings.HasPrefix(command, "LISTEN"):
// Handle listen
if strings.HasSuffix(command, "ON") {
return "%L 1", "", nil
}
if strings.HasSuffix(command, "OFF") {
return "%L 0", "", nil
}
return "", "", errors.New("Neither ON nor OFF after LISTEN")
case strings.HasPrefix(command, "MYCALL"):
m := ""
// Handle MYCALL
s := strings.Split(command, " ")
if len(s) > 1 {
// send own callsign to PACTOR controller
m = s[1]
} else {
return "", "", errors.New("Invalid MYCALL command")
}
return fmt.Sprintf("%s%s", "I ", m), "", nil
case strings.HasPrefix(command, "COMPRESSION"):
// Handle COMPRESSION
return "", "", nil
case strings.HasPrefix(command, "BW"):
// Has no meaning for PACTOR
return "", "", nil
case strings.HasPrefix(command, "CHAT"):
// Has no meaning for PACTOR
return "", "", nil
case strings.HasPrefix(command, "WINLINK SESSION"):
// Has no meaning for PACTOR, just return "OK"
return "", "", nil
case strings.HasPrefix(command, "P2P SESSION"):
// Has no meaning for PACTOR, just return "OK"
return "", "", nil
case strings.HasPrefix(command, "CWID"):
// Has no meaning for PACTOR, just return "OK"
return "", "", nil
case strings.HasPrefix(command, "PUBLIC"):
// Has no meaning for PACTOR, just return "OK"
return "", "", nil
case strings.HasPrefix(command, "VERSION"):
return "", "VERSION 1.0.0", nil
default:
// Handle unrecognized commands
return "", "", errors.New("Unknown command")
}
}
func tcpCmdServer(Config *Userconfig) {
// Start listening for connections
listener, err := net.Listen("tcp", Config.ServerAddress)
if err != nil {
s.Protocol <- fmt.Sprintf(color.InWhiteOverRed("Error starting server: %v\n"), err)
log.Println(err)
return
}
defer listener.Close()
s.Protocol <- fmt.Sprintf("TCP Protocol Server listening on %s\n", Config.ServerAddress)
for {
// Accept incoming connection
conn, err := listener.Accept()
if err != nil {
s.Protocol <- fmt.Sprintf("Error accepting connection: %v\n", err)
continue
}
// Handle connection in a separate goroutine
go handleTCPCmdConnection(conn)
}
}
func tcpDataServer(Config *Userconfig) {
listener, err := net.Listen("tcp", Config.DataAddress)
if err != nil {
s.Protocol <- fmt.Sprintf(color.InWhiteOverRed("Error starting server: %v\n"), err)
log.Println(err)
return
}
defer listener.Close()
s.Protocol <- fmt.Sprintf("TCP Data Server listening on %s\n", Config.DataAddress)
for {
// Accept incoming connection
conn, err := listener.Accept()
if err != nil {
s.Protocol <- fmt.Sprintf("Error accepting connection: %v\n", err)
continue
}
// Handle connection in a separate goroutine
go handleTCPDataConnection(conn)
}
}

157
tui.go Normal file
View File

@@ -0,0 +1,157 @@
package main
import (
"fmt"
"github.com/TwiN/go-color"
"github.com/jroimartin/gocui"
"log"
"strings"
"time"
"unicode"
)
func printable(r rune) rune {
if r == rune('\u000d') || r == rune('\u000a') { // ctrl-r or ctrl-n
return rune('\u000a') //ctrl-n
}
if unicode.IsGraphic(r) {
return r
}
return rune('\u002e') // dot
}
func Ternary(condition bool, valueIfTrue, valueIfFalse interface{}) interface{} {
if condition {
return valueIfTrue
}
return valueIfFalse
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
lwX := int(float32(maxX) * 0.8)
// "data" view: Top-left large view
if v, err := g.SetView("data", 0, 0, lwX-1, maxY/2-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Data"
v.Wrap = true
v.Autoscroll = true
}
// "protocol" view: Bottom-left large view
if v, err := g.SetView("protocol", 0, maxY/2, lwX-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Protocol"
v.Wrap = true
v.Autoscroll = true
}
// "status" view: Smaller view on the right
if v, err := g.SetView("status", lwX, 0, maxX-1, maxY-1); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Status"
}
return nil
}
func keybindings(g *gocui.Gui) error {
// Quit the application with Ctrl+C
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
return err
}
if err := g.SetKeybinding("", gocui.KeyCtrlT, gocui.ModNone, toggleMode); err != nil {
return err
}
return nil
}
func protocolUpdate(g *gocui.Gui) {
for {
msg := <-s.Protocol
g.Update(func(g *gocui.Gui) error {
v, err := g.View("protocol")
if err != nil {
log.Println(err.Error())
return err
}
fmt.Fprint(v, msg)
return nil
})
}
}
func pactorUpdate(g *gocui.Gui) {
for {
if s.ToPactor.GetLen() > 0 {
msg, err := s.ToPactor.Dequeue(256)
if err != nil {
log.Println(err.Error())
continue
}
g.Update(func(g *gocui.Gui) error {
v, err := g.View("data")
if err != nil {
log.Println(err.Error())
return err
}
fmt.Fprint(v, strings.Map(printable, string(msg)))
return nil
})
}
if s.FromPactor.GetLen() > 0 {
msg, err := s.FromPactor.Dequeue(256)
if err != nil {
log.Println(err.Error())
continue
}
g.Update(func(g *gocui.Gui) error {
v, err := g.View("data")
if err != nil {
log.Println(err.Error())
return err
}
fmt.Fprint(v, color.InRed(strings.Map(printable, string(msg))))
return nil
})
}
time.Sleep(3 * time.Second)
}
}
func statusUpdate(g *gocui.Gui) {
for {
time.Sleep(1 * time.Second)
g.Update(func(g *gocui.Gui) error {
v, err := g.View("status")
if err != nil {
log.Println(err.Error())
} else {
v.Clear()
fmt.Fprintln(v, time.Now().Format("15:04:05"))
fmt.Fprintln(v, Ternary(s.VARAMode, "VARA", "TCP"))
fmt.Fprintln(v, Ternary(s.Status&StatusTCPCmdActive != 0, color.InGreen("TCP CMD"), color.InRed("TCP CMD")))
fmt.Fprintln(v, Ternary(s.Status&StatusTCPDataActive != 0, color.InGreen("TCP DATA"), color.InRed("TCP DATA")))
fmt.Fprintln(v, fmt.Sprintf("\nCMD S: %d\nCMD R: %d\nDTA S: %d\nDTA R: %d", s.Command.Cmd.GetLen(), s.Command.Response.GetLen(), s.Data.Data.GetLen(), s.Data.Response.GetLen()))
}
return nil
})
}
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
func toggleMode(g *gocui.Gui, v *gocui.View) error {
s.Protocol <- fmt.Sprintf(color.InPurple("Toggle mode\n"))
s.VARAMode = !s.VARAMode
return nil
}