Merge branch 'main' of code.madriguera.me:GoEducation/badges

This commit is contained in:
Daniel Esteban 2026-04-21 21:48:36 +02:00
commit 8fc41e4eff
8 changed files with 286 additions and 0 deletions

3
.gitignore vendored
View file

@ -25,3 +25,6 @@ go.work.sum
# env file # env file
.env .env
# logogen cache
cmd/logogen/cache/*.bin

View file

@ -1,2 +1,70 @@
# badges # badges
TinyGo badge firmware for multiple hardware targets (GopherBadge, NiceBadge, GoBadge/PyBadge).
## Hardware targets
| Target | Build tag | Resolution |
|--------|-----------|------------|
| GopherBadge | `gopher_badge` | 320×240 |
| NiceBadge | `nicenano` | 240×135 |
| GoBadge / PyBadge | `pybadge` | 160×128 |
## Generating logo.bin
The badge firmware embeds a `logo.bin` file displayed at boot (`showLogoBin` in `badge.go`).
Use the `logogen` command to convert any image to the required hex format.
### Setup
```sh
cd cmd/logogen
go mod download
```
### Usage
```sh
# By target name
go run . -size nicebadge -image tinygo
go run . -size gopherbadge -image gopherconeu
go run . -size gobadge -image golab
# By explicit resolution
go run . -size 320x240 -image /path/to/logo.jpg
# Write directly to the project root logo.bin
go run . -size nicebadge -image tinygo -output ../../logo.bin
```
### Flags
| Flag | Default | Description |
|------|---------|-------------|
| `-size` | *(required)* | Target name (`nicebadge`, `gopherbadge`, `gobadge`, `pybadge`) or explicit resolution (`320x240`) |
| `-image` | *(required)* | File path (relative or absolute) or short name |
| `-output` | `logo.bin` | Output file |
| `-images-dir` | `images` | Directory with source images |
| `-cache-dir` | `cache` | Directory for cached `.bin` files |
### Adding images
Place source images inside `cmd/logogen/images/`:
```
cmd/logogen/images/
tinygo.jpg ← source image for short name "tinygo"
tinygo_240x135.png ← predefined variant for nicebadge (skip resize)
tinygo_320x240.png ← predefined variant for gopherbadge (skip resize)
gopherconeu.jpg
golab.png
...
```
**Resolution priority** for a given short name + target:
1. `<name>_<WxH>.png` / `.jpg` — predefined resized variant, used as-is (no resize)
2. `<name>.png` / `.jpg` / `.jpeg` — base image, resized with BiLinear interpolation
3. Cache hit (`cache/<name>_<WxH>.bin`) — skips all image processing on repeat runs
Generated `.bin` files in `cache/` are gitignored.

0
cmd/logogen/cache/.gitkeep vendored Normal file
View file

5
cmd/logogen/go.mod Normal file
View file

@ -0,0 +1,5 @@
module github.com/conejoninja/gopherbadge/cmd/logogen
go 1.22.1
require golang.org/x/image v0.23.0

2
cmd/logogen/go.sum Normal file
View file

@ -0,0 +1,2 @@
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=

View file

BIN
cmd/logogen/logogen Executable file

Binary file not shown.

208
cmd/logogen/main.go Normal file
View file

@ -0,0 +1,208 @@
// logogen converts an image to a .bin file suitable for embedding in badge firmware.
//
// Usage:
//
// logogen -size <target|WxH> -image <path|shortname> [-output logo.bin] [-images-dir images] [-cache-dir cache]
//
// Examples:
//
// logogen -size nicebadge -image tinygo
// logogen -size gopherbadge -image gopherconeu
// logogen -size 320x240 -image /path/to/logo.jpg
// logogen -size gobadge -image ./my-logo.png -output logo.bin
//
// Short names resolve to image files inside -images-dir.
// If a predefined resized variant exists (e.g. tinygo_320x240.png), it is used directly.
// Generated .bin files are cached in -cache-dir to avoid reprocessing.
package main
import (
"flag"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"log"
"os"
"path/filepath"
"strconv"
"strings"
draw2 "golang.org/x/image/draw"
)
type resolution struct {
width, height int
}
func (r resolution) String() string {
return fmt.Sprintf("%dx%d", r.width, r.height)
}
var predefinedTargets = map[string]resolution{
"nicebadge": {240, 135},
"gopherbadge": {320, 240},
"gobadge": {160, 128},
"pybadge": {160, 128},
}
func main() {
size := flag.String("size", "", "Resolution: WxH or target name (nicebadge, gopherbadge, gobadge, pybadge)")
imageArg := flag.String("image", "", "Image: file path (relative/absolute) or short name (tinygo, gopherconeu, golab, ...)")
output := flag.String("output", "logo.bin", "Output .bin file path")
imagesDir := flag.String("images-dir", "images", "Directory with source images and predefined variants")
cacheDir := flag.String("cache-dir", "cache", "Directory for cached .bin files")
flag.Parse()
if *size == "" {
flag.Usage()
log.Fatal("\nMust specify -size (e.g. -size nicebadge or -size 320x240)")
}
if *imageArg == "" {
flag.Usage()
log.Fatal("\nMust specify -image (file path or short name)")
}
res := parseSize(*size)
// Determine whether -image is a file path or a short name.
// Heuristic: contains path separator, starts with '.' or '..' , or has a file extension → path.
isPath := strings.ContainsAny(*imageArg, "/\\") ||
strings.HasPrefix(*imageArg, ".") ||
filepath.Ext(*imageArg) != ""
var baseName string // used as cache/predefined key
var srcPath string // resolved source image file
if isPath {
absPath, err := filepath.Abs(*imageArg)
if err != nil {
log.Fatalf("invalid image path %q: %v", *imageArg, err)
}
srcPath = absPath
base := filepath.Base(absPath)
baseName = strings.TrimSuffix(base, filepath.Ext(base))
} else {
baseName = *imageArg
srcPath = findSourceImage(*imageArg, res, *imagesDir)
}
cacheFile := filepath.Join(*cacheDir, fmt.Sprintf("%s_%s.bin", baseName, res))
// 1. Check cache.
if data, err := os.ReadFile(cacheFile); err == nil {
fmt.Printf("Using cached: %s\n", cacheFile)
writeOutput(*output, data)
fmt.Printf("Written: %s (%s)\n", *output, res)
return
}
// 2. Load, resize if needed, convert.
img := loadAndResize(srcPath, res)
binData := convertToBin(img, res)
// 3. Save cache.
if err := os.MkdirAll(*cacheDir, 0755); err != nil {
log.Printf("Warning: cannot create cache dir %s: %v", *cacheDir, err)
} else if err := os.WriteFile(cacheFile, binData, 0644); err != nil {
log.Printf("Warning: cannot save cache %s: %v", cacheFile, err)
} else {
fmt.Printf("Cached: %s\n", cacheFile)
}
writeOutput(*output, binData)
fmt.Printf("Generated: %s (%s, %d bytes)\n", *output, res, len(binData))
}
// parseSize accepts either a target name (nicebadge, gopherbadge, ...) or WxH format.
func parseSize(s string) resolution {
if res, ok := predefinedTargets[strings.ToLower(s)]; ok {
return res
}
parts := strings.SplitN(strings.ToLower(s), "x", 2)
if len(parts) != 2 {
log.Fatalf("invalid size %q: must be WxH (e.g. 320x240) or a target name (nicebadge, gopherbadge, gobadge, pybadge)", s)
}
w, err1 := strconv.Atoi(parts[0])
h, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil || w <= 0 || h <= 0 {
log.Fatalf("invalid size %q: width and height must be positive integers", s)
}
return resolution{w, h}
}
// findSourceImage looks for the best source image for a short name in imagesDir.
// Priority:
// 1. <name>_<WxH>.png / .jpg — predefined resized variant (no resize needed)
// 2. <name>.png / .jpg / .jpeg — base source image (will be resized)
func findSourceImage(name string, res resolution, imagesDir string) string {
resStr := res.String()
extensions := []string{".png", ".jpg", ".jpeg"}
// Try predefined resized variant first.
for _, ext := range extensions {
p := filepath.Join(imagesDir, fmt.Sprintf("%s_%s%s", name, resStr, ext))
if _, err := os.Stat(p); err == nil {
fmt.Printf("Using predefined: %s\n", p)
return p
}
}
// Try base source image.
for _, ext := range extensions {
p := filepath.Join(imagesDir, name+ext)
if _, err := os.Stat(p); err == nil {
return p
}
}
log.Fatalf("image not found for short name %q in directory %q\n"+
"Expected one of: %s_%s.png, %s.png, %s.jpg, ...",
name, imagesDir, name, resStr, name, name)
return ""
}
// loadAndResize opens a JPEG or PNG and scales it to the target resolution.
// If the image is already the exact target size, scaling is skipped.
func loadAndResize(path string, res resolution) image.Image {
f, err := os.Open(path)
if err != nil {
log.Fatalf("cannot open image %s: %v", path, err)
}
defer f.Close()
img, format, err := image.Decode(f)
if err != nil {
log.Fatalf("cannot decode image %s: %v", path, err)
}
fmt.Printf("Loaded: %s (%s, %dx%d)\n", path, format, img.Bounds().Dx(), img.Bounds().Dy())
if img.Bounds().Dx() == res.width && img.Bounds().Dy() == res.height {
return img
}
fmt.Printf("Resizing to %s...\n", res)
dst := image.NewRGBA(image.Rect(0, 0, res.width, res.height))
draw2.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw2.Over, nil)
return dst
}
// convertToBin encodes the image pixels as a hex string: "RRGGBB" per pixel, row-major.
// This matches the format expected by showLogoBin() in badge.go.
func convertToBin(img image.Image, res resolution) []byte {
var sb strings.Builder
sb.Grow(res.width * res.height * 6)
for y := 0; y < res.height; y++ {
for x := 0; x < res.width; x++ {
r, g, b, _ := img.At(x, y).RGBA()
fmt.Fprintf(&sb, "%02x%02x%02x", uint8(r>>8), uint8(g>>8), uint8(b>>8))
}
}
return []byte(sb.String())
}
func writeOutput(path string, data []byte) {
if err := os.WriteFile(path, data, 0644); err != nil {
log.Fatalf("cannot write output %s: %v", path, err)
}
}