1. Fixed ByteFIFO (fifo.go): Changed Dequeue(), DequeueOrWait(), and DequeueOrWaitContext() to always copy() data into a new byte slice instead of just slicing the buffer. 2. Fixed TCP server logic (tcpserver.go): Changed the send goroutine to poll for data availability (without dequeueing) using GetLen(), then sleep briefly to batch incoming data, and finally dequeue all available data at once. This avoids the race condition where we were splitting dequeue operations and losing bytes in between.
1016 lines
26 KiB
Go
1016 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"math/bits"
|
|
"net"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/TwiN/go-color"
|
|
"github.com/albenik/go-serial/v2"
|
|
"github.com/augustoroman/hexdump"
|
|
|
|
"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
|
|
write sync.Mutex
|
|
read sync.Mutex
|
|
close sync.Mutex
|
|
bufLen sync.Mutex
|
|
}
|
|
|
|
type Modem struct {
|
|
devicePath string
|
|
localAddr string
|
|
remoteAddr string
|
|
|
|
state State
|
|
|
|
device *serial.Port
|
|
tcpdevice net.Conn
|
|
mux pmux
|
|
wg sync.WaitGroup
|
|
flags pflags
|
|
goodChunks int
|
|
packetcounter bool
|
|
chanbusy bool
|
|
cmdlineinit string
|
|
initfile string
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
const (
|
|
SerialTimeout = 1
|
|
PactorChannel = 4
|
|
NMEAChannel = 249
|
|
TRXControlChannel = 253
|
|
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) {
|
|
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 strings.HasPrefix(p.devicePath, "tcp://") {
|
|
if p.tcpdevice, err = net.Dial("tcp", strings.Replace(p.devicePath, "tcp://", "", 1)); err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Check if serial device exists
|
|
if err := p.checkSerialDevice(); err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
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)
|
|
time.Sleep(3 * time.Second)
|
|
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
|
|
}
|
|
if _, _, err = p.writeAndGetResponse("", -1, false, 10240); err != nil {
|
|
return err
|
|
}
|
|
time.Sleep(time.Second)
|
|
// 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)
|
|
s.DeviceType = modem
|
|
|
|
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, 1)
|
|
_, 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 func() {
|
|
err := file.Close()
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
}()
|
|
|
|
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 && s.Command.Cmd.GetLen() == 0) {
|
|
|
|
// 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: "+hex.EncodeToString([]byte(ans)), 1)
|
|
// TODO: Catch errors!
|
|
}
|
|
|
|
// TX TRX control data
|
|
|
|
trxcmd, err := s.ToTRX.Dequeue(1024)
|
|
if err == nil {
|
|
writeDebug("Write TRX command ("+strconv.Itoa(len(trxcmd))+"): "+hexdump.Dump(trxcmd), 1)
|
|
_, _, err := p.writeAndGetResponse(string(trxcmd), TRXControlChannel, false, chunkSize)
|
|
if err != nil {
|
|
writeDebug("Error when sending TRX Command: "+err.Error(), 0)
|
|
}
|
|
/*if len(ans) > 2 {
|
|
s.FromTRX.Enqueue([]byte(ans[2:]))
|
|
}*/
|
|
//s.FromTRX.Enqueue([]byte(ans))
|
|
//writeDebug("TRX CMD answer from modem: \n"+hexdump.Dump([]byte(ans)), 1)
|
|
}
|
|
|
|
// 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:
|
|
writeDebug(fmt.Sprintf("Modem received %d bytes from PACTOR, enqueueing to Data.Response: %s", len(res), hex.EncodeToString(res)), 1)
|
|
err := s.Data.Response.Enqueue(res)
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
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)
|
|
case TRXControlChannel:
|
|
//if !bytes.Equal(res, trxcmd) {
|
|
err := s.FromTRX.Enqueue(res)
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
writeDebug("TRX CMD answer:\n"+hexdump.Dump(res), 1)
|
|
//}
|
|
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)
|
|
//var n int
|
|
_ = p.write(string([]byte{0xaa, 0xaa, 0x00, 0x01, 0x05, 0x4a, 0x48, 0x4f, 0x53, 0x54, 0x30, 0xfb, 0x3d}))
|
|
time.Sleep(100 * time.Millisecond)
|
|
for {
|
|
n, _, err := p.read(100)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if n == 0 {
|
|
break
|
|
}
|
|
}
|
|
err = p.write("\rRESTART\r")
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
time.Sleep(1000 * time.Millisecond)
|
|
for {
|
|
n, a, err := p.read(100)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if n == 0 {
|
|
break
|
|
}
|
|
copy(buff, a)
|
|
//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)
|
|
}
|
|
|
|
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.stophostmode()
|
|
p.setState(Closed)
|
|
writeDebug("PACTOR close() finished", 1)
|
|
return nil
|
|
}
|
|
|
|
// 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(fmt.Sprintf("*** SUCCESS on channel %d: %s", ch, string(payload)), 1)
|
|
if ch == NMEAChannel && s.Userconfig.GPSdAddress != "" {
|
|
if s.GPSStream.GetLen() == 0 {
|
|
s.GPSStream.Enqueue("$" + string(bytes.Trim(payload, "\x00"))) //need to remove the trailing NULL byte
|
|
}
|
|
}
|
|
/* if ch == TRXControlChannel {
|
|
writeDebug("TRX Control channel message: "+string(payload), 1)
|
|
s.FromTRX.Enqueue(payload)
|
|
}
|
|
*/
|
|
|
|
}
|
|
if int(head[1]) == 2 {
|
|
writeDebug("*** ERROR: "+string(payload), 0)
|
|
}
|
|
if int(head[1]) != 7 && int(head[1]) != 3 {
|
|
if !s.VARAMode && ch == PactorChannel {
|
|
s.Command.Response.Enqueue(fmt.Sprintf("%s\n", payload))
|
|
}
|
|
writeDebug("Message from Modem: "+string(payload), 1)
|
|
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)
|
|
var 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: "+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
|
|
}
|
|
|
|
// *** 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
|
|
|
|
/*
|
|
// unstuff: 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
|
|
}
|
|
*/
|
|
|
|
/*
|
|
// stuff: 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"+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
|
|
}
|
|
*/
|
|
|
|
/*
|
|
// checksum: helper function: calculates the CCITT-CRC16 checksum
|
|
func checksum(s string) uint16 {
|
|
tochecksum, _ := hex.DecodeString(s[4:])
|
|
chksum := bits.ReverseBytes16(crc16.ChecksumCCITT(tochecksum))
|
|
return chksum
|
|
}
|
|
*/
|
|
|
|
// checkcrc: 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
|
|
}
|
|
|
|
/*
|
|
// docrc: 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
|
|
}
|
|
*/
|
|
|
|
// writeChannel: Write channel to serial connection (NOT thread safe)- If used, make sure to 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)
|
|
}
|
|
|
|
o := []byte{170, 170, byte(ch), 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
|
|
}
|
|
writeDebug("Done writing channel", 2)
|
|
/*if !isCommand {
|
|
p.cmdBuf <- "%Q"
|
|
}*/
|
|
return nil
|
|
}
|
|
|
|
// read: Read readsize devices from serial connection (thread safe). If readsize==-1 try to check how many data is there
|
|
func (p *Modem) read(readsize int) (int, []byte, error) {
|
|
var chunkSize int
|
|
if readsize == -1 {
|
|
t, err := p.device.ReadyToRead()
|
|
if err != nil {
|
|
chunkSize = math.MaxInt
|
|
writeDebug("ERROR in ReadyToRead: "+err.Error(), 3)
|
|
} else {
|
|
chunkSize = int(t)
|
|
if chunkSize > 0 {
|
|
writeDebug(fmt.Sprintf("chunksize: %d", chunkSize), 3)
|
|
}
|
|
}
|
|
} else {
|
|
chunkSize = readsize
|
|
}
|
|
buf := make([]byte, chunkSize)
|
|
if strings.HasPrefix(p.devicePath, "tcp://") {
|
|
err := p.tcpdevice.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
n, err := p.tcpdevice.Read(buf)
|
|
if err != nil {
|
|
writeDebug("Error received during read: "+err.Error(), 1)
|
|
return 0, []byte{}, err
|
|
}
|
|
return n, buf[0:n], nil
|
|
} else {
|
|
if err := p.checkSerialDevice(); err != nil {
|
|
writeDebug(err.Error(), 1)
|
|
return 0, []byte{}, err
|
|
}
|
|
|
|
p.mux.device.Lock()
|
|
defer p.mux.device.Unlock()
|
|
err := p.device.SetReadTimeout(100) // 100 ms
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
// Write to serial connection (thread safe)
|
|
//
|
|
// No other read/write operation allowed during this time
|
|
func (p *Modem) write(cmd string) error {
|
|
if strings.HasPrefix(p.devicePath, "tcp://") {
|
|
|
|
//TCP connection
|
|
out := cmd + "\r"
|
|
sent, err := p.tcpdevice.Write([]byte(out))
|
|
if err == nil {
|
|
return err
|
|
} else {
|
|
writeDebug(err.Error(), 2)
|
|
out = out[sent:]
|
|
return nil
|
|
}
|
|
|
|
} else {
|
|
|
|
//Serial connection
|
|
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 {
|
|
|
|
// check if the serial line is clear-to-send
|
|
status, err := cts(p.device)
|
|
if err != nil {
|
|
writeDebug("GetModemStatusBits failed. cmd: "+cmd+" Error: "+err.Error(), 1)
|
|
return err
|
|
}
|
|
|
|
if status {
|
|
for {
|
|
sent, err := p.device.Write([]byte(out))
|
|
if err == nil {
|
|
break
|
|
} else {
|
|
writeDebug(err.Error(), 2)
|
|
out = out[sent:]
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|