From ff2252c54525843be55f491d55b2371b1e1a31c7 Mon Sep 17 00:00:00 2001 From: Carlos Cardoso Date: Thu, 11 Feb 2021 09:38:02 -0500 Subject: [PATCH] added support for EP0099 (#5) EP0099 is a stackable 4-channel relay hat that communicates with platforms via i2c interface. Manufacturer: 52pi Wiki page: https://wiki.52pi.com/index.php/DockerPi_4_Channel_Relay_SKU:_EP-0099 --- ep0099/doc.go | 9 +++ ep0099/ep0099.go | 110 +++++++++++++++++++++++++++++ ep0099/ep0099_test.go | 155 +++++++++++++++++++++++++++++++++++++++++ ep0099/example_test.go | 52 ++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 ep0099/doc.go create mode 100644 ep0099/ep0099.go create mode 100644 ep0099/ep0099_test.go create mode 100644 ep0099/example_test.go diff --git a/ep0099/doc.go b/ep0099/doc.go new file mode 100644 index 0000000..7455bdd --- /dev/null +++ b/ep0099/doc.go @@ -0,0 +1,9 @@ +// Copyright 2021 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that caan be found in the LICENSE file. + +// Package ep0099 controls a EP-0099 Raspberry Pi HAT with 4 relays via I2C. +// +// Datasheet +// https://wiki.52pi.com/index.php/DockerPi_4_Channel_Relay_SKU:_EP-0099 +package ep0099 diff --git a/ep0099/ep0099.go b/ep0099/ep0099.go new file mode 100644 index 0000000..1c278f6 --- /dev/null +++ b/ep0099/ep0099.go @@ -0,0 +1,110 @@ +// Copyright 2021 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package ep0099 + +import ( + "errors" + + "periph.io/x/conn/v3/i2c" +) + +var errInvalidAddress = errors.New("Invalid EP-0099 address") +var errInvalidChannel = errors.New("Invalid EP-0099 channel") + +type State byte + +const ( + StateOff State = 0x00 + StateOn State = 0xFF +) + +type Dev struct { + i2c i2c.Dev + state [4]State +} + +func New(bus i2c.Bus, address uint16) (*Dev, error) { + if err := isValidAddress(address); err != nil { + return nil, err + } + + d := &Dev{ + i2c: i2c.Dev{Bus: bus, Addr: address}, + } + + if err := d.reset(); err != nil { + return nil, err + } + + return d, nil +} + +func (d *Dev) Halt() error { + return d.reset() +} + +func (d *Dev) On(channel uint8) error { + if !isValidChannel(channel) { + return errInvalidChannel + } + + _, err := d.i2c.Write([]byte{channel, byte(StateOn)}) + d.state[channel-1] = StateOn + return err +} + +func (d *Dev) Off(channel uint8) error { + if !isValidChannel(channel) { + return errInvalidChannel + } + + _, err := d.i2c.Write([]byte{channel, byte(StateOff)}) + d.state[channel-1] = StateOff + return err +} + +func (d *Dev) State(channel uint8) (State, error) { + if !isValidChannel(channel) { + return 0, errInvalidChannel + } + return d.state[channel-1], nil +} + +func (d *Dev) AvailableChannels() []uint8 { + return []uint8{0x01, 0x02, 0x03, 0x04} +} + +func (s State) String() string { + if s == StateOff { + return "off" + } + return "on" +} + +func (d *Dev) reset() error { + for _, channel := range d.AvailableChannels() { + err := d.Off(channel) + if err != nil { + return err + } + } + return nil +} + +// Addresses in EP0099 are configured via DIP Switches on the board. +// Up to 4 HATs can be stacked and each one need a different address to +// work. +func isValidAddress(address uint16) error { + switch address { + case 0x10, 0x11, 0x12, 0x13: + return nil + default: + return errInvalidAddress + } +} + +func isValidChannel(channel uint8) bool { + return channel >= 1 && channel <= 4 +} diff --git a/ep0099/ep0099_test.go b/ep0099/ep0099_test.go new file mode 100644 index 0000000..5f14442 --- /dev/null +++ b/ep0099/ep0099_test.go @@ -0,0 +1,155 @@ +// Copyright 2021 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package ep0099 + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "testing" + + "periph.io/x/conn/v3/i2c/i2ctest" +) + +const ( + testDefaultValidAddress = 0x10 +) + +func TestNewBuildsInstanceSuccessfully(t *testing.T) { + bus := initTestBus() + + dev, err := New(bus, testDefaultValidAddress) + if err != nil { + t.Fatal("New should not return error, got: ", err) + } + + if bus.Ops[0].Addr != testDefaultValidAddress { + t.Fatal("Expected operations on address ", testDefaultValidAddress, " got ", bus.Ops[0].Addr) + } + + checkDevReset(t, dev, bus) +} + +func TestNewReturnsInvalidAddress(t *testing.T) { + bus := initTestBus() + + _, err := New(bus, 0x00) + if !errors.Is(err, errInvalidAddress) { + t.Fatal("New should return address validation error, got: ", err) + } +} + +func TestAvailableChannels(t *testing.T) { + bus := initTestBus() + expected := []uint8{0x01, 0x02, 0x03, 0x04} + + dev, _ := New(bus, testDefaultValidAddress) + list := dev.AvailableChannels() + + if !reflect.DeepEqual(expected, list) { + t.Fatal("Available channels should be ", expected, " got ", list) + } +} + +func TestHalt(t *testing.T) { + bus := initTestBus() + dev, _ := New(bus, testDefaultValidAddress) + + dev.Halt() + checkDevReset(t, dev, bus) +} + +func TestOn(t *testing.T) { + bus := initTestBus() + dev, _ := New(bus, testDefaultValidAddress) + + err := dev.On(3) + + if err != nil { + t.Fatal("Should not return error, got ", err) + } + + checkBusHasWrite(t, bus, []byte{3, byte(StateOn)}) + checkChannelState(t, dev, 3, StateOn) +} + +func TestOff(t *testing.T) { + bus := initTestBus() + dev, _ := New(bus, testDefaultValidAddress) + + err := dev.Off(4) + + if err != nil { + t.Fatal("Should not return error, got ", err) + } + + checkBusHasWrite(t, bus, []byte{4, byte(StateOff)}) + checkChannelState(t, dev, 4, StateOff) +} + +func TestReturnErrorForInvalidChannel(t *testing.T) { + bus := initTestBus() + dev, _ := New(bus, testDefaultValidAddress) + + if err := dev.On(98); err != errInvalidChannel { + t.Fatal("On should return invalid channel error, got ", err) + } + + if err := dev.Off(98); err != errInvalidChannel { + t.Fatal("Off should return invalid channel error, got ", err) + } + + if err := dev.Off(98); err != errInvalidChannel { + t.Fatal("Off should return invalid channel error, got ", err) + } +} + +func TestStateToString(t *testing.T) { + if s := fmt.Sprintf("%s", StateOn); s != "on" { + t.Fatal("StateOn as string should be 'on', got ", s) + } + + if s := fmt.Sprintf("%s", StateOff); s != "off" { + t.Fatal("StateOn as string should be 'off', got ", s) + } +} + +func initTestBus() *i2ctest.Record { + return &i2ctest.Record{ + Bus: nil, + Ops: []i2ctest.IO{}, + } +} + +func checkChannelState(t *testing.T, dev *Dev, channel uint8, state State) { + if actual, _ := dev.State(channel); actual != state { + msg := fmt.Sprintf("Channel %d should have state %s, got: %s", channel, state, actual) + t.Fatal(msg) + } +} + +func checkBusHasWrite(t *testing.T, bus *i2ctest.Record, data []byte) { + for _, op := range bus.Ops { + if bytes.Equal(op.W, data) { + return + } + } + t.Fatal("Expected data ", data, " to be written but it never did") +} + +func checkDevReset(t *testing.T, dev *Dev, bus *i2ctest.Record) { + checkBusHasWrite(t, bus, []byte{1, byte(StateOff)}) + checkChannelState(t, dev, 1, StateOff) + + checkBusHasWrite(t, bus, []byte{2, byte(StateOff)}) + checkChannelState(t, dev, 2, StateOff) + + checkBusHasWrite(t, bus, []byte{3, byte(StateOff)}) + checkChannelState(t, dev, 3, StateOff) + + checkBusHasWrite(t, bus, []byte{4, byte(StateOff)}) + checkChannelState(t, dev, 4, StateOff) +} diff --git a/ep0099/example_test.go b/ep0099/example_test.go new file mode 100644 index 0000000..b0d5b9d --- /dev/null +++ b/ep0099/example_test.go @@ -0,0 +1,52 @@ +// Copyright 2021 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package ep0099_test + +import ( + "log" + "time" + + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/devices/v3/ep0099" + "periph.io/x/host/v3" +) + +func Example() { + // Initializes host to manage bus and devices + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Opens default bus + bus, err := i2creg.Open("") + if err != nil { + log.Fatal(err) + } + defer bus.Close() + + // Initializes device using current I2C bus and device address. + // Address should be provided as configured on the board's DIP switches. + dev, err := ep0099.New(bus, 0x10) + if err != nil { + log.Fatal(err) + } + defer dev.Halt() + + // Run device demo + for _, channel := range dev.AvailableChannels() { + state, _ := dev.State(channel) + log.Printf("[channel %#x] Initial state: %s", channel, state) + + dev.On(channel) + state, _ = dev.State(channel) + log.Printf("[channel %#x] State after .On: %s", channel, state) + + dev.Off(channel) + state, _ = dev.State(channel) + log.Printf("[channel %#x] State after .Off: %s", channel, state) + + time.Sleep(2 * time.Second) + } +}