mirror of https://github.com/periph/devices
added support for EP0099
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-0099pull/5/head
parent
296894bc24
commit
d344905ced
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2018 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
|
||||||
|
//
|
||||||
|
// Datasheet
|
||||||
|
// https://wiki.52pi.com/index.php/DockerPi_4_Channel_Relay_SKU:_EP-0099
|
||||||
|
//
|
||||||
|
// Examples
|
||||||
|
// https://github.com/geeekpi/dockerpi
|
||||||
|
package ep0099
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright 2018 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 InvalidAddressError = errors.New("Invalid EP-0099 address")
|
||||||
|
var InvalidChannelError = errors.New("Invalid EP-0099 channel")
|
||||||
|
|
||||||
|
type State byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateOff State = 0x00
|
||||||
|
StateOn State = 0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dev struct {
|
||||||
|
i2c i2c.Dev
|
||||||
|
state map[uint8]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},
|
||||||
|
state: buildChannelsList(),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 err := d.isValidChannel(channel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.i2c.Write([]byte{channel, byte(StateOn)})
|
||||||
|
d.state[channel] = StateOn
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dev) Off(channel uint8) error {
|
||||||
|
if err := d.isValidChannel(channel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.i2c.Write([]byte{channel, byte(StateOff)})
|
||||||
|
d.state[channel] = StateOff
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dev) State(channel uint8) (State, error) {
|
||||||
|
if err := d.isValidChannel(channel); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return d.state[channel], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dev) AvailableChannels() []uint8 {
|
||||||
|
return []uint8{0x01, 0x02, 0x03, 0x04}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s State) String() string {
|
||||||
|
if s == StateOff {
|
||||||
|
return "off"
|
||||||
|
}
|
||||||
|
return "on"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset resets the registers to the default values.
|
||||||
|
func (d *Dev) reset() error {
|
||||||
|
for channel := 1; channel <= len(d.state); channel++ {
|
||||||
|
d.Off(uint8(channel))
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
validAddresses := [...]uint16{0x10, 0x11, 0x12, 0x13}
|
||||||
|
|
||||||
|
for _, addr := range validAddresses {
|
||||||
|
if address == addr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return InvalidAddressError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dev) isValidChannel(channel uint8) error {
|
||||||
|
if _, exists := d.state[channel]; !exists {
|
||||||
|
return InvalidChannelError
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EP-0099 offers 4 channels per board
|
||||||
|
func buildChannelsList() map[uint8]State {
|
||||||
|
// Using a map instead of list since indexes of channels are not zero-based
|
||||||
|
// values. That would cause loops to have to correct channel ids while
|
||||||
|
// looping through items or reading/setting values.
|
||||||
|
// With maps, keys correspond to actual channels on the board.
|
||||||
|
return map[uint8]State{
|
||||||
|
1: StateOff,
|
||||||
|
2: StateOff,
|
||||||
|
3: StateOff,
|
||||||
|
4: StateOff,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,153 @@
|
|||||||
|
// Copyright 2018 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"
|
||||||
|
"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, InvalidAddressError) {
|
||||||
|
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)
|
||||||
|
channels := dev.AvailableChannels()
|
||||||
|
|
||||||
|
if len(channels) != len(expected) {
|
||||||
|
t.Fatal("Available channels len should be ", len(expected), ", got ", len(channels))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(expected); i++ {
|
||||||
|
if channels[i] != expected[i] {
|
||||||
|
t.Fatal("Available channels should be ", expected, " got ", channels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHalt(t *testing.T) {
|
||||||
|
bus := initTestBus()
|
||||||
|
dev, _ := New(bus, testDefaultValidAddress)
|
||||||
|
|
||||||
|
resetTestBusOps(bus)
|
||||||
|
|
||||||
|
dev.Halt()
|
||||||
|
checkDevReset(t, dev, bus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOn(t *testing.T) {
|
||||||
|
bus := initTestBus()
|
||||||
|
dev, _ := New(bus, testDefaultValidAddress)
|
||||||
|
|
||||||
|
resetTestBusOps(bus)
|
||||||
|
|
||||||
|
err := dev.On(3)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Should not return error, got ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBusWrites(t, bus, 0, []byte{3, byte(StateOn)})
|
||||||
|
checkChannelState(t, dev, 3, StateOn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOff(t *testing.T) {
|
||||||
|
bus := initTestBus()
|
||||||
|
dev, _ := New(bus, testDefaultValidAddress)
|
||||||
|
|
||||||
|
resetTestBusOps(bus)
|
||||||
|
|
||||||
|
err := dev.Off(4)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Should not return error, got ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBusWrites(t, bus, 0, []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 != InvalidChannelError {
|
||||||
|
t.Fatal("On should return invalid channel error, got ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dev.Off(98); err != InvalidChannelError {
|
||||||
|
t.Fatal("Off should return invalid channel error, got ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initTestBus() *i2ctest.Record {
|
||||||
|
return &i2ctest.Record{
|
||||||
|
Bus: nil,
|
||||||
|
Ops: []i2ctest.IO{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetTestBusOps(bus *i2ctest.Record) {
|
||||||
|
bus.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 checkBusWrites(t *testing.T, bus *i2ctest.Record, index int, data []byte) {
|
||||||
|
if !bytes.Equal(bus.Ops[index].W, data) {
|
||||||
|
t.Fatal("Expected data ", data, " to be written, got ", bus.Ops[index].W)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDevReset(t *testing.T, dev *Dev, bus *i2ctest.Record) {
|
||||||
|
checkBusWrites(t, bus, 0, []byte{1, byte(StateOff)})
|
||||||
|
checkChannelState(t, dev, 1, StateOff)
|
||||||
|
|
||||||
|
checkBusWrites(t, bus, 1, []byte{2, byte(StateOff)})
|
||||||
|
checkChannelState(t, dev, 2, StateOff)
|
||||||
|
|
||||||
|
checkBusWrites(t, bus, 2, []byte{3, byte(StateOff)})
|
||||||
|
checkChannelState(t, dev, 3, StateOff)
|
||||||
|
|
||||||
|
checkBusWrites(t, bus, 3, []byte{4, byte(StateOff)})
|
||||||
|
checkChannelState(t, dev, 4, StateOff)
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2018 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue