1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
|
// Command epdctl is a small CLI for the epd7in3e panel. Every
// command runs Init, draws, then Sleep. With -dry-run out.png a
// command instead writes the quantised 800x480 frame to out.png, so
// the pipeline can be checked without hardware.
package main
import (
"context"
"errors"
"flag"
"fmt"
"image"
"image/png"
"math"
"os"
_ "image/jpeg" // register JPEG with image.Decode
_ "image/png" // register PNG with image.Decode
"go-epd7in3e"
"golang.org/x/image/draw"
)
// panelColours lists the six panel colours with their Palette index.
var panelColours = []struct {
name string
col epd7in3e.Color
index uint8
}{
{"black", epd7in3e.Black, 0},
{"white", epd7in3e.White, 1},
{"yellow", epd7in3e.Yellow, 2},
{"red", epd7in3e.Red, 3},
{"blue", epd7in3e.Blue, 4},
{"green", epd7in3e.Green, 5},
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "epdctl: %v\n", err)
os.Exit(1)
}
}
func run(args []string) error {
if len(args) == 0 {
usage()
return errors.New("no command given")
}
cmd, rest := args[0], args[1:]
switch cmd {
case "clear":
return doClear(rest)
case "bars":
return doBars(rest)
case "show":
return doShow(rest)
case "help", "-h", "--help":
usage()
return nil
}
return fmt.Errorf("unknown command %q (want clear, bars, show or help)", cmd)
}
func usage() {
fmt.Fprint(os.Stderr, `epdctl drives the Waveshare epd7in3e e-paper panel.
Usage:
epdctl clear [black|white|yellow|red|blue|green] fill the panel (default white)
epdctl bars six vertical colour bars
epdctl show <file.png|file.jpg> letterbox, dither and display
Each command initialises the panel, draws, then puts it to sleep.
Add -dry-run out.png to write the quantised 800x480 frame to out.png
instead of touching the hardware. Add -v to log every command and
busy-wait duration to stderr (bring-up diagnostics, HARDWARE_CHECK.md).
`)
}
// dryRunFlag registers the -dry-run flag: when it names a file, the
// quantised frame is written there and the panel is never opened.
func dryRunFlag(fs *flag.FlagSet) *string {
return fs.String("dry-run", "", "write the quantised frame to this PNG file instead of touching the panel")
}
func verboseFlag(fs *flag.FlagSet) *bool {
return fs.Bool("v", false, "log every command and busy-wait duration to stderr")
}
// onPanel opens the HAT and runs the fixed lifecycle: Init, op,
// Sleep, then power off. Dither: true is set explicitly so the
// zero-value Opts field cannot turn it off.
func onPanel(v bool, op func(d *epd7in3e.Dev, ctx context.Context) error) error {
d, err := epd7in3e.NewHAT(&epd7in3e.Opts{Dither: true, Verbose: v})
if err != nil {
return err
}
ctx := context.Background()
if err := d.Init(ctx); err != nil {
return err
}
if err := op(d, ctx); err != nil {
return err
}
if err := d.Sleep(ctx); err != nil {
return err
}
return d.Halt()
}
// writeFrame saves the quantised frame as a PNG.
func writeFrame(path string, frame *image.Paletted) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("create %s: %w", path, err)
}
if err := png.Encode(f, frame); err != nil {
f.Close()
return fmt.Errorf("encode %s: %w", path, err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("close %s: %w", path, err)
}
b := frame.Bounds()
fmt.Printf("wrote %dx%d preview to %s\n", b.Dx(), b.Dy(), path)
return nil
}
func doClear(args []string) error {
fs := flag.NewFlagSet("clear", flag.ExitOnError)
out := dryRunFlag(fs)
v := verboseFlag(fs)
fs.Parse(args)
name := "white"
if fs.NArg() == 1 {
name = fs.Arg(0)
}
var c epd7in3e.Color
found := false
for _, pc := range panelColours {
if pc.name == name {
c, found = pc.col, true
break
}
}
if !found {
return fmt.Errorf("unknown colour %q (want black, white, yellow, red, blue or green)", name)
}
if *out != "" {
return writeFrame(*out, solidFrame(c))
}
return onPanel(*v, func(d *epd7in3e.Dev, ctx context.Context) error {
return d.Clear(ctx, c)
})
}
func doBars(args []string) error {
fs := flag.NewFlagSet("bars", flag.ExitOnError)
out := dryRunFlag(fs)
v := verboseFlag(fs)
fs.Parse(args)
if fs.NArg() != 0 {
return errors.New("bars takes no arguments")
}
frame := barsFrame()
if *out != "" {
return writeFrame(*out, frame)
}
return onPanel(*v, func(d *epd7in3e.Dev, ctx context.Context) error {
return d.Display(ctx, frame)
})
}
// barsFrame builds six vertical bars, one per panel colour.
func barsFrame() *image.Paletted {
img := image.NewPaletted(image.Rect(0, 0, epd7in3e.Width, epd7in3e.Height), epd7in3e.Palette)
for i, pc := range panelColours {
x0, x1 := i*epd7in3e.Width/6, (i+1)*epd7in3e.Width/6
for x := x0; x < x1; x++ {
for y := 0; y < epd7in3e.Height; y++ {
img.Pix[y*img.Stride+x] = pc.index
}
}
}
return img
}
func doShow(args []string) error {
fs := flag.NewFlagSet("show", flag.ExitOnError)
out := dryRunFlag(fs)
v := verboseFlag(fs)
fs.Parse(args)
if fs.NArg() != 1 {
return errors.New("show needs one image file (png or jpg)")
}
f, err := os.Open(fs.Arg(0))
if err != nil {
return fmt.Errorf("open image: %w", err)
}
defer f.Close()
src, format, err := image.Decode(f)
if err != nil {
return fmt.Errorf("decode %s: %w", fs.Arg(0), err)
}
if format != "png" && format != "jpeg" {
return fmt.Errorf("%s: %s image, want png or jpg", fs.Arg(0), format)
}
frame := letterboxDithered(src)
if *out != "" {
return writeFrame(*out, frame)
}
return onPanel(*v, func(d *epd7in3e.Dev, ctx context.Context) error {
return d.Display(ctx, frame)
})
}
// letterboxDithered scales src to fit within 800x480 preserving its
// aspect ratio, centers it on a white field, then dithers the result
// to the panel's palette.
func letterboxDithered(src image.Image) *image.Paletted {
b := src.Bounds()
w, h := b.Dx(), b.Dy()
if w <= 0 || h <= 0 {
w, h = 1, 1
}
scale := math.Min(float64(epd7in3e.Width)/float64(w), float64(epd7in3e.Height)/float64(h))
sw := int(math.Round(float64(w) * scale))
sh := int(math.Round(float64(h) * scale))
if sw < 1 {
sw = 1
}
if sh < 1 {
sh = 1
}
scaled := scaleTo(src, sw, sh)
canvas := image.NewNRGBA(image.Rect(0, 0, epd7in3e.Width, epd7in3e.Height))
for i := range canvas.Pix {
canvas.Pix[i] = 255 // white letterbox
}
// Center the scaled image. The offset goes on the destination
// rectangle: draw.Draw's source point is a point in the source's
// coordinate system that is translated to the destination's
// top-left, so passing the offset there would crop the source
// instead of shifting it.
ox := (epd7in3e.Width - sw) / 2
oy := (epd7in3e.Height - sh) / 2
draw.Draw(canvas, image.Rect(ox, oy, ox+sw, oy+sh), scaled, scaled.Bounds().Min, draw.Over)
return epd7in3e.Quantise(canvas, true)
}
// scaleTo resizes src to exactly sw x sh with Catmull-Rom filtering.
func scaleTo(src image.Image, sw, sh int) *image.NRGBA {
b := src.Bounds()
if n, ok := src.(*image.NRGBA); ok && n.Rect.Dx() == b.Dx() && n.Rect.Dy() == b.Dy() {
return scaleToNRGBA(n, b, sw, sh)
}
n := image.NewNRGBA(b)
draw.Draw(n, b, src, b.Min, draw.Src)
return scaleToNRGBA(n, b, sw, sh)
}
func scaleToNRGBA(src *image.NRGBA, sr image.Rectangle, sw, sh int) *image.NRGBA {
dst := image.NewNRGBA(image.Rect(0, 0, sw, sh))
draw.CatmullRom.Scale(dst, dst.Bounds(), src, sr, draw.Src, nil)
return dst
}
// solidFrame builds an 800x480 frame in one colour, for the
// dry-run preview.
func solidFrame(c epd7in3e.Color) *image.Paletted {
img := image.NewPaletted(image.Rect(0, 0, epd7in3e.Width, epd7in3e.Height), epd7in3e.Palette)
var idx uint8
for _, pc := range panelColours {
if pc.col == c {
idx = pc.index
break
}
}
for i := range img.Pix {
img.Pix[i] = idx
}
return img
}
|