diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5f8ad6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,100 @@ +# Created by https://www.gitignore.io/api/go,linux,intellij+all + +### Go ### +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### Intellij+all Patch ### +# Ignores the whole idea folder +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# End of https://www.gitignore.io/api/go,linux,intellij+all + + +bin/ +pkg/ +src/ + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d36f1e9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "proto"] + path = proto + url = git@github.com:LED-Freaks/LedD-protobuf.git diff --git a/ledd.go b/ledd.go new file mode 100644 index 0000000..fd2a890 --- /dev/null +++ b/ledd.go @@ -0,0 +1,483 @@ +package main + +import ( + "encoding/binary" + "gopkg.in/mgo.v2" + "net" + "os" + "os/signal" + "syscall" + + "fmt" + "gen/ledd" + "github.com/golang/protobuf/proto" + "github.com/lucasb-eyer/go-colorful" + "github.com/op/go-logging" + "gopkg.in/yaml.v2" + "io/ioutil" +) + +// CONSTANTS + +const VERSION = "0.1" +const LOG_BACKEND = "BH" +const LOG_CLIENTS = "CH" + +// STRUCTS + +type Config struct { + Name string + Daemon struct { + Frontend struct { + Host string + Port int + } + Backend struct { + Host string + Port int + } + } + Mongodb struct { + Host string + Port int + Database string + } +} + +type BackendManager struct { + backends map[string]*Backend + broadcast chan []byte + register chan *Backend + unregister chan *Backend +} + +type Backend struct { + name string + platformType string + version string + channel int32 + resolution int32 + socket net.Conn + data chan []byte +} + +type ClientManager struct { + clients map[*Client]bool + broadcast chan []byte + register chan *Client + unregister chan *Client +} + +type Client struct { + platform string + socket net.Conn + data chan []byte +} + +type LED struct { + name string + channel []int32 + backend string + color chan colorful.Color +} + +type LEDManager struct { + leds map[string]*LED + broadcast chan colorful.Color + add chan *LED + remove chan *LED +} + +// GLOBAL VARS + +var log = logging.MustGetLogger("LedD") +var backManager = BackendManager{} +var clientManager = ClientManager{} +var ledManager = LEDManager{} +var LEDCollection = &mgo.Collection{} +var config = Config{} + +// SOCKET SETUP + +func setupSocket(host string, port int, logTag string, backend bool) (func(), error) { + ln, err := net.Listen("tcp4", fmt.Sprintf("%s:%d", host, port)) + if err != nil { + return nil, err + } + log.Infof("[%s] Ready to handle connections.", logTag) + + return func() { + for { + conn, err := ln.Accept() + if err != nil { + log.Warningf("%s", err) + } + log.Infof("[%s] New connection from %s", logTag, conn.RemoteAddr()) + if backend { + backend := &Backend{socket: conn, data: make(chan []byte)} + go backManager.receive(backend) + go backManager.send(backend) + } else { + client := &Client{socket: conn, data: make(chan []byte)} + go clientManager.receive(client) + go clientManager.send(client) + } + } + }, nil +} + +// LED MANAGER + +func (manager *LEDManager) start() { + for { + select { + case led := <-manager.add: + log.Debugf("[%s] Request to add led: %s", led.backend, led.name) + manager.leds[led.name] = led + go manager.color(led) + err := LEDCollection.Insert(led) + if err != nil { + log.Warning("[%s] Error while adding LED to database: %s", LOG_BACKEND, err) + } + case led := <-manager.remove: + if _, ok := manager.leds[led.name]; ok { + log.Debugf("[%s] Request to remove led %s", led.backend, led.name) + delete(manager.leds, led.name) + } + case color := <-manager.broadcast: + for _, led := range manager.leds { + select { + case led.color <- color: + } + } + } + } +} + +func (manager *LEDManager) color(led *LED) { + for { + select { + case color := <-led.color: + led.setColor(color) + } + } +} + +// BACKEND HANDLER + +func (manager *BackendManager) start() { + for { + select { + case backend := <-manager.register: + manager.backends[backend.name] = backend + log.Debugf("[%s] New backend: %s", LOG_BACKEND, backend.niceName()) + wrapperMsg := &ledd.BackendWrapperMessage{ + Msg: &ledd.BackendWrapperMessage_MLedd{ + MLedd: &ledd.LedD{ + Name: config.Name, + }, + }, + } + + data, err := proto.Marshal(wrapperMsg) + if err != nil { + log.Warningf("[%s] Failed to encode protobuf: %s", backend.niceName(), err) + } + + backend.data <- data + case backend := <-manager.unregister: + if _, ok := manager.backends[backend.name]; ok { + log.Debugf("[%s] Backend %s removed: connection terminated", LOG_BACKEND, backend.socket.RemoteAddr()) + close(backend.data) + delete(manager.backends, backend.name) + } + case message := <-manager.broadcast: + for _, backend := range manager.backends { + select { + case backend.data <- message: + default: + close(backend.data) + delete(manager.backends, backend.name) + } + } + } + } +} + +func (manager *BackendManager) stop() { + for _, backend := range manager.backends { + close(backend.data) + } +} + +func (manager *BackendManager) send(backend *Backend) { + defer backend.socket.Close() + for { + select { + case message, ok := <-backend.data: + if !ok { + return + } + backend.socket.Write(message) + } + } +} + +func (manager *BackendManager) receive(backend *Backend) { + for { + message := make([]byte, 4096) + + length, err := backend.socket.Read(message) + if err != nil { + log.Warningf("[%s] Read failed: %s", backend.niceName(), err) + manager.unregister <- backend + backend.socket.Close() + break + } + + if length > 0 { + msgLen := binary.BigEndian.Uint32(message[0:3]) + + log.Debugf("[%s] Read %d bytes, first protobuf is %d long", backend.niceName(), length, msgLen) + + backendMsg := &ledd.BackendWrapperMessage{} + err = proto.Unmarshal(message[4:msgLen], backendMsg) + if err != nil { + log.Warningf("[%s] Couldn't decode protobuf msg!", backend.niceName()) + continue + } + + switch msg := backendMsg.Msg.(type) { + case *ledd.BackendWrapperMessage_MBackend: + nBackend := msg.MBackend + backend.name = nBackend.Name + backend.channel = nBackend.Channel + backend.resolution = nBackend.Resolution + backend.platformType = nBackend.Type + backend.version = nBackend.Version + log.Infof("[%s] %s is now identified as %s", LOG_BACKEND, backend.socket.RemoteAddr(), backend.niceName()) + backManager.register <- backend + } + } + } +} + +// CLIENT HANDLER + +func (manager *ClientManager) start() { + for { + select { + case client := <-manager.register: + manager.clients[client] = true + log.Debugf("[%s] New frontend (%s)", LOG_CLIENTS, client.socket.RemoteAddr(), client.platform) + case client := <-manager.unregister: + if _, ok := manager.clients[client]; ok { + log.Debugf("[%s] Removed client (%s)", LOG_CLIENTS, client.socket.RemoteAddr(), client.platform) + close(client.data) + delete(manager.clients, client) + } + case message := <-manager.broadcast: + for connection := range manager.clients { + select { + case connection.data <- message: + default: + close(connection.data) + delete(manager.clients, connection) + } + } + } + } +} + +func (manager *ClientManager) send(client *Client) { + defer client.socket.Close() + for { + select { + case message, ok := <-client.data: + if !ok { + return + } + client.socket.Write(message) + } + } +} + +func (manager *ClientManager) receive(client *Client) { + for { + message := make([]byte, 4096) + length, err := client.socket.Read(message) + if err != nil { + log.Warningf("[%s] Read failed: %s", client.socket.RemoteAddr(), err) + manager.unregister <- client + client.socket.Close() + break + } + if length > 0 { + msgLen := binary.BigEndian.Uint32(message[0:3]) + + log.Debugf("[%s] Read %d bytes, first protobuf is %d long", client.socket.RemoteAddr(), length, msgLen) + + clientMsg := &ledd.ClientWrapperMessage{} + err = proto.Unmarshal(message[4:msgLen], clientMsg) + if err != nil { + log.Warningf("[%s] Couldn't decode protobuf msg!", client.socket.RemoteAddr()) + continue + } + + switch msg := clientMsg.Msg.(type) { + case *ledd.ClientWrapperMessage_MClient: + client.platform = msg.MClient.Type + log.Infof("[%s] %s is now identified as client (%s)", LOG_CLIENTS, client.socket.RemoteAddr(), client.platform) + clientManager.register <- client + case *ledd.ClientWrapperMessage_MGetLed: + allLED := make([]*ledd.LED, 0) + + for _, led := range ledManager.leds { + allLED = append(allLED, &ledd.LED{Name: led.name}) + } + + data, err := proto.Marshal(&ledd.ClientWrapperMessage{Leds: allLED}) + if err != nil { + log.Errorf("[%s] Error encoding protobuf: %s", client.socket.RemoteAddr(), err) + } + + client.data <- data + case *ledd.ClientWrapperMessage_MAddLed: + backend, ok := backManager.backends[msg.MAddLed.Backend] + if !ok { + log.Warningf("[%s] Can't add LED for non-existing backend %s", client.socket.RemoteAddr(), msg.MAddLed.Backend) + } + + nLED := &LED{ + name: msg.MAddLed.Name, + channel: msg.MAddLed.Channel, + backend: backend.name, + } + + ledManager.add <- nLED + case *ledd.ClientWrapperMessage_MSetLed: + led, ok := ledManager.leds[msg.MSetLed.Name] + if !ok { + log.Warningf("[%s] Failed to set LED %s: LED not found", client.socket.RemoteAddr(), msg.MSetLed.Name) + } + + led.color <- colorful.Hcl(msg.MSetLed.Colour.Hue, msg.MSetLed.Colour.Chroma, msg.MSetLed.Colour.Light) + } + } + } +} + +// HELPER + +func check(e error) { + if e != nil { + panic(e) + } +} + +func (backend *Backend) niceName() string { + if backend.name != "" { + return backend.name + } else { + return backend.socket.RemoteAddr().String() + } +} + +func (led *LED) setColor(color colorful.Color) { + backend := backManager.backends[led.backend] + + if len(led.channel) != 3 { + log.Warningf("[%s] Currently only RGB LEDs are supported", led.name) + return + } + + cMap := make(map[int32]int32) + + cMap[led.channel[0]] = int32(color.R * float64(backend.resolution)) + cMap[led.channel[1]] = int32(color.G * float64(backend.resolution)) + cMap[led.channel[2]] = int32(color.B * float64(backend.resolution)) + + wrapperMsg := &ledd.BackendWrapperMessage{ + Msg: &ledd.BackendWrapperMessage_MSetChannel{ + MSetChannel: &ledd.BackendSetChannel{ + NewChannelValues: cMap}}} + + data, err := proto.Marshal(wrapperMsg) + if err != nil { + log.Warningf("[%s] Failed to encode protobuf msg to %s: %s", led.name, backend.name, err) + } + + backend.data <- data +} + +// MAIN + +func main() { + killSignals := make(chan os.Signal, 1) + signal.Notify(killSignals, syscall.SIGINT, syscall.SIGTERM) + + log.Info("LedD", VERSION) + + content, err := ioutil.ReadFile("ledd.yaml") + check(err) + + err = yaml.Unmarshal(content, &config) + check(err) + + session, err := mgo.Dial(fmt.Sprintf("%s:%d", config.Mongodb.Host, config.Mongodb.Port)) + check(err) + defer session.Close() + + LEDCollection = session.DB(config.Mongodb.Database).C("led") + + backManager = BackendManager{ + backends: make(map[string]*Backend), + broadcast: make(chan []byte), + register: make(chan *Backend), + unregister: make(chan *Backend), + } + + go backManager.start() + + clientManager = ClientManager{ + clients: make(map[*Client]bool), + broadcast: make(chan []byte), + register: make(chan *Client), + unregister: make(chan *Client), + } + + go clientManager.start() + + ledManager = LEDManager{ + leds: make(map[string]*LED), + broadcast: make(chan colorful.Color), + add: make(chan *LED), + remove: make(chan *LED), + } + + go ledManager.start() + + var dbLEDs = make([]LED, 0) + err = LEDCollection.Find(nil).All(&dbLEDs) + if err != nil { + log.Notice("Failed to load LEDs from db. If there should be LEDs, check db connection") + } + + for _, l := range dbLEDs { + ledManager.add <- &l + } + + backendThread, err := setupSocket(config.Daemon.Backend.Host, config.Daemon.Backend.Port, LOG_BACKEND, true) + check(err) + go backendThread() + + frontendThread, err := setupSocket(config.Daemon.Frontend.Host, config.Daemon.Frontend.Port, LOG_CLIENTS, false) + check(err) + go frontendThread() + + log.Infof("All connection handler ready.") + + <-killSignals + + backManager.stop() +} diff --git a/ledd.yaml b/ledd.yaml new file mode 100644 index 0000000..5764405 --- /dev/null +++ b/ledd.yaml @@ -0,0 +1,12 @@ +name: TestDaemon +daemon: + frontend: + host: "" + port: 5635 + backend: + host: "" + port: 5640 +mongodb: + host: "127.0.0.1" + port: 27017 + database: "ledd" \ No newline at end of file diff --git a/proto b/proto new file mode 160000 index 0000000..fe9a6d4 --- /dev/null +++ b/proto @@ -0,0 +1 @@ +Subproject commit fe9a6d440c8e4344527c1cd50b5a7f50db6dd8ac