fix: Save and close taking time when Flex or Icom is not connected
This commit is contained in:
+35
-4
@@ -4,6 +4,7 @@ package cat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@@ -25,9 +26,10 @@ type Flex struct {
|
|||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
wmu sync.Mutex // serialises writes to conn
|
dialCancel context.CancelFunc // cancels an in-flight Connect dial (Interrupt/Stop); nil when not dialing
|
||||||
|
wmu sync.Mutex // serialises writes to conn
|
||||||
seq int
|
seq int
|
||||||
handle string
|
handle string
|
||||||
model string
|
model string
|
||||||
@@ -171,7 +173,19 @@ func (f *Flex) Connect() error {
|
|||||||
if host == "" {
|
if host == "" {
|
||||||
return fmt.Errorf("flex: no radio IP configured")
|
return fmt.Errorf("flex: no radio IP configured")
|
||||||
}
|
}
|
||||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, strconv.Itoa(port)), 5*time.Second)
|
// Cancellable dial: Interrupt() (called by Stop/Start) aborts it at once so a
|
||||||
|
// dead radio's 5 s dial timeout doesn't make Stop / Settings "Save & Close"
|
||||||
|
// wait several seconds for the poll goroutine to give up.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
f.mu.Lock()
|
||||||
|
f.dialCancel = cancel
|
||||||
|
f.mu.Unlock()
|
||||||
|
var d net.Dialer
|
||||||
|
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||||
|
cancel()
|
||||||
|
f.mu.Lock()
|
||||||
|
f.dialCancel = nil
|
||||||
|
f.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("flex: connect %s:%d: %w", host, port, err)
|
return fmt.Errorf("flex: connect %s:%d: %w", host, port, err)
|
||||||
}
|
}
|
||||||
@@ -224,6 +238,23 @@ func (f *Flex) Disconnect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interrupt aborts an in-flight Connect dial so Stop()/Start() (Settings
|
||||||
|
// "Save & Close", CAT backend switch) don't block on a dead radio's 5 s dial
|
||||||
|
// timeout. Satisfies the Manager's optional interruptible interface. Safe to call
|
||||||
|
// anytime and from another goroutine; a no-op when not dialing.
|
||||||
|
func (f *Flex) Interrupt() {
|
||||||
|
f.mu.Lock()
|
||||||
|
cancel := f.dialCancel
|
||||||
|
c := f.conn
|
||||||
|
f.mu.Unlock()
|
||||||
|
if cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
if c != nil {
|
||||||
|
_ = c.Close() // unblock the reader if we're already past the dial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// send writes a sequenced command (C<seq>|<cmd>) to the radio and returns the
|
// send writes a sequenced command (C<seq>|<cmd>) to the radio and returns the
|
||||||
// sequence number (so the caller can match the R<seq> response, e.g. to learn a
|
// sequence number (so the caller can match the R<seq> response, e.g. to learn a
|
||||||
// new spot's index). Returns 0 when not connected. Best effort.
|
// new spot's index). Returns 0 when not connected. Best effort.
|
||||||
|
|||||||
@@ -444,8 +444,14 @@ func (b *IcomSerial) write(payload ...byte) error {
|
|||||||
|
|
||||||
// recv waits for a frame the reader routed to respCh that satisfies match, or
|
// recv waits for a frame the reader routed to respCh that satisfies match, or
|
||||||
// times out. The reader has already discarded echoes and split off scope frames,
|
// times out. The reader has already discarded echoes and split off scope frames,
|
||||||
// so recv only ever sees candidate control replies.
|
// so recv only ever sees candidate control replies. It also bails out at once if
|
||||||
|
// Interrupt() fires (Stop) so an in-flight ReadState — which can be a dozen reads,
|
||||||
|
// each up to icomReadTimeout when the rig is slow — doesn't make Stop/Save-&-Close
|
||||||
|
// wait several seconds for the poll goroutine to finish.
|
||||||
func (b *IcomSerial) recv(timeout time.Duration, match func(civ.Decoded) bool) (civ.Decoded, error) {
|
func (b *IcomSerial) recv(timeout time.Duration, match func(civ.Decoded) bool) (civ.Decoded, error) {
|
||||||
|
b.dialMu.Lock()
|
||||||
|
cancel := b.dialCancel
|
||||||
|
b.dialMu.Unlock()
|
||||||
deadline := time.After(timeout)
|
deadline := time.After(timeout)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -453,6 +459,8 @@ func (b *IcomSerial) recv(timeout time.Duration, match func(civ.Decoded) bool) (
|
|||||||
if match(f) {
|
if match(f) {
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
case <-cancel:
|
||||||
|
return civ.Decoded{}, fmt.Errorf("icom: interrupted")
|
||||||
case <-deadline:
|
case <-deadline:
|
||||||
return civ.Decoded{}, fmt.Errorf("icom: timeout waiting for response")
|
return civ.Decoded{}, fmt.Errorf("icom: timeout waiting for response")
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-4
@@ -3,6 +3,7 @@
|
|||||||
package cat
|
package cat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -27,9 +28,10 @@ type TCI struct {
|
|||||||
digitalDefault string // surfaced when the rig reports a digital mode (FT8/…)
|
digitalDefault string // surfaced when the rig reports a digital mode (FT8/…)
|
||||||
spotsEnabled bool // mirror cluster spots onto the TCI panorama
|
spotsEnabled bool // mirror cluster spots onto the TCI panorama
|
||||||
|
|
||||||
mu sync.Mutex // guards conn + writes + state
|
mu sync.Mutex // guards conn + writes + state
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
ready bool
|
dialCancel context.CancelFunc // cancels an in-flight Connect dial (Interrupt/Stop)
|
||||||
|
ready bool
|
||||||
|
|
||||||
// Cached state pushed by the radio.
|
// Cached state pushed by the radio.
|
||||||
device string
|
device string
|
||||||
@@ -70,8 +72,18 @@ func (t *TCI) Connect() error {
|
|||||||
return fmt.Errorf("tci: no host configured")
|
return fmt.Errorf("tci: no host configured")
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("ws://%s", net.JoinHostPort(host, strconv.Itoa(port)))
|
url := fmt.Sprintf("ws://%s", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||||
|
// Cancellable dial so Interrupt() (Stop / Settings "Save & Close") aborts it at
|
||||||
|
// once instead of waiting out a dead server's 5 s handshake timeout.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
t.mu.Lock()
|
||||||
|
t.dialCancel = cancel
|
||||||
|
t.mu.Unlock()
|
||||||
dialer := websocket.Dialer{HandshakeTimeout: 5 * time.Second}
|
dialer := websocket.Dialer{HandshakeTimeout: 5 * time.Second}
|
||||||
conn, _, err := dialer.Dial(url, nil)
|
conn, _, err := dialer.DialContext(ctx, url, nil)
|
||||||
|
cancel()
|
||||||
|
t.mu.Lock()
|
||||||
|
t.dialCancel = nil
|
||||||
|
t.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("tci: connect %s: %w", url, err)
|
return fmt.Errorf("tci: connect %s: %w", url, err)
|
||||||
}
|
}
|
||||||
@@ -122,6 +134,22 @@ func (t *TCI) Disconnect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interrupt aborts an in-flight Connect dial so Stop()/Start() don't block on a
|
||||||
|
// dead server's handshake timeout. Satisfies the Manager's interruptible
|
||||||
|
// interface. Safe from another goroutine; a no-op when not dialing.
|
||||||
|
func (t *TCI) Interrupt() {
|
||||||
|
t.mu.Lock()
|
||||||
|
cancel := t.dialCancel
|
||||||
|
c := t.conn
|
||||||
|
t.mu.Unlock()
|
||||||
|
if cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
if c != nil {
|
||||||
|
_ = c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ReadState returns the cached state pushed by the radio.
|
// ReadState returns the cached state pushed by the radio.
|
||||||
func (t *TCI) ReadState() (RigState, error) {
|
func (t *TCI) ReadState() (RigState, error) {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user