feat: Denkovi USB 4/8 (selectable) + generic USB-serial relay board (CH340/LCUS)

- Denkovi (FT245 D2XX) now selectable as 4 or 8 relays (a 4-relay board just uses
  the low nibble); NewDenkovi takes a count, driven by the device's Channels.
- New 'usbrelay' backend: cheap CH340/LCUS USB-serial boards over a COM port
  using the common A0 protocol ([0xA0][relay][state][checksum]); write-only, so
  Status is a shadow. Channel count configurable (1/2/4/8/16).
- StationDevice gains Channels; deviceRelayCount() drives label/relay counts for
  the variable-size types. Device editor: channel selector + COM-port picker
  (usbrelay) / FTDI-serial (denkovi).

Both untested on hardware; the A0 command set / Denkovi bit order may need a tweak
after a live test.
This commit is contained in:
2026-07-20 17:23:38 +02:00
parent c07a17dc47
commit fe69bc308c
8 changed files with 189 additions and 23 deletions
+23 -6
View File
@@ -11397,14 +11397,28 @@ func boolStr(b bool) string {
// StationDevice is one configured relay board for the Station Control tab.
type StationDevice struct {
ID string `json:"id"`
Type string `json:"type"` // "webswitch" | "kmtronic" | "denkovi"
Type string `json:"type"` // "webswitch" | "kmtronic" | "denkovi" | "usbrelay"
Name string `json:"name"`
Host string `json:"host"`
Host string `json:"host"` // IP for network boards; FTDI serial for denkovi; COM port for usbrelay
User string `json:"user,omitempty"` // KMTronic HTTP auth (blank = none)
Pass string `json:"pass,omitempty"`
Channels int `json:"channels,omitempty"` // usbrelay only: number of relays (1/2/4/8/16)
Labels []string `json:"labels"` // per-relay label (index 0 = relay 1)
}
// deviceRelayCount is the relay count for a configured device — fixed by type,
// except the Denkovi (4 or 8) and the generic USB-serial board, whose channel
// count the user picks.
func deviceRelayCount(d StationDevice) int {
if d.Type == "usbrelay" || d.Type == "denkovi" {
if d.Channels >= 1 {
return d.Channels
}
return 8
}
return relayCountFor(d.Type)
}
// relayCountFor returns a device type's fixed relay count.
func relayCountFor(typ string) int {
switch typ {
@@ -11422,7 +11436,10 @@ func deviceDriver(d StationDevice) relaydev.Device {
return relaydev.NewKMTronic(d.Host, d.User, d.Pass)
case "denkovi":
// Host carries the FTDI serial number (e.g. "DAE0006K"), not a hostname.
return relaydev.NewDenkovi(d.Host)
return relaydev.NewDenkovi(d.Host, deviceRelayCount(d))
case "usbrelay":
// Host carries the COM port (e.g. "COM5"); CH340/LCUS "A0" serial protocol.
return relaydev.NewSerialRelay(d.Host, deviceRelayCount(d))
default:
return relaydev.NewWebswitch(d.Host)
}
@@ -11458,7 +11475,7 @@ func (a *App) SaveStationDevices(devs []StationDevice) error {
}
for i := range devs {
switch devs[i].Type {
case "kmtronic", "denkovi", "webswitch":
case "kmtronic", "denkovi", "usbrelay", "webswitch":
// known type — keep
default:
devs[i].Type = "webswitch"
@@ -11466,7 +11483,7 @@ func (a *App) SaveStationDevices(devs []StationDevice) error {
if strings.TrimSpace(devs[i].ID) == "" {
devs[i].ID = fmt.Sprintf("dev%d-%d", i, time.Now().UnixNano())
}
n := relayCountFor(devs[i].Type)
n := deviceRelayCount(devs[i])
for len(devs[i].Labels) < n {
devs[i].Labels = append(devs[i].Labels, "")
}
@@ -11507,7 +11524,7 @@ func (a *App) GetStationStatus() []StationDeviceStatus {
go func(i int) {
defer wg.Done()
d := devs[i]
n := relayCountFor(d.Type)
n := deviceRelayCount(d)
ds := StationDeviceStatus{ID: d.ID, Name: d.Name, Type: d.Type}
ctx, cancel := context.WithTimeout(a.ctx, 6*time.Second)
defer cancel()