fix: cache relay-board drivers so Denkovi/USB-serial boards stay connected
The station-control code rebuilt a fresh driver on every status poll and every relay set. Stateful boards (Denkovi FTDI D2XX, USB-serial) hold an OS handle only one opener can own, so the first poll opened the board and leaked the handle, and every poll after failed with 'device in use' — the relays greyed out a second after Save and auto-control never switched. Drivers are now cached per device and reused, closed on config change. Adds a Test-connection button + detect feedback in the device editor (reported by VK4MA).
This commit is contained in:
@@ -490,6 +490,8 @@ type App struct {
|
||||
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
||||
relayAutoLast map[string]bool // deviceID|relay → last applied on/off, so we only switch on a real change
|
||||
relayAutoOn atomic.Bool // cached "auto-control enabled" so the CAT hot path skips work when off
|
||||
relayDrvMu sync.Mutex // guards the cached relay drivers below
|
||||
relayDrv map[string]cachedRelay // deviceID → live driver (reused across polls; stateful boards can't be reopened per call)
|
||||
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
|
||||
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
|
||||
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
|
||||
@@ -11433,8 +11435,15 @@ func relayCountFor(typ string) int {
|
||||
}
|
||||
}
|
||||
|
||||
// deviceDriver builds the wire driver for a configured device.
|
||||
func deviceDriver(d StationDevice) relaydev.Device {
|
||||
// cachedRelay is a live driver plus the config signature it was built from, so a
|
||||
// changed device (new COM port, serial or channel count) rebuilds it.
|
||||
type cachedRelay struct {
|
||||
key string
|
||||
dev relaydev.Device
|
||||
}
|
||||
|
||||
// buildDeviceDriver builds a fresh wire driver for a configured device.
|
||||
func buildDeviceDriver(d StationDevice) relaydev.Device {
|
||||
switch d.Type {
|
||||
case "kmtronic":
|
||||
return relaydev.NewKMTronic(d.Host, d.User, d.Pass)
|
||||
@@ -11449,12 +11458,74 @@ func deviceDriver(d StationDevice) relaydev.Device {
|
||||
}
|
||||
}
|
||||
|
||||
// deviceKey is the config signature that, when unchanged, lets us reuse a device's
|
||||
// open driver (and its OS handle) instead of rebuilding it every poll.
|
||||
func deviceKey(d StationDevice) string {
|
||||
return fmt.Sprintf("%s|%s|%s|%s|%d", d.Type, d.Host, d.User, d.Pass, deviceRelayCount(d))
|
||||
}
|
||||
|
||||
// driverFor returns the cached, still-open driver for a device, building it once
|
||||
// and reusing it thereafter. Stateful boards (Denkovi FTDI, USB-serial) hold an OS
|
||||
// handle that only one opener can own, so rebuilding a driver every call — as the
|
||||
// old code did — leaked the handle and made every poll after the first fail with
|
||||
// "device in use", greying the relays out. Reuse fixes that.
|
||||
func (a *App) driverFor(d StationDevice) relaydev.Device {
|
||||
key := deviceKey(d)
|
||||
a.relayDrvMu.Lock()
|
||||
defer a.relayDrvMu.Unlock()
|
||||
if a.relayDrv == nil {
|
||||
a.relayDrv = map[string]cachedRelay{}
|
||||
}
|
||||
if c, ok := a.relayDrv[d.ID]; ok {
|
||||
if c.key == key {
|
||||
return c.dev
|
||||
}
|
||||
_ = c.dev.Close() // config changed → release the old handle before rebuilding
|
||||
delete(a.relayDrv, d.ID)
|
||||
}
|
||||
dev := buildDeviceDriver(d)
|
||||
a.relayDrv[d.ID] = cachedRelay{key: key, dev: dev}
|
||||
return dev
|
||||
}
|
||||
|
||||
// closeRelayDrivers closes and drops every cached driver (e.g. after the device
|
||||
// list changes) so stale handles are released and rebuilt fresh on next use.
|
||||
func (a *App) closeRelayDrivers() {
|
||||
a.relayDrvMu.Lock()
|
||||
defer a.relayDrvMu.Unlock()
|
||||
for id, c := range a.relayDrv {
|
||||
_ = c.dev.Close()
|
||||
delete(a.relayDrv, id)
|
||||
}
|
||||
}
|
||||
|
||||
// ListDenkoviDevices returns the FTDI serial numbers of connected Denkovi/FTDI
|
||||
// boards, for the settings picker. Windows-only (FTDI D2XX).
|
||||
func (a *App) ListDenkoviDevices() ([]string, error) {
|
||||
return relaydev.ListDenkovi()
|
||||
}
|
||||
|
||||
// StationTestResult is the outcome of probing one relay board, for the settings
|
||||
// dialog's Connect/Test button so the user gets a clear connected/failed message.
|
||||
type StationTestResult struct {
|
||||
OK bool `json:"ok"`
|
||||
Relays int `json:"relays"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TestStationDevice opens the given board and reads its state, returning a clear
|
||||
// success/failure so the settings UI can tell the user whether it's reachable.
|
||||
// Reuses the cached driver (no double-open conflict with the live poll).
|
||||
func (a *App) TestStationDevice(d StationDevice) StationTestResult {
|
||||
ctx, cancel := context.WithTimeout(a.ctx, 6*time.Second)
|
||||
defer cancel()
|
||||
states, err := a.driverFor(d).Status(ctx)
|
||||
if err != nil {
|
||||
return StationTestResult{OK: false, Error: err.Error()}
|
||||
}
|
||||
return StationTestResult{OK: true, Relays: len(states)}
|
||||
}
|
||||
|
||||
// GetStationDevices returns the configured relay boards (without live state).
|
||||
func (a *App) GetStationDevices() []StationDevice {
|
||||
out := []StationDevice{}
|
||||
@@ -11497,6 +11568,9 @@ func (a *App) SaveStationDevices(devs []StationDevice) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Release any open board handles so the new config reopens cleanly (a changed
|
||||
// COM port / FTDI serial must not stay held by a stale driver).
|
||||
a.closeRelayDrivers()
|
||||
return a.settings.SetGlobal(a.ctx, keyStationDevices, string(b))
|
||||
}
|
||||
|
||||
@@ -11532,7 +11606,7 @@ func (a *App) GetStationStatus() []StationDeviceStatus {
|
||||
ds := StationDeviceStatus{ID: d.ID, Name: d.Name, Type: d.Type}
|
||||
ctx, cancel := context.WithTimeout(a.ctx, 6*time.Second)
|
||||
defer cancel()
|
||||
states, err := deviceDriver(d).Status(ctx)
|
||||
states, err := a.driverFor(d).Status(ctx)
|
||||
if err != nil {
|
||||
ds.Error = err.Error()
|
||||
} else {
|
||||
@@ -11563,7 +11637,7 @@ func (a *App) StationSetRelay(id string, relay int, on bool) error {
|
||||
if d.ID == id {
|
||||
ctx, cancel := context.WithTimeout(a.ctx, 6*time.Second)
|
||||
defer cancel()
|
||||
return deviceDriver(d).Set(ctx, relay, on)
|
||||
return a.driverFor(d).Set(ctx, relay, on)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("station device %q not found", id)
|
||||
|
||||
Reference in New Issue
Block a user