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 [](https://godoc.org/github.com/mcuadros/go-rpi-rgb-led-matrix) [](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
+ ];
+}