mirror of
https://github.com/Crocmagnon/display-epaper.git
synced 2024-11-21 13:38:03 +01:00
initial commit
This commit is contained in:
commit
019bc682e9
4 changed files with 503 additions and 0 deletions
310
epd/epd.go
Normal file
310
epd/epd.go
Normal file
|
@ -0,0 +1,310 @@
|
|||
package epd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"periph.io/x/conn/v3/gpio"
|
||||
"periph.io/x/conn/v3/physic"
|
||||
"periph.io/x/conn/v3/spi"
|
||||
"periph.io/x/conn/v3/spi/spireg"
|
||||
"periph.io/x/host/v3/bcm283x"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
Width = 800
|
||||
Height = 480
|
||||
)
|
||||
|
||||
type EPD struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
resetPin gpio.PinOut
|
||||
dcPin gpio.PinOut
|
||||
busyPin gpio.PinIn
|
||||
csPin gpio.PinOut
|
||||
pwrPin gpio.PinOut
|
||||
|
||||
partFlag byte
|
||||
|
||||
spi spi.Conn
|
||||
spiReg spi.PortCloser
|
||||
}
|
||||
|
||||
func New() (*EPD, error) {
|
||||
epd := &EPD{
|
||||
width: Width,
|
||||
height: Height,
|
||||
resetPin: bcm283x.GPIO17,
|
||||
dcPin: bcm283x.GPIO25,
|
||||
busyPin: bcm283x.GPIO24,
|
||||
csPin: bcm283x.GPIO8,
|
||||
pwrPin: bcm283x.GPIO18,
|
||||
partFlag: 1,
|
||||
}
|
||||
|
||||
if err := epd.resetPin.Out(gpio.Low); err != nil {
|
||||
return nil, fmt.Errorf("setting reset pin to low: %w", err)
|
||||
}
|
||||
if err := epd.dcPin.Out(gpio.Low); err != nil {
|
||||
return nil, fmt.Errorf("setting dc pin to low: %w", err)
|
||||
}
|
||||
if err := epd.csPin.Out(gpio.Low); err != nil {
|
||||
return nil, fmt.Errorf("setting cs pin to low: %w", err)
|
||||
}
|
||||
if err := epd.pwrPin.Out(gpio.High); err != nil {
|
||||
return nil, fmt.Errorf("setting pwr pin to low: %w", err)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if epd.spiReg, err = spireg.Open("0"); err != nil {
|
||||
return nil, fmt.Errorf("opening SPI: %w", err)
|
||||
}
|
||||
|
||||
c, err := epd.spiReg.Connect(4*physic.MegaHertz, spi.Mode0, 8)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connecting to SPI: %w", err)
|
||||
}
|
||||
|
||||
epd.spi = c
|
||||
|
||||
return epd, nil
|
||||
}
|
||||
|
||||
func (e *EPD) TurnOff() error {
|
||||
log.Println("turning off...")
|
||||
if err := e.spiReg.Close(); err != nil {
|
||||
return fmt.Errorf("closing SPI: %w", err)
|
||||
}
|
||||
|
||||
e.resetPin.Out(gpio.Low)
|
||||
e.dcPin.Out(gpio.Low)
|
||||
e.pwrPin.Out(gpio.Low)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EPD) reset() {
|
||||
e.resetPin.Out(gpio.High)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
e.resetPin.Out(gpio.Low)
|
||||
time.Sleep(4 * time.Millisecond)
|
||||
e.resetPin.Out(gpio.High)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (e *EPD) sendCommand(cmd byte) {
|
||||
log.Printf("sending command 0x%02X\n", cmd)
|
||||
e.dcPin.Out(gpio.Low)
|
||||
e.csPin.Out(gpio.Low)
|
||||
if _, err := e.spiWrite([]byte{cmd}); err != nil {
|
||||
log.Fatalf("writing to spi: %v", err)
|
||||
}
|
||||
e.csPin.Out(gpio.High)
|
||||
}
|
||||
|
||||
func (e *EPD) sendData(data byte) {
|
||||
e.dcPin.Out(gpio.High)
|
||||
e.csPin.Out(gpio.Low)
|
||||
if _, err := e.spiWrite([]byte{data}); err != nil {
|
||||
log.Fatalf("writing to spi: %v", err)
|
||||
}
|
||||
e.csPin.Out(gpio.High)
|
||||
}
|
||||
|
||||
func (e *EPD) sendDataSlice(data []byte) {
|
||||
log.Printf("sending data slice %v\n", len(data))
|
||||
e.dcPin.Out(gpio.High)
|
||||
toSend := len(data)
|
||||
const maxSize = 4096
|
||||
if toSend <= maxSize {
|
||||
e.csPin.Out(gpio.Low)
|
||||
if _, err := e.spiWrite(data); err != nil {
|
||||
log.Fatalf("writing to spi: %v", err)
|
||||
}
|
||||
e.csPin.Out(gpio.High)
|
||||
return
|
||||
}
|
||||
|
||||
cursor := 0
|
||||
for cursor < toSend {
|
||||
chunk := data[cursor:min(cursor+maxSize, toSend)]
|
||||
e.csPin.Out(gpio.Low)
|
||||
if _, err := e.spiWrite(chunk); err != nil {
|
||||
log.Fatalf("writing to spi: %v", err)
|
||||
}
|
||||
e.csPin.Out(gpio.High)
|
||||
log.Printf("sent chunk %v\n", cursor)
|
||||
cursor = min(cursor+maxSize, toSend)
|
||||
}
|
||||
log.Printf("sent chunk %v\n", cursor)
|
||||
}
|
||||
|
||||
func (e *EPD) spiWrite(write []byte) ([]byte, error) {
|
||||
read := make([]byte, len(write))
|
||||
|
||||
if err := e.spi.Tx(write, read); err != nil {
|
||||
return nil, fmt.Errorf("tx: %w", err)
|
||||
}
|
||||
|
||||
return read, nil
|
||||
}
|
||||
|
||||
func (e *EPD) readBusy() {
|
||||
e.sendCommand(0x71)
|
||||
busy := e.busyPin.Read()
|
||||
for busy == gpio.Low {
|
||||
e.sendCommand(0x71)
|
||||
busy = e.busyPin.Read()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (e *EPD) Init() {
|
||||
log.Println("initializing EPD")
|
||||
e.reset()
|
||||
|
||||
e.sendCommand(0x01)
|
||||
e.sendData(0x07)
|
||||
e.sendData(0x07)
|
||||
e.sendData(0x3f)
|
||||
e.sendData(0x3f)
|
||||
|
||||
e.sendCommand(0x06)
|
||||
e.sendData(0x17)
|
||||
e.sendData(0x17)
|
||||
e.sendData(0x28)
|
||||
e.sendData(0x17)
|
||||
|
||||
e.sendCommand(0x04)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
e.readBusy()
|
||||
|
||||
e.sendCommand(0x00)
|
||||
e.sendData(0x0f)
|
||||
|
||||
e.sendCommand(0x61)
|
||||
e.sendData(0x03)
|
||||
e.sendData(0x20)
|
||||
e.sendData(0x01)
|
||||
e.sendData(0xe0)
|
||||
|
||||
e.sendCommand(0x15)
|
||||
e.sendData(0x00)
|
||||
|
||||
e.sendCommand(0x50)
|
||||
e.sendData(0x11)
|
||||
e.sendData(0x07)
|
||||
|
||||
e.sendCommand(0x60)
|
||||
e.sendData(0x22)
|
||||
}
|
||||
|
||||
func (e *EPD) Clear() {
|
||||
log.Println("clearing epd")
|
||||
redBuf := make([]byte, Width*Height/8)
|
||||
for i := range redBuf {
|
||||
redBuf[i] = 0x00
|
||||
}
|
||||
|
||||
blackBuf := make([]byte, Width*Height/8)
|
||||
for i := range blackBuf {
|
||||
blackBuf[i] = 0xff
|
||||
}
|
||||
|
||||
e.sendCommand(0x10)
|
||||
e.sendDataSlice(blackBuf)
|
||||
|
||||
e.sendCommand(0x13)
|
||||
e.sendDataSlice(redBuf)
|
||||
|
||||
//e.refresh()
|
||||
}
|
||||
|
||||
func (e *EPD) refresh() {
|
||||
log.Println("refreshing...")
|
||||
e.sendCommand(0x12)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
e.readBusy()
|
||||
}
|
||||
|
||||
func (e *EPD) Sleep() error {
|
||||
log.Println("sleeping...")
|
||||
e.sendCommand(0x02)
|
||||
e.readBusy()
|
||||
|
||||
e.sendCommand(0x07)
|
||||
e.sendData(0xa5)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
if err := e.TurnOff(); err != nil {
|
||||
return fmt.Errorf("turning off: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Color int
|
||||
|
||||
const (
|
||||
White Color = iota
|
||||
Red
|
||||
Black
|
||||
)
|
||||
|
||||
func (e *EPD) Fill(c Color) {
|
||||
log.Println("filling... (not doing anything yet)")
|
||||
|
||||
//switch c {
|
||||
//case White:
|
||||
// e.Draw(nil, nil)
|
||||
//case Black:
|
||||
// e.Draw(image.Black, nil)
|
||||
//case Red:
|
||||
// e.Draw(nil, image.Black)
|
||||
//}
|
||||
}
|
||||
|
||||
func (e *EPD) Draw(black image.Image, red image.Image) {
|
||||
log.Println("drawing...")
|
||||
if black != nil {
|
||||
log.Println("sending black")
|
||||
e.sendCommand(0x10) // write bw data
|
||||
e.sendImg(black)
|
||||
}
|
||||
if red != nil {
|
||||
log.Println("sending red")
|
||||
e.sendCommand(0x13) // write red data
|
||||
e.sendImg(red)
|
||||
}
|
||||
if black != nil || red != nil {
|
||||
e.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EPD) sendImg(img image.Image) {
|
||||
log.Println("sending img...")
|
||||
// TODO check img size
|
||||
for row := 0; row < e.height; row++ {
|
||||
for col := 0; col < e.width; col += 8 {
|
||||
// this loop converts individual pixels into a single byte
|
||||
// 8-pixels at a time and then sends that byte to render
|
||||
var b byte = 0xFF
|
||||
for px := 0; px < 8; px++ {
|
||||
var pixel = img.At(col+px, row)
|
||||
if isdark(pixel.RGBA()) {
|
||||
b &= ^(0x80 >> (px % 8))
|
||||
}
|
||||
}
|
||||
e.sendData(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isdark(r, g, b, _ uint32) bool {
|
||||
return r < 255 || g < 255 || b < 255
|
||||
}
|
14
go.mod
Normal file
14
go.mod
Normal file
|
@ -0,0 +1,14 @@
|
|||
module github.com/Crocmagnon/display-epaper
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require periph.io/x/host/v3 v3.8.2
|
||||
|
||||
require periph.io/x/conn/v3 v3.7.0
|
||||
|
||||
require (
|
||||
github.com/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195
|
||||
golang.org/x/image v0.20.0
|
||||
)
|
||||
|
||||
require github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
14
go.sum
Normal file
14
go.sum
Normal file
|
@ -0,0 +1,14 @@
|
|||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=
|
||||
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195 h1:Vdz2cBh5Fw2MYHWi3ED2PraDQaWEUhNCr1XFHrP4N5A=
|
||||
github.com/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195/go.mod h1:1Vk0LDW6jG5cGc2D9RQUxHaE0vYhTvIwSo9mOL6K4/U=
|
||||
github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e h1:ZAvbj5hI/G/EbAYAcj4yCXUNiFKefEhH0qfImDDD0/8=
|
||||
github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY=
|
||||
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
|
||||
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
|
||||
periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA=
|
||||
periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg=
|
||||
periph.io/x/host/v3 v3.8.2 h1:ayKUDzgUCN0g8+/xM9GTkWaOBhSLVcVHGTfjAOi8OsQ=
|
||||
periph.io/x/host/v3 v3.8.2/go.mod h1:yFL76AesNHR68PboofSWYaQTKmvPXsQH2Apvp/ls/K4=
|
165
main.go
Normal file
165
main.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Crocmagnon/display-epaper/epd"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dimg"
|
||||
_ "golang.org/x/image/bmp"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"os"
|
||||
"periph.io/x/host/v3"
|
||||
)
|
||||
|
||||
const fontName = "default"
|
||||
|
||||
func main() {
|
||||
log.Println("starting...")
|
||||
|
||||
font, err := truetype.Parse(goregular.TTF)
|
||||
if err != nil {
|
||||
log.Fatalf("loading font: %v\n", err)
|
||||
}
|
||||
fontCache := MyFontCache{}
|
||||
fontCache.Store(draw2d.FontData{Name: fontName}, font)
|
||||
draw2d.SetFontCache(fontCache)
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
if err := run(); err != nil {
|
||||
log.Fatal("error: ", err)
|
||||
}
|
||||
} else {
|
||||
img, err := getBlack()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
draw2dimg.SaveToPngFile("black.png", img)
|
||||
|
||||
img, err = getRed()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
draw2dimg.SaveToPngFile("red.png", img)
|
||||
|
||||
}
|
||||
|
||||
log.Println("done")
|
||||
}
|
||||
|
||||
func run() error {
|
||||
_, err := host.Init()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing host: %w", err)
|
||||
}
|
||||
|
||||
display, err := epd.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing epd: %w", err)
|
||||
}
|
||||
|
||||
display.Init()
|
||||
display.Clear()
|
||||
|
||||
black, err := getBlack()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting black: %w", err)
|
||||
}
|
||||
|
||||
red, err := getRed()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting red: %w", err)
|
||||
}
|
||||
|
||||
display.Draw(black, red)
|
||||
|
||||
log.Println("sleeping...")
|
||||
|
||||
if err := display.Sleep(); err != nil {
|
||||
return fmt.Errorf("sleeping: %w", err)
|
||||
}
|
||||
|
||||
log.Println("done")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBlack() (*image.RGBA, error) {
|
||||
img := newWhite()
|
||||
|
||||
gc := draw2dimg.NewGraphicContext(img)
|
||||
|
||||
gc.SetFillColor(color.RGBA{0, 0, 0, 255})
|
||||
gc.SetStrokeColor(color.RGBA{0, 0, 0, 255})
|
||||
gc.SetLineWidth(2)
|
||||
|
||||
text(gc, "Hello, world", 18, 110, 50)
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func getRed() (*image.RGBA, error) {
|
||||
img := newBlack()
|
||||
gc := draw2dimg.NewGraphicContext(img)
|
||||
gc.SetFillColor(color.RGBA{0, 0, 0, 255})
|
||||
gc.SetStrokeColor(color.RGBA{255, 255, 255, 255})
|
||||
gc.SetLineWidth(2)
|
||||
|
||||
rect(gc, 10, 10, 50, 50)
|
||||
rect(gc, 60, 10, 100, 50)
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func text(gc *draw2dimg.GraphicContext, s string, size, x, y float64) {
|
||||
gc.SetFontData(draw2d.FontData{Name: fontName})
|
||||
gc.SetFontSize(size)
|
||||
gc.StrokeStringAt(s, x, y)
|
||||
}
|
||||
|
||||
func newWhite() *image.RGBA {
|
||||
img := image.NewRGBA(image.Rect(0, 0, epd.Width, epd.Height))
|
||||
for y := 0; y < epd.Height; y++ {
|
||||
for x := 0; x < epd.Width; x++ {
|
||||
img.Set(x, y, color.White)
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func newBlack() *image.RGBA {
|
||||
img := image.NewRGBA(image.Rect(0, 0, epd.Width, epd.Height))
|
||||
for y := 0; y < epd.Height; y++ {
|
||||
for x := 0; x < epd.Width; x++ {
|
||||
img.Set(x, y, color.Black)
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func rect(gc *draw2dimg.GraphicContext, x1, y1, x2, y2 float64) {
|
||||
gc.BeginPath()
|
||||
gc.MoveTo(x1, y1)
|
||||
gc.LineTo(x2, y1)
|
||||
gc.LineTo(x2, y2)
|
||||
gc.LineTo(x1, y2)
|
||||
gc.Close()
|
||||
gc.FillStroke()
|
||||
}
|
||||
|
||||
type MyFontCache map[string]*truetype.Font
|
||||
|
||||
func (fc MyFontCache) Store(fd draw2d.FontData, font *truetype.Font) {
|
||||
fc[fd.Name] = font
|
||||
}
|
||||
|
||||
func (fc MyFontCache) Load(fd draw2d.FontData) (*truetype.Font, error) {
|
||||
font, stored := fc[fd.Name]
|
||||
if !stored {
|
||||
return nil, fmt.Errorf("font %s is not stored in font cache", fd.Name)
|
||||
}
|
||||
return font, nil
|
||||
}
|
Loading…
Reference in a new issue