A quick and dirty implementation

This commit is contained in:
Tony Blyler 2020-12-03 23:08:46 -05:00
parent 3682810e27
commit f6e9c70eb3
352 changed files with 242881 additions and 0 deletions

165
light/light.go Normal file
View file

@ -0,0 +1,165 @@
package light
import (
"context"
"encoding/csv"
"fmt"
"io"
"os"
"time"
"golang.org/x/sync/errgroup"
)
// BasicLight describes a light that simply can be turned on/off, like a relay
type BasicLight interface {
On() error
Off() error
State() (isOn bool, err error)
Close() error
}
// BasicLightConductorSchedule says which basic lights should be on/off for the second that maps to their index
type BasicLightConductorSchedule struct {
On [][]string
Off [][]string
}
// NewBasicLightConductorScheduleFromFile creates a new asicLightConductorSchedule instance from a file path
func NewBasicLightConductorScheduleFromFile(filePath string) (*BasicLightConductorSchedule, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open basic light conductor schedule file at %s: %w", filePath, err)
}
defer file.Close()
csvReader := csv.NewReader(file)
header, err := csvReader.Read()
if err != nil {
return nil, fmt.Errorf("failed to get header for basic light conductor schedule file %s: %w", filePath, err)
}
schedule := &BasicLightConductorSchedule{}
for lineNumber := 2; true; lineNumber++ {
record, err := csvReader.Read()
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("failed to get record at line %d of basic light conductor schedule file %s: %w", lineNumber, filePath, err)
}
onRow := []string{}
offRow := []string{}
for columnNumber, val := range record {
switch val {
case "on":
onRow = append(onRow, header[columnNumber])
case "off":
offRow = append(offRow, header[columnNumber])
default:
return nil, fmt.Errorf(
"invalid setting %s at line %d column %d of basic light conductor schedule file %s: %w",
val,
lineNumber,
columnNumber+1,
filePath,
err,
)
}
}
schedule.On = append(schedule.On, onRow)
schedule.Off = append(schedule.Off, offRow)
}
return schedule, nil
}
// BasicLightConductor turns basic lights on/off for a given schedule when told to conduct
type BasicLightConductor struct {
basicLights map[string]BasicLight
schedule *BasicLightConductorSchedule
}
// NewBasicLightConductor creates a basic light conductor instance for the given basic lights and schedule
func NewBasicLightConductor(basicLights map[string]BasicLight, schedule *BasicLightConductorSchedule) *BasicLightConductor {
return &BasicLightConductor{
basicLights: basicLights,
schedule: schedule,
}
}
func (blc *BasicLightConductor) setStatesForSecondInterval(i int) error {
errGroup := errgroup.Group{}
errGroup.Go(func() error {
for _, alias := range blc.schedule.Off[i] {
basicLight, ok := blc.basicLights[alias]
if !ok {
return fmt.Errorf("invalid alias %s for basic light provided at schedule index %d", alias, i)
}
err := basicLight.Off()
if err != nil {
return fmt.Errorf("failed to turn off basic light alias %s: %w", alias, err)
}
}
return nil
})
errGroup.Go(func() error {
for _, alias := range blc.schedule.On[i] {
basicLight, ok := blc.basicLights[alias]
if !ok {
return fmt.Errorf("invalid alias %s for basic light provided at schedule index %d", alias, i)
}
err := basicLight.On()
if err != nil {
return fmt.Errorf("failed to turn on basic light alias %s: %w", alias, err)
}
}
return nil
})
return errGroup.Wait()
}
// Start begins turning the basic lights on/off for the given schedule
func (blc *BasicLightConductor) Start(ctx context.Context) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
scheduleLen := len(blc.schedule.Off)
if onLen := len(blc.schedule.On); scheduleLen != onLen {
return fmt.Errorf("schedule for on/off light aliases do not have the same amount of entries! on: %d off: %d", onLen, scheduleLen)
}
err := blc.setStatesForSecondInterval(0)
if err != nil {
return err
}
for i := 1; i < scheduleLen; i++ {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
err = blc.setStatesForSecondInterval(i)
if err != nil {
return err
}
}
}
return nil
}

68
light/rpi/basic_light.go Normal file
View file

