adds display package

This commit is contained in:
2023-12-21 10:06:48 -05:00
parent 31895881a9
commit 5343f26ad9
28 changed files with 2412 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
+100
View File
@@ -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 {}
}
+71
View File
@@ -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()
}
+88
View File
@@ -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)
}
}
+139
View File
@@ -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},
},
}
)
+178
View File
@@ -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},
},
}
)
+216
View File
@@ -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},
},
}
)
+61
View File
@@ -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,
}
)
+49
View File
@@ -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)
})
}
}
+3
View File
@@ -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
+14
View File
@@ -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 ./...
+21
View File
@@ -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.
+97
View File
@@ -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)
<img width="250" src="https://cloud.githubusercontent.com/assets/1573114/20248154/c17c1f2e-a9dd-11e6-805b-bf7d8ee73121.gif" align="right" />
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.
<img src="https://cloud.githubusercontent.com/assets/1573114/20248173/2e2f97ae-a9de-11e6-95e6-e0548199501d.gif" align="right" width="100" />
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)
+77
View File
@@ -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
}
+139
View File
@@ -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
}
+204
View File
@@ -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
}
@@ -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
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
+274
View File
@@ -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 <led-matrix-c.h>
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,
}
}
+79
View File
@@ -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))
}
+54
View File
@@ -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)
}
+142
View File
@@ -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()
}
+81
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
package mempool
type Block struct {
Height int64 `json:"height"`
}
+7
View File
@@ -0,0 +1,7 @@
{ pkgs ? import <nixpkgs> { } }:
with pkgs;
mkShell {
nativeBuildInputs = [
go
];
}