From 2d7b6f5bd51030e77f8c469c1a8d84049d419da2 Mon Sep 17 00:00:00 2001 From: boyska Date: Sat, 29 Jul 2017 02:07:06 +0200 Subject: [PATCH] direttoforo UI client for GPIO --- cmd/direttoforo-led/led.go | 203 +++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 cmd/direttoforo-led/led.go diff --git a/cmd/direttoforo-led/led.go b/cmd/direttoforo-led/led.go new file mode 100644 index 0000000..a503c3e --- /dev/null +++ b/cmd/direttoforo-led/led.go @@ -0,0 +1,203 @@ +package main + +import ( + "bufio" + "encoding/json" + "flag" + "fmt" + "net" + "os" + "os/signal" + "strconv" + "strings" + "sync" + "time" + + "github.com/davecheney/gpio" + "github.com/pkg/errors" + + "git.lattuga.net/boyska/direttoforo" +) + +// appicc consumes a channel representing a stream of direttoforo.State updates +// for each message, GPIOs are set accordingly +// if the pin cannot be opened, this function will fail early. However, caller has no way to receive this +// information nor informations about the error +// this function will live as long as the chan "s" will +func appicc(gpios gpioMapFlag, s <-chan direttoforo.State) error { + unassigned := make([]string, 0) + pins := make(map[string]gpio.Pin) + for name, pinnumber := range gpios { + pin, err := gpio.OpenPin(pinnumber, gpio.ModeOutput) + if err != nil { + return errors.Wrap(err, "error opening pin") + } + pins[name] = pin + } + for state := range s { + fmt.Println("led", state) + for sname, sstate := range state.Streams { + pin, ok := pins[sname] + if ok { + if sstate.Up { + pin.Set() + } else { + pin.Clear() + } + } else { + // warn about it? + alreadywarned := false + for _, v := range unassigned { + if v == sname { + alreadywarned = true + } + } + if !alreadywarned { + unassigned = append(unassigned, sname) + fmt.Fprintf(os.Stderr, "INFO: stream '%s' unassigned\n", sname) + } + } + } + } + for _, pin := range pins { + pin.Clear() + pin.Close() + } + fmt.Println("led poweroff") + return nil +} + +func connect(bindpath string, s chan<- direttoforo.State, quit <-chan interface{}) { + defer close(s) +OuterLoop: + for { + select { + case <-quit: + return + default: + time.Sleep(time.Second) + } + conn, err := net.Dial("unix", bindpath) + if err != nil { + fmt.Fprintln(os.Stderr, "error connecting", err) + continue OuterLoop + } + defer conn.Close() + hello := []byte("DIRETTOFORO V1\n") // hello size is specified by protocol to be constant + b := bufio.NewReader(conn) + nread, err := b.Read(hello) + if err != nil { + fmt.Fprintln(os.Stderr, "error reading protocol version", err) + continue OuterLoop + } + if nread != 15 || string(hello) != "DIRETTOFORO V1\n" { + fmt.Fprintln(os.Stderr, "error: protocol version mismatch") + continue OuterLoop + } + + // read data from connection and put in a chan + msgs := make(chan direttoforo.State) + go func() { + decoder := json.NewDecoder(conn) + var newState direttoforo.State + for { + err = decoder.Decode(&newState) + if err != nil { + fmt.Fprintln(os.Stderr, "error decoding", err) + time.Sleep(time.Second) + close(msgs) + break + } + msgs <- newState + } + }() + MsgLoop: // copy msgs to s, but handles quit signal, too + for { + select { + case <-quit: + return + case msg := <-msgs: + if msg.Streams == nil { // msgs closed + break MsgLoop + } + s <- msg + } + } + } +} + +// TODO: maybe create a map[string]int instead? +type gpioMapFlag map[string]int + +func (i *gpioMapFlag) String() string { + return fmt.Sprint(*i) +} + +func (i *gpioMapFlag) Set(value string) error { + for _, keyvalue := range strings.Split(value, ",") { + kv := strings.SplitN(keyvalue, ":", 2) + if len(kv) != 2 { + return fmt.Errorf("Invalid keyvalue: %s", keyvalue) + } + value, err := strconv.Atoi(kv[1]) + if err != nil { + return err + } + for k, v := range *i { + if v == value { + return fmt.Errorf("Can't assign both '%s' and '%s' to %d", k, kv[0], value) + } + } + (*i)[kv[0]] = value + } + return nil +} + +func main() { + gpios := make(gpioMapFlag) + bindpath := flag.String("bindpath", "/var/lib/direttoforo/ui.sock", "UNIX domain socket path for UIs") + flag.Var(&gpios, "gpios", "Comma-separated list of stream-GPIOnumber to be used. Example: rec:11,stream:7") + flag.Parse() + + fmt.Println(gpios) + states := make(chan direttoforo.State) + connectcloser := make(chan interface{}) + allDone := make(chan interface{}) + ledDone := make(chan error) + var wg sync.WaitGroup + wg.Add(2) + go func() { + wg.Wait() + close(allDone) + }() + go func() { + err := appicc(gpios, states) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + ledDone <- err // appicc has no termination notification. This provides a way to have one + wg.Done() + }() + go func() { + connect(*bindpath, states, connectcloser) + wg.Done() + }() + killed := make(chan os.Signal, 1) + signal.Notify(killed, os.Interrupt) // ctrl-c + for { + select { + case sig := <-killed: + fmt.Println("killed by", sig) + close(connectcloser) + case err := <-ledDone: + // led controller failed early (error on OpenPin) + if err != nil { + close(connectcloser) + } + // else: it was closed because we are shutting + // down + case <-allDone: + os.Exit(0) + } + } +}