Skip to content

Commit

Permalink
Adds MQTT digital output writer callbacks
Browse files Browse the repository at this point in the history
Adds a corresponding digital output and corresponding writer
abstraction. On initializing the handler, a callback is added to trigger
a write based on a specific payload.
  • Loading branch information
mhemeryck committed Nov 26, 2018
1 parent 0171f6d commit 8f0449b
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 62 deletions.
38 changes: 9 additions & 29 deletions digital_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ import (
"log"
"os"
"path"
"path/filepath"
"regexp"
"time"
)

const (
// Filename to check for
Filename = "di_value"
// TrueValue is the value considered to be true
TrueValue = "1"
// FolderRegex represents to regular expression used for finding the required file to read from
FolderRegex = "di_[0-9]_[0-9]{2}"
// DiFilename to check for
DiFilename = "di_value"
// DiTrueValue is the value considered to be true
DiTrueValue = "1"
// DiFolderRegex represents to regular expression used for finding the required file to read from
DiFolderRegex = "di_[0-9]_[0-9]{2}"
)

// DigitalInput interface for doing the polling
Expand All @@ -42,7 +40,7 @@ func (d *DigitalInputReader) Update(events chan *DigitalInputReader) (err error)
b := make([]byte, 1)
_, err = d.f.Read(b)
// Check it's true
value := bytes.Equal(b, []byte(TrueValue))
value := bytes.Equal(b, []byte(DiTrueValue))
// Push out an event in case of a leading edge
if !d.Value && value {
events <- d
Expand Down Expand Up @@ -84,33 +82,15 @@ func (d *DigitalInputReader) Close() error {

// NewDigitalInputReader creates a new DigitalInput and opens the file handle
func NewDigitalInputReader(folder string, topic string) (d *DigitalInputReader, err error) {
f, err := os.Open(path.Join(folder, Filename))
f, err := os.Open(path.Join(folder, DiFilename))
d = &DigitalInputReader{Topic: topic, Path: folder, f: f}
return
}

// findDigitalInputPaths finds all digital inputs in a given root folder
func findDigitalInputPaths(root string) (paths []string, err error) {
// Compile regex first
regex, err := regexp.Compile(FolderRegex)
// Walk the folder structure
err = filepath.Walk(root,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if regex.MatchString(info.Name()) {
paths = append(paths, path)
}
return err
})
return
}

// FindDigitalInputReaders crawls the root (sys) folder to find any matching digial inputs and creates corresponding DigitalInputReader instances from these.
func FindDigitalInputReaders(root string) (readers []DigitalInputReader, err error) {
// Find the paths first
paths, err := findDigitalInputPaths(root)
paths, err := findPathsByRegex(root, DiFolderRegex)
if err != nil {
log.Println(err)
return
Expand Down
30 changes: 1 addition & 29 deletions digital_input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func setup(folder string) (dir string, filename string, f *os.File, err error) {
}
}
// Create temporary path
tmpfn := filepath.Join(dir, Filename)
tmpfn := filepath.Join(dir, DiFilename)
// Create temporary file handle
f, err = os.Create(tmpfn)
if err != nil {
Expand Down Expand Up @@ -207,34 +207,6 @@ func TestPollError(t *testing.T) {
}
}

func TestFindDigitalInputPaths(t *testing.T) {
folder := "di_1_01"
// Create temporary folder, only if it does not exist already
root, err := ioutil.TempDir("", "unipitt")
if err != nil {
t.Fatal(err)
}
dir := filepath.Join(root, folder)
if _, pathErr := os.Stat(dir); pathErr != nil {
err := os.Mkdir(dir, os.ModePerm)
if err != nil {
t.Fail()
}
}
defer os.RemoveAll(root) // clean up

// Find
paths, err := findDigitalInputPaths(root)

// Check output
if err != nil {
t.Fail()
}
if len(paths) != 1 {
t.Fatalf("Expected to find 1 matching path, found %d\n", len(paths))
}
}

func TestFindDigitalInputReaders(t *testing.T) {
folder := "di_1_01"
// Create temporary folder, only if it does not exist already
Expand Down
72 changes: 72 additions & 0 deletions digital_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package unipitt

import (
"log"
"os"
"path"
)

const (
// DoFilename to check for
DoFilename = "do_value"
// DoTrueValue digital output true value to write
DoTrueValue = "1\n"
// DoFalseValue digital output false value to write
DoFalseValue = "0\n"
// DoFolderRegex regular expression used for finding folders which contain digital output
DoFolderRegex = "do_[0-9]_[0-9]{2}"
)

// DigitalOutput represents the digital outputs of the unipi board
type DigitalOutput interface {
Update(bool) error
}

// DigitalOutputWriter implements the digital output specifically for writing outputs to files
type DigitalOutputWriter struct {
Topic string
Path string
}

// Update writes the updated value to the digital output
func (d *DigitalOutputWriter) Update(value bool) (err error) {
f, err := os.Create(path.Join(d.Path, DoFilename))
defer f.Close()
if err != nil {
return err
}

if value {
_, err = f.WriteString(DoTrueValue)
} else {
_, err = f.WriteString(DoFalseValue)
}
if err == nil {
log.Printf("Update value of digital output %s to %t\n", d.Topic, value)
}
return err
}

// NewDigitalOutputWriter creates a new digital output writer instance from a a given matching folder
func NewDigitalOutputWriter(folder string) (d *DigitalOutputWriter) {
// Read topic as the trailing folder path
_, topic := path.Split(folder)
return &DigitalOutputWriter{Topic: topic, Path: folder}
}

// FindDigitalOutputWriters generates the output writes from a given path
func FindDigitalOutputWriters(root string) (writerMap map[string]DigitalOutputWriter, err error) {
paths, err := findPathsByRegex(root, DoFolderRegex)
if err != nil {
log.Println(err)
return
}
log.Printf("Found %d matching digital output paths\n", len(paths))
writerMap = make(map[string]DigitalOutputWriter)
var d *DigitalOutputWriter
for _, path := range paths {
d = NewDigitalOutputWriter(path)
writerMap[d.Topic] = *d
}
return
}
108 changes: 108 additions & 0 deletions digital_output_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package unipitt

import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"testing"
)

func TestNewDigitalOutputWriter(t *testing.T) {
folder := "foo/bar"
d := NewDigitalOutputWriter(folder)
if d.Topic != "bar" {
t.Fatalf("Expected topic %s, got %s\n", "bar", d.Topic)
}
if d.Path != folder {
t.Fatalf("Expected path %s, got %s\n", folder, d.Path)
}
}

func TestUpdateDigitalOutputWriter(t *testing.T) {
folder := "do_2_01"

// Create temporary folder, only if it does not exist already
sysFsRoot, err := ioutil.TempDir("", "unipitt")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(sysFsRoot)
doFolder := filepath.Join(sysFsRoot, folder)
if _, pathErr := os.Stat(doFolder); pathErr != nil {
err := os.Mkdir(doFolder, os.ModePerm)
if err != nil {
t.Fatal(err)
}
}
fname := filepath.Join(doFolder, DoFilename)

d := NewDigitalOutputWriter(doFolder)

cases := []struct {
Given bool
Expected string
}{
{Given: false, Expected: DoFalseValue},
{Given: true, Expected: DoTrueValue},
}
for _, testCase := range cases {
err := d.Update(testCase.Given)
if err != nil {
t.Fatal(err)
}
f, err := os.Open(fname)
if err != nil {
t.Fatal(err)
}
b := make([]byte, 2)
f.Read(b)
if !bytes.Equal(b, []byte(testCase.Expected)) {
t.Fatalf("Expected %s, got %s\n", testCase.Expected, string(b))
}
}
}

func TestUpdateDigitalOutputWriterBogusFolder(t *testing.T) {
folder := "/foo/bar"
d := NewDigitalOutputWriter(folder)
err := d.Update(false)
if err == nil {
t.Fatal("Expected an error, found none")
}
}

func TestFindDigitalOutputWriters(t *testing.T) {
folder := "do_2_01"

// Create temporary folder, only if it does not exist already
sysFsRoot, err := ioutil.TempDir("", "unipitt")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(sysFsRoot)
doFolder := filepath.Join(sysFsRoot, folder)
if _, pathErr := os.Stat(doFolder); pathErr != nil {
err := os.Mkdir(doFolder, os.ModePerm)
if err != nil {
t.Fatal(err)
}
}
writerMap, err := FindDigitalOutputWriters(sysFsRoot)
if err != nil {
t.Fatal(err)
}
if writer, ok := writerMap[folder]; !ok {
t.Fatalf("Expected to find writer with topic %s in map for name %s\n", writer.Topic, folder)
}

}

func TestFindDigitalOutputWritersNoFolder(t *testing.T) {
folder := "foo"

_, err := FindDigitalOutputWriters(folder)
if err == nil {
t.Fatal("Expected no mapping to be found")
}
}
37 changes: 33 additions & 4 deletions unipitt.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
const (
// SysFsRoot default root folder to search for digital inputs
SysFsRoot = "/sys/devices/platform/unipi_plc"
// MsgTrueValue is the MQTT true value to check for
MsgTrueValue = "ON"
)

// Unipitt defines the interface with unipi board
Expand All @@ -20,13 +22,21 @@ type Unipitt interface {

// Handler implements handles all unipi to MQTT interactions
type Handler struct {
readers []DigitalInputReader
client mqtt.Client
readers []DigitalInputReader
writerMap map[string]DigitalOutputWriter
client mqtt.Client
}

// NewHandler prepares and sets up an entire unipitt handler
func NewHandler(broker string, clientID string, caFile string, sysFsRoot string) (h *Handler, err error) {
h = &Handler{}
// Digital writer setup
// Set message handler as callback
h.writerMap, err = FindDigitalOutputWriters(sysFsRoot)
if err != nil {
log.Printf("Error creating a map of digital output writers: %s\n", err)
}

// MQTT setup
opts := mqtt.NewClientOptions()
opts.AddBroker(broker)
Expand All @@ -39,10 +49,30 @@ func NewHandler(broker string, clientID string, caFile string, sysFsRoot string)
opts.SetTLSConfig(tlsConfig)
}
}

// Callbacks for subscribe
var cb mqtt.MessageHandler = func(c mqtt.Client, msg mqtt.Message) {
if writer, ok := h.writerMap[msg.Topic()]; ok {
err := writer.Update(string(msg.Payload()) == MsgTrueValue)
if err != nil {
log.Printf("Error updating digital output with topic %s: %s\n", writer.Topic, err)
}
} else {
log.Printf("Error matching a writer for given topic %s\n", msg.Topic())
}
}
opts.OnConnect = func(c mqtt.Client) {
for topic := range h.writerMap {
if token := c.Subscribe(topic, 0, cb); token.Wait() && token.Error() != nil {
log.Print(err)
}
}
}

h.client = mqtt.NewClient(opts)
err = h.connect()
if err != nil {
log.Println("Error connecting to MQTT broker ...")
log.Printf("Error connecting to MQTT broker: %s\n ...", err)
}

// Digital Input reader setup
Expand All @@ -57,7 +87,6 @@ func NewHandler(broker string, clientID string, caFile string, sysFsRoot string)
// Poll starts the actual polling and pushing to MQTT
func (h *Handler) Poll(done chan bool, interval int, payload string) (err error) {
events := make(chan *DigitalInputReader)
defer close(events)

// Start polling
log.Printf("Initiate polling for %d readers\n", len(h.readers))
Expand Down
24 changes: 24 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package unipitt

import (
"os"
"path/filepath"
"regexp"
)

// findPathsByRegex find matching paths where a regex matches (on the name of a given oflder, not full path)
func findPathsByRegex(root string, pattern string) (paths []string, err error) {
regex, err := regexp.Compile(pattern)
// Walk the folder structure
err = filepath.Walk(root,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if regex.MatchString(info.Name()) {
paths = append(paths, path)
}
return err
})
return
}

0 comments on commit 8f0449b

Please sign in to comment.