From 7b201f6e6360810ab4b1c7c0ae0dad17017cd068 Mon Sep 17 00:00:00 2001 From: Adrian Zuercher Date: Mon, 21 Apr 2025 09:56:30 +0200 Subject: [PATCH] insert routes to main and driver --- README.md | 47 +++++++++++++++++- driver/artNet.go | 84 ++++++++++++++++++++++++------- driver/bus.go | 94 +++++++++++++++++++++++++++++++++++ driver/webSocket.go | 63 ++++++++++++++++++++++++ main.go | 103 ++++++++++++++++----------------------- models/bus.go | 66 ++++++++++++------------- models/jsonData.go | 5 +- models/jsonResponse.go | 7 +++ models/set.go | 6 +-- server/models/clients.go | 51 +++++++++++++++++++ 10 files changed, 405 insertions(+), 121 deletions(-) create mode 100644 driver/bus.go create mode 100644 driver/webSocket.go create mode 100644 models/jsonResponse.go create mode 100644 server/models/clients.go diff --git a/README.md b/README.md index 50fbc69..7e88b4e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,45 @@ -# ArtNet Driver -Tecamino artNet driver DMX-Communication +# 🎛️ ArtNet Driver + +**Tecamino ArtNet Driver** — A lightweight driver for DMX communication using the ArtNet protocol over TCP/IP. + +This tool allows you to send DMX values to an ArtNet-compatible device, enabling remote lighting control and stage automation. + +--- + +## 🚀 Features + +- 🔌 Sends DMX data over the ArtNet protocol (UDP) +- 📁 Configurable working and config directories +- 🐞 Optional debug logging for development and troubleshooting +- ⚙️ Easy to run with CLI flags + +--- + +## 🧰 Command Line Arguments + +| Flag | Default | Description | +|-------------------|-------------|--------------------------------------| +| `--port` | `8110` | Port on which the server listens | +| `--cfg` | `./cfg` | Path to the configuration directory | +| `--workingDirectory` | `"."` | Working directory for runtime files | +| `--debug` | `false` | Enable verbose debug logging | + +--- + +## 🏁 Getting Started + +### 🔧 Run the Driver + +```bash +go run main.go --port=8110 --cfg=./cfg --workingDirectory=. --debug=true + +``` +### 🔧 Build the App + +To compile the Go code into a runnable binary: + +#### On Linux or macOS: + +```bash +go build -o artnet-driver +``` \ No newline at end of file diff --git a/driver/artNet.go b/driver/artNet.go index cec3dfd..6875d62 100644 --- a/driver/artNet.go +++ b/driver/artNet.go @@ -1,29 +1,77 @@ package driver -import "artNet/models" +import ( + "artNet/cfg" + "artNet/models" + serverModels "artNet/server/models" + "fmt" + + "github.com/tecamino/tecamino-logger/logging" +) type ArtNetDriver struct { - Bus []*models.Bus + Name string `yaml:"driver" json:"driver"` + Buses map[string]*models.Bus `yaml:"buses,omitempty" json:"buses,omitempty"` + cfgHandler *cfg.Cfg `yaml:"-" json:"-"` + Connections serverModels.Clients `yaml:"-"` + Log *logging.Logger `yaml:"-"` } // initialize new Art-Net driver -func NewDriver() *ArtNetDriver { - return &ArtNetDriver{} +// cfgDir config directory +// name name of driver +func NewDriver(cfgDir, name string, debug bool) (*ArtNetDriver, error) { + if cfgDir == "" { + cfgDir = "./cfg" + } + + logger, err := logging.NewLogger(name, debug) + if err != nil { + panic(err) + } + + logger.Debug("artNet.NewDriver", "initialize "+name+" driver") + d := ArtNetDriver{ + Name: name, + Buses: make(map[string]*models.Bus), + cfgHandler: cfg.NewCfgHandler(cfgDir, name), + Connections: serverModels.NewClients(), + Log: logger, + } + + if err := d.LoadCfg(); err != nil { + logger.Error("artNet.NewDriver", "error load driver configuration: "+err.Error()) + return nil, err + } + + return &d, nil } -// adds new Art-Net interface to driver port 0 = 6454 (default art-net) -func (d *ArtNetDriver) NewInterface(ip string, port int) *models.Bus { - i := models.NewBus(ip, port) - d.Bus = append(d.Bus, i) - return i +// loads driver config +func (d *ArtNetDriver) LoadCfg() error { + d.Log.Debug("artNet.LoadCfg", "load driver configuration") + if err := d.cfgHandler.LoadCfg(d); err != nil { + return err + } + + for _, b := range d.Buses { + d.NewBus(b.Name, b.Ip, *b.Port) + } + return nil } -// dmxData[46] = byte(255) // Channel 1: Red -// dmxData[47] = byte(255) // Channel 2: Green -// dmxData[48] = byte(255) // Channel 3: Blue -// dmxData[49] = byte(255) // Channel 4: White -// dmxData[50] = byte(255) // Channel 5: Amber -// dmxData[51] = byte(255) // Channel 6: UV Lila -// dmxData[52] = byte(255) // Channel 7: 0-9 10-255 strobo -// dmxData[53] = byte(255) // Channel 8: 0-5 stop 6-127 static position 128-255 Motorgeschwindigkeit -// dmxData[54] = byte(255) // Channel 9: 0-50 coincitence 51 -100 two color 101-150 all color +// adds new Art-Net bus/interface to driver port 0 = 6454 (default art-net) +func (d *ArtNetDriver) NewBus(name, ip string, port int) *models.Bus { + b := models.NewBus(name, ip, port) + d.Buses[name] = b + return b +} + +func (d *ArtNetDriver) SetValue(set models.Set) error { + if _, ok := d.Buses[set.Bus]; !ok { + return fmt.Errorf("no bus '%s' found", set.Bus) + } + d.Buses[set.Bus].Data.SetValue(set.Address, set.Value) + + return d.Buses[set.Bus].SendData() +} diff --git a/driver/bus.go b/driver/bus.go new file mode 100644 index 0000000..4f826cb --- /dev/null +++ b/driver/bus.go @@ -0,0 +1,94 @@ +package driver + +import ( + "artNet/auth" + "artNet/models" + "fmt" + "net" + "net/http" + + "github.com/gin-gonic/gin" +) + +// sends a list of all buses in the driver +func (d *ArtNetDriver) GetAllBuses(c *gin.Context) { + var data any + if len(d.Buses) == 0 { + data = "no buses avaiable" + } else { + data = *d + } + + c.JSON(200, gin.H{ + "buses": data, + }) +} + +func (d *ArtNetDriver) CreateBus(c *gin.Context) { + _, err := auth.GetIDFromAuth(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "id: " + err.Error()}) + return + } + + var payload models.Bus + + if err := c.BindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "json: " + err.Error()}) + return + } + + if payload.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "bus name missing"}) + return + } else if addr := net.ParseIP(payload.Ip); addr == nil { + + c.JSON(http.StatusBadRequest, gin.H{"error": "wrong ip '" + payload.Ip + "'"}) + return + } + + if _, ok := d.Buses[payload.Name]; ok { + c.JSON(http.StatusOK, gin.H{"message": "bus " + payload.Name + " exists already"}) + return + } + + bus := d.NewBus(payload.Name, payload.Ip, payload.GetPort()) + c.JSON(http.StatusOK, gin.H{ + "message": fmt.Sprintf("bus '%s' successfully created with ip: %s and on port: %d", bus.Name, bus.Ip, bus.GetPort()), + }) + d.cfgHandler.SaveCfg(*d) +} + +func (d *ArtNetDriver) RemoveBus(c *gin.Context) { + _, err := auth.GetIDFromAuth(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "id: " + err.Error()}) + return + } + + var payload models.Bus + + if err := c.BindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "json: " + err.Error()}) + return + } + + if payload.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "bus name missing"}) + return + } + + if _, ok := d.Buses[payload.Name]; !ok { + c.JSON(http.StatusOK, gin.H{"message": "bus " + payload.Name + " not found"}) + c.JSON(http.StatusOK, models.JsonResponse{ + Message: "bus " + payload.Name + " not found"}) + return + } else { + delete(d.Buses, payload.Name) + } + + c.JSON(http.StatusOK, models.JsonResponse{ + Message: fmt.Sprintf("bus '%s' successfully removed", payload.Name), + }) + d.cfgHandler.SaveCfg(*d) +} diff --git a/driver/webSocket.go b/driver/webSocket.go new file mode 100644 index 0000000..cf0f02e --- /dev/null +++ b/driver/webSocket.go @@ -0,0 +1,63 @@ +package driver + +import ( + "context" + "log" + "time" + + "artNet/auth" + "artNet/models" + + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" + "github.com/gin-gonic/gin" +) + +const ( + OnCreate = "onCreate" + OnChange = "onChange" + OnDelete = "onDelete" +) + +func (d *ArtNetDriver) Websocket(c *gin.Context) { + id, err := auth.GetIDFromAuth(c) + if err != nil { + d.Log.Error("artNet.webSocket.Websocket", "error GetIDFromAuth: "+err.Error()) + log.Println("error id:", err) + return + } + d.Log.Debug("artNet.webSocket.Websocket", "authorization id token: "+id) + + ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Minute) + defer cancel() + conn, err := d.Connections.ConnectRecievingWsConnection(id, c) + if err != nil { + d.Log.Error("artNet.webSocket.Websocket", "error connecting recieving websocket conection: "+err.Error()) + return + } + defer d.Connections.DisconnectRecievingWsConnection(id, websocket.StatusInternalError, "Internal error") + + var request models.JsonData + //Read loop + for { + + err := wsjson.Read(ctx, conn, &request) + if err != nil { + d.Log.Error("artNet.webSocket.Websocket", "read error:"+err.Error()) + log.Println("WebSocket read error:", err) + break + } + + // Set + if request.Set != nil { + for _, set := range *request.Set { + if err = d.SetValue(set); err != nil { + d.Log.Error("artNet.webSocket.Websocket", "set value error"+err.Error()) + log.Println(err) + continue + } + time.Sleep(23 * time.Millisecond) + } + } + } +} diff --git a/main.go b/main.go index 885b614..e230d10 100644 --- a/main.go +++ b/main.go @@ -3,71 +3,52 @@ package main import ( "artNet/driver" "artNet/server" - "fmt" - "math" - "time" + "flag" + "os" + + "github.com/gin-gonic/gin" ) +const DriverName string = "ArtNetDriver" + func main() { + // cli flags + port := flag.Uint("port-http", 8110, "http server listening port") + cfgDir := flag.String("cfg", "./cfg", "config directory") + workingDir := flag.String("workingDirectory", ".", "current working directory") + debug := flag.Bool("debug", false, "debug logging") + flag.Parse() - bus := driver.NewDriver().NewInterface("2.0.0.1", 0) - d, _ := bus.AddDevice(47, 9) - - var i uint8 - - s := server.NewServer() - - s.ServeHttp(8120) - - for { - if i == math.MaxUint8 { - i = 0 - } - err := d.SetChannelValue(0, i) - if err != nil { - fmt.Println(err) - } - err = d.SetChannelValue(1, i) - if err != nil { - fmt.Println(err) - } - err = d.SetChannelValue(2, i) - if err != nil { - fmt.Println(err) - } - err = d.SetChannelValue(3, i) - if err != nil { - fmt.Println(err) - } - err = d.SetChannelValue(4, i) - if err != nil { - fmt.Println(err) - } - err = d.SetChannelValue(5, i) - if err != nil { - fmt.Println(err) - } - err = d.SetChannelValue(6, i) - if err != nil { - fmt.Println(err) - } - err = d.SetChannelValue(7, i) - if err != nil { - fmt.Println(err) - } - err = d.SetChannelValue(8, i) - if err != nil { - fmt.Println(err) - } - - if err := bus.SendData(); err != nil { - fmt.Println(12, err) - panic(err) - } - time.Sleep(100 * time.Microsecond) - i += 1 - i = 0 - + //change working directory only if value is given + if *workingDir != "." && *workingDir != "" { + os.Chdir(*workingDir) } + //initialize new ArtNet driver + artNetDriver, err := driver.NewDriver(*cfgDir, DriverName, *debug) + if err != nil { + artNetDriver.Log.Error("main", "error initialize new artnet driver "+err.Error()) + panic(err) + } + + //initialize new server + artNetDriver.Log.Error("main", "initialize new server instance") + s := server.NewServer() + + //set routes + artNetDriver.Log.Error("main", "setting routes") + s.Routes.GET("/ws", artNetDriver.Websocket) + s.Routes.GET("/buses/all", artNetDriver.GetAllBuses) + s.Routes.POST("/buses/create", artNetDriver.CreateBus) + s.Routes.POST("/buses/remove", artNetDriver.RemoveBus) + + s.Routes.GET("/", func(c *gin.Context) { + c.String(200, "ArtNet Driver WebSocket Server is running!") + }) + + // start http server + if err := s.ServeHttp(*port); err != nil { + artNetDriver.Log.Error("main", "error http server "+err.Error()) + panic(err) + } } diff --git a/models/bus.go b/models/bus.go index 0a7c77e..8e06a7f 100644 --- a/models/bus.go +++ b/models/bus.go @@ -2,6 +2,7 @@ package models import ( "fmt" + "log" "net" "time" @@ -15,43 +16,45 @@ const ( // Art-Net Interface type Bus struct { - ip string `yaml:"ip"` - port int `yaml:"port"` - Devices []*Device `yaml:"devices"` - Data *DMX + Name string `yaml:"name" json:"name"` + Ip string `yaml:"ip" json:"ip"` + Port *int `yaml:"port" json:"port,omitempty"` + Data *DMX `yaml:"-" json:"-"` } // adds new Art-Net interface to driver port 0 = 6454 (default art-net) -func NewBus(ip string, port int) *Bus { +func NewBus(name, ip string, port int) *Bus { if port == 0 { port = artPort } i := Bus{ - ip: ip, - port: port, + Name: name, + Ip: ip, + Port: &port, Data: NewDMXUniverse(), } return &i } -// adds new dmx device to interface -func (i *Bus) AddDevice(address uint, channels uint) (*Device, error) { - d := NewDevice(address, channels, i.Data) - i.Devices = append(i.Devices, d) - return d, nil +// get bus port from pointer +func (b *Bus) GetPort() int { + if b.Port == nil { + return artPort + } + return *b.Port } // start polling dmx data in milliseconds 0 = aprox. 44Hertz -func (i *Bus) Poll(interval time.Duration) error { +func (b *Bus) Poll(interval time.Duration) error { if interval == 0 { interval = 23 } // Send packet over UDP conn, err := net.DialUDP("udp", nil, &net.UDPAddr{ - IP: net.ParseIP(i.ip), - Port: i.port, + IP: net.ParseIP(b.Ip), + Port: *b.Port, }) if err != nil { @@ -63,7 +66,7 @@ func (i *Bus) Poll(interval time.Duration) error { for { go func() { for { - if reached, _ := isUDPReachable(i.ip); !reached { + if reached, _ := isUDPReachable(b.Ip); !reached { if errCount > 20 { break } else { @@ -77,7 +80,7 @@ func (i *Bus) Poll(interval time.Duration) error { } }() - _, err = conn.Write(NewArtNetPackage(i.Data)) + _, err = conn.Write(NewArtNetPackage(b.Data)) if err != nil { return err } @@ -90,11 +93,11 @@ func (i *Bus) Poll(interval time.Duration) error { } // start polling dmx data in milliseconds 0 = aprox. 44Hertz -func (i *Bus) SendData() error { +func (b *Bus) SendData() error { // Send packet over UDP conn, err := net.DialUDP("udp", nil, &net.UDPAddr{ - IP: net.ParseIP(i.ip), - Port: i.port, + IP: net.ParseIP(b.Ip), + Port: *b.Port, }) if err != nil { @@ -102,29 +105,21 @@ func (i *Bus) SendData() error { } defer conn.Close() - errChan := make(chan error) go func() { - if reached, _ := isUDPReachable(i.ip); !reached { - errChan <- fmt.Errorf("device not reachable") + if reached, err := isUDPReachable(b.Ip); err != nil { + log.Println(err) + return + } else if !reached { + log.Println("device not reachable") return } - errChan <- nil }() - err = <-errChan - if err != nil { - return err - } - - _, err = conn.Write(NewArtNetPackage(i.Data)) + _, err = conn.Write(NewArtNetPackage(b.Data)) return err } -const ( - protocolICMP = 1 -) - func isUDPReachable(ip string) (recieved bool, err error) { p := fastping.NewPinger() ra, err := net.ResolveIPAddr("ip4:icmp", ip) @@ -132,10 +127,11 @@ func isUDPReachable(ip string) (recieved bool, err error) { return } p.AddIPAddr(ra) + p.OnRecv = func(addr *net.IPAddr, rtt time.Duration) { recieved = true - return } + p.OnIdle = func() {} err = p.Run() diff --git a/models/jsonData.go b/models/jsonData.go index 6457c35..b30421b 100644 --- a/models/jsonData.go +++ b/models/jsonData.go @@ -1,7 +1,8 @@ package models type JsonData struct { - Set *[]Set `json:"set,omitempty"` + Set *[]Set `json:"set,omitempty"` + Create *[]Bus `json:"create,omitempty"` } func NewRequest() *JsonData { @@ -9,7 +10,7 @@ func NewRequest() *JsonData { } -func (r *JsonData) AddSet(bus, address uint, value uint8) { +func (r *JsonData) AddSet(bus string, address uint, value uint8) { if r.Set == nil { r.Set = &[]Set{} } diff --git a/models/jsonResponse.go b/models/jsonResponse.go new file mode 100644 index 0000000..57eddf6 --- /dev/null +++ b/models/jsonResponse.go @@ -0,0 +1,7 @@ +package models + +type JsonResponse struct { + Error bool `json:"error,omitempty"` + Message string `json:"message,omitempty"` + Data string `json:"data,omitempty"` +} diff --git a/models/set.go b/models/set.go index 4d5b5da..d3ca9ac 100644 --- a/models/set.go +++ b/models/set.go @@ -1,7 +1,7 @@ package models type Set struct { - Bus uint `json:"bus"` - Address uint `json:"address"` - Value uint8 `json:"value"` + Bus string `json:"bus"` + Address uint `json:"address"` + Value uint8 `json:"value"` } diff --git a/server/models/clients.go b/server/models/clients.go new file mode 100644 index 0000000..465d04c --- /dev/null +++ b/server/models/clients.go @@ -0,0 +1,51 @@ +package models + +import ( + "fmt" + + "github.com/coder/websocket" + "github.com/gin-gonic/gin" +) + +type Clients map[string]Client + +type Client struct { + Connected *bool `json:"connected"` + SndConn *websocket.Conn `json:"-"` //sending connection + RvcConn *websocket.Conn `json:"-"` // revieving connection +} + +func NewClients() Clients { + return make(Clients) +} + +// Connect a recieving websocket connection +func (c *Clients) ConnectRecievingWsConnection(id string, ctx *gin.Context) (*websocket.Conn, error) { + conn, err := websocket.Accept(ctx.Writer, ctx.Request, &websocket.AcceptOptions{ + OriginPatterns: []string{"*"}, + }) + + if err != nil { + return nil, fmt.Errorf("error accept websocket client: %s", err) + } + + b := true + (*c)[id] = Client{ + Connected: &b, + RvcConn: conn, + } + return conn, nil +} + +func (c *Clients) RemoveClient(id string) { + delete(*c, id) +} + +func (c *Clients) GetClientPointer(id string) *bool { + return (*c)[id].Connected +} + +func (c *Clients) DisconnectRecievingWsConnection(id string, code websocket.StatusCode, reason string) { + *(*c)[id].Connected = false + (*c)[id].RvcConn.Close(code, reason) +}