feat: Denkovi USB 8-relay board (FT245 D2XX bit-bang)

Third relay device alongside WebSwitch and KMTronic. Despite enumerating as a
COM port, this board is driven via FTDI D2XX synchronous bit-bang (one byte = 8
relays, bit 0 = relay 1), not serial ASCII — so we load ftd2xx.dll at runtime
(no CGO) and call FT_OpenEx/FT_SetBitMode/FT_Write, addressing the board by its
FTDI serial (e.g. DAE0006K), exactly like the vendor tool. Windows-only (stub
elsewhere); ListDenkoviDevices enumerates connected serials for the picker.

Wired into relaydev (Count/Status/Set), app.go (deviceDriver/relayCountFor/type
whitelist + ListDenkoviDevices binding), and the Station device editor (new type
+ FTDI-serial field with Detect). Untested on hardware; bit order / on-polarity
may need a flip after a live test.
This commit is contained in:
2026-07-20 16:50:04 +02:00
parent 3cef885934
commit c07a17dc47
8 changed files with 279 additions and 11 deletions
+22 -6
View File
@@ -11397,7 +11397,7 @@ 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"
Type string `json:"type"` // "webswitch" | "kmtronic" | "denkovi"
Name string `json:"name"`
Host string `json:"host"`
User string `json:"user,omitempty"` // KMTronic HTTP auth (blank = none)
@@ -11407,18 +11407,31 @@ type StationDevice struct {
// relayCountFor returns a device type's fixed relay count.
func relayCountFor(typ string) int {
if typ == "kmtronic" {
switch typ {
case "kmtronic", "denkovi":
return 8
default:
return 5 // webswitch 1216H
}
return 5 // webswitch 1216H
}
// deviceDriver builds the wire driver for a configured device.
func deviceDriver(d StationDevice) relaydev.Device {
if d.Type == "kmtronic" {
switch d.Type {
case "kmtronic":
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)
default:
return relaydev.NewWebswitch(d.Host)
}
return relaydev.NewWebswitch(d.Host)
}
// 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()
}
// GetStationDevices returns the configured relay boards (without live state).
@@ -11444,7 +11457,10 @@ func (a *App) SaveStationDevices(devs []StationDevice) error {
return fmt.Errorf("db not initialized")
}
for i := range devs {
if devs[i].Type != "kmtronic" {
switch devs[i].Type {
case "kmtronic", "denkovi", "webswitch":
// known type — keep
default:
devs[i].Type = "webswitch"
}
if strings.TrimSpace(devs[i].ID) == "" {