From 5343f26ad96f5b9ec05b4520572f465bb1e12022 Mon Sep 17 00:00:00 2001 From: David Lick Date: Thu, 21 Dec 2023 10:06:48 -0500 Subject: [PATCH] adds display package --- Makefile | 1 + cmd/rpi/main.go | 100 +++++++ display/display.go | 71 +++++ display/slot.go | 88 ++++++ fonts/5x4.go | 139 +++++++++ fonts/7x5.go | 178 ++++++++++++ fonts/9x6.go | 216 ++++++++++++++ fonts/fonts.go | 61 ++++ fonts/fonts_test.go | 49 ++++ go-rpi-rgb-led-matrix/.gitmodules | 3 + go-rpi-rgb-led-matrix/.travis.yml | 14 + go-rpi-rgb-led-matrix/LICENSE | 21 ++ go-rpi-rgb-led-matrix/README.md | 97 +++++++ go-rpi-rgb-led-matrix/canvas.go | 77 +++++ go-rpi-rgb-led-matrix/canvas_test.go | 139 +++++++++ go-rpi-rgb-led-matrix/emulator/emulator.go | 204 +++++++++++++ .../examples/animation/main.go | 98 +++++++ go-rpi-rgb-led-matrix/examples/basic/main.go | 59 ++++ go-rpi-rgb-led-matrix/examples/image/main.go | 72 +++++ .../examples/rpc/client/main.go | 39 +++ .../examples/rpc/server/main.go | 44 +++ go-rpi-rgb-led-matrix/matrix.go | 274 ++++++++++++++++++ go-rpi-rgb-led-matrix/rpc/client.go | 79 +++++ go-rpi-rgb-led-matrix/rpc/server.go | 54 ++++ go-rpi-rgb-led-matrix/toolkit.go | 142 +++++++++ mempool/mempool.go | 81 ++++++ mempool/models.go | 5 + shell.nix | 7 + 28 files changed, 2412 insertions(+) create mode 100644 Makefile create mode 100644 cmd/rpi/main.go create mode 100644 display/display.go create mode 100644 display/slot.go create mode 100644 fonts/5x4.go create mode 100644 fonts/7x5.go create mode 100644 fonts/9x6.go create mode 100644 fonts/fonts.go create mode 100644 fonts/fonts_test.go create mode 100644 go-rpi-rgb-led-matrix/.gitmodules create mode 100644 go-rpi-rgb-led-matrix/.travis.yml create mode 100644 go-rpi-rgb-led-matrix/LICENSE create mode 100644 go-rpi-rgb-led-matrix/README.md create mode 100644 go-rpi-rgb-led-matrix/canvas.go create mode 100644 go-rpi-rgb-led-matrix/canvas_test.go create mode 100644 go-rpi-rgb-led-matrix/emulator/emulator.go create mode 100644 go-rpi-rgb-led-matrix/examples/animation/main.go create mode 100644 go-rpi-rgb-led-matrix/examples/basic/main.go create mode 100644 go-rpi-rgb-led-matrix/examples/image/main.go create mode 100644 go-rpi-rgb-led-matrix/examples/rpc/client/main.go create mode 100644 go-rpi-rgb-led-matrix/examples/rpc/server/main.go create mode 100644 go-rpi-rgb-led-matrix/matrix.go create mode 100644 go-rpi-rgb-led-matrix/rpc/client.go create mode 100644 go-rpi-rgb-led-matrix/rpc/server.go create mode 100644 go-rpi-rgb-led-matrix/toolkit.go create mode 100644 mempool/mempool.go create mode 100644 mempool/models.go create mode 100644 shell.nix diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Makefile @@ -0,0 +1 @@ + diff --git a/cmd/rpi/main.go b/cmd/rpi/main.go new file mode 100644 index 0000000..9ecebb1 --- /dev/null +++ b/cmd/rpi/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "log" + + "git.dvdt.dev/david/bitcoin-ticker-pi/display" + "git.dvdt.dev/david/bitcoin-ticker-pi/fonts" + rgbmatrix "github.com/mcuadros/go-rpi-rgb-led-matrix" +) + +func main() { + cfg := rgbmatrix.DefaultConfig + cfg.Rows = 32 + cfg.Cols = 64 + + m, _ := rgbmatrix.NewRGBLedMatrix(&cfg) + c := rgbmatrix.NewCanvas(m) + + d := display.New(c, + display.NewSlot(9, 5, 4, 0, 0), + display.NewSlot(5, 5, 4, 44, 0), + display.NewSlot(7, 9, 6, 10, 10), + display.NewSlot(12, 5, 4, 7, 27), + display.NewSlot(8, 5, 4, 28, 27), + ) + err := d.UpdateTopLeft([]display.Segment{ + {Text: "dollar", Color: fonts.Green}, + {Text: "4", Color: fonts.White}, + {Text: "2", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + }) + if err != nil { + log.Println(err) + } + + err = d.UpdateTopRight([]display.Segment{ + {Text: "satoshi", Color: fonts.Orange}, + {Text: "2", Color: fonts.White}, + {Text: ".", Color: fonts.White}, + {Text: "5", Color: fonts.White}, + {Text: "K", Color: fonts.White}, + }) + if err != nil { + log.Println(err) + } + + err = d.UpdateCenter([]display.Segment{ + {Text: "height", Color: fonts.Blue}, + {Text: "8", Color: fonts.White}, + {Text: "2", Color: fonts.White}, + {Text: "1", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + }) + if err != nil { + log.Println(err) + } + + err = d.UpdateBottomLeft([]display.Segment{ + {Text: "low", Color: fonts.Green}, + {Text: "1", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "medium", Color: fonts.Orange}, + {Text: "2", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "high", Color: fonts.Red}, + {Text: "3", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + }) + if err != nil { + log.Println(err) + } + + err = d.UpdateBottomRight([]display.Segment{ + {Text: "1", Color: fonts.White}, + {Text: "2", Color: fonts.White}, + {Text: ":", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: ":", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + {Text: "0", Color: fonts.White}, + }) + if err != nil { + log.Println(err) + } + + err = d.Render() + if err != nil { + log.Println(err) + } + + select {} +} diff --git a/display/display.go b/display/display.go new file mode 100644 index 0000000..5494660 --- /dev/null +++ b/display/display.go @@ -0,0 +1,71 @@ +package display + +import ( + rgbmatrix "git.dvdt.dev/david/bitcoin-ticker-pi/go-rpi-rgb-led-matrix" +) + +type display struct { + canvas *rgbmatrix.Canvas + + tl, tr, c, bl, br *Slot +} + +func New(canvas *rgbmatrix.Canvas, tl, tr, c, bl, br *Slot) *display { + return &display{ + canvas: canvas, + tl: tl, + tr: tr, + c: c, + bl: bl, + br: br, + } +} + +func (d *display) UpdateTopLeft(text []Segment) error { + err := d.tl.Update(text) + if err != nil { + return err + } + d.tl.Draw(d.canvas) + return nil +} + +func (d *display) UpdateTopRight(text []Segment) error { + err := d.tr.Update(text) + if err != nil { + return err + } + d.tr.Draw(d.canvas) + return nil +} + +func (d *display) UpdateCenter(text []Segment) error { + err := d.c.Update(text) + if err != nil { + return err + } + d.c.Draw(d.canvas) + return nil +} + +func (d *display) UpdateBottomLeft(text []Segment) error { + err := d.bl.Update(text) + if err != nil { + return err + } + d.bl.Draw(d.canvas) + return nil +} + +func (d *display) UpdateBottomRight(text []Segment) error { + err := d.br.Update(text) + if err != nil { + return err + } + d.bl.Draw(d.canvas) + return nil +} + +func (d *display) Render() error { + return d.canvas.Render() +} diff --git a/display/slot.go b/display/slot.go new file mode 100644 index 0000000..e9ebf21 --- /dev/null +++ b/display/slot.go @@ -0,0 +1,88 @@ +package display + +import ( + "errors" + "fmt" + "image/color" + + "git.dvdt.dev/david/bitcoin-ticker-pi/fonts" + rgbmatrix "git.dvdt.dev/david/bitcoin-ticker-pi/go-rpi-rgb-led-matrix" +) + +var ( + ErrUnsupportedSlotHeight = errors.New("unsupported slot height") +) + +type Segment struct { + Text string + Color color.RGBA + bitmap [][]color.RGBA +} + +func (s *Segment) Draw(canvas *rgbmatrix.Canvas, x, y int) { + for i, row := range s.bitmap { + drawRow(canvas, row, x, y+i) + } +} + +func drawRow(canvas *rgbmatrix.Canvas, row []color.RGBA, x, y int) { + for i := 0; i < len(row); i++ { + canvas.Set(x+i, y, row[i]) + } +} + +type Slot struct { + segmentLimit int + segmentHeight int + segmentWidth int + x, y int + text []Segment +} + +func NewSlot(limit, height, width, x, y int) *Slot { + return &Slot{ + segmentLimit: limit, + segmentHeight: height, + segmentWidth: width, + x: x, + y: y, + text: []Segment{}, + } +} + +func (s *Slot) Update(text []Segment) error { + if len(text) > s.segmentLimit { + text = text[:s.segmentLimit-3] + text = append(text, []Segment{ + {Text: ".", Color: fonts.White}, + {Text: ".", Color: fonts.White}, + {Text: ".", Color: fonts.White}, + }...) + } + + var font fonts.Font + switch s.segmentHeight { + case 5: + font = fonts.Face5x4 + case 7: + font = fonts.Face7x5 + default: + return fmt.Errorf("%w: %d", ErrUnsupportedSlotHeight, s.segmentHeight) + } + + for i, t := range text { + c, err := font.ToColor(t.Text, t.Color) + if err != nil { + return err + } + text[i].bitmap = c + } + s.text = text + return nil +} + +func (s *Slot) Draw(canvas *rgbmatrix.Canvas) { + for i, segment := range s.text { + segment.Draw(canvas, s.x+(i*s.segmentWidth), s.y) + } +} diff --git a/fonts/5x4.go b/fonts/5x4.go new file mode 100644 index 0000000..5f12fd9 --- /dev/null +++ b/fonts/5x4.go @@ -0,0 +1,139 @@ +package fonts + +var ( + Face5x4 = Font{ + "1": { + {0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + }, + "2": { + {1, 1, 0, 0}, + {0, 0, 1, 0}, + {0, 1, 0, 0}, + {1, 0, 0, 0}, + {1, 1, 1, 0}, + }, + "3": { + {1, 1, 0, 0}, + {0, 0, 1, 0}, + {0, 1, 0, 0}, + {0, 0, 1, 0}, + {1, 1, 0, 0}, + }, + "4": { + {0, 0, 1, 0}, + {0, 1, 1, 0}, + {1, 0, 1, 0}, + {1, 1, 1, 0}, + {0, 0, 1, 0}, + }, + "5": { + {1, 1, 1, 0}, + {1, 0, 0, 0}, + {1, 1, 0, 0}, + {0, 0, 1, 0}, + {1, 1, 0, 0}, + }, + "6": { + {0, 1, 1, 0}, + {1, 0, 0, 0}, + {1, 1, 1, 0}, + {1, 0, 1, 0}, + {1, 1, 1, 0}, + }, + "7": { + {1, 1, 1, 0}, + {0, 0, 1, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + }, + "8": { + {1, 1, 1, 0}, + {1, 0, 1, 0}, + {1, 1, 1, 0}, + {1, 0, 1, 0}, + {1, 1, 1, 0}, + }, + "9": { + {1, 1, 1, 0}, + {1, 0, 1, 0}, + {1, 1, 1, 0}, + {0, 0, 1, 0}, + {0, 1, 1, 0}, + }, + "0": { + {0, 1, 0, 0}, + {1, 0, 1, 0}, + {1, 0, 1, 0}, + {1, 0, 1, 0}, + {0, 1, 0, 0}, + }, + "satoshi": { + {0, 1, 0, 0}, + {1, 1, 1, 0}, + {1, 1, 1, 0}, + {1, 1, 1, 0}, + {0, 1, 0, 0}, + }, + "dollar": { + {0, 1, 1, 1}, + {1, 0, 1, 0}, + {0, 1, 1, 0}, + {0, 0, 1, 1}, + {1, 1, 1, 0}, + }, + "height": { + {1, 1, 1, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + {0, 1, 0, 0}, + {1, 1, 1, 0}, + }, + "low": { + {1, 0, 0, 0}, + {1, 0, 0, 0}, + {1, 0, 0, 0}, + {1, 0, 0, 0}, + {1, 1, 1, 0}, + }, + "medium": { + {1, 0, 1, 0}, + {1, 1, 1, 0}, + {1, 0, 1, 0}, + {1, 0, 1, 0}, + {1, 0, 1, 0}, + }, + "high": { + {1, 0, 1, 0}, + {1, 0, 1, 0}, + {1, 1, 1, 0}, + {1, 0, 1, 0}, + {1, 0, 1, 0}, + }, + "K": { + {1, 0, 1, 0}, + {1, 0, 1, 0}, + {1, 1, 0, 0}, + {1, 0, 1, 0}, + {1, 0, 1, 0}, + }, + ".": { + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 0, 0, 0}, + {0, 1, 0, 0}, + }, + ":": { + {0, 0, 0, 0}, + {0, 1, 0, 0}, + {0, 0, 0, 0}, + {0, 1, 0, 0}, + {0, 0, 0, 0}, + }, + } +) diff --git a/fonts/7x5.go b/fonts/7x5.go new file mode 100644 index 0000000..d35b14b --- /dev/null +++ b/fonts/7x5.go @@ -0,0 +1,178 @@ +package fonts + +var ( + Face7x5 = Font{ + "1": { + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + }, + "2": { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 0, 1, 0, 0}, + {0, 1, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 0}, + }, + "3": { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 0, 1, 0, 0}, + {0, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + "4": { + {0, 0, 0, 1, 0}, + {0, 0, 1, 1, 0}, + {0, 1, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + }, + "5": { + {1, 1, 1, 1, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 0, 0}, + {0, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + "6": { + {0, 1, 1, 1, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + "7": { + {1, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 0, 1, 1, 0}, + {0, 0, 1, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + }, + "8": { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + "9": { + {1, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 1, 1, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 0, 0, 1, 0}, + {0, 1, 1, 1, 0}, + }, + "0": { + {0, 1, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 1, 1, 0}, + {1, 1, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {0, 1, 1, 0, 0}, + }, + "satoshi": { + {0, 0, 1, 0, 0}, + {1, 1, 1, 1, 1}, + {0, 0, 0, 0, 0}, + {1, 1, 1, 1, 1}, + {0, 0, 0, 0, 0}, + {1, 1, 1, 1, 1}, + {0, 0, 1, 0, 0}, + }, + "dollar": { + {0, 0, 1, 0, 0}, + {1, 1, 1, 1, 1}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 1}, + {0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + {0, 0, 1, 0, 0}, + }, + "height": { + {1, 1, 1, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0}, + {1, 1, 1, 0, 0}, + }, + "low": { + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0}, + {1, 1, 1, 1, 0}, + }, + "medium": { + {1, 0, 0, 1, 0}, + {1, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + }, + "high": { + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 1, 1, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + }, + "K": { + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 1, 0, 0}, + {1, 1, 0, 0, 0}, + {1, 0, 1, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 1, 0}, + }, + ".": { + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 1, 0, 0, 0}, + }, + ":": { + {0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0}, + {0, 1, 1, 0, 0}, + {0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0}, + {0, 1, 1, 0, 0}, + {0, 0, 0, 0, 0}, + }, + } +) diff --git a/fonts/9x6.go b/fonts/9x6.go new file mode 100644 index 0000000..110278c --- /dev/null +++ b/fonts/9x6.go @@ -0,0 +1,216 @@ +package fonts + +var ( + Face9x6 = Font{ + "1": { + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + }, + "2": { + {0, 1, 1, 1, 0, 0}, + {1, 1, 0, 1, 1, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 1, 1, 0}, + {0, 0, 1, 1, 0, 0}, + {0, 1, 1, 0, 0, 0}, + {1, 1, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 1, 1, 1, 1, 0}, + }, + "3": { + {0, 1, 1, 0, 0, 0}, + {1, 0, 0, 1, 0, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 0, 1, 0, 0}, + {0, 0, 0, 0, 1, 0}, + {1, 0, 0, 1, 0, 0}, + {0, 1, 1, 0, 0, 0}, + }, + "4": { + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 1, 1, 0}, + {0, 0, 1, 0, 1, 0}, + {0, 1, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 1, 1, 1, 1, 0}, + {0, 0, 0, 0, 1, 0}, + }, + "5": { + {1, 1, 1, 1, 1, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 1, 1, 0, 0, 0}, + {0, 0, 0, 1, 0, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 0, 1, 0}, + {1, 0, 0, 1, 0, 0}, + {0, 1, 1, 0, 0, 0}, + }, + "6": { + {0, 0, 1, 1, 0, 0}, + {0, 1, 0, 0, 1, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 1, 1, 0, 0}, + {1, 1, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {0, 1, 1, 1, 0, 0}, + }, + "7": { + {1, 1, 1, 1, 1, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 1, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 1, 0, 0, 0, 0}, + {0, 1, 0, 0, 0, 0}, + {0, 1, 0, 0, 0, 0}, + {0, 1, 0, 0, 0, 0}, + {0, 1, 0, 0, 0, 0}, + }, + "8": { + {0, 1, 1, 1, 0, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {0, 1, 1, 1, 0, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {0, 1, 1, 1, 0, 0}, + }, + "9": { + {0, 1, 1, 1, 0, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {0, 1, 1, 1, 1, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 1, 0, 0, 1, 0}, + {0, 0, 1, 1, 0, 0}, + }, + "0": { + {0, 1, 1, 1, 0, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 1, 1, 0}, + {1, 0, 1, 0, 1, 0}, + {1, 1, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {0, 1, 0, 0, 1, 0}, + {0, 0, 1, 1, 0, 0}, + }, + "satoshi": { + {0, 0, 0, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {1, 1, 1, 1, 1, 0}, + {0, 0, 0, 0, 0, 0}, + {1, 1, 1, 1, 1, 0}, + {0, 0, 0, 0, 0, 0}, + {1, 1, 1, 1, 1, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + }, + "dollar": { + {0, 1, 1, 1, 0, 0}, + {1, 0, 1, 0, 1, 0}, + {1, 0, 1, 0, 0, 0}, + {0, 1, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 1, 0, 0}, + {1, 0, 1, 0, 1, 0}, + {1, 0, 1, 0, 1, 0}, + {0, 1, 1, 1, 0, 0}, + }, + "height": { + {1, 1, 1, 1, 1, 0}, + {0, 1, 1, 1, 0, 0}, + {1, 0, 1, 0, 1, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {1, 0, 1, 0, 1, 0}, + {0, 1, 1, 1, 0, 0}, + {1, 1, 1, 1, 1, 0}, + }, + "low": { + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 1, 1, 1, 1, 0}, + }, + "medium": { + {1, 0, 0, 0, 1, 0}, + {1, 1, 0, 1, 1, 0}, + {1, 0, 1, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + }, + "high": { + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 1, 1, 1, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 1, 0}, + }, + "K": { + {1, 0, 0, 0, 1, 0}, + {1, 0, 0, 0, 0, 0}, + {1, 0, 0, 1, 0, 0}, + {1, 0, 1, 0, 0, 0}, + {1, 1, 1, 0, 0, 0}, + {1, 1, 0, 0, 0, 0}, + {1, 0, 1, 0, 0, 0}, + {1, 0, 0, 1, 0, 0}, + {1, 0, 0, 0, 1, 0}, + }, + ".": { + {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0, 0}, + {0, 1, 1, 0, 0, 0}, + }, + ":": { + {0, 0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0, 0}, + {0, 1, 1, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + {0, 1, 1, 0, 0, 0}, + {0, 1, 1, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, + }, + } +) diff --git a/fonts/fonts.go b/fonts/fonts.go new file mode 100644 index 0000000..51f60c0 --- /dev/null +++ b/fonts/fonts.go @@ -0,0 +1,61 @@ +package fonts + +import ( + "errors" + "fmt" + "image/color" +) + +type Font map[string][][]byte + +var ( + ErrCharNotFound = errors.New("unrecognized character") +) + +func (f Font) ToColor(char string, c color.RGBA) ([][]color.RGBA, error) { + bitmap, found := f[char] + if !found { + return nil, fmt.Errorf("%w: %s", ErrCharNotFound, char) + } + + colorMappedChar := make([][]color.RGBA, 0, len(bitmap)) + for _, row := range bitmap { + crow := make([]color.RGBA, 0, len(row)) + for _, pixel := range row { + crowColor := c + crowColor.R = crowColor.R * pixel + crowColor.G = crowColor.G * pixel + crowColor.B = crowColor.B * pixel + crowColor.A = 0xff * pixel + crow = append(crow, crowColor) + } + + colorMappedChar = append(colorMappedChar, crow) + } + + return colorMappedChar, nil +} + +var ( + White = color.RGBA{ + R: 255, G: 255, B: 255, A: 255, + } + Black = color.RGBA{ + R: 0, G: 0, B: 0, A: 255, + } + DarkGrey = color.RGBA{ + R: 70, G: 70, B: 70, A: 255, + } + Green = color.RGBA{ + R: 34, G: 177, B: 76, A: 255, + } + Orange = color.RGBA{ + R: 255, G: 126, B: 0, A: 255, + } + Red = color.RGBA{ + R: 153, G: 0, B: 48, A: 255, + } + Blue = color.RGBA{ + R: 0, G: 183, B: 239, A: 255, + } +) diff --git a/fonts/fonts_test.go b/fonts/fonts_test.go new file mode 100644 index 0000000..5bf5a7a --- /dev/null +++ b/fonts/fonts_test.go @@ -0,0 +1,49 @@ +package fonts + +import ( + "fmt" + "image/color" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFont_ToColor(t *testing.T) { + tests := []struct { + name string + font Font + char string + color color.RGBA + wantMappedChar [][]color.RGBA + wantError error + }{ + { + name: "it should error if char mapping doesn't exist", + font: Font{}, + char: "not found", + wantError: fmt.Errorf("%w: %s", ErrCharNotFound, "not found"), + }, + { + name: "it should produce a color mapped character", + font: Face5x4, + char: "1", + color: White, + wantMappedChar: [][]color.RGBA{ + {{R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 255}, {R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 0}}, + {{R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 255}, {R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 0}}, + {{R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 255}, {R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 0}}, + {{R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 255}, {R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 0}}, + {{R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 255}, {R: 255, G: 255, B: 255, A: 0}, {R: 255, G: 255, B: 255, A: 0}}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mappedChar, err := test.font.ToColor(test.char, test.color) + + assert.Equal(t, test.wantMappedChar, mappedChar) + assert.Equal(t, test.wantError, err) + }) + } +} diff --git a/go-rpi-rgb-led-matrix/.gitmodules b/go-rpi-rgb-led-matrix/.gitmodules new file mode 100644 index 0000000..492da44 --- /dev/null +++ b/go-rpi-rgb-led-matrix/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/rpi-rgb-led-matrix"] + path = lib/rpi-rgb-led-matrix + url = https://github.com/hzeller/rpi-rgb-led-matrix.git diff --git a/go-rpi-rgb-led-matrix/.travis.yml b/go-rpi-rgb-led-matrix/.travis.yml new file mode 100644 index 0000000..378d24a --- /dev/null +++ b/go-rpi-rgb-led-matrix/.travis.yml @@ -0,0 +1,14 @@ +language: go + +go: + - 1.6 + - 1.7 + - tip + +before_install: + - cd $GOPATH/src/github.com/mcuadros/go-rpi-rgb-led-matrix/lib/rpi-rgb-led-matrix/ + - git submodule update --init + - make + - cd $GOPATH/src/github.com/mcuadros/go-rpi-rgb-led-matrix/ + - go get -t -v ./... + - go install -v ./... diff --git a/go-rpi-rgb-led-matrix/LICENSE b/go-rpi-rgb-led-matrix/LICENSE new file mode 100644 index 0000000..81e256d --- /dev/null +++ b/go-rpi-rgb-led-matrix/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Máximo Cuadros + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/go-rpi-rgb-led-matrix/README.md b/go-rpi-rgb-led-matrix/README.md new file mode 100644 index 0000000..d387d73 --- /dev/null +++ b/go-rpi-rgb-led-matrix/README.md @@ -0,0 +1,97 @@ +# go-rpi-rgb-led-matrix [![GoDoc](https://godoc.org/github.com/mcuadros/go-rpi-rgb-led-matrix?status.svg)](https://godoc.org/github.com/mcuadros/go-rpi-rgb-led-matrix) [![Build Status](https://travis-ci.org/mcuadros/go-rpi-rgb-led-matrix.svg?branch=master)](https://travis-ci.org/mcuadros/go-rpi-rgb-led-matrix) + + +Go binding for [`rpi-rgb-led-matrix`](https://github.com/hzeller/rpi-rgb-led-matrix) an excellent C++ library to control [RGB LED displays](https://learn.adafruit.com/32x16-32x32-rgb-led-matrix/overview) with Raspberry Pi GPIO. + +This library includes the basic bindings to control de LED Matrix directly and also a convenient [ToolKit](https://godoc.org/github.com/mcuadros/go-rpi-rgb-led-matrix#ToolKit) with more high level functions. Also some [examples](https://github.com/mcuadros/go-rpi-rgb-led-matrix/tree/master/examples) are included to test the library and the configuration. + +The [`Canvas`](https://godoc.org/github.com/mcuadros/go-rpi-rgb-led-matrix#Canvas) struct implements the [`image.Image`](https://golang.org/pkg/image/#Image) interface from the Go standard library. This makes the interaction with the matrix simple as work with a normal image in Go, allowing the usage of any Go library build around the `image.Image` interface. + +To learn about the configuration and the wiring go to the [original library](https://github.com/hzeller/rpi-rgb-led-matrix), is highly detailed and well explained. + +Installation +------------ + +The recommended way to install `go-rpi-rgb-led-matrix` is: + +```sh +go get github.com/mcuadros/go-rpi-rgb-led-matrix +``` + +Then you will get an **expected** error like this: + +``` +# github.com/mcuadros/go-rpi-rgb-led-matrix +/usr/bin/ld: cannot find -lrgbmatrix +collect2: error: ld returned 1 exit status +``` + +This happens because you need to compile the `rgbmatrix` C bindings: +```sh +cd $GOPATH/src/github.com/mcuadros/go-rpi-rgb-led-matrix/lib/rpi-rgb-led-matrix/ +git submodule update --init +make +cd $GOPATH/src/github.com/mcuadros/go-rpi-rgb-led-matrix/ +go install -v ./... +``` + +Examples +-------- + +Setting all the pixels to white: + +```go +// create a new Matrix instance with the DefaultConfig +m, _ := rgbmatrix.NewRGBLedMatrix(&rgbmatrix.DefaultConfig) + +// create the Canvas, implements the image.Image interface +c := rgbmatrix.NewCanvas(m) +defer c.Close() // don't forgot close the Matrix, if not your leds will remain on + +// using the standard draw.Draw function we copy a white image onto the Canvas +draw.Draw(c, c.Bounds(), &image.Uniform{color.White}, image.ZP, draw.Src) + +// don't forget call Render to display the new led status +c.Render() +``` + +Playing a GIF into your matrix during 30 seconds: + +```go +// create a new Matrix instance with the DefaultConfig +m, _ := rgbmatrix.NewRGBLedMatrix(&rgbmatrix.DefaultConfig) + +// create a ToolKit instance +tk := rgbmatrix.NewToolKit(m) +defer tk.Close() // don't forgot close the Matrix, if not your leds will remain on + +// open the gif file for reading +file, _ := os.Open("mario.gif") + +// play of the gif using the io.Reader +close, _ := tk.PlayGIF(f) +fatal(err) + +// we wait 30 seconds and then we stop the playing gif sending a True to the returned chan +time.Sleep(time.Second * 30) +close <- true +``` + +The image of the header was recorded using this few lines, the running _Mario_ gif, and three 32x64 pannels. + + +Check the folder [`examples`](https://github.com/mcuadros/go-rpi-rgb-led-matrix/tree/master/examples) folder for more examples + + +Matrix Emulation +---------------- + +As part of the library an small Matrix emulator is provided. The emulator renderize a virtual RGB matrix on a window in your desktop, without needing a real RGB matrix connected to your computer. + +To execute the emulator set the `MATRIX_EMULATOR` environment variable to `1`, then when `NewRGBLedMatrix` is used, a `emulator.Emulator` is returned instead of a interface the real board. + + +License +------- + +MIT, see [LICENSE](LICENSE) diff --git a/go-rpi-rgb-led-matrix/canvas.go b/go-rpi-rgb-led-matrix/canvas.go new file mode 100644 index 0000000..3b7d7f5 --- /dev/null +++ b/go-rpi-rgb-led-matrix/canvas.go @@ -0,0 +1,77 @@ +package rgbmatrix + +import ( + "image" + "image/color" + "image/draw" +) + +// Canvas is a image.Image representation of a WS281x matrix, it implements +// image.Image interface and can be used with draw.Draw for example +type Canvas struct { + w, h int + m Matrix + closed bool +} + +// NewCanvas returns a new Canvas using the given width and height and creates +// a new WS281x matrix using the given config +func NewCanvas(m Matrix) *Canvas { + w, h := m.Geometry() + return &Canvas{ + w: w, + h: h, + m: m, + } +} + +// Render update the display with the data from the LED buffer +func (c *Canvas) Render() error { + return c.m.Render() +} + +// ColorModel returns the canvas' color model, always color.RGBAModel +func (c *Canvas) ColorModel() color.Model { + return color.RGBAModel +} + +// Bounds return the topology of the Canvas +func (c *Canvas) Bounds() image.Rectangle { + return image.Rect(0, 0, c.w, c.h) +} + +// At returns the color of the pixel at (x, y) +func (c *Canvas) At(x, y int) color.Color { + return c.m.At(c.position(x, y)) +} + +// Set set LED at position x,y to the provided 24-bit color value +func (c *Canvas) Set(x, y int, color color.Color) { + c.m.Set(c.position(x, y), color) +} + +func (c *Canvas) position(x, y int) int { + return x + (y * c.w) +} + +// Clear set all the leds on the matrix with color.Black +func (c *Canvas) Clear() error { + draw.Draw(c, c.Bounds(), &image.Uniform{color.Black}, image.ZP, draw.Src) + return c.m.Render() +} + +// Close clears the matrix and close the matrix +func (c *Canvas) Close() error { + c.Clear() + return c.m.Close() +} + +// Matrix is an interface that represent any RGB matrix, very useful for testing +type Matrix interface { + Geometry() (width, height int) + At(position int) color.Color + Set(position int, c color.Color) + Apply([]color.Color) error + Render() error + Close() error +} diff --git a/go-rpi-rgb-led-matrix/canvas_test.go b/go-rpi-rgb-led-matrix/canvas_test.go new file mode 100644 index 0000000..3bb1972 --- /dev/null +++ b/go-rpi-rgb-led-matrix/canvas_test.go @@ -0,0 +1,139 @@ +package rgbmatrix + +import ( + "image/color" + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type CanvasSuite struct{} + +var _ = Suite(&CanvasSuite{}) + +func (s *CanvasSuite) TestNewCanvas(c *C) { + canvas := NewCanvas(NewMatrixMock()) + c.Assert(canvas, NotNil) + c.Assert(canvas.w, Equals, 64) + c.Assert(canvas.h, Equals, 32) +} + +func (s *CanvasSuite) TestRender(c *C) { + m := NewMatrixMock() + canvas := &Canvas{m: m} + canvas.Render() + + c.Assert(m.called["Render"], Equals, true) +} + +func (s *CanvasSuite) TestColorModel(c *C) { + canvas := &Canvas{} + + c.Assert(canvas.ColorModel(), Equals, color.RGBAModel) +} + +func (s *CanvasSuite) TestBounds(c *C) { + + canvas := &Canvas{w: 10, h: 20} + + b := canvas.Bounds() + c.Assert(b.Min.X, Equals, 0) + c.Assert(b.Min.Y, Equals, 0) + c.Assert(b.Max.X, Equals, 10) + c.Assert(b.Max.Y, Equals, 20) +} + +func (s *CanvasSuite) TestAt(c *C) { + m := NewMatrixMock() + canvas := &Canvas{w: 10, h: 20, m: m} + canvas.At(5, 15) + + c.Assert(m.called["At"], Equals, 155) +} + +func (s *CanvasSuite) TestSet(c *C) { + m := NewMatrixMock() + canvas := &Canvas{w: 10, h: 20, m: m} + canvas.Set(5, 15, color.White) + + c.Assert(m.called["Set"], Equals, 155) + c.Assert(m.colors[155], Equals, color.White) +} + +func (s *CanvasSuite) TestClear(c *C) { + m := NewMatrixMock() + + canvas := &Canvas{w: 10, h: 20, m: m} + err := canvas.Clear() + c.Assert(err, IsNil) + + for _, px := range m.colors { + c.Assert(px, Equals, color.Black) + } + + c.Assert(m.called["Render"], Equals, true) +} + +func (s *CanvasSuite) TestClose(c *C) { + m := NewMatrixMock() + canvas := &Canvas{w: 10, h: 20, m: m} + err := canvas.Close() + c.Assert(err, IsNil) + + for _, px := range m.colors { + c.Assert(px, Equals, color.Black) + } + + c.Assert(m.called["Render"], Equals, true) +} + +type MatrixMock struct { + called map[string]interface{} + colors []color.Color +} + +func NewMatrixMock() *MatrixMock { + return &MatrixMock{ + called: make(map[string]interface{}, 0), + colors: make([]color.Color, 200), + } +} + +func (m *MatrixMock) Geometry() (width, height int) { + return 64, 32 +} + +func (m *MatrixMock) Initialize() error { + m.called["Initialize"] = true + return nil +} + +func (m *MatrixMock) At(position int) color.Color { + m.called["At"] = position + return color.Black +} + +func (m *MatrixMock) Set(position int, c color.Color) { + m.called["Set"] = position + m.colors[position] = c +} + +func (m *MatrixMock) Apply(leds []color.Color) error { + for position, l := range leds { + m.Set(position, l) + } + + return m.Render() +} + +func (m *MatrixMock) Render() error { + m.called["Render"] = true + return nil +} + +func (m *MatrixMock) Close() error { + m.called["Close"] = true + return nil +} diff --git a/go-rpi-rgb-led-matrix/emulator/emulator.go b/go-rpi-rgb-led-matrix/emulator/emulator.go new file mode 100644 index 0000000..60ab8be --- /dev/null +++ b/go-rpi-rgb-led-matrix/emulator/emulator.go @@ -0,0 +1,204 @@ +package emulator + +import ( + "fmt" + "image" + "image/color" + "os" + "sync" + + "golang.org/x/exp/shiny/driver" + "golang.org/x/exp/shiny/screen" + "golang.org/x/mobile/event/paint" + "golang.org/x/mobile/event/size" +) + +const DefaultPixelPitch = 12 +const windowTitle = "RGB led matrix emulator" + +type Emulator struct { + PixelPitch int + Gutter int + Width int + Height int + GutterColor color.Color + PixelPitchToGutterRatio int + Margin int + + leds []color.Color + w screen.Window + s screen.Screen + wg sync.WaitGroup + + isReady bool +} + +func NewEmulator(w, h, pixelPitch int, autoInit bool) *Emulator { + e := &Emulator{ + Width: w, + Height: h, + GutterColor: color.Gray{Y: 20}, + PixelPitchToGutterRatio: 2, + Margin: 10, + } + e.updatePixelPitchForGutter(pixelPitch / e.PixelPitchToGutterRatio) + + if autoInit { + e.Init() + } + + return e +} + +// Init initialize the emulator, creating a new Window and waiting until is +// painted. If something goes wrong the function panics +func (e *Emulator) Init() { + e.leds = make([]color.Color, e.Width*e.Height) + + e.wg.Add(1) + go driver.Main(e.mainWindowLoop) + e.wg.Wait() +} + +func (e *Emulator) mainWindowLoop(s screen.Screen) { + var err error + e.s = s + // Calculate initial window size based on whatever our gutter/pixel pitch currently is. + dims := e.matrixWithMarginsRect() + e.w, err = s.NewWindow(&screen.NewWindowOptions{ + Title: windowTitle, + Width: dims.Max.X, + Height: dims.Max.Y, + }) + + if err != nil { + panic(err) + } + + defer e.w.Release() + + var sz size.Event + for { + evn := e.w.NextEvent() + switch evn := evn.(type) { + case paint.Event: + e.drawContext(sz) + if e.isReady { + continue + } + + e.Apply(make([]color.Color, e.Width*e.Height)) + e.wg.Done() + e.isReady = true + case size.Event: + sz = evn + + case error: + fmt.Fprintln(os.Stderr, e) + } + } +} + +func (e *Emulator) drawContext(sz size.Event) { + e.updatePixelPitchForGutter(e.calculateGutterForViewableArea(sz.Size())) + // Fill entire background with white. + e.w.Fill(sz.Bounds(), color.White, screen.Src) + // Fill matrix display rectangle with the gutter color. + e.w.Fill(e.matrixWithMarginsRect(), e.GutterColor, screen.Src) + // Set all LEDs to black. + e.Apply(make([]color.Color, e.Width*e.Height)) +} + +// Some formulas that allowed me to better understand the drawable area. I found that the math was +// easiest when put in terms of the Gutter width, hence the addition of PixelPitchToGutterRatio. +// +// PixelPitch = PixelPitchToGutterRatio * Gutter +// DisplayWidth = (PixelPitch * LEDColumns) + (Gutter * (LEDColumns - 1)) + (2 * Margin) +// Gutter = (DisplayWidth - (2 * Margin)) / (PixelPitchToGutterRatio * LEDColumns + LEDColumns - 1) +// +// MMMMMMMMMMMMMMMM.....MMMM +// MGGGGGGGGGGGGGGG.....GGGM +// MGLGLGLGLGLGLGLG.....GLGM +// MGGGGGGGGGGGGGGG.....GGGM +// MGLGLGLGLGLGLGLG.....GLGM +// MGGGGGGGGGGGGGGG.....GGGM +// ......................... +// MGGGGGGGGGGGGGGG.....GGGM +// MGLGLGLGLGLGLGLG.....GLGM +// MGGGGGGGGGGGGGGG.....GGGM +// MMMMMMMMMMMMMMMM.....MMMM +// +// where: +// M = Margin +// G = Gutter +// L = LED + +// matrixWithMarginsRect Returns a Rectangle that describes entire emulated RGB Matrix, including margins. +func (e *Emulator) matrixWithMarginsRect() image.Rectangle { + upperLeftLED := e.ledRect(0, 0) + lowerRightLED := e.ledRect(e.Width-1, e.Height-1) + return image.Rect(upperLeftLED.Min.X-e.Margin, upperLeftLED.Min.Y-e.Margin, lowerRightLED.Max.X+e.Margin, lowerRightLED.Max.Y+e.Margin) +} + +// ledRect Returns a Rectangle for the LED at col and row. +func (e *Emulator) ledRect(col int, row int) image.Rectangle { + x := (col * (e.PixelPitch + e.Gutter)) + e.Margin + y := (row * (e.PixelPitch + e.Gutter)) + e.Margin + return image.Rect(x, y, x+e.PixelPitch, y+e.PixelPitch) +} + +// calculateGutterForViewableArea As the name states, calculates the size of the gutter for a given viewable area. +// It's easier to understand the geometry of the matrix on screen when put in terms of the gutter, +// hence the shift toward calculating the gutter size. +func (e *Emulator) calculateGutterForViewableArea(size image.Point) int { + maxGutterInX := (size.X - 2*e.Margin) / (e.PixelPitchToGutterRatio*e.Width + e.Width - 1) + maxGutterInY := (size.Y - 2*e.Margin) / (e.PixelPitchToGutterRatio*e.Height + e.Height - 1) + if maxGutterInX < maxGutterInY { + return maxGutterInX + } + return maxGutterInY +} + +func (e *Emulator) updatePixelPitchForGutter(gutterWidth int) { + e.PixelPitch = e.PixelPitchToGutterRatio * gutterWidth + e.Gutter = gutterWidth +} + +func (e *Emulator) Geometry() (width, height int) { + return e.Width, e.Height +} + +func (e *Emulator) Apply(leds []color.Color) error { + defer func() { e.leds = make([]color.Color, e.Height*e.Width) }() + + var c color.Color + for col := 0; col < e.Width; col++ { + for row := 0; row < e.Height; row++ { + c = e.At(col + (row * e.Width)) + e.w.Fill(e.ledRect(col, row), c, screen.Over) + } + } + + e.w.Publish() + return nil +} + +func (e *Emulator) Render() error { + return e.Apply(e.leds) +} + +func (e *Emulator) At(position int) color.Color { + if e.leds[position] == nil { + return color.Black + } + + return e.leds[position] +} + +func (e *Emulator) Set(position int, c color.Color) { + e.leds[position] = color.RGBAModel.Convert(c) +} + +func (e *Emulator) Close() error { + return nil +} diff --git a/go-rpi-rgb-led-matrix/examples/animation/main.go b/go-rpi-rgb-led-matrix/examples/animation/main.go new file mode 100644 index 0000000..aa5a1b9 --- /dev/null +++ b/go-rpi-rgb-led-matrix/examples/animation/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "flag" + "image" + "image/color" + "time" + + "github.com/RockKeeper/go-rpi-rgb-led-matrix" + "github.com/fogleman/gg" +) + +var ( + rows = flag.Int("led-rows", 32, "number of rows supported") + cols = flag.Int("led-cols", 32, "number of columns supported") + parallel = flag.Int("led-parallel", 1, "number of daisy-chained panels") + chain = flag.Int("led-chain", 2, "number of displays daisy-chained") + brightness = flag.Int("brightness", 100, "brightness (0-100)") + hardware_mapping = flag.String("led-gpio-mapping", "regular", "Name of GPIO mapping used.") + show_refresh = flag.Bool("led-show-refresh", false, "Show refresh rate.") + inverse_colors = flag.Bool("led-inverse", false, "Switch if your matrix has inverse colors on.") + disable_hardware_pulsing = flag.Bool("led-no-hardware-pulse", false, "Don't use hardware pin-pulse generation.") +) + +func main() { + config := &rgbmatrix.DefaultConfig + config.Rows = *rows + config.Cols = *cols + config.Parallel = *parallel + config.ChainLength = *chain + config.Brightness = *brightness + config.HardwareMapping = *hardware_mapping + config.ShowRefreshRate = *show_refresh + config.InverseColors = *inverse_colors + config.DisableHardwarePulsing = *disable_hardware_pulsing + + m, err := rgbmatrix.NewRGBLedMatrix(config) + fatal(err) + + tk := rgbmatrix.NewToolKit(m) + defer tk.Close() + + tk.PlayAnimation(NewAnimation(image.Point{64, 32})) +} + +func init() { + flag.Parse() +} + +func fatal(err error) { + if err != nil { + panic(err) + } +} + +type Animation struct { + ctx *gg.Context + position image.Point + dir image.Point + stroke int +} + +func NewAnimation(sz image.Point) *Animation { + return &Animation{ + ctx: gg.NewContext(sz.X, sz.Y), + dir: image.Point{1, 1}, + stroke: 5, + } +} + +func (a *Animation) Next() (image.Image, <-chan time.Time, error) { + defer a.updatePosition() + + a.ctx.SetColor(color.Black) + a.ctx.Clear() + + a.ctx.DrawCircle(float64(a.position.X), float64(a.position.Y), float64(a.stroke)) + a.ctx.SetColor(color.RGBA{255, 0, 0, 255}) + a.ctx.Fill() + return a.ctx.Image(), time.After(time.Millisecond * 50), nil +} + +func (a *Animation) updatePosition() { + a.position.X += 1 * a.dir.X + a.position.Y += 1 * a.dir.Y + + if a.position.Y+a.stroke > a.ctx.Height() { + a.dir.Y = -1 + } else if a.position.Y-a.stroke < 0 { + a.dir.Y = 1 + } + + if a.position.X+a.stroke > a.ctx.Width() { + a.dir.X = -1 + } else if a.position.X-a.stroke < 0 { + a.dir.X = 1 + } +} diff --git a/go-rpi-rgb-led-matrix/examples/basic/main.go b/go-rpi-rgb-led-matrix/examples/basic/main.go new file mode 100644 index 0000000..bdc01a8 --- /dev/null +++ b/go-rpi-rgb-led-matrix/examples/basic/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "flag" + "fmt" + "image/color" + + "github.com/RockKeeper/go-rpi-rgb-led-matrix" +) + +var ( + rows = flag.Int("led-rows", 32, "number of rows supported") + cols = flag.Int("led-cols", 32, "number of columns supported") + parallel = flag.Int("led-parallel", 1, "number of daisy-chained panels") + chain = flag.Int("led-chain", 2, "number of displays daisy-chained") + brightness = flag.Int("brightness", 100, "brightness (0-100)") + hardware_mapping = flag.String("led-gpio-mapping", "regular", "Name of GPIO mapping used.") + show_refresh = flag.Bool("led-show-refresh", false, "Show refresh rate.") + inverse_colors = flag.Bool("led-inverse", false, "Switch if your matrix has inverse colors on.") + disable_hardware_pulsing = flag.Bool("led-no-hardware-pulse", false, "Don't use hardware pin-pulse generation.") +) + +func main() { + config := &rgbmatrix.DefaultConfig + config.Rows = *rows + config.Cols = *cols + config.Parallel = *parallel + config.ChainLength = *chain + config.Brightness = *brightness + config.HardwareMapping = *hardware_mapping + config.ShowRefreshRate = *show_refresh + config.InverseColors = *inverse_colors + config.DisableHardwarePulsing = *disable_hardware_pulsing + + m, err := rgbmatrix.NewRGBLedMatrix(config) + fatal(err) + + c := rgbmatrix.NewCanvas(m) + defer c.Close() + + bounds := c.Bounds() + for x := bounds.Min.X; x < bounds.Max.X; x++ { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + fmt.Println("x", x, "y", y) + c.Set(x, y, color.RGBA{255, 0, 0, 255}) + c.Render() + } + } +} + +func init() { + flag.Parse() +} + +func fatal(err error) { + if err != nil { + panic(err) + } +} diff --git a/go-rpi-rgb-led-matrix/examples/image/main.go b/go-rpi-rgb-led-matrix/examples/image/main.go new file mode 100644 index 0000000..6f73631 --- /dev/null +++ b/go-rpi-rgb-led-matrix/examples/image/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "flag" + "os" + "time" + + "github.com/RockKeeper/go-rpi-rgb-led-matrix" + "github.com/disintegration/imaging" +) + +var ( + rows = flag.Int("led-rows", 32, "number of rows supported") + cols = flag.Int("led-cols", 32, "number of columns supported") + parallel = flag.Int("led-parallel", 1, "number of daisy-chained panels") + chain = flag.Int("led-chain", 2, "number of displays daisy-chained") + brightness = flag.Int("brightness", 100, "brightness (0-100)") + hardware_mapping = flag.String("led-gpio-mapping", "regular", "Name of GPIO mapping used.") + show_refresh = flag.Bool("led-show-refresh", false, "Show refresh rate.") + inverse_colors = flag.Bool("led-inverse", false, "Switch if your matrix has inverse colors on.") + disable_hardware_pulsing = flag.Bool("led-no-hardware-pulse", false, "Don't use hardware pin-pulse generation.") + img = flag.String("image", "", "image path") + + rotate = flag.Int("rotate", 0, "rotate angle, 90, 180, 270") +) + +func main() { + f, err := os.Open(*img) + fatal(err) + + config := &rgbmatrix.DefaultConfig + config.Rows = *rows + config.Cols = *cols + config.Parallel = *parallel + config.ChainLength = *chain + config.Brightness = *brightness + config.HardwareMapping = *hardware_mapping + config.ShowRefreshRate = *show_refresh + config.InverseColors = *inverse_colors + config.DisableHardwarePulsing = *disable_hardware_pulsing + + m, err := rgbmatrix.NewRGBLedMatrix(config) + fatal(err) + + tk := rgbmatrix.NewToolKit(m) + defer tk.Close() + + switch *rotate { + case 90: + tk.Transform = imaging.Rotate90 + case 180: + tk.Transform = imaging.Rotate180 + case 270: + tk.Transform = imaging.Rotate270 + } + + close, err := tk.PlayGIF(f) + fatal(err) + + time.Sleep(time.Second * 30) + close <- true +} + +func init() { + flag.Parse() +} + +func fatal(err error) { + if err != nil { + panic(err) + } +} diff --git a/go-rpi-rgb-led-matrix/examples/rpc/client/main.go b/go-rpi-rgb-led-matrix/examples/rpc/client/main.go new file mode 100644 index 0000000..bcfe434 --- /dev/null +++ b/go-rpi-rgb-led-matrix/examples/rpc/client/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "os" + "time" + + "github.com/RockKeeper/go-rpi-rgb-led-matrix" + "github.com/RockKeeper/go-rpi-rgb-led-matrix/rpc" +) + +var ( + img = flag.String("image", "", "image path") +) + +func main() { + f, err := os.Open(*img) + fatal(err) + + m, err := rpc.NewClient("tcp", "10.20.20.20:1234") + fatal(err) + + tk := rgbmatrix.NewToolKit(m) + close, err := tk.PlayGIF(f) + fatal(err) + + time.Sleep(time.Second * 3) + close <- true +} + +func init() { + flag.Parse() +} + +func fatal(err error) { + if err != nil { + panic(err) + } +} diff --git a/go-rpi-rgb-led-matrix/examples/rpc/server/main.go b/go-rpi-rgb-led-matrix/examples/rpc/server/main.go new file mode 100644 index 0000000..338d036 --- /dev/null +++ b/go-rpi-rgb-led-matrix/examples/rpc/server/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "flag" + + "github.com/RockKeeper/go-rpi-rgb-led-matrix" + "github.com/RockKeeper/go-rpi-rgb-led-matrix/rpc" +) + +var ( + rows = flag.Int("led-rows", 32, "number of rows supported") + cols = flag.Int("led-cols", 32, "number of columns supported") + parallel = flag.Int("led-parallel", 1, "number of daisy-chained panels") + chain = flag.Int("led-chain", 2, "number of displays daisy-chained") + brightness = flag.Int("brightness", 100, "brightness (0-100)") + hardware_mapping = flag.String("led-gpio-mapping", "regular", "Name of GPIO mapping used.") + show_refresh = flag.Bool("led-show-refresh", false, "Show refresh rate.") + inverse_colors = flag.Bool("led-inverse", false, "Switch if your matrix has inverse colors on.") + disable_hardware_pulsing = flag.Bool("led-no-hardware-pulse", false, "Don't use hardware pin-pulse generation.") +) + +func main() { + config := &rgbmatrix.DefaultConfig + config.Rows = *rows + config.Cols = *cols + config.Parallel = *parallel + config.ChainLength = *chain + config.Brightness = *brightness + config.HardwareMapping = *hardware_mapping + config.ShowRefreshRate = *show_refresh + config.InverseColors = *inverse_colors + config.DisableHardwarePulsing = *disable_hardware_pulsing + + m, err := rgbmatrix.NewRGBLedMatrix(config) + fatal(err) + + rpc.Serve(m) +} + +func fatal(err error) { + if err != nil { + panic(err) + } +} diff --git a/go-rpi-rgb-led-matrix/matrix.go b/go-rpi-rgb-led-matrix/matrix.go new file mode 100644 index 0000000..7c0a2f9 --- /dev/null +++ b/go-rpi-rgb-led-matrix/matrix.go @@ -0,0 +1,274 @@ +package rgbmatrix + +/* +#cgo CFLAGS: -std=c99 -I${SRCDIR}/lib/rpi-rgb-led-matrix/include -DSHOW_REFRESH_RATE +#cgo LDFLAGS: -lrgbmatrix -L${SRCDIR}/lib/rpi-rgb-led-matrix/lib -lstdc++ -lm +#include + +void led_matrix_swap(struct RGBLedMatrix *matrix, struct LedCanvas *offscreen_canvas, + int width, int height, const uint32_t pixels[]) { + + + int i, x, y; + uint32_t color; + for (x = 0; x < width; ++x) { + for (y = 0; y < height; ++y) { + i = x + (y * width); + color = pixels[i]; + + led_canvas_set_pixel(offscreen_canvas, x, y, + (color >> 16) & 255, (color >> 8) & 255, color & 255); + } + } + + offscreen_canvas = led_matrix_swap_on_vsync(matrix, offscreen_canvas); +} + +void set_show_refresh_rate(struct RGBLedMatrixOptions *o, int show_refresh_rate) { + o->show_refresh_rate = show_refresh_rate != 0 ? 1 : 0; +} + +void set_disable_hardware_pulsing(struct RGBLedMatrixOptions *o, int disable_hardware_pulsing) { + o->disable_hardware_pulsing = disable_hardware_pulsing != 0 ? 1 : 0; +} + +void set_inverse_colors(struct RGBLedMatrixOptions *o, int inverse_colors) { + o->inverse_colors = inverse_colors != 0 ? 1 : 0; +} +*/ +import "C" +import ( + "fmt" + "image/color" + "os" + "unsafe" + + "git.dvdt.dev/david/bitcoin-ticker-pi/go-rpi-rgb-led-matrix/emulator" +) + +// DefaultConfig default WS281x configuration +var DefaultConfig = HardwareConfig{ + Rows: 32, + Cols: 32, + ChainLength: 1, + Parallel: 1, + PWMBits: 11, + PWMLSBNanoseconds: 130, + Brightness: 100, + ScanMode: Progressive, +} + +// HardwareConfig rgb-led-matrix configuration +type HardwareConfig struct { + // Rows the number of rows supported by the display, so 32 or 16. + Rows int + // Cols the number of columns supported by the display, so 32 or 64 . + Cols int + // ChainLengthis the number of displays daisy-chained together + // (output of one connected to input of next). + ChainLength int + // Parallel is the number of parallel chains connected to the Pi; in old Pis + // with 26 GPIO pins, that is 1, in newer Pis with 40 interfaces pins, that + // can also be 2 or 3. The effective number of pixels in vertical direction is + // then thus rows * parallel. + Parallel int + // Set PWM bits used for output. Default is 11, but if you only deal with + // limited comic-colors, 1 might be sufficient. Lower require less CPU and + // increases refresh-rate. + PWMBits int + // Change the base time-unit for the on-time in the lowest significant bit in + // nanoseconds. Higher numbers provide better quality (more accurate color, + // less ghosting), but have a negative impact on the frame rate. + PWMLSBNanoseconds int // the DMA channel to use + // Brightness is the initial brightness of the panel in percent. Valid range + // is 1..100 + Brightness int + // ScanMode progressive or interlaced + ScanMode ScanMode // strip color layout + // Disable the PWM hardware subsystem to create pulses. Typically, you don't + // want to disable hardware pulsing, this is mostly for debugging and figuring + // out if there is interference with the sound system. + // This won't do anything if output enable is not connected to GPIO 18 in + // non-standard wirings. + DisableHardwarePulsing bool + + ShowRefreshRate bool + InverseColors bool + + // Name of GPIO mapping used + HardwareMapping string +} + +func (c *HardwareConfig) geometry() (width, height int) { + return c.Cols * c.ChainLength, c.Rows * c.Parallel +} + +func (c *HardwareConfig) toC() *C.struct_RGBLedMatrixOptions { + o := &C.struct_RGBLedMatrixOptions{} + o.rows = C.int(c.Rows) + o.cols = C.int(c.Cols) + o.chain_length = C.int(c.ChainLength) + o.parallel = C.int(c.Parallel) + o.pwm_bits = C.int(c.PWMBits) + o.pwm_lsb_nanoseconds = C.int(c.PWMLSBNanoseconds) + o.brightness = C.int(c.Brightness) + o.scan_mode = C.int(c.ScanMode) + o.hardware_mapping = C.CString(c.HardwareMapping) + + if c.ShowRefreshRate == true { + C.set_show_refresh_rate(o, C.int(1)) + } else { + C.set_show_refresh_rate(o, C.int(0)) + } + + if c.DisableHardwarePulsing == true { + C.set_disable_hardware_pulsing(o, C.int(1)) + } else { + C.set_disable_hardware_pulsing(o, C.int(0)) + } + + if c.InverseColors == true { + C.set_inverse_colors(o, C.int(1)) + } else { + C.set_inverse_colors(o, C.int(0)) + } + + return o +} + +type ScanMode int8 + +const ( + Progressive ScanMode = 0 + Interlaced ScanMode = 1 +) + +// RGBLedMatrix matrix representation for ws281x +type RGBLedMatrix struct { + Config *HardwareConfig + + height int + width int + matrix *C.struct_RGBLedMatrix + buffer *C.struct_LedCanvas + leds []C.uint32_t +} + +const MatrixEmulatorENV = "MATRIX_EMULATOR" + +// NewRGBLedMatrix returns a new matrix using the given size and config +func NewRGBLedMatrix(config *HardwareConfig) (c Matrix, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("error creating matrix: %v", r) + } + } + }() + + if isMatrixEmulator() { + return buildMatrixEmulator(config), nil + } + + w, h := config.geometry() + m := C.led_matrix_create_from_options(config.toC(), nil, nil) + b := C.led_matrix_create_offscreen_canvas(m) + c = &RGBLedMatrix{ + Config: config, + width: w, height: h, + matrix: m, + buffer: b, + leds: make([]C.uint32_t, w*h), + } + if m == nil { + return nil, fmt.Errorf("unable to allocate memory") + } + + return c, nil +} + +func isMatrixEmulator() bool { + if os.Getenv(MatrixEmulatorENV) == "1" { + return true + } + + return false +} + +func buildMatrixEmulator(config *HardwareConfig) Matrix { + w, h := config.geometry() + return emulator.NewEmulator(w, h, emulator.DefaultPixelPitch, true) +} + +// Initialize initialize library, must be called once before other functions are +// called. +func (c *RGBLedMatrix) Initialize() error { + return nil +} + +// Geometry returns the width and the height of the matrix +func (c *RGBLedMatrix) Geometry() (width, height int) { + return c.width, c.height +} + +// Apply set all the pixels to the values contained in leds +func (c *RGBLedMatrix) Apply(leds []color.Color) error { + for position, l := range leds { + c.Set(position, l) + } + + return c.Render() +} + +// Render update the display with the data from the LED buffer +func (c *RGBLedMatrix) Render() error { + w, h := c.Config.geometry() + + C.led_matrix_swap( + c.matrix, + c.buffer, + C.int(w), C.int(h), + (*C.uint32_t)(unsafe.Pointer(&c.leds[0])), + ) + + c.leds = make([]C.uint32_t, w*h) + return nil +} + +// At return an Color which allows access to the LED display data as +// if it were a sequence of 24-bit RGB values. +func (c *RGBLedMatrix) At(position int) color.Color { + return uint32ToColor(c.leds[position]) +} + +// Set set LED at position x,y to the provided 24-bit color value. +func (c *RGBLedMatrix) Set(position int, color color.Color) { + c.leds[position] = C.uint32_t(colorToUint32(color)) +} + +// Close finalizes the ws281x interface +func (c *RGBLedMatrix) Close() error { + C.led_matrix_delete(c.matrix) + return nil +} + +func colorToUint32(c color.Color) uint32 { + if c == nil { + return 0 + } + + // A color's RGBA method returns values in the range [0, 65535] + red, green, blue, _ := c.RGBA() + return (red>>8)<<16 | (green>>8)<<8 | blue>>8 +} + +func uint32ToColor(u C.uint32_t) color.Color { + return color.RGBA{ + uint8(u>>16) & 255, + uint8(u>>8) & 255, + uint8(u>>0) & 255, + 0, + } +} diff --git a/go-rpi-rgb-led-matrix/rpc/client.go b/go-rpi-rgb-led-matrix/rpc/client.go new file mode 100644 index 0000000..0016762 --- /dev/null +++ b/go-rpi-rgb-led-matrix/rpc/client.go @@ -0,0 +1,79 @@ +package rpc + +import ( + "encoding/gob" + "image/color" + "net/rpc" + + "github.com/RockKeeper/go-rpi-rgb-led-matrix" +) + +func init() { + gob.Register(color.RGBA{}) +} + +// RGBLedMatrix matrix representation for ws281x +type Client struct { + network string + addr string + client *rpc.Client + leds []color.Color +} + +// NewRGBLedMatrix returns a new matrix using the given size and config +func NewClient(network, addr string) (rgbmatrix.Matrix, error) { + client, err := rpc.DialHTTP(network, addr) + if err != nil { + return nil, err + } + + return &Client{ + network: network, + addr: addr, + client: client, + leds: make([]color.Color, 2048), + }, nil +} + +// Geometry returns the width and the height of the matrix +func (c *Client) Geometry() (width, height int) { + var reply *GeometryReply + err := c.client.Call("RPCMatrix.Geometry", &GeometryArgs{}, &reply) + if err != nil { + panic(err) + } + + return reply.Width, reply.Height +} + +func (c *Client) Apply(leds []color.Color) error { + defer func() { c.leds = make([]color.Color, 2048) }() + + var reply *ApplyReply + return c.client.Call("RPCMatrix.Apply", &ApplyArgs{Colors: leds}, &reply) +} + +// Render update the display with the data from the LED buffer +func (c *Client) Render() error { + return c.Apply(c.leds) +} + +// At return an Color which allows access to the LED display data as +// if it were a sequence of 24-bit RGB values. +func (c *Client) At(position int) color.Color { + if c.leds[position] == nil { + return color.Black + } + + return c.leds[position] +} + +// Set set LED at position x,y to the provided 24-bit color value. +func (m *Client) Set(position int, c color.Color) { + m.leds[position] = color.RGBAModel.Convert(c) +} + +// Close finalizes the ws281x interface +func (c *Client) Close() error { + return c.Apply(make([]color.Color, 2048)) +} diff --git a/go-rpi-rgb-led-matrix/rpc/server.go b/go-rpi-rgb-led-matrix/rpc/server.go new file mode 100644 index 0000000..b98b190 --- /dev/null +++ b/go-rpi-rgb-led-matrix/rpc/server.go @@ -0,0 +1,54 @@ +package rpc + +import ( + "fmt" + "image/color" + "log" + "net" + "net/http" + "net/rpc" + + "github.com/RockKeeper/go-rpi-rgb-led-matrix" +) + +type RPCMatrix struct { + m rgbmatrix.Matrix +} + +type GeometryArgs struct{} +type GeometryReply struct{ Width, Height int } + +func (m *RPCMatrix) Geometry(_ *GeometryArgs, reply *GeometryReply) error { + w, h := m.m.Geometry() + reply.Width = w + reply.Height = h + + return nil +} + +type ApplyArgs struct{ Colors []color.Color } +type ApplyReply struct{} + +func (m *RPCMatrix) Apply(args *ApplyArgs, reply *ApplyReply) error { + return m.m.Apply(args.Colors) +} + +type CloseArgs struct{} +type CloseReply struct{} + +func (m *RPCMatrix) Close(_ *CloseArgs, _ *CloseReply) error { + return m.m.Close() +} + +func Serve(m rgbmatrix.Matrix) { + rpc.Register(&RPCMatrix{m}) + + rpc.HandleHTTP() + l, e := net.Listen("tcp", ":1234") + if e != nil { + log.Fatal("listen error:", e) + } + + fmt.Println(l) + http.Serve(l, nil) +} diff --git a/go-rpi-rgb-led-matrix/toolkit.go b/go-rpi-rgb-led-matrix/toolkit.go new file mode 100644 index 0000000..f5500d7 --- /dev/null +++ b/go-rpi-rgb-led-matrix/toolkit.go @@ -0,0 +1,142 @@ +package rgbmatrix + +import ( + "image" + "image/draw" + "image/gif" + "io" + "time" +) + +// ToolKit is a convinient set of function to operate with a led of Matrix +type ToolKit struct { + // Canvas is the Canvas wrapping the Matrix, if you want to instanciate + // a ToolKit with a custom Canvas you can use directly the struct, + // without calling NewToolKit + Canvas *Canvas + + // Transform function if present is applied just before draw the image to + // the Matrix, this is a small example: + // tk.Transform = func(img image.Image) *image.NRGBA { + // return imaging.Fill(img, 64, 96, imaging.Center, imaging.Lanczos) + // } + Transform func(img image.Image) *image.NRGBA +} + +// NewToolKit returns a new ToolKit wrapping the given Matrix +func NewToolKit(m Matrix) *ToolKit { + return &ToolKit{ + Canvas: NewCanvas(m), + } +} + +// PlayImage draws the given image during the given delay +func (tk *ToolKit) PlayImage(i image.Image, delay time.Duration) error { + start := time.Now() + defer func() { time.Sleep(delay - time.Since(start)) }() + + if tk.Transform != nil { + i = tk.Transform(i) + } + + draw.Draw(tk.Canvas, tk.Canvas.Bounds(), i, image.ZP, draw.Over) + return tk.Canvas.Render() +} + +type Animation interface { + Next() (image.Image, <-chan time.Time, error) +} + +// PlayAnimation play the image during the delay returned by Next, until an err +// is returned, if io.EOF is returned, PlayAnimation finish without an error +func (tk *ToolKit) PlayAnimation(a Animation) error { + var err error + var i image.Image + var n <-chan time.Time + + for { + i, n, err = a.Next() + if err != nil { + break + } + + if err := tk.PlayImageUntil(i, n); err != nil { + return err + } + } + + if err == io.EOF { + return nil + } + + return err +} + +// PlayImageUntil draws the given image until is notified to stop +func (tk *ToolKit) PlayImageUntil(i image.Image, notify <-chan time.Time) error { + defer func() { + <-notify + }() + + if tk.Transform != nil { + i = tk.Transform(i) + } + + draw.Draw(tk.Canvas, tk.Canvas.Bounds(), i, image.ZP, draw.Over) + return tk.Canvas.Render() +} + +// PlayImages draws a sequence of images during the given delays, the len of +// images should be equal to the len of delay. If loop is true the function +// loops over images until a true is sent to the returned chan +func (tk *ToolKit) PlayImages(images []image.Image, delay []time.Duration, loop int) chan bool { + quit := make(chan bool, 0) + + go func() { + l := len(images) + i := 0 + for { + select { + case <-quit: + return + default: + tk.PlayImage(images[i], delay[i]) + } + + i++ + if i >= l { + if loop == 0 { + i = 0 + continue + } + + break + } + } + }() + + return quit +} + +// PlayGIF reads and draw a gif file from r. It use the contained images and +// delays and loops over it, until a true is sent to the returned chan +func (tk *ToolKit) PlayGIF(r io.Reader) (chan bool, error) { + gif, err := gif.DecodeAll(r) + if err != nil { + return nil, err + } + + delay := make([]time.Duration, len(gif.Delay)) + images := make([]image.Image, len(gif.Image)) + for i, image := range gif.Image { + images[i] = image + delay[i] = time.Millisecond * time.Duration(gif.Delay[i]) * 10 + } + + return tk.PlayImages(images, delay, gif.LoopCount), nil +} + +// Close close the toolkit and the inner canvas +func (tk *ToolKit) Close() error { + return tk.Canvas.Close() +} diff --git a/mempool/mempool.go b/mempool/mempool.go new file mode 100644 index 0000000..d2428b0 --- /dev/null +++ b/mempool/mempool.go @@ -0,0 +1,81 @@ +package mempool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + + "github.com/gorilla/websocket" +) + +var ( + ErrNotWebsocket = errors.New("provided addr was not websocket") +) + +type mempool struct { + addr string + ws *websocket.Conn +} + +func New(addr string) *mempool { + return &mempool{ + addr: addr, + } +} + +func (m *mempool) Init() error { + u, err := url.Parse(m.addr) + if err != nil { + return err + } + + if u.Scheme != "wss" && u.Scheme != "ws" { + return fmt.Errorf("%w: got %s", ErrNotWebsocket, u.Scheme) + } + + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + return err + } + + m.ws = c + return nil +} + +func (m *mempool) Listen(ctx context.Context, blocks chan Block, errs chan error, done chan struct{}) { + for { + select { + case <-ctx.Done(): + close(blocks) + close(errs) + close(done) + return + default: + _, message, err := m.ws.ReadMessage() + if err != nil { + errs <- err + continue + } + + var b Block + err = json.Unmarshal(message, &b) + if err != nil { + errs <- err + continue + } + + blocks <- b + } + } +} + +func (m *mempool) Shutdown(ctx context.Context) error { + err := m.ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + return err + } + m.ws.Close() + return nil +} diff --git a/mempool/models.go b/mempool/models.go new file mode 100644 index 0000000..3582b37 --- /dev/null +++ b/mempool/models.go @@ -0,0 +1,5 @@ +package mempool + +type Block struct { + Height int64 `json:"height"` +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d125db1 --- /dev/null +++ b/shell.nix @@ -0,0 +1,7 @@ +{ pkgs ? import { } }: +with pkgs; +mkShell { + nativeBuildInputs = [ + go + ]; +}