commit 477313169a965776d7fca62ff4df8aa75a0cd3bb
Author: Martin Ashby <martin@ashbysoft.com>
Date: Sun, 27 Sep 2026 22:38:35 +0100
initial
Diffstat:
8 files changed, 229 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+images/
+api/
diff --git a/README.md b/README.md
@@ -0,0 +1,3 @@
+# trminimal
+
+Martin's very minimal trmnl server.
diff --git a/fonts/ARIAL.TTF b/fonts/ARIAL.TTF
Binary files differ.
diff --git a/go.mod b/go.mod
@@ -0,0 +1,11 @@
+module mfashby.net/trminimal/v2
+
+go 1.25.0
+
+require (
+ github.com/gogpu/gg v0.52.5 // indirect
+ github.com/gogpu/gpucontext v0.28.0 // indirect
+ github.com/gogpu/gputypes v0.5.2 // indirect
+ golang.org/x/image v0.44.0 // indirect
+ golang.org/x/text v0.40.0 // indirect
+)
diff --git a/go.sum b/go.sum
@@ -0,0 +1,10 @@
+github.com/gogpu/gg v0.52.5 h1:aDAxjwcUYWTaEcKiAVSX52FPtYKtMhuFYCQYK2SwJwE=
+github.com/gogpu/gg v0.52.5/go.mod h1:mKiEGiR8lT7BCECfSoxRYBQ96HrDc0DHv/CNDxYQG5M=
+github.com/gogpu/gpucontext v0.28.0 h1:W27+0PRnHLVRscDJ2L6r4B1JlaKjlV/EmL0Wxx8vGIc=
+github.com/gogpu/gpucontext v0.28.0/go.mod h1:5rpKj+DpixgAFm8ArBXFXOZQRbeJSFncVMsqyyA9Rhc=
+github.com/gogpu/gputypes v0.5.2 h1:3sbKI0+XW36Ekd3d4QhkbpdY7gbgMVgp8Q4+ZNdGhtE=
+github.com/gogpu/gputypes v0.5.2/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
+golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
+golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
+golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
+golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
diff --git a/hello.go b/hello.go
@@ -0,0 +1,43 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/gogpu/gg"
+ "github.com/gogpu/gg/text"
+)
+
+const (
+ outfile = "images/hello.png"
+)
+
+func main() {
+ err := do();
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func do() error {
+ dc := gg.NewContext(600, 800)
+ defer dc.Close()
+
+ dc.ClearWithColor(gg.White)
+ dc.SetColor(gg.Black)
+
+ source, err := text.NewFontSourceFromFile("fonts/ARIAL.TTF")
+ if err != nil {
+ return err
+ }
+ defer source.Close()
+ dc.SetFont(source.Face(32))
+ nowstr := time.Now().Local().Format("Mon, 02 Jan 2006")
+ dc.DrawString(fmt.Sprintf("Hello, it is %s", nowstr), 20, 32)
+ err = dc.SavePNG(outfile)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/rotate_display.go b/rotate_display.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "os"
+ "path"
+ "slices"
+)
+
+const (
+ apifile = "api/display"
+ imagesbase = "https://trmnl.mfashby.net/images/"
+)
+var (
+ rotation = []string{"weather.png", "hello.png"}
+)
+
+
+func main() {
+ err := do()
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func do() error {
+ dispraw, err := os.ReadFile(apifile)
+ var current string
+ var d Display
+ if os.IsNotExist(err) {
+ current = rotation[0]
+ } else if err != nil {
+ return err
+ } else {
+ err = json.Unmarshal(dispraw, &d)
+ if err != nil {
+ return err
+ }
+ current = d.Filename
+ }
+ ix := slices.IndexFunc(rotation, func (x string) bool {
+ return x == current
+ })
+ ix = (ix + 1) % len(rotation)
+ d = NewDisplay(imagesbase + rotation[ix])
+ dispraw, err = json.MarshalIndent(d, "", " ")
+ if err != nil {
+ return err
+ }
+ err = os.WriteFile(apifile, dispraw, 0644)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func NewDisplay(imageUrl string) Display {
+ filename := path.Base(imageUrl)
+ return Display{
+ ImageUrl: imageUrl,
+ Filename: filename,
+ RefreshRate: 300,
+ ResetFirmware: false,
+ UpdateFirmware: false,
+ FirmwareUrl: "https://trmnl.com/firmware/1.0.0.bin",
+ SpecialFunction: "identify",
+ Action: "identify",
+ }
+}
+
+type Display struct {
+ ImageUrl string `json:"image_url"`
+ Filename string `json:"filename"`
+ RefreshRate int `json:"refresh_rate"`
+ ResetFirmware bool `json:"reset_firmware"`
+ UpdateFirmware bool `json:"update_firmware"`
+ FirmwareUrl string `json:"firmware_url"`
+ SpecialFunction string `json:"special_function"`
+ Action string `json:"action"`
+}
+\ No newline at end of file
diff --git a/weather.go b/weather.go
@@ -0,0 +1,77 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+
+ "github.com/gogpu/gg"
+ "github.com/gogpu/gg/text"
+)
+
+const (
+ // Weather for Calverley
+ weatherApiUrl = "https://api.open-meteo.com/v1/forecast?latitude=58.829906&longitude=-1.687577&daily=temperature_2m_max,temperature_2m_min,precipitation_hours&models=ukmo_seamless&forecast_days=3"
+ outfile = "images/weather.png"
+)
+
+func main() {
+ err := domain();
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func domain() error {
+ res, err := http.DefaultClient.Get(weatherApiUrl)
+ if err != nil {
+ return err
+ }
+ if res.StatusCode < 200 || res.StatusCode >= 400 {
+ return fmt.Errorf("Error status code from %s: %d %s", weatherApiUrl, res.StatusCode, res.Status)
+ }
+ var weather WeatherApiRes
+ err = json.NewDecoder(res.Body).Decode(&weather)
+ if err != nil {
+ return err
+ }
+
+ dc := gg.NewContext(600, 800)
+ defer dc.Close()
+
+ dc.ClearWithColor(gg.White)
+ dc.SetColor(gg.Black)
+
+ // Divider lines
+ dc.SetLineWidth(5)
+ dc.DrawLine(0, 233, 600, 233)
+ dc.DrawLine(0, 466, 600, 466)
+
+ source, err := text.NewFontSourceFromFile("fonts/ARIAL.TTF")
+ if err != nil {
+ return err
+ }
+ defer source.Close()
+ dc.SetFont(source.Face(32))
+ for ix, time := range weather.Daily.Time {
+ dc.DrawString(time, 20, float64((ix*233) + 32))
+ dc.DrawString(fmt.Sprintf("Max %.1f°C", weather.Daily.Temp2MMax[ix]), 20, float64((ix*233) + 64))
+ dc.DrawString(fmt.Sprintf("Min %.1f°C", weather.Daily.Temp2MMin[ix]), 20, float64((ix*233) + 96))
+ }
+
+ err = dc.SavePNG(outfile)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+type WeatherApiRes struct {
+ Daily struct {
+ Time []string `json:"time"`
+ Temp2MMax []float64 `json:"temperature_2m_max"`
+ Temp2MMin []float64 `json:"temperature_2m_min"`
+ PrecipitationHours []float64 `json:"precipitation_hours"`
+ } `json:"daily"`
+}
+\ No newline at end of file