//go:build windows // Denkovi USB 8-channel relay board (FT245RL). Despite enumerating as a virtual // COM port, this board is NOT driven by serial/ASCII: the FT245's 8 data lines // each drive a relay, controlled through FTDI's D2XX "bit-bang" mode. One byte // written = the 8 relays at once (bit 0 = relay 1). We load ftd2xx.dll at runtime // (no CGO) and call the D2XX API directly, exactly as the vendor's tool does // (which addresses the board by its FTDI serial, e.g. "DAE0006K"). package relaydev import ( "context" "fmt" "strings" "sync" "syscall" "unsafe" ) var ( ftdll = syscall.NewLazyDLL("ftd2xx.dll") procOpenEx = ftdll.NewProc("FT_OpenEx") procClose = ftdll.NewProc("FT_Close") procSetBitMode = ftdll.NewProc("FT_SetBitMode") procSetBaudRate = ftdll.NewProc("FT_SetBaudRate") procWrite = ftdll.NewProc("FT_Write") procPurge = ftdll.NewProc("FT_Purge") procCreateInfo = ftdll.NewProc("FT_CreateDeviceInfoList") procGetInfoDetail = ftdll.NewProc("FT_GetDeviceInfoDetail") ) const ( ftOpenBySerial = 1 // FT_OPEN_BY_SERIAL_NUMBER ftBitModeSyncBB = 0x04 // FT_BITMODE_SYNC_BITBANG (the mode Denkovi documents) ftPurgeRX = 1 // FT_PURGE_RX ) func ftOK(r uintptr) bool { return r == 0 } // FT_OK == 0 type denkovi struct { serial string count int mu sync.Mutex shadow byte // last output byte (bit n = relay n+1); authoritative state h uintptr // FT handle opened bool } // NewDenkovi builds a driver for a Denkovi USB 8-relay board identified by its // FTDI serial number (shown by the vendor tool / FT_PROG, e.g. "DAE0006K"). func NewDenkovi(serial string) Device { return &denkovi{serial: strings.TrimSpace(serial), count: 8} } func (d *denkovi) Count() int { return d.count } // ensureOpen opens the board and puts it in synchronous bit-bang mode with all 8 // lines as outputs. Idempotent. func (d *denkovi) ensureOpen() error { if d.opened { return nil } if err := ftdll.Load(); err != nil { return fmt.Errorf("FTDI D2XX driver (ftd2xx.dll) not found — install the FTDI D2XX driver / Denkovi software: %w", err) } if d.serial == "" { return fmt.Errorf("no FTDI serial number set for the Denkovi board") } ser, err := syscall.BytePtrFromString(d.serial) if err != nil { return err } var h uintptr if r, _, _ := procOpenEx.Call(uintptr(unsafe.Pointer(ser)), ftOpenBySerial, uintptr(unsafe.Pointer(&h))); !ftOK(r) { return fmt.Errorf("cannot open Denkovi board %q (FT_OpenEx status %d) — is it connected and not in use by another app?", d.serial, r) } // All 8 lines output, synchronous bit-bang. if r, _, _ := procSetBitMode.Call(h, 0xFF, ftBitModeSyncBB); !ftOK(r) { procClose.Call(h) return fmt.Errorf("FT_SetBitMode failed (status %d)", r) } procSetBaudRate.Call(h, 9600) // bit-bang pin-update clock; relays don't need speed d.h = h d.opened = true // Make the hardware match our shadow (starts all-off on first open). return d.writeLocked() } // writeLocked pushes the shadow byte to the relays. Caller holds d.mu. func (d *denkovi) writeLocked() error { var written uint32 b := d.shadow if r, _, _ := procWrite.Call(d.h, uintptr(unsafe.Pointer(&b)), 1, uintptr(unsafe.Pointer(&written))); !ftOK(r) { return fmt.Errorf("FT_Write failed (status %d)", r) } // Synchronous bit-bang echoes each written byte into the RX buffer; drop it so // it doesn't fill over the life of the connection. procPurge.Call(d.h, ftPurgeRX) return nil } func (d *denkovi) Set(ctx context.Context, relay int, on bool) error { if relay < 1 || relay > d.count { return fmt.Errorf("relay %d out of range 1..%d", relay, d.count) } d.mu.Lock() defer d.mu.Unlock() if err := d.ensureOpen(); err != nil { return err } bit := byte(1) << uint(relay-1) // relay 1 → bit 0 if on { d.shadow |= bit } else { d.shadow &^= bit } return d.writeLocked() } func (d *denkovi) Status(ctx context.Context) ([]bool, error) { d.mu.Lock() defer d.mu.Unlock() if err := d.ensureOpen(); err != nil { return nil, err } out := make([]bool, d.count) for i := 0; i < d.count; i++ { out[i] = d.shadow&(byte(1)<= 0 { b = b[:i] } return strings.TrimSpace(string(b)) } func indexByte(b []byte, c byte) int { for i := range b { if b[i] == c { return i } } return -1 }