// Driver for the P1AM-100 base controller. // // This is an embedded device on the P1AM-100 board. // Based on v1.0.1 of the Arduino library: https://github.com/facts-engineering/P1AM/tree/1.0.1 package p1am import ( "encoding/binary" "errors" "fmt" "machine" "time" ) type P1AM struct { bus *machine.SPI slaveSelectPin, slaveAckPin, baseEnablePin machine.Pin // SkipAutoConfig will skip loading a default configuration into each module. SkipAutoConfig bool Slots int // Access slots via Slot() slots []Slot } var Controller = P1AM{ bus: machine.SPI0, slaveSelectPin: machine.BASE_SLAVE_SELECT_PIN, slaveAckPin: machine.BASE_SLAVE_ACK_PIN, baseEnablePin: machine.BASE_ENABLE_PIN, } type baseSlotConstants struct { DI, DO, AI, AO, Status, Config, DataSize byte } func (p *P1AM) Initialize() error { p.slaveSelectPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) p.slaveAckPin.Configure(machine.PinConfig{Mode: machine.PinInput}) p.baseEnablePin.Configure(machine.PinConfig{Mode: machine.PinOutput}) if err := p.bus.Configure(machine.SPIConfig{ Frequency: 1000000, Mode: 2, LSBFirst: false, }); err != nil { return err } p.SetEnabled(true) time.Sleep(100 * time.Millisecond) if err := p.waitAck(5 * time.Second); err != nil { return errors.New("no base controller activity; check external supply connection") } for i := 0; i < 5; i++ { if err := p.handleHDR(MOD_HDR); err == nil { time.Sleep(5 * time.Millisecond) slots, err := p.spiSendRecvByte(0xFF) if err == nil && slots > 0 && slots <= 15 { p.Slots = int(slots) break } } if i > 2 { // Try restarting the base controller p.SetEnabled(false) time.Sleep(10 * time.Millisecond) p.SetEnabled(true) time.Sleep(10 * time.Millisecond) } } if p.Slots <= 0 || p.Slots > 15 { return errors.New("zero modules in the base") } moduleIDs := make([]uint32, p.Slots) p.waitAck(200 * time.Millisecond) if err := binary.Read(p, binary.LittleEndian, &moduleIDs); err != nil { return err } baseConstants := make([]baseSlotConstants, p.Slots) p.slots = make([]Slot, p.Slots) for i := 1; i <= p.Slots; i++ { slot := p.Slot(i) slot.p = p slot.slot = byte(i) slot.ID = moduleIDs[i-1] // What if 0xFFFFFFFF isn't at position -2? slot.Props = &modules[len(modules)-2] for j := 0; j < len(modules); j++ { if modules[j].ModuleID == slot.ID { slot.Props = &modules[j] } bc := &baseConstants[i-1] bc.DI = slot.Props.DI bc.DO = slot.Props.DO bc.AI = slot.Props.AI bc.AO = slot.Props.AO bc.Status = slot.Props.Status bc.Config = slot.Props.Config bc.DataSize = slot.Props.DataSize } } p.waitAck(200 * time.Millisecond) if err := binary.Write(p, binary.LittleEndian, &baseConstants); err != nil { return err } if !p.SkipAutoConfig { for i := 1; i <= p.Slots; i++ { s := p.Slot(i) if s.Props.Config > 0 { cfg := defaultConfig[s.ID] if cfg != nil { s.Configure(cfg) } } } } return nil } func (p *P1AM) Version() ([3]byte, error) { if err := p.handleHDR(VERSION_HDR); err != nil { return [3]byte{}, err } var buf [4]byte if err := p.spiSendRecvBuf(nil, buf[:]); err != nil { return [3]byte{}, err } return [3]byte{ byte(buf[1] >> 4), byte(buf[1] & 0xF), byte(buf[0]), }, p.dataSync() } func (p *P1AM) Active() (bool, error) { if _, err := p.spiSendRecvByte(ACTIVE_HDR); err != nil { return false, err } if err := p.waitAck(200 * time.Millisecond); err != nil { return false, err } buf, err := p.spiSendRecvByte(DUMMY) defer p.dataSync() return buf != 0, err } const wdToggleTime = 100 * time.Millisecond func (p *P1AM) ConfigureWatchdog(interval time.Duration, reset bool) error { ms := interval / time.Millisecond toggleMs := wdToggleTime / time.Millisecond resetB := byte(0) if reset { resetB = 1 } buf := [6]byte{ CONFIGWD_HDR, byte(ms), byte(ms >> 8), byte(toggleMs), byte(toggleMs >> 8), resetB, } if err := p.spiSendRecvBuf(buf[:], nil); err != nil { return err } return p.dataSync() } func (p *P1AM) sendWatchdog(hdr byte) error { if _, err := p.spiSendRecvByte(hdr); err != nil { return err } if err := p.waitAck(200 * time.Millisecond); err != nil { return err } if _, err := p.spiSendRecvByte(DUMMY); err != nil { return err } return p.dataSync() } func (p *P1AM) StartWatchdog() error { return p.sendWatchdog(STARTWD_HDR) } func (p *P1AM) StopWatchdog() error { return p.sendWatchdog(STOPWD_HDR) } func (p *P1AM) PetWatchdog() error { return p.sendWatchdog(PETWD_HDR) } func (p *P1AM) Slot(i int) *Slot { if i < 1 || i > p.Slots { return nil } return &p.slots[i-1] } type Slot struct { p *P1AM slot byte ID uint32 // TODO: Embed this? Props *ModuleProps } func (s *Slot) Configure(data []byte) error { if s == nil { return errors.New("invalid slot") } if len(data) != int(s.Props.Config) { return fmt.Errorf("expected %d config bytes, got %d", s.Props.Config, len(data)) } if len(data) == 0 { return errors.New("no config bytes") } out := make([]byte, len(data)+2) out[0] = CFG_HDR out[1] = s.slot copy(out[2:], data) if err := s.p.spiSendRecvBuf(out, nil); err != nil { return err } time.Sleep(100 * time.Millisecond) s.p.dataSync() s.p.dataSync() return nil } func (s *Slot) ReadDiscrete() (uint32, error) { if s == nil { return 0, errors.New("invalid slot") } bytes := s.Props.DI out := [2]byte{ READ_DISCRETE_HDR, s.slot, } if err := s.p.spiSendRecvBuf(out[:], nil); err != nil { return 0, err } if err := s.p.waitAck(200 * time.Millisecond); err != nil { return 0, err } var data [4]byte if err := s.p.spiSendRecvBuf(nil, data[:bytes]); err != nil { return 0, err } err := s.p.dataSync() return binary.LittleEndian.Uint32(data[:]), err } func (s *Slot) WriteDiscrete(value uint32) error { return s.writeDiscrete(0, value) } func (s *Slot) writeDiscrete(channel byte, value uint32) error { if s == nil { return errors.New("invalid slot") } bytes := s.Props.DO buf := [7]byte{ WRITE_DISCRETE_HDR, s.slot, channel, } binary.LittleEndian.PutUint32(buf[3:], value) out := buf[:3+bytes] if channel != 0 { out = buf[:4] out[3] &= 1 } if err := s.p.spiSendRecvBuf(out, nil); err != nil { return err } return s.p.dataSync() } type Channel struct { s *Slot channel int } func (s *Slot) Channel(channel int) Channel { return Channel{ s: s, channel: channel, } } func (c Channel) ReadDiscrete() (bool, error) { if c.channel < 1 || c.channel > int(c.s.Props.DI)*8 { return false, errors.New("invalid channel") } data, err := c.s.ReadDiscrete() return (data>>(c.channel-1))&1 == 1, err } func (c Channel) WriteDiscrete(value bool) error { if c.channel < 1 || c.channel > int(c.s.Props.DO)*8 { return errors.New("invalid channel") } data := uint32(0) if value { data = 1 } return c.s.writeDiscrete(byte(c.channel), data) } const ackTimeout = 200 * time.Millisecond func awaitPin(pin machine.Pin, state bool, timeout time.Duration) bool { start := time.Now() for pin.Get() != state { time.Sleep(100 * time.Microsecond) if time.Since(start) > timeout { return false } } return true // TODO: Use channels when https://github.com/tinygo-org/tinygo/pull/1402 is merged. // edge := machine.PinRising // if state { // edge = machine.PinFalling // } // ch := make(chan struct{}, 1) // defer close(ch) // pin.SetInterrupt(edge, func(machine.Pin) { // ch <- struct{}{} // }) // defer pin.SetInterrupt(0, nil) // select { // case <-ch: // return true // case <-time.After(timeout): // return false // } } var dataSyncErr = errors.New("base sync timeout") func (p *P1AM) dataSync() error { if !awaitPin(p.slaveAckPin, true, ackTimeout) { return dataSyncErr } time.Sleep(time.Microsecond) if !awaitPin(p.slaveAckPin, false, ackTimeout) { return dataSyncErr } time.Sleep(time.Microsecond) if !awaitPin(p.slaveAckPin, true, ackTimeout) { return dataSyncErr } time.Sleep(time.Microsecond) return nil } func (p *P1AM) handleHDR(HDR byte) error { for !p.slaveAckPin.Get() { } if _, err := p.spiSendRecvByte(HDR); err != nil { return err } return p.spiTimeout(MAX_TIMEOUT*time.Millisecond, HDR, 2*time.Second) } func (p *P1AM) Read(data []byte) (int, error) { return len(data), p.spiSendRecvBuf(nil, data) } func (p *P1AM) Write(data []byte) (int, error) { return len(data), p.spiSendRecvBuf(data, nil) } func (p *P1AM) spiSendRecvBuf(w, r []byte) error { p.slaveSelectPin.Low() defer p.slaveSelectPin.High() return p.bus.Tx(w, r) } func (p *P1AM) spiSendRecvByte(data byte) (byte, error) { p.slaveSelectPin.Low() defer p.slaveSelectPin.High() return p.bus.Transfer(data) } func (p *P1AM) waitAck(timeout time.Duration) error { return p.spiTimeout(timeout, 0, 0) } var timeoutErr = errors.New("timeout") func (p *P1AM) spiTimeout(timeout time.Duration, resendMsg byte, retryPeriod time.Duration) error { end := time.Now().Add(timeout) retry := time.Now().Add(retryPeriod) for time.Now().Before(end) { if p.slaveAckPin.Get() { time.Sleep(50 * time.Microsecond) return nil } if retryPeriod > 0 && time.Now().After(retry) { p.spiSendRecvByte(resendMsg) retry = retry.Add(retryPeriod) } } return timeoutErr } func (p *P1AM) SetEnabled(enabled bool) { p.baseEnablePin.Set(enabled) }