commit 019bc682e9c028052d3e3b6e7240702a42e412f1 Author: Gabriel Augendre Date: Sat Sep 14 21:34:02 2024 +0200 initial commit diff --git a/epd/epd.go b/epd/epd.go new file mode 100644 index 0000000..25a28c4 --- /dev/null +++ b/epd/epd.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0668192 --- /dev/null +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cd20eae --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..fd9fe88 --- /dev/null +++ b/main.go @@ -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 +}