@ -0,0 +1,68 @@
package rpi
import (
"fmt"
"sync"
"github.com/warthog618/gpiod"
)
// BasicLight is a BasicLight implementation for the Raspberry Pi
type BasicLight struct {
pin int
line *gpiod.Line
manager *Manager
isOn bool
isOnLock sync.Mutex
}
// On turns on the light
func (bl *BasicLight) On() error {
bl.isOnLock.Lock()
defer bl.isOnLock.Unlock()
if bl.isOn {
return nil
}
err := bl.line.SetValue(0)
if err != nil {
return fmt.Errorf("failed to set pin %d to low (on): %w", bl.pin, err)
}
bl.isOn = true
return nil
}
// Off turns off the light
func (bl *BasicLight) Off() error {
bl.isOnLock.Lock()
defer bl.isOnLock.Unlock()
if !bl.isOn {
return nil
}
err := bl.line.SetValue(1)
if err != nil {
return fmt.Errorf("failed to set pin %d to high (on): %w", bl.pin, err)
}
bl.isOn = false
return nil
}
// State of whether the light is on or off
func (bl *BasicLight) State() (isOn bool, err error) {
bl.isOnLock.Lock()
defer bl.isOnLock.Unlock()
return bl.isOn, nil
}
// Close underlying open connection
func (bl *BasicLight) Close() error {
return bl.manager.closeLine(bl.pin)
}

121
light/rpi/manager.go Normal file
View file

@ -0,0 +1,121 @@
package rpi
import (
"errors"
"fmt"
"sync"
"github.com/warthog618/gpiod"
)
const (
// DefaultChipName to try to use if one is not provided
DefaultChipName = "gpiochip0"
)
var (
// ErrLineAlreadyRequested denotes that the requested line is not available because it has already been requested and not Closed
ErrLineAlreadyRequested = errors.New("line has already been requested")
// ErrLineNotRequested denotes that the given line cannot be acted on because it was not previously requested
ErrLineNotRequested = errors.New("line was not previously requested")
)
// ManagerConfig configuration for a Manager
type ManagerConfig struct {
ChipName string
}
// GetChipName from the config
func (mc *ManagerConfig) GetChipName() string {
if mc.ChipName == "" {
return DefaultChipName
}
return mc.ChipName
}
// Manager for overarching raspberry pi GPIO management
type Manager struct {
config *ManagerConfig
chip *gpiod.Chip
lines map[int]*gpiod.Line
linesLock sync.Mutex
}
// NewManager creates a new Manager instance for the given config
func NewManager(config ManagerConfig) (*Manager, error) {
chip, err := gpiod.NewChip(config.GetChipName())
if err != nil {
return nil, fmt.Errorf("failed to open GPIO chip %s: %w", config.GetChipName(), err)
}
return &Manager{
config: &config,
chip: chip,
lines: make(map[int]*gpiod.Line),
}, nil
}
// GetBasicLight for the given pin if available
func (m *Manager) GetBasicLight(pin int) (*BasicLight, error) {
line, err := m.requestLine(pin)
if err != nil {
return nil, err
}
return &BasicLight{
pin: pin,
line: line,
manager: m,
}, nil
}
func (m *Manager) requestLine(pin int) (*gpiod.Line, error) {
m.linesLock.Lock()
defer m.linesLock.Unlock()
_, exists := m.lines[pin]
if exists {
return nil, fmt.Errorf("%w pin %d", ErrLineAlreadyRequested, pin)
}
line, err := m.chip.RequestLine(pin, gpiod.AsOutput(0))
if err != nil {
return nil, fmt.Errorf("failed to request line for pin %d: %w", pin, err)
}
m.lines[pin] = line
return line, nil
}
func (m *Manager) closeLine(pin int) error {
m.linesLock.Lock()
defer m.linesLock.Unlock()
return m.closeLineHelper(pin)
}
func (m *Manager) closeLineHelper(pin int) error {
line := m.lines[pin]
if line == nil {
return fmt.Errorf("%w pin %d", ErrLineNotRequested, pin)
}
delete(m.lines, pin)
return line.Close()
}
// Close this Manager and all of its associated lines
func (m *Manager) Close() error {
m.linesLock.Lock()
defer m.linesLock.Unlock()
for pin := range m.lines {
m.closeLineHelper(pin)
}
return m.chip.Close()
}