A quick and dirty implementation
This commit is contained in:
parent
3682810e27
commit
f6e9c70eb3
352 changed files with 242881 additions and 0 deletions
165
light/light.go
Normal file
165
light/light.go
Normal 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
68
light/rpi/basic_light.go
Normal 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
121
light/rpi/manager.go
Normal 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()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue