Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ include/version.h
/net-commander
docs
data
test
test
dist/
88 changes: 88 additions & 0 deletions scripts/build_release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# scripts/build_release.sh
# Build firmware and assemble release artifacts:
# - <version>-merged.bin (single-file flash at 0x0)
# - <version>-firmware.bin (app-only image at 0x10000)
# - <version>-checksums.txt (SHA-256 sums)
# Output lands in dist/

set -euo pipefail

ENV="${1:-Esp32dev}"
BUILD_DIR=".pio/build/$ENV"
OUT_DIR="dist"

# Resolve version from git describe
VERSION="$(git describe --tags --dirty 2>/dev/null || echo unknown)"
if [[ "$VERSION" == "unknown" ]]; then
echo "warn: not in a git repo or no tags found; using 'unknown' as version" >&2
fi

# Locate pio — prefer PATH, fall back to PlatformIO's penv
if command -v pio >/dev/null 2>&1; then
PIO=pio
elif [[ -x "$HOME/.platformio/penv/bin/pio" ]]; then
PIO="$HOME/.platformio/penv/bin/pio"
else
echo "error: pio not found on PATH or in ~/.platformio/penv/bin" >&2
exit 1
fi

# Build first (no-op if everything up to date)
echo "==> Building $ENV"
"$PIO" run -e "$ENV"

# Sanity check build artifacts
for f in firmware.bin bootloader.bin partitions.bin; do
if [[ ! -f "$BUILD_DIR/$f" ]]; then
echo "error: missing $BUILD_DIR/$f after build" >&2
exit 1
fi
done

# Locate boot_app0.bin in the framework package (not in the build dir)
BOOT_APP0=$(find "$HOME/.platformio/packages/framework-arduinoespressif32" \
-path "*/tools/partitions/boot_app0.bin" 2>/dev/null | head -1)
if [[ -z "$BOOT_APP0" ]]; then
echo "error: boot_app0.bin not found in framework-arduinoespressif32 package" >&2
exit 1
fi

# Locate esptool — prefer PATH, fall back to PlatformIO-bundled
if command -v esptool.py >/dev/null 2>&1; then
ESPTOOL_CMD=(esptool.py)
elif [[ -f "$HOME/.platformio/packages/tool-esptoolpy/esptool.py" ]]; then
ESPTOOL_CMD=(python3 "$HOME/.platformio/packages/tool-esptoolpy/esptool.py")
else
echo "error: esptool.py not found on PATH or in tool-esptoolpy package" >&2
exit 1
fi

mkdir -p "$OUT_DIR"

MERGED="$OUT_DIR/${VERSION}-merged.bin"
APPONLY="$OUT_DIR/${VERSION}-firmware.bin"
CHECKSUMS="$OUT_DIR/${VERSION}-checksums.txt"

echo "==> Merging into $MERGED"
"${ESPTOOL_CMD[@]}" --chip esp32 merge_bin -o "$MERGED" \
--flash_mode dio --flash_freq 40m --flash_size 4MB \
0x1000 "$BUILD_DIR/bootloader.bin" \
0x8000 "$BUILD_DIR/partitions.bin" \
0xe000 "$BOOT_APP0" \
0x10000 "$BUILD_DIR/firmware.bin"

cp "$BUILD_DIR/firmware.bin" "$APPONLY"

echo "==> Generating checksums"
( cd "$OUT_DIR" && sha256sum \
"$(basename "$MERGED")" \
"$(basename "$APPONLY")" \
> "$(basename "$CHECKSUMS")" )

echo ""
echo "Release artifacts:"
ls -lh "$MERGED" "$APPONLY" "$CHECKSUMS"
echo ""
echo "Flash with:"
echo " esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash 0x0 $MERGED"
171 changes: 171 additions & 0 deletions scripts/icon_prep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env bash
# icon_prep.sh — batch-prep PNG icons for GUIslice Image2C
#
# Reads one or more source PNGs, outputs bilevel white-on-transparent PNGs
# (PNG32 RGBA on disk to bypass Java decoder bug with 1-bit greyscale + tRNS),
# optionally generating Image2C-compatible 1bpp C arrays in the same pass.
#
# Output direction is the contract; input direction is auto-detected by mean
# brightness and inverted only when needed.
#
# Works with both ImageMagick v6 (convert/identify) and v7 (magick).
# C array generation requires python3 and pbm_to_gslc_c.py alongside this script.
#
# Usage:
# ./icon_prep.sh [-s WxH] [-i auto|on|off] [-c] <file.png> [file.png ...]
# ./icon_prep.sh -s 40x40 -c *.png
#
# Output:
# ./prepped/<basename>_<WxH>px.png
# ./prepped/<basename>_<WxH>px.c (only with -c)
#
# Re-runs silently overwrite existing outputs.

set -euo pipefail

# --- defaults ---
OUT_DIR="prepped"
SIZE="40x40" # default; override with -s
INVERT_MODE="auto" # auto | on | off
THRESHOLD="50%" # b/w split point; lower = thinner strokes, higher = fatter
EMIT_C=false # -c flag: also emit Image2C-format .c file

usage() {
cat <<EOF
Usage: $0 [-s WxH] [-i auto|on|off] [-c] [-h] <file.png> [file.png ...]

-s WxH Resize to WxH pixels. Default: 40x40.
-i MODE Invert behaviour:
auto detect from mean brightness (default)
on force invert
off skip invert
-c Also emit C array (.c file) for direct firmware use.
Requires python3 + pbm_to_gslc_c.py in script directory.
-h Show this help.

Output: ./${OUT_DIR}/<basename>_<WxH>px.png (+ .c with -c)
EOF
exit "${1:-0}"
}

# --- args ---
while getopts ":s:i:ch" opt; do
case "$opt" in
s) SIZE="$OPTARG" ;;
i) INVERT_MODE="$OPTARG" ;;
c) EMIT_C=true ;;
h) usage 0 ;;
\?) echo "unknown flag: -$OPTARG" >&2; usage 1 ;;
:) echo "flag -$OPTARG needs an argument" >&2; usage 1 ;;
esac
done
shift $((OPTIND - 1))

case "$INVERT_MODE" in
auto|on|off) ;;
*) echo "invalid -i mode: $INVERT_MODE (use auto|on|off)" >&2; exit 1 ;;
esac

[[ $# -ge 1 ]] || usage 1

# --- preflight: pick ImageMagick binary (v7 magick > v6 convert) ---
if command -v magick >/dev/null 2>&1; then
IM_CONVERT=(magick)
IM_IDENTIFY=(magick identify)
elif command -v convert >/dev/null 2>&1 && command -v identify >/dev/null 2>&1; then
IM_CONVERT=(convert)
IM_IDENTIFY=(identify)
else
echo "error: ImageMagick not found." >&2
echo " v6 (convert/identify): sudo apt install imagemagick" >&2
echo " v7 (magick): sudo snap install imagemagick" >&2
exit 1
fi

# --- preflight for -c flag ---
if [[ "$EMIT_C" == true ]]; then
command -v python3 >/dev/null 2>&1 \
|| { echo "error: python3 not found (needed for -c)" >&2; exit 1; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PY_SCRIPT="$SCRIPT_DIR/pbm_to_gslc_c.py"
[[ -f "$PY_SCRIPT" ]] \
|| { echo "error: pbm_to_gslc_c.py not found at $PY_SCRIPT" >&2; exit 1; }
fi

mkdir -p "$OUT_DIR"

# --- process ---
for src in "$@"; do
if [[ ! -f "$src" ]]; then
echo "skip (not a file): $src"
continue
fi

# effective size — flag wins; otherwise read source dimensions
if [[ -n "$SIZE" ]]; then
eff_size="$SIZE"
else
eff_size=$("${IM_IDENTIFY[@]}" -format "%wx%h" "$src")
fi

# decide invert direction
case "$INVERT_MODE" in
on)
do_invert=true
tag="forced→invert"
;;
off)
do_invert=false
tag="forced→keep"
;;
auto)
mean_pct=$("${IM_CONVERT[@]}" "$src" \
-background white -alpha remove -alpha off \
-colorspace Gray \
-format "%[fx:int(mean*100)]" info:)
if (( mean_pct > 50 )); then
do_invert=true
tag="bright→invert"
else
do_invert=false
tag="dark→keep"
fi
;;
esac

# build output paths
base=$(basename "$src")
stem="${base%.*}"
out="$OUT_DIR/${stem}_${eff_size}px.png"

# PNG pipeline: PNG32 RGBA on disk dodges Java's 1-bit greyscale + tRNS bug
args=("$src" -background white -alpha remove -alpha off)
[[ -n "$SIZE" ]] && args+=(-resize "${SIZE}!")
[[ "$do_invert" == true ]] && args+=(-negate)
args+=(
-threshold "$THRESHOLD"
-type Bilevel
-transparent black
"PNG32:$out"
)
"${IM_CONVERT[@]}" "${args[@]}"
echo "✓ $out [$tag]"

# optional C-array emission
if [[ "$EMIT_C" == true ]]; then
cfile="${out%.png}.c"
cname="${stem}_${eff_size}px"
"${IM_CONVERT[@]}" "$out" \
-background black -alpha remove -alpha off \
-negate \
pbm:- | \
python3 "$PY_SCRIPT" \
--name "$cname" \
--src "$(basename "$out")" \
--fg FFFFFF \
> "$cfile"
echo " → $cfile"
fi
done

echo "done — $OUT_DIR/"
125 changes: 125 additions & 0 deletions scripts/pbm_to_gslc_c.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""
pbm_to_gslc_c.py — wrap a P4 PBM stream in GUIslice 1bpp C array boilerplate.

Reads binary PBM (P4) from stdin (typically piped from ImageMagick), emits
a .c file matching Image2C's output format byte-for-byte.

Usage (inside icon_prep.sh):
convert IN.png ... pbm:- | pbm_to_gslc_c.py --name foo --src foo.png > foo.c
"""
import argparse
import os
import sys


WIKI_URL = "https://github.com/ImpulseAdventure/GUIslice/wiki/Display-Images-from-FLASH"
COMMENT_COL = 100 # column to align trailing `// 0x____ ...` comments
BYTES_PER_LINE = 16


def parse_args():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--name", required=True,
help="C array name (e.g. back_40x40px)")
p.add_argument("--src", required=True,
help="Source filename for header comment (e.g. back_40x40px.png)")
p.add_argument("--fg", default="FFFFFF",
help="Foreground colour as RRGGBB hex (default: FFFFFF)")
return p.parse_args()


def read_pbm(stream):
"""Read P4 binary PBM from stream. Returns (width, height, data_bytes)."""
magic = stream.readline().strip()
if magic != b"P4":
sys.exit(f"error: expected P4 PBM, got {magic!r}")
# Skip comment lines, find dimensions
while True:
line = stream.readline().strip()
if line and not line.startswith(b"#"):
break
width, height = map(int, line.split())
data = stream.read()
expected = ((width + 7) // 8) * height
if len(data) != expected:
sys.exit(f"error: expected {expected} data bytes, got {len(data)}")
return width, height, data


def format_data_lines(data):
"""Format byte data as C array lines with Image2C-style trailing offset comments."""
lines = []
total = len(data)
for i in range(0, total, BYTES_PER_LINE):
chunk = data[i:i + BYTES_PER_LINE]
end_offset = i + len(chunk)
bytes_str = ", ".join(f"0x{b:02X}" for b in chunk)
# Trailing comma for all but the final chunk
if end_offset < total:
bytes_str += ","
padding = max(1, COMMENT_COL - len(bytes_str))
# Image2C labels these "pixels" though they're bytes — preserve quirk
line = f"{bytes_str}{' ' * padding}// 0x{end_offset:04X} ({end_offset}) pixels"
lines.append(line)
return lines


def emit(name, src, width, height, data, fg_rgb):
r, g, b = fg_rgb
mem_size = len(data)
out = []
out.append("//" + "-" * 78)
out.append("// File Generated by GUIslice_Image2C")
out.append("//" + "-" * 78)
out.append(f"// Generated from : {src}")
out.append(f"// Dimensions : {width}x{height} pixels")
out.append("// Bits Per Pixel : 1 Bits")
out.append(f"// Memory Size : {mem_size} Bytes")
out.append("// Little Endian : true")
out.append("//" + "-" * 78)
out.append("")
out.append("// For details on how to generate this file please refer to")
out.append(f"// {WIKI_URL}")
out.append("")
out.append('#include "GUIslice.h"')
out.append('#include "GUIslice_config.h"')
out.append("")
out.append("#if (GSLC_USE_PROGMEM)")
out.append(" #if defined(__AVR__)")
out.append(" #include <avr/pgmspace.h>")
out.append(" #else")
out.append(" #include <pgmspace.h>")
out.append(" #endif")
out.append("#endif")
out.append("")
out.append(f"const unsigned char {name}[{mem_size}+7] GSLC_PMEM = {{")
# 16-bit big-endian height + width, each split into two byte literals
out.append(f"0x{(height >> 8) & 0xFF:02X}, // Height of image")
out.append(f"0x{height & 0xFF:02X},")
out.append(f"0x{(width >> 8) & 0xFF:02X}, // Width of image")
out.append(f"0x{width & 0xFF:02X},")
# RGB foreground colour as right-aligned decimal (matches Image2C style)
out.append(f"{r:3d}, // red color")
out.append(f"{g:3d}, // green color")
out.append(f"{b:3d}, // blue color")
out.extend(format_data_lines(data))
out.append("};")
return "\n".join(out) + "\n"


def main():
args = parse_args()
# Parse foreground hex
fg_hex = args.fg.lstrip("#")
if len(fg_hex) != 6:
sys.exit("error: --fg must be 6-hex-digit RRGGBB")
fg_rgb = tuple(int(fg_hex[i:i+2], 16) for i in (0, 2, 4))

width, height, data = read_pbm(sys.stdin.buffer)
c_source = emit(args.name, args.src, width, height, data, fg_rgb)
sys.stdout.write(c_source)


if __name__ == "__main__":
main()
Loading