mirror of
https://github.com/la5nta/pat
synced 2026-08-13 17:46:54 -04:00
413 lines
9.5 KiB
Go
413 lines
9.5 KiB
Go
// Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
|
|
// Use of this source code is governed by the MIT-license that can be
|
|
// found in the LICENSE file.
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/la5nta/pat/cfg"
|
|
"github.com/la5nta/pat/internal/debug"
|
|
|
|
"github.com/harenber/ptc-go/v2/pactor"
|
|
"github.com/la5nta/wl2k-go/transport"
|
|
"github.com/la5nta/wl2k-go/transport/ardop"
|
|
"github.com/la5nta/wl2k-go/transport/ax25/agwpe"
|
|
"github.com/n8jja/Pat-Vara/vara"
|
|
|
|
// Register stateless dialers
|
|
_ "github.com/la5nta/wl2k-go/transport/ax25"
|
|
_ "github.com/la5nta/wl2k-go/transport/telnet"
|
|
)
|
|
|
|
var (
|
|
dialing *transport.URL // The connect URL currently being dialed (if any)
|
|
|
|
adTNC *ardop.TNC // Pointer to the ARDOP TNC used by Listen and Connect
|
|
agwpeTNC *agwpe.TNCPort // Pointer to the AGWPE TNC combined TNC and Port
|
|
pModem *pactor.Modem
|
|
varaHFModem *vara.Modem
|
|
varaFMModem *vara.Modem
|
|
|
|
// Context cancellation function for aborting while dialing.
|
|
dialCancelFunc func() = func() {}
|
|
)
|
|
|
|
func hasSSID(str string) bool { return strings.Contains(str, "-") }
|
|
|
|
func connectAny(connectStr ...string) bool {
|
|
for _, str := range connectStr {
|
|
if Connect(str) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func Connect(connectStr string) (success bool) {
|
|
if connectStr == "" {
|
|
return false
|
|
} else if aliased, ok := config.ConnectAliases[connectStr]; ok {
|
|
return Connect(aliased)
|
|
}
|
|
|
|
// Hack around bug in frontend which may occur if the status updates too quickly.
|
|
if websocketHub != nil {
|
|
defer func() { time.Sleep(time.Second); websocketHub.UpdateStatus() }()
|
|
}
|
|
|
|
debug.Printf("connectStr: %s", connectStr)
|
|
url, err := transport.ParseURL(connectStr)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return false
|
|
}
|
|
|
|
// TODO: Remove after some release cycles (2023-05-21)
|
|
// Rewrite legacy serial-tnc scheme.
|
|
if url.Scheme == MethodSerialTNCDeprecated {
|
|
log.Printf("Transport scheme %s:// is deprecated, use %s:// instead.", MethodSerialTNCDeprecated, MethodAX25SerialTNC)
|
|
url.Scheme = MethodAX25SerialTNC
|
|
}
|
|
|
|
// Rewrite the generic ax25:// scheme to use a specified AX.25 engine.
|
|
if url.Scheme == MethodAX25 {
|
|
url.Scheme = defaultAX25Method()
|
|
}
|
|
|
|
// Init TNCs
|
|
switch url.Scheme {
|
|
case MethodAX25AGWPE:
|
|
if err := initAGWPE(); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
case MethodArdop:
|
|
if err := initArdopTNC(); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
case MethodPactor:
|
|
ptCmdInit := ""
|
|
if val, ok := url.Params["init"]; ok {
|
|
ptCmdInit = strings.Join(val, "\n")
|
|
}
|
|
if err := initPactorModem(ptCmdInit); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
case MethodVaraHF:
|
|
if err := initVaraHFModem(); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
case MethodVaraFM:
|
|
if err := initVaraFMModem(); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Set default userinfo (mycall)
|
|
if url.User == nil {
|
|
url.SetUser(fOptions.MyCall)
|
|
}
|
|
|
|
// Set default host interface address
|
|
if url.Host == "" {
|
|
switch url.Scheme {
|
|
case MethodAX25Linux:
|
|
url.Host = config.AX25Linux.Port
|
|
case MethodAX25SerialTNC:
|
|
url.Host = config.SerialTNC.Path
|
|
if hbaud := config.SerialTNC.HBaud; hbaud > 0 {
|
|
url.Params.Set("hbaud", fmt.Sprint(hbaud))
|
|
}
|
|
if sbaud := config.SerialTNC.SerialBaud; sbaud > 0 {
|
|
url.Params.Set("serial_baud", fmt.Sprint(sbaud))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Radio Only?
|
|
radioOnly := fOptions.RadioOnly
|
|
if v := url.Params.Get("radio_only"); v != "" {
|
|
radioOnly, _ = strconv.ParseBool(v)
|
|
}
|
|
if radioOnly {
|
|
if hasSSID(fOptions.MyCall) {
|
|
log.Println("Radio Only does not support callsign with SSID")
|
|
return
|
|
}
|
|
|
|
if strings.HasPrefix(url.Scheme, MethodAX25) {
|
|
log.Printf("Radio-Only is not available for %s", url.Scheme)
|
|
return
|
|
}
|
|
url.SetUser(url.User.Username() + "-T")
|
|
}
|
|
|
|
// QSY
|
|
var revertFreq func()
|
|
if freq := url.Params.Get("freq"); freq != "" {
|
|
revertFreq, err = qsy(url.Scheme, freq)
|
|
if err != nil {
|
|
log.Printf("Unable to QSY: %s", err)
|
|
return
|
|
}
|
|
defer revertFreq()
|
|
}
|
|
var currFreq Frequency
|
|
if vfo, _, ok, _ := VFOForTransport(url.Scheme); ok {
|
|
f, _ := vfo.GetFreq()
|
|
currFreq = Frequency(f)
|
|
}
|
|
|
|
// Wait for a clear channel
|
|
switch url.Scheme {
|
|
case MethodArdop:
|
|
waitBusy(adTNC)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
dialCancelFunc = func() { dialing = nil; cancel() }
|
|
defer dialCancelFunc()
|
|
|
|
// Signal web gui that we are dialing a connection
|
|
dialing = url
|
|
websocketHub.UpdateStatus()
|
|
|
|
log.Printf("Connecting to %s (%s)...", url.Target, url.Scheme)
|
|
conn, err := transport.DialURLContext(ctx, url)
|
|
|
|
// Signal web gui that we are no longer dialing
|
|
dialing = nil
|
|
websocketHub.UpdateStatus()
|
|
|
|
eventLog.LogConn("connect "+connectStr, currFreq, conn, err)
|
|
|
|
switch {
|
|
case errors.Is(err, context.Canceled):
|
|
log.Printf("Connect cancelled")
|
|
return
|
|
case err != nil:
|
|
log.Printf("Unable to establish connection to remote: %s", err)
|
|
return
|
|
}
|
|
|
|
err = exchange(conn, url.Target, false)
|
|
if err != nil {
|
|
log.Printf("Exchange failed: %s", err)
|
|
} else {
|
|
log.Println("Disconnected.")
|
|
success = true
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func qsy(method, addr string) (revert func(), err error) {
|
|
noop := func() {}
|
|
rig, rigName, ok, err := VFOForTransport(method)
|
|
if err != nil {
|
|
return noop, err
|
|
} else if !ok {
|
|
return noop, fmt.Errorf("hamlib rig '%s' not loaded", rigName)
|
|
}
|
|
|
|
log.Printf("QSY %s: %s", method, addr)
|
|
_, oldFreq, err := setFreq(rig, addr)
|
|
if err != nil {
|
|
return noop, err
|
|
}
|
|
|
|
time.Sleep(3 * time.Second)
|
|
return func() {
|
|
time.Sleep(time.Second)
|
|
log.Printf("QSX %s: %.3f", method, float64(oldFreq)/1e3)
|
|
rig.SetFreq(oldFreq)
|
|
}, nil
|
|
}
|
|
|
|
func waitBusy(b transport.BusyChannelChecker) {
|
|
printed := false
|
|
|
|
for b.Busy() {
|
|
if !printed && fOptions.IgnoreBusy {
|
|
log.Println("Ignoring busy channel!")
|
|
break
|
|
} else if !printed {
|
|
log.Println("Waiting for clear channel...")
|
|
printed = true
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func initArdopTNC() error {
|
|
if adTNC != nil && adTNC.Ping() == nil {
|
|
return nil
|
|
}
|
|
|
|
if adTNC != nil {
|
|
adTNC.Close()
|
|
}
|
|
|
|
var err error
|
|
adTNC, err = ardop.OpenTCP(config.Ardop.Addr, fOptions.MyCall, config.Locator)
|
|
if err != nil {
|
|
return fmt.Errorf("ARDOP TNC initialization failed: %w", err)
|
|
}
|
|
|
|
if !config.Ardop.ARQBandwidth.IsZero() {
|
|
if err := adTNC.SetARQBandwidth(config.Ardop.ARQBandwidth); err != nil {
|
|
return fmt.Errorf("unable to set ARQ bandwidth for ardop TNC: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := adTNC.SetCWID(config.Ardop.CWID); err != nil {
|
|
return fmt.Errorf("unable to configure CWID for ardop TNC: %w", err)
|
|
}
|
|
|
|
if v, err := adTNC.Version(); err != nil {
|
|
return fmt.Errorf("ARDOP TNC initialization failed: %s", err)
|
|
} else {
|
|
log.Printf("ARDOP TNC (%s) initialized", v)
|
|
}
|
|
|
|
transport.RegisterDialer(MethodArdop, adTNC)
|
|
|
|
if !config.Ardop.PTTControl {
|
|
return nil
|
|
}
|
|
|
|
rig, ok := rigs[config.Ardop.Rig]
|
|
if !ok {
|
|
return fmt.Errorf("unable to set PTT rig '%s': Not defined or not loaded", config.Ardop.Rig)
|
|
}
|
|
|
|
adTNC.SetPTT(rig)
|
|
return nil
|
|
}
|
|
|
|
func initPactorModem(cmdlineinit string) error {
|
|
if pModem != nil {
|
|
pModem.Close()
|
|
}
|
|
var err error
|
|
pModem, err = pactor.OpenModem(config.Pactor.Path, config.Pactor.Baudrate, fOptions.MyCall, config.Pactor.InitScript, cmdlineinit)
|
|
if err != nil || pModem == nil {
|
|
return fmt.Errorf("pactor initialization failed: %w", err)
|
|
}
|
|
|
|
transport.RegisterDialer(MethodPactor, pModem)
|
|
|
|
return nil
|
|
}
|
|
|
|
func initVaraHFModem() error {
|
|
if varaHFModem != nil && varaHFModem.Ping() {
|
|
return nil
|
|
}
|
|
if varaHFModem != nil {
|
|
varaHFModem.Close()
|
|
}
|
|
m, err := initVaraModem(MethodVaraHF, config.VaraHF)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if bw := config.VaraHF.Bandwidth; bw != 0 {
|
|
if err := m.SetBandwidth(fmt.Sprint(bw)); err != nil {
|
|
m.Close()
|
|
return err
|
|
}
|
|
}
|
|
varaHFModem = m
|
|
return nil
|
|
}
|
|
|
|
func initVaraFMModem() error {
|
|
if varaFMModem != nil && varaFMModem.Ping() {
|
|
return nil
|
|
}
|
|
if varaFMModem != nil {
|
|
varaFMModem.Close()
|
|
}
|
|
m, err := initVaraModem(MethodVaraFM, config.VaraFM)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
varaFMModem = m
|
|
return nil
|
|
}
|
|
|
|
func initVaraModem(scheme string, conf cfg.VaraConfig) (*vara.Modem, error) {
|
|
vConf := vara.ModemConfig{
|
|
Host: conf.Host(),
|
|
CmdPort: conf.CmdPort(),
|
|
DataPort: conf.DataPort(),
|
|
}
|
|
m, err := vara.NewModem(scheme, fOptions.MyCall, vConf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("vara initialization failed: %w", err)
|
|
}
|
|
transport.RegisterDialer(scheme, m)
|
|
|
|
if conf.PTTControl {
|
|
rig, ok := rigs[conf.Rig]
|
|
if !ok {
|
|
m.Close()
|
|
return nil, fmt.Errorf("unable to set PTT rig '%s': not defined or not loaded", conf.Rig)
|
|
}
|
|
m.SetPTT(rig)
|
|
}
|
|
v, _ := m.Version()
|
|
log.Printf("VARA modem (%s) initialized", v)
|
|
return m, nil
|
|
}
|
|
|
|
func initAGWPE() error {
|
|
if agwpeTNC != nil && agwpeTNC.Ping() == nil {
|
|
return nil
|
|
}
|
|
|
|
if agwpeTNC != nil {
|
|
agwpeTNC.Close()
|
|
}
|
|
|
|
var err error
|
|
agwpeTNC, err = agwpe.OpenPortTCP(config.AGWPE.Addr, config.AGWPE.RadioPort, fOptions.MyCall)
|
|
if err != nil {
|
|
return fmt.Errorf("AGWPE TNC initialization failed: %w", err)
|
|
}
|
|
|
|
if v, err := agwpeTNC.Version(); err != nil {
|
|
return fmt.Errorf("AGWPE TNC initialization failed: %w", err)
|
|
} else {
|
|
log.Printf("AGWPE TNC (%s) initialized", v)
|
|
}
|
|
|
|
transport.RegisterContextDialer(MethodAX25AGWPE, agwpeTNC)
|
|
return nil
|
|
}
|
|
|
|
// defaultAX25Method resolves the generic ax25:// scheme to a implementation specific scheme.
|
|
func defaultAX25Method() string {
|
|
switch config.AX25.Engine {
|
|
case cfg.AX25EngineAGWPE:
|
|
return MethodAX25AGWPE
|
|
case cfg.AX25EngineSerialTNC:
|
|
return MethodAX25SerialTNC
|
|
case cfg.AX25EngineLinux:
|
|
return MethodAX25Linux
|
|
default:
|
|
panic(fmt.Sprintf("invalid ax25 engine: %s", config.AX25.Engine))
|
|
}
|
|
}
|