mirror of
https://github.com/la5nta/pat
synced 2026-08-13 17:46:54 -04:00
461 lines
11 KiB
Go
461 lines
11 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 app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/la5nta/pat/cfg"
|
|
"github.com/la5nta/pat/internal/buildinfo"
|
|
"github.com/la5nta/pat/internal/debug"
|
|
"github.com/la5nta/pat/internal/prehook"
|
|
|
|
"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"
|
|
)
|
|
|
|
func hasSSID(str string) bool { return strings.Contains(str, "-") }
|
|
|
|
func (a *App) Connect(connectStr string) (success bool) {
|
|
if connectStr == "" {
|
|
return false
|
|
} else if aliased, ok := a.config.ConnectAliases[connectStr]; ok {
|
|
return a.Connect(aliased)
|
|
}
|
|
|
|
// Replace placeholders
|
|
connectStr = strings.ReplaceAll(connectStr, cfg.PlaceholderMycall, a.options.MyCall)
|
|
|
|
// Prompt if Winlink account is unconfirmed
|
|
if confirmed := a.promptUnconfirmedAccount(); !confirmed {
|
|
return false
|
|
}
|
|
|
|
// Hack around bug in frontend which may occur if the status updates too quickly.
|
|
if a.websocketHub.NumClients() > 0 {
|
|
defer func() { time.Sleep(time.Second); a.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 = a.defaultAX25Method()
|
|
}
|
|
|
|
// Init TNCs
|
|
switch url.Scheme {
|
|
case MethodAX25AGWPE:
|
|
if err := a.initAGWPE(); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
case MethodArdop:
|
|
if err := a.initARDOP(); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
case MethodPactor:
|
|
ptCmdInit := ""
|
|
if val, ok := url.Params["init"]; ok {
|
|
ptCmdInit = strings.Join(val, "\n")
|
|
}
|
|
if err := a.initPACTOR(ptCmdInit); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
case MethodVaraHF:
|
|
if err := a.initVARAHF(); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
case MethodVaraFM:
|
|
if err := a.initVARAFM(); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Set default userinfo (mycall)
|
|
if url.User == nil {
|
|
url.SetUser(a.options.MyCall)
|
|
}
|
|
|
|
// Set default host interface address
|
|
if url.Host == "" {
|
|
switch url.Scheme {
|
|
case MethodAX25Linux:
|
|
url.Host = a.config.AX25Linux.Port
|
|
case MethodAX25SerialTNC:
|
|
url.Host = a.config.SerialTNC.Path
|
|
if hbaud := a.config.SerialTNC.HBaud; hbaud > 0 {
|
|
url.Params.Set("hbaud", fmt.Sprint(hbaud))
|
|
}
|
|
if sbaud := a.config.SerialTNC.SerialBaud; sbaud > 0 {
|
|
url.Params.Set("serial_baud", fmt.Sprint(sbaud))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Radio Only?
|
|
radioOnly := a.options.RadioOnly
|
|
if v := url.Params.Get("radio_only"); v != "" {
|
|
radioOnly, _ = strconv.ParseBool(v)
|
|
}
|
|
if radioOnly {
|
|
if hasSSID(a.options.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 = a.qsy(url.Scheme, freq)
|
|
if err != nil {
|
|
log.Printf("Unable to QSY: %s", err)
|
|
return
|
|
}
|
|
defer revertFreq()
|
|
}
|
|
var currFreq Frequency
|
|
if vfo, _, ok, _ := a.VFOForTransport(url.Scheme); ok {
|
|
f, _ := vfo.GetFreq()
|
|
currFreq = Frequency(f)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
a.dialCancelFunc = func() { a.dialing = nil; cancel() }
|
|
defer a.dialCancelFunc()
|
|
|
|
// Signal web gui that we are dialing a connection
|
|
a.dialing = url
|
|
a.websocketHub.UpdateStatus()
|
|
|
|
prehookScript := prehook.Script{
|
|
Dir: a.options.PrehooksPath,
|
|
File: url.Params.Get("prehook"),
|
|
Args: url.Params["prehook-arg"],
|
|
}
|
|
if prehookScript.File != "" {
|
|
if err := prehookScript.VerifyFile(); err != nil {
|
|
log.Printf("invalid prehook: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
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
|
|
a.dialing = nil
|
|
a.websocketHub.UpdateStatus()
|
|
|
|
a.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
|
|
}
|
|
|
|
if prehookScript.File != "" {
|
|
log.Println("Running prehook...")
|
|
prehookScript.Env = append([]string{
|
|
buildinfo.AppName + "_DIAL_URL=" + connectStr,
|
|
buildinfo.AppName + "_REMOTE_ADDR=" + conn.RemoteAddr().String(),
|
|
buildinfo.AppName + "_LOCAL_ADDR=" + conn.LocalAddr().String(),
|
|
}, append(os.Environ(), a.Env()...)...)
|
|
conn = prehook.Wrap(conn)
|
|
if err := prehookScript.Execute(ctx, conn); err != nil {
|
|
conn.Close()
|
|
log.Printf("Prehook script failed: %s", err)
|
|
return
|
|
}
|
|
log.Println("Prehook succeeded")
|
|
}
|
|
|
|
err = a.exchange(conn, url.Target, false)
|
|
if err != nil {
|
|
log.Printf("Exchange failed: %s", err)
|
|
} else {
|
|
log.Println("Disconnected.")
|
|
success = true
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (a *App) qsy(method, addr string) (revert func(), err error) {
|
|
noop := func() {}
|
|
rig, rigName, ok, err := a.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 (a *App) onBusyChannel(ctx context.Context) (abort bool) {
|
|
if a.options.IgnoreBusy {
|
|
log.Println("Ignoring busy channel!")
|
|
return false
|
|
}
|
|
|
|
log.Println("Waiting for clear channel...")
|
|
select {
|
|
case <-ctx.Done():
|
|
// The channel is no longer busy.
|
|
log.Println("Channel clear")
|
|
return false
|
|
case resp := <-a.promptHub.Prompt(ctx, 5*time.Minute, PromptKindBusyChannel, "Waiting for clear channel..."):
|
|
return resp.Value == "abort" || resp.Err == context.DeadlineExceeded
|
|
}
|
|
}
|
|
|
|
// ARDOP returns the initialized ARDOP modem, initializing it if necessary.
|
|
func (a *App) ARDOP() (*ardop.TNC, error) {
|
|
if err := a.initARDOP(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.ardop, nil
|
|
}
|
|
|
|
func (a *App) initARDOP() error {
|
|
if a.ardop != nil && a.ardop.Ping() == nil {
|
|
return nil
|
|
}
|
|
|
|
if a.ardop != nil {
|
|
a.ardop.Close()
|
|
}
|
|
|
|
var err error
|
|
a.ardop, err = ardop.OpenTCP(a.config.Ardop.Addr, a.options.MyCall, a.config.Locator)
|
|
if err != nil {
|
|
return fmt.Errorf("ARDOP TNC initialization failed: %w", err)
|
|
}
|
|
|
|
a.ardop.SetBusyFunc(a.onBusyChannel)
|
|
|
|
if !a.config.Ardop.ARQBandwidth.IsZero() {
|
|
if err := a.ardop.SetARQBandwidth(a.config.Ardop.ARQBandwidth); err != nil {
|
|
return fmt.Errorf("unable to set ARQ bandwidth for ardop TNC: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := a.ardop.SetCWID(a.config.Ardop.CWID); err != nil {
|
|
return fmt.Errorf("unable to configure CWID for ardop TNC: %w", err)
|
|
}
|
|
|
|
if v, err := a.ardop.Version(); err != nil {
|
|
return fmt.Errorf("ARDOP TNC initialization failed: %s", err)
|
|
} else {
|
|
log.Printf("ARDOP TNC (%s) initialized", v)
|
|
}
|
|
|
|
transport.RegisterDialer(MethodArdop, a.ardop)
|
|
|
|
if !a.config.Ardop.PTTControl {
|
|
return nil
|
|
}
|
|
|
|
rig, ok := a.rigs[a.config.Ardop.Rig]
|
|
if !ok {
|
|
return fmt.Errorf("unable to set PTT rig '%s': Not defined or not loaded", a.config.Ardop.Rig)
|
|
}
|
|
|
|
a.ardop.SetPTT(rig)
|
|
return nil
|
|
}
|
|
|
|
func (a *App) initPACTOR(cmdlineinit string) error {
|
|
if a.pactor != nil {
|
|
a.pactor.Close()
|
|
}
|
|
var err error
|
|
a.pactor, err = pactor.OpenModem(a.config.Pactor.Path, a.config.Pactor.Baudrate, a.options.MyCall, a.config.Pactor.InitScript, cmdlineinit)
|
|
if err != nil || a.pactor == nil {
|
|
return fmt.Errorf("pactor initialization failed: %w", err)
|
|
}
|
|
|
|
transport.RegisterDialer(MethodPactor, a.pactor)
|
|
|
|
return nil
|
|
}
|
|
|
|
// VARAHF returns the initialized VARA HF modem, initializing it if necessary.
|
|
func (a *App) VARAHF() (*vara.Modem, error) {
|
|
if err := a.initVARAHF(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.varaHF, nil
|
|
}
|
|
|
|
func (a *App) initVARAHF() error {
|
|
if a.varaHF != nil && a.varaHF.Ping() {
|
|
return nil
|
|
}
|
|
if a.varaHF != nil {
|
|
a.varaHF.Close()
|
|
}
|
|
m, err := a.initVARA(MethodVaraHF, a.config.VaraHF)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if bw := a.config.VaraHF.Bandwidth; bw != 0 {
|
|
if err := m.SetBandwidth(fmt.Sprint(bw)); err != nil {
|
|
m.Close()
|
|
return err
|
|
}
|
|
}
|
|
a.varaHF = m
|
|
return nil
|
|
}
|
|
|
|
// VARAFM returns the initialized VARA FM modem, initializing it if necessary.
|
|
func (a *App) VARAFM() (*vara.Modem, error) {
|
|
if err := a.initVARAFM(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.varaFM, nil
|
|
}
|
|
|
|
func (a *App) initVARAFM() error {
|
|
if a.varaFM != nil && a.varaFM.Ping() {
|
|
return nil
|
|
}
|
|
if a.varaFM != nil {
|
|
a.varaFM.Close()
|
|
}
|
|
m, err := a.initVARA(MethodVaraFM, a.config.VaraFM)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.varaFM = m
|
|
return nil
|
|
}
|
|
|
|
func (a *App) initVARA(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, a.options.MyCall, vConf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("vara initialization failed: %w", err)
|
|
}
|
|
transport.RegisterDialer(scheme, m)
|
|
m.SetBusyFunc(a.onBusyChannel)
|
|
|
|
if conf.PTTControl {
|
|
rig, ok := a.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
|
|
}
|
|
|
|
// AGWPE returns the initialized AGWPE TNC, initializing it if necessary.
|
|
func (a *App) AGWPE() (*agwpe.TNCPort, error) {
|
|
if err := a.initAGWPE(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.agwpe, nil
|
|
}
|
|
|
|
func (a *App) initAGWPE() error {
|
|
if a.agwpe != nil && a.agwpe.Ping() == nil {
|
|
return nil
|
|
}
|
|
|
|
if a.agwpe != nil {
|
|
a.agwpe.Close()
|
|
}
|
|
|
|
var err error
|
|
a.agwpe, err = agwpe.OpenPortTCP(a.config.AGWPE.Addr, a.config.AGWPE.RadioPort, a.options.MyCall)
|
|
if err != nil {
|
|
return fmt.Errorf("AGWPE TNC initialization failed: %w", err)
|
|
}
|
|
|
|
if v, err := a.agwpe.Version(); err != nil {
|
|
return fmt.Errorf("AGWPE TNC initialization failed: %w", err)
|
|
} else {
|
|
log.Printf("AGWPE TNC (%s) initialized", v)
|
|
}
|
|
|
|
transport.RegisterContextDialer(MethodAX25AGWPE, a.agwpe)
|
|
return nil
|
|
}
|
|
|
|
// defaultAX25Method resolves the generic ax25:// scheme to a implementation specific scheme.
|
|
func (a *App) defaultAX25Method() string {
|
|
switch a.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", a.config.AX25.Engine))
|
|
}
|
|
}
|