From 9698a95f169fa837061e46f65460cfe7ef27f5ad Mon Sep 17 00:00:00 2001 From: ptrcnull Date: Sun, 2 Jan 2022 05:28:31 +0100 Subject: [PATCH] feat: Initial commit --- .gitignore | 1 + draw.go | 23 ++++++ framebuffer/fb.go | 201 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 10 +++ go.sum | 10 +++ main.go | 130 ++++++++++++++++++++++++++++++ 6 files changed, 375 insertions(+) create mode 100644 .gitignore create mode 100644 draw.go create mode 100644 framebuffer/fb.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdcab7a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +watchface diff --git a/draw.go b/draw.go new file mode 100644 index 0000000..1512d9e --- /dev/null +++ b/draw.go @@ -0,0 +1,23 @@ +package main + +import ( + "image" + "image/color" + "image/draw" +) + +func Fill(img draw.Image, rect image.Rectangle, c color.Color) { + for x := rect.Min.X; x < rect.Max.X; x++ { + for y := rect.Min.Y; y < rect.Max.Y; y++ { + img.Set(x, y, c) + } + } +} + +func Copy(dst, src draw.Image, rect image.Rectangle) { + for x := rect.Min.X; x < rect.Max.X; x++ { + for y := rect.Min.Y; y < rect.Max.Y; y++ { + dst.Set(x, y, src.At(x, y)) + } + } +} diff --git a/framebuffer/fb.go b/framebuffer/fb.go new file mode 100644 index 0000000..bca4953 --- /dev/null +++ b/framebuffer/fb.go @@ -0,0 +1,201 @@ +package framebuffer + +import ( + "golang.org/x/sys/unix" + "image" + "image/color" + "image/draw" + "os" + "syscall" + "unsafe" +) + +const ( + FBIOGET_VSCREENINFO = 0x4600 + FBIOGET_FSCREENINFO = 0x4602 + FBIOBLANK = 0x4611 +) + +var _ draw.Image = (*SimpleRGBA)(nil) + +// Open expects a framebuffer device as its argument (such as "/dev/fb0"). The +// device will be memory-mapped to a local buffer. Writing to the device changes +// the screen output. +// The returned Device implements the draw.Image interface. This means that you +// can use it to copy to and from other images. +// After you are done using the Device, call Close on it to unmap the memory and +// close the framebuffer file. +func Open(device string) (*Device, error) { + file, err := os.OpenFile(device, os.O_RDWR, os.ModeDevice) + if err != nil { + return nil, err + } + + _ = unix.IoctlSetInt(int(file.Fd()), FBIOBLANK, 0) + _ = unix.IoctlSetInt(int(file.Fd()), FBIOBLANK, 4) + _ = unix.IoctlSetInt(int(file.Fd()), FBIOBLANK, 0) + + fixInfo, _ := getFixScreenInfo(file.Fd()) + varInfo, _ := getVarScreenInfo(file.Fd()) + + pixels, err := syscall.Mmap( + int(file.Fd()), + 0, + int(fixInfo.smemLen), + //int(varInfo.Xres*varInfo.Yres*varInfo.bitsPerPixel/8), + syscall.PROT_READ|syscall.PROT_WRITE, + syscall.MAP_SHARED, + ) + if err != nil { + file.Close() + return nil, err + } + + return &Device{ + file: file, + SimpleRGBA: &SimpleRGBA{ + Pixels: pixels, + Stride: int(fixInfo.lineLength), + Xres: int(varInfo.xres), + Yres: int(varInfo.yres), + }, + }, nil +} + +// Device represents the frame buffer. It implements the draw.Image interface. +type Device struct { + *SimpleRGBA + file *os.File +} + +type SimpleRGBA struct { + Pixels []byte + Stride int + Xres int + Yres int +} + +func (s *SimpleRGBA) ColorModel() color.Model { + return color.RGBAModel +} + +func (s *SimpleRGBA) Bounds() image.Rectangle { + return image.Rect(0, 0, s.Xres, s.Yres) +} + +func (s *SimpleRGBA) At(x, y int) color.Color { + if x < 0 || x > s.Xres || y < 0 || y > s.Yres { + return color.RGBA{} + } + i := y*s.Stride + x*4 + n := s.Pixels[i : i+4 : i+4] // Small cap improves performance, see https://golang.org/issue/27857 + return color.RGBA{R: n[0], G: n[1], B: n[2], A: n[3]} +} + +func (s *SimpleRGBA) Black(rect image.Rectangle) { + start := rect.Min.Y*s.Stride + rect.Min.X*4 + end := rect.Max.Y*s.Stride + rect.Max.X*4 + 4 + for i := start; i < end; i++ { + s.Pixels[i] = 0 + } +} + +func (s *SimpleRGBA) Set(x, y int, c color.Color) { + r, g, b, a := c.RGBA() + i := y*s.Stride + x*4 + n := s.Pixels[i : i+4 : i+4] // Small cap improves performance, see https://golang.org/issue/27857 + n[0] = byte(r) + n[1] = byte(g) + n[2] = byte(b) + n[3] = byte(a) +} + +// Close unmaps the framebuffer memory and closes the device file. Call this +// function when you are done using the frame buffer. +func (d *Device) Close() { + syscall.Munmap(d.Pixels) + d.file.Close() +} + +func ioctlPtr(fd uintptr, req uint, arg unsafe.Pointer) error { + _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, uintptr(req), uintptr(arg)) + if err != 0 { + return err + } + + return nil +} + +func getFixScreenInfo(fd uintptr) (*fixScreenInfo, error) { + var value fixScreenInfo + err := ioctlPtr(fd, FBIOGET_FSCREENINFO, unsafe.Pointer(&value)) + return &value, err +} + +func getVarScreenInfo(fd uintptr) (*varScreenInfo, error) { + var value varScreenInfo + err := ioctlPtr(fd, FBIOGET_VSCREENINFO, unsafe.Pointer(&value)) + return &value, err +} + +type fixScreenInfo struct { + id [16]byte + smemStart uint32 + smemLen uint32 + fbType uint32 + typeAux uint32 + visual uint32 + xPanStep uint16 + yPanStep uint16 + yWrapStep uint16 + lineLength uint32 + mmioStart uint32 + mmioLen uint32 + accel uint32 + capabilities uint16 + reserved [2]uint16 +} + +type bitField struct { + offset uint32 + length uint32 + msbRight uint32 +} + +type varScreenInfo struct { + xres uint32 + yres uint32 + xresVirtual uint32 + yresVirtual uint32 + xoffset uint32 + yoffset uint32 + + bitsPerPixel uint32 + grayscale uint32 + + red bitField + green bitField + blue bitField + transp bitField + nonstd uint32 + + activate uint32 + + height uint32 + width uint32 + + accelFlags uint32 + + pixclock uint32 + leftMargin uint32 + rightMargin uint32 + upperMargin uint32 + lowerMargin uint32 + hsyncLen uint32 + vsyncLen uint32 + sync uint32 + vmode uint32 + rotate uint32 + colorspace uint32 + reserved [4]uint32 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..214ab6d --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.ddd.rip/ptrcnull/watchface + +go 1.17 + +require ( + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 + github.com/gonutz/framebuffer v1.0.0 + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d224e27 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +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/gonutz/framebuffer v1.0.0 h1:wWFTPqT2+AQ2DllFTOhLWKaxGxUmXmMsMh2wWXgX0LQ= +github.com/gonutz/framebuffer v1.0.0/go.mod h1:wbfYEFSpBxkC4CWzipKZDlKisTkAWors57aJ99aqqhQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ac89b52 --- /dev/null +++ b/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "image" + "image/color" + "io/ioutil" + "strings" + "time" + + "golang.org/x/image/draw" + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" + + "github.com/golang/freetype" + "github.com/golang/freetype/truetype" + + "git.ddd.rip/ptrcnull/watchface/framebuffer" +) + +var Gray = color.RGBA{R: 60, G: 60, B: 60, A: 60} +var LightGray = color.RGBA{R: 150, G: 150, B: 150, A: 150} +var White = color.RGBA{R: 255, G: 255, B: 255, A: 255} + +func addLabel(img draw.Image, face font.Face, rect image.Rectangle, label string) { + point := fixed.Point26_6{X: fixed.Int26_6(rect.Min.X * 64), Y: fixed.Int26_6(rect.Max.Y * 64)} + + d := &font.Drawer{ + Dst: img, + Src: image.NewUniform(LightGray), + Face: face, + Dot: point, + } + d.DrawString(label) +} + +var SecondClock = image.Rect(62, 162, 298, 198) +var MinuteClock = image.Rect(108, 162, 253, 198) +var Battery = image.Rect(90, 240, 150, 240+32) + +var notoSans *truetype.Font + +func sized(size float64) font.Face { + return truetype.NewFace(notoSans, &truetype.Options{ + Size: size, + Hinting: font.HintingFull, + DPI: 0, + }) +} + +func main() { + fontData, _ := ioutil.ReadFile("/usr/share/fonts/noto/NotoSansMono-Regular.ttf") + notoSans, _ = freetype.ParseFont(fontData) + + fb, _ := framebuffer.Open("/dev/fb0") + Fill(fb, fb.Bounds(), color.RGBA{}) + + face := Face{ + //tmp: &framebuffer.SimpleRGBA{ + // Pixels: make([]uint8, fb.Xres*fb.Yres*4), + // Stride: fb.Yres * 4, + // Xres: fb.Xres, + // Yres: fb.Yres, + //}, + tmp: image.NewRGBA(fb.Bounds()), + fb: fb.SimpleRGBA, + } + + simple := true + go face.MinuteClock(time.Now()) + go face.Battery() + + ticker := time.NewTicker(time.Second) + for { + t := time.Now() + if simple { + if t.Second() == 0 { + go face.MinuteClock(t) + go face.Battery() + } + } else { + go face.SecondClock(t) + if t.Second() == 0 { + go face.Battery() + } + } + <-ticker.C + } +} + +type Face struct { + tmp draw.Image + fb draw.Image +} + +func (f *Face) Battery() { + Fill(f.tmp, Battery, color.RGBA{}) + addLabel(f.tmp, sized(32), Battery, getBattery()) + Copy(f.fb, f.tmp, Battery) +} + +func (f *Face) SecondClock(t time.Time) { + Fill(f.tmp, SecondClock, color.RGBA{}) + addLabel(f.tmp, sized(48), SecondClock, t.Format("15:04:05")) + Copy(f.fb, f.tmp, SecondClock) +} + +func (f *Face) MinuteClock(t time.Time) { + Fill(f.tmp, MinuteClock, color.RGBA{}) + addLabel(f.tmp, sized(48), MinuteClock, t.Format("15:04")) + Copy(f.fb, f.tmp, MinuteClock) +} + +//func Loop(duration time.Duration, handler func()) { +// for { +// go handler() +// time.Sleep(duration) +// } +//} + +func getBattery() string { + res, _ := ioutil.ReadFile("/sys/class/power_supply/battery/capacity") + value := strings.Trim(string(res), "\n") + if len(value) == 1 { + value = "0" + value + } + if value == "100" { + return "uwu" + } + return value + "%" +}