diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d675a01..832be1c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,7 +2,7 @@ # [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.18, 1.17, 1-bullseye, 1.18-bullseye, 1.17-bullseye, 1-buster, 1.18-buster, 1.17-buster ARG VARIANT="1.18-bullseye" -FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} +FROM mcr.microsoft.com/vscode/devcontainers/go:${VARIANT} # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="none" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 91ff96a..1f281f9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -44,6 +44,7 @@ "features": { "docker-in-docker": "latest", "kubectl-helm-minikube": "latest", - "git": "latest" + "git": "latest", + "ghcr.io/devcontainers/features/docker-in-docker:2": {} } } diff --git a/animation.go b/animation.go new file mode 100644 index 0000000..25ddc21 --- /dev/null +++ b/animation.go @@ -0,0 +1,148 @@ +package main + +import ( + "bytes" + b64 "encoding/base64" + "encoding/json" + "image" + "image/color" + "log" + "os" + "time" + + rgbmatrix "gitea.wagshome.duckdns.org/publicWagsHome/go-rpi-rgb-led-matrix" + "github.com/disintegration/imaging" + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/fogleman/gg" +) + +// contents of struct mostly don't matter for toolkit. +type incomingMessage struct { + Type string `json:"type"` + Image string `json:"image"` +} + +type Animation struct { + ctx *gg.Context + position image.Point + dir image.Point + height int + width int + stroke int + image []image.Image + images map[string]image.Image + updown string + mqmsg chan mqtt.Message + msg string + + countDown int +} + +// animator is a wrapping function for go routine that can receive an mq channel +func animator(tk *rgbmatrix.ToolKit, mqMessages chan mqtt.Message) { + //Playanimation comes from the toolkit, all it takes is an animation struct + tk.PlayAnimation(NewAnimation(image.Point{127, 64}, mqMessages)) +} + +// initializes the struct for the an play animation function, this could all be dumped into function that's wrapping go routine if I wanted +func NewAnimation(sz image.Point, mqMessages chan mqtt.Message) *Animation { + imageMap := make(map[string]image.Image) + reader, err := os.Open("marioUp.png") + if err != nil { + log.Fatal(err) + } + rawMario, _, err := image.Decode(reader) + + //marioUp := imaging.FlipH(imaging.Resize(rawMario, 16, 16, imaging.Lanczos)) + marioUp := imaging.Resize(rawMario, 16, 16, imaging.Lanczos) + imageMap["marioUp"] = marioUp + reader, err = os.Open("marioDown.png") + if err != nil { + log.Fatal(err) + } + rawMario, _, err = image.Decode(reader) + if err != nil { + log.Fatal(err) + } + //marioDown := imaging.FlipH(imaging.Resize(rawMario, 16, 16, imaging.Lanczos)) + marioDown := imaging.Resize(rawMario, 16, 16, imaging.Lanczos) + imageMap["marioDown"] = marioDown + return &Animation{ + ctx: gg.NewContext(sz.X, sz.Y), + dir: image.Point{1, 1}, + height: 8, + width: 8, + stroke: 8, + images: imageMap, + updown: "marioUp", + mqmsg: mqMessages, + countDown: 5000, + } +} + +func appendImage(img string, a *Animation) { + baseImage, _ := b64.StdEncoding.DecodeString(img) + bigImage, _, _ := image.Decode(bytes.NewReader(baseImage)) + a.images["doorbell"] = imaging.Resize(bigImage, 64, 64, imaging.Lanczos) +} + +// what happens each frame, at an interval of 50 milliseconds +func (a *Animation) Next() (image.Image, <-chan time.Time, error) { + var incoming incomingMessage + + a.animateMario() + if a.images["doorbell"] != nil { + if a.countDown > 0 { + a.ctx.DrawImageAnchored(a.images["doorbell"], 0, 0, 0, 0) + a.countDown -= 50 + } else { + //a.image = a.image[:len(a.image)-1] + delete(a.images, "doorbell") + a.countDown = 5000 + } + } + a.ctx.SetColor(color.White) + select { + case msg := <-a.mqmsg: + json.Unmarshal([]byte(string(msg.Payload())), &incoming) + if incoming.Type == "doorbell" { + a.msg = string(msg.Payload()) + a.ctx.DrawString(a.msg, 5, 9) + } else { + go appendImage(incoming.Image, a) + } + default: + } + return a.ctx.Image(), time.After(time.Millisecond * 50), nil +} + +func (a *Animation) animateMario() { + defer a.updateMarioPosition() + a.ctx.SetColor(color.Black) + a.ctx.Clear() + if a.dir.X == 1 { + a.ctx.DrawImageAnchored(a.images[a.updown], a.position.X, a.position.Y, 0.5, 0.5) + } else { + a.ctx.DrawImageAnchored(imaging.FlipH(a.images[a.updown]), a.position.X, a.position.Y, 0.5, 0.5) + } +} + +// what mario does every frame +func (a *Animation) updateMarioPosition() { + a.position.X += 1 * a.dir.X + a.position.Y += 1 * a.dir.Y + + if a.position.Y+a.height > a.ctx.Height() { + a.dir.Y = -1 + a.updown = "marioUp" + } else if a.position.Y-a.height < 0 { + a.updown = "marioDown" + a.dir.Y = 1 + } + + if a.position.X+a.width > a.ctx.Width() { + a.dir.X = -1 + } else if a.position.X-a.width < 0 { + a.dir.X = 1 + } +} diff --git a/go.mod b/go.mod index f1247c2..d2a7061 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module rgb -go 1.18 +go 1.21.2 + +toolchain go1.21.6 require ( gitea.wagshome.duckdns.org/publicWagsHome/go-rpi-rgb-led-matrix v1.0.0 diff --git a/go.sum b/go.sum index 410c4b1..4b8e28a 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,11 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp/shiny v0.0.0-20231006140011-7918f672742d h1:grE48C8cjIY0aiHVmFyYgYxxSARQWBABLXKZfQPrBhY= golang.org/x/exp/shiny v0.0.0-20231006140011-7918f672742d/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= @@ -39,3 +42,4 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/main.go b/main.go index 551fdef..0bde2fc 100644 --- a/main.go +++ b/main.go @@ -1,48 +1,18 @@ package main import ( - "bytes" - b64 "encoding/base64" - "encoding/json" "flag" "fmt" - "image" - "image/color" _ "image/jpeg" "log" "os" "os/signal" "syscall" - "time" rgbmatrix "gitea.wagshome.duckdns.org/publicWagsHome/go-rpi-rgb-led-matrix" - "github.com/disintegration/imaging" - MQTT "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang" - "github.com/fogleman/gg" ) -// contents of struct mostly don't matter for toolkit. -type incomingImage struct { - Image string `json:"image"` -} - -type Animation struct { - ctx *gg.Context - position image.Point - dir image.Point - height int - width int - stroke int - image []image.Image - images map[string]image.Image - updown string - mqmsg chan mqtt.Message - msg string - - countDown int -} - // flags from cmd line var ( rows = flag.Int("led-rows", 64, "number of rows supported") @@ -57,47 +27,6 @@ var ( led_slowdown_gpio = flag.Int("led-slowdown-gpio", 1, "GPIO pin slowdown") ) -// listens on topic for messages -func listener(mqMessages chan mqtt.Message) { - opts := setupMQTT() - client := MQTT.NewClient(opts) - topic := "home/rgbboard" - if token := client.Connect(); token.Wait() && token.Error() != nil { - log.Println(fmt.Sprintf("failed to connect to mq: %s", token.Error().Error())) - panic(token.Error()) - } - log.Println("connected") - client.Subscribe(topic, 0, func(client mqtt.Client, msg mqtt.Message) { - log.Println("Receiving ", string(msg.Payload()), " on topic: ", msg.Topic()) - mqMessages <- msg - }) -} - -// animator is a wrapping function for go routine that can receive an mq channel -func animator(tk *rgbmatrix.ToolKit, mqMessages chan mqtt.Message) { - //Playanimation comes from the toolkit, all it takes is an animation struct - tk.PlayAnimation(NewAnimation(image.Point{128, 64}, mqMessages)) - -} - -// connection lost management -func onConnectionLostHandler(c MQTT.Client, reason error) { - log.Fatalf(reason.Error()) -} - -// setup connection to mqtt, topic to listen to, qos -func setupMQTT() *mqtt.ClientOptions { - opts := MQTT.NewClientOptions() - opts.AddBroker(fmt.Sprintf("tcp://%s:%s", os.Getenv("MQTTBROKER"), os.Getenv("MQTTPORT"))) - opts.SetUsername(os.Getenv("MQTTUSER")) - opts.SetPassword(os.Getenv("MQTTPASSWORD")) - opts.SetClientID("rgbboard") - opts.SetAutoReconnect(true) - opts.SetConnectionLostHandler(onConnectionLostHandler) - - return opts -} - // runs before main, parses flags func init() { flag.Parse() @@ -110,108 +39,6 @@ func fatal(err error) { } } -// initializes the struct for the an play animation function, this could all be dumped into function that's wrapping go routine if I wanted -func NewAnimation(sz image.Point, mqMessages chan mqtt.Message) *Animation { - imageMap := make(map[string]image.Image) - reader, err := os.Open("marioUp.png") - if err != nil { - log.Fatal(err) - } - rawMario, _, err := image.Decode(reader) - - //marioUp := imaging.FlipH(imaging.Resize(rawMario, 16, 16, imaging.Lanczos)) - marioUp := imaging.Resize(rawMario, 16, 16, imaging.Lanczos) - imageMap["marioUp"] = marioUp - reader, err = os.Open("marioDown.png") - if err != nil { - log.Fatal(err) - } - rawMario, _, err = image.Decode(reader) - if err != nil { - log.Fatal(err) - } - //marioDown := imaging.FlipH(imaging.Resize(rawMario, 16, 16, imaging.Lanczos)) - marioDown := imaging.Resize(rawMario, 16, 16, imaging.Lanczos) - imageMap["marioDown"] = marioDown - return &Animation{ - ctx: gg.NewContext(sz.X, sz.Y), - dir: image.Point{1, 1}, - height: 8, - width: 8, - stroke: 8, - images: imageMap, - updown: "marioUp", - mqmsg: mqMessages, - countDown: 5000, - } -} - -func appendImage(img string, a *Animation) { - baseImage, _ := b64.StdEncoding.DecodeString(img) - bigImage, _, _ := image.Decode(bytes.NewReader(baseImage)) - a.images["doorbell"] = imaging.Resize(bigImage, 64, 64, imaging.Lanczos) -} - -// what happens each frame, at an interval of 50 milliseconds -func (a *Animation) Next() (image.Image, <-chan time.Time, error) { - incoming := incomingImage{} - a.animateMario() - if a.images["doorbell"] != nil { - if a.countDown > 0 { - a.ctx.DrawImageAnchored(a.images["doorbell"], 0, 0, 0, 0) - a.countDown -= 50 - } else { - //a.image = a.image[:len(a.image)-1] - delete(a.images, "doorbell") - a.countDown = 5000 - } - } - a.ctx.SetColor(color.White) - select { - case msg := <-a.mqmsg: - json.Unmarshal([]byte(string(msg.Payload())), &incoming) - if incoming.Image == "" { - a.msg = string(msg.Payload()) - a.ctx.DrawString(a.msg, 5, 9) - } else { - go appendImage(incoming.Image, a) - } - default: - } - return a.ctx.Image(), time.After(time.Millisecond * 50), nil -} - -func (a *Animation) animateMario() { - defer a.updateMarioPosition() - a.ctx.SetColor(color.Black) - a.ctx.Clear() - if a.dir.X == 1 { - a.ctx.DrawImageAnchored(a.images[a.updown], a.position.X, a.position.Y, 0.5, 0.5) - } else { - a.ctx.DrawImageAnchored(imaging.FlipH(a.images[a.updown]), a.position.X, a.position.Y, 0.5, 0.5) - } -} - -// what mario does every frame -func (a *Animation) updateMarioPosition() { - a.position.X += 1 * a.dir.X - a.position.Y += 1 * a.dir.Y - - if a.position.Y+a.height > a.ctx.Height() { - a.dir.Y = -1 - a.updown = "marioUp" - } else if a.position.Y-a.height < 0 { - a.updown = "marioDown" - a.dir.Y = 1 - } - - if a.position.X+a.width > a.ctx.Width() { - a.dir.X = -1 - } else if a.position.X-a.width < 0 { - a.dir.X = 1 - } -} - func main() { config := &rgbmatrix.DefaultConfig config.Rows = *rows diff --git a/mqttcoms.go b/mqttcoms.go new file mode 100644 index 0000000..6263cf0 --- /dev/null +++ b/mqttcoms.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "log" + "os" + + MQTT "github.com/eclipse/paho.mqtt.golang" + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +// listens on topic for messages +func listener(mqMessages chan mqtt.Message) { + opts := setupMQTT() + client := MQTT.NewClient(opts) + topic := "home/rgbboard" + if token := client.Connect(); token.Wait() && token.Error() != nil { + log.Println(fmt.Sprintf("failed to connect to mq: %s", token.Error().Error())) + panic(token.Error()) + } + log.Println("connected") + client.Subscribe(topic, 0, func(client mqtt.Client, msg mqtt.Message) { + log.Println("Receiving ", string(msg.Payload()), " on topic: ", msg.Topic()) + mqMessages <- msg + }) +} + +// connection lost management +func onConnectionLostHandler(c MQTT.Client, reason error) { + log.Fatalf(reason.Error()) +} + +// setup connection to mqtt, topic to listen to, qos +func setupMQTT() *mqtt.ClientOptions { + opts := MQTT.NewClientOptions() + opts.AddBroker(fmt.Sprintf("tcp://%s:%s", os.Getenv("MQTTBROKER"), os.Getenv("MQTTPORT"))) + opts.SetUsername(os.Getenv("MQTTUSER")) + opts.SetPassword(os.Getenv("MQTTPASSWORD")) + opts.SetClientID("rgbboard") + opts.SetAutoReconnect(true) + opts.SetConnectionLostHandler(onConnectionLostHandler) + + return opts +}