mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Add read-only assets to overlay apps and improve apps
This commit is contained in:
1 parent
f64406277c
commit
9d5b3af7a3
43 files changed
+1478
-514
No files matched your search
@@ -24,4 +24,5 @@ App/apps/*/*.bin
|
||||
App/apps/*/*.map
|
||||
App/apps/*/*.app
|
||||
App/apps/*/*_app_blob.h
|
||||
App/apps/*/*_assets.h
|
||||
build/apps/
|
||||
+22
-3
@@ -38,10 +38,14 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* First unpublished/public baseline: all services currently present below are
|
||||
* ABI major 1, API level 1. */
|
||||
/* API levels within ABI major 1:
|
||||
* 1 baseline: every service up to and including beam_draw
|
||||
* 2 ticks_ms, rand32, asset_read (+ app_header_t asset_size / asset_crc) */
|
||||
#define APP_ABI_MAJOR 1u
|
||||
#define APP_API_LEVEL 1u
|
||||
#define APP_API_LEVEL 2u
|
||||
|
||||
/* Minimum API level of an app that ships read-only assets (pack_app.py). */
|
||||
#define APP_API_ASSETS 2u
|
||||
|
||||
/* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by
|
||||
* value so the app stays independent of the firmware headers. */
|
||||
@@ -226,6 +230,21 @@ typedef struct app_api {
|
||||
void (*beam_rx)(bool start); /* arm or stop FSK reception */
|
||||
uint8_t (*beam_rx_poll)(uint16_t *packet); /* APP_BEAM_RX_* */
|
||||
void (*beam_draw)(const char *status); /* MAIN display with one BEAM center line */
|
||||
|
||||
/* ---- API level 2: time, randomness, read-only assets ---- */
|
||||
/* Free-running millisecond clock with 10 ms resolution (SysTick). Compare
|
||||
* with unsigned subtraction: (api->ticks_ms() - start) >= period. */
|
||||
uint32_t (*ticks_ms)(void);
|
||||
/* xorshift32 PRNG, never 0. The resident state persists across launches
|
||||
* and is re-mixed with RSSI noise and SysTick jitter at each launch, so apps
|
||||
* need no seed of their own. */
|
||||
uint32_t (*rand32)(void);
|
||||
/* Copy len bytes of this app's assets, starting at offset, into buf. The
|
||||
* read is clamped to the packed asset size; returns the byte count copied
|
||||
* (0 past the end or when the app has no assets). The loader verified the
|
||||
* assets' CRC before launch. buf may live in the overlay (.bss) or on the
|
||||
* stack. */
|
||||
uint16_t (*asset_read)(uint16_t offset, void *buf, uint16_t len);
|
||||
} app_api_t;
|
||||
|
||||
/* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
# Shared artwork helpers for overlay-app title screens (used by gen_assets.py).
|
||||
#
|
||||
# Canvas is a 128x64 1-bit picture. Canvas.pages() packs it the way the LCD
|
||||
# buffers are laid out: 8 pages of 128 column bytes, LSB = top row; page 0 is
|
||||
# the status line, pages 1..7 the frame buffer. An app streams the result into
|
||||
# A->status_line and A->fb with two asset_read calls.
|
||||
#
|
||||
# The logo font draws letters as stroke skeletons: polylines on a 14-row grid
|
||||
# (x right, y down), scaled, drawn with a square pen and sheared into italics.
|
||||
# Corners are cut by diagonal segments (chamfered arcade look), and each word
|
||||
# is outlined by a one-pixel ring drawn one blank pixel away from the letters.
|
||||
W, H = 128, 64
|
||||
PROMPT_TOP = 56 # page 7 stays blank: the app blinks its prompt there
|
||||
|
||||
LOGO = {
|
||||
"A": (9, [[(0, 13), (0, 3), (3, 0), (6, 0), (9, 3), (9, 13)], [(0, 8), (9, 8)]]),
|
||||
"B": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 4), (7, 6), (0, 6)],
|
||||
[(7, 6), (9, 8), (9, 11), (7, 13), (0, 13)]]),
|
||||
"C": (9, [[(9, 0), (3, 0), (0, 3), (0, 10), (3, 13), (9, 13)]]),
|
||||
"D": (9, [[(0, 0), (6, 0), (9, 3), (9, 10), (6, 13), (0, 13), (0, 0)]]),
|
||||
"E": (9, [[(9, 0), (0, 0), (0, 13), (9, 13)], [(0, 6), (6, 6)]]),
|
||||
"I": (6, [[(0, 0), (6, 0)], [(3, 0), (3, 13)], [(0, 13), (6, 13)]]),
|
||||
"K": (9, [[(0, 0), (0, 13)], [(9, 0), (3, 6), (0, 6)], [(3, 6), (9, 12), (9, 13)]]),
|
||||
"L": (8, [[(0, 0), (0, 13), (8, 13)]]),
|
||||
"M": (12, [[(0, 13), (0, 0), (6, 6), (12, 0), (12, 13)]]),
|
||||
"N": (9, [[(0, 13), (0, 0), (9, 13), (9, 0)]]),
|
||||
"O": (9, [[(3, 0), (6, 0), (9, 3), (9, 10), (6, 13), (3, 13), (0, 10), (0, 3), (3, 0)]]),
|
||||
"P": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 5), (7, 7), (0, 7)]]),
|
||||
"R": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 5), (7, 7), (0, 7)], [(5, 7), (9, 11), (9, 13)]]),
|
||||
"S": (9, [[(9, 0), (2, 0), (0, 2), (0, 4), (2, 6), (7, 6), (9, 8), (9, 11), (7, 13), (0, 13)]]),
|
||||
"T": (10, [[(0, 0), (10, 0)], [(5, 0), (5, 13)]]),
|
||||
"U": (9, [[(0, 0), (0, 10), (3, 13), (6, 13), (9, 10), (9, 0)]]),
|
||||
}
|
||||
LOGO_KX, LOGO_KY = 1.0, 1.1 # default skeleton scale
|
||||
LOGO_BRUSH = 3 # stroke thickness in pixels
|
||||
LOGO_GAP = 3 # pixels between letters
|
||||
LOGO_SHEAR = 4 # one pixel of slant every LOGO_SHEAR rows
|
||||
LOGO_RING = 2 # outline distance from the letters (0 = none)
|
||||
|
||||
def line(pts, x0, y0, x1, y1):
|
||||
"""Bresenham segment into the pts set."""
|
||||
dx, dy = abs(x1 - x0), -abs(y1 - y0)
|
||||
sx, sy = (1 if x0 < x1 else -1), (1 if y0 < y1 else -1)
|
||||
err = dx + dy
|
||||
while True:
|
||||
pts.add((x0, y0))
|
||||
if x0 == x1 and y0 == y1:
|
||||
return
|
||||
e2 = 2 * err
|
||||
if e2 >= dy:
|
||||
err += dy
|
||||
x0 += sx
|
||||
if e2 <= dx:
|
||||
err += dx
|
||||
y0 += sy
|
||||
|
||||
def logo_glyph(ch, kx, ky):
|
||||
"""(advance width, pixel set) of one logo letter, before shearing."""
|
||||
width, polylines = LOGO[ch]
|
||||
skeleton = set()
|
||||
for poly in polylines:
|
||||
pts = [(round(x * kx), round(y * ky)) for x, y in poly]
|
||||
for a, b in zip(pts, pts[1:]):
|
||||
line(skeleton, *a, *b)
|
||||
pixels = {(x + dx, y + dy) for x, y in skeleton
|
||||
for dx in range(LOGO_BRUSH) for dy in range(LOGO_BRUSH)}
|
||||
return round(width * kx) + LOGO_BRUSH, pixels
|
||||
|
||||
class Canvas:
|
||||
def __init__(self):
|
||||
self.px = [[0] * W for _ in range(H)]
|
||||
|
||||
def set(self, x, y, on=1):
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
self.px[y][x] = on
|
||||
|
||||
def rect(self, x0, y0, x1, y1):
|
||||
for y in range(y0, y1 + 1):
|
||||
for x in range(x0, x1 + 1):
|
||||
self.set(x, y)
|
||||
|
||||
def sprite(self, x, y, cols, scale=1):
|
||||
"""Column-major sprite (LSB = top row), as the apps store them."""
|
||||
for i, c in enumerate(cols):
|
||||
for bit in range(8):
|
||||
if c >> bit & 1:
|
||||
for dx in range(scale):
|
||||
for dy in range(scale):
|
||||
self.set(x + i * scale + dx, y + bit * scale + dy)
|
||||
|
||||
def word(self, text, y, kx=LOGO_KX, ky=LOGO_KY):
|
||||
"""Centered, outlined italic logo word; returns its letter height."""
|
||||
height = round(13 * ky) + LOGO_BRUSH
|
||||
glyphs = [logo_glyph(ch, kx, ky) for ch in text]
|
||||
total = sum(w for w, _ in glyphs) + LOGO_GAP * (len(glyphs) - 1) + (height - 1) // LOGO_SHEAR
|
||||
x = (W - total) // 2
|
||||
solid = set()
|
||||
for width, pixels in glyphs:
|
||||
for gx, gy in pixels:
|
||||
solid.add((x + gx + (height - 1 - gy) // LOGO_SHEAR, y + gy))
|
||||
x += width + LOGO_GAP
|
||||
grown = [solid] # solid dilated by 0, 1, ... pixels
|
||||
for _ in range(LOGO_RING):
|
||||
grown.append({(px + dx, py + dy) for px, py in grown[-1]
|
||||
for dx in (-1, 0, 1) for dy in (-1, 0, 1)})
|
||||
ring = grown[-1] - grown[-2] if LOGO_RING else set()
|
||||
for px, py in solid | ring:
|
||||
self.set(px, py)
|
||||
return height
|
||||
|
||||
def two_words(self, first, second):
|
||||
"""Logo on two lines from the top of the screen; returns the bottom row."""
|
||||
top = LOGO_RING # keep the outline on screen
|
||||
height = self.word(first, top)
|
||||
second_top = top + height + 2 * LOGO_RING + 1
|
||||
return second_top + self.word(second, second_top) + LOGO_RING
|
||||
|
||||
def pages(self):
|
||||
"""Title-screen payload: status page then the 7 frame-buffer pages."""
|
||||
if any(self.px[y][x] for y in range(PROMPT_TOP, H) for x in range(W)):
|
||||
raise SystemExit("title: the bottom page is reserved for the prompt")
|
||||
out = bytearray()
|
||||
for page in range(H // 8):
|
||||
for x in range(W):
|
||||
b = 0
|
||||
for bit in range(8):
|
||||
if self.px[page * 8 + bit][x]:
|
||||
b |= 1 << bit
|
||||
out.append(b)
|
||||
return bytes(out)
|
||||
|
||||
def preview(self, path, scale=4):
|
||||
"""Binary PGM, LCD-like colours (dark pixels on a pale panel)."""
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P5 %d %d 255\n" % (W * scale, H * scale))
|
||||
for y in range(H):
|
||||
row = bytes((40 if self.px[y][x] else 200)
|
||||
for x in range(W) for _ in range(scale))
|
||||
f.write(row * scale)
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
# Shared builder for overlay-app read-only assets (served by api->asset_read).
|
||||
#
|
||||
# Each app has a gen_assets.py that declares its texts and data with this
|
||||
# module, then calls Assets.main(): it writes the asset blob packed into the
|
||||
# .app by pack_app.py, and a C header of offsets the app includes. The header
|
||||
# is generated before compiling, so code and data can never drift apart.
|
||||
#
|
||||
# a = Assets("SNAKE")
|
||||
# a.text("T_PAUSE", "PAUSE") # NUL-terminated string
|
||||
# a.table("T_STATE", ["TX", "RX"]) # fixed-stride strings (+ _STRIDE)
|
||||
# a.u16("SPR_HEAD", [0x6566, 0x0FD2]) # packed little-endian data
|
||||
# a.main() # argv: <out.bin> <out.h>
|
||||
#
|
||||
# Texts come first, so their offsets stay small (cheap immediates in Thumb
|
||||
# code). TEXT_MAX is the longest text including its NUL: the size of the
|
||||
# app's shared text buffer, read in one asset_read by T().
|
||||
import os, re, struct, sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def _cdefine(header, name):
|
||||
pat = re.compile(r"^\s*#define\s+" + re.escape(name) + r"\s+(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\b")
|
||||
with open(os.path.join(HERE, header)) as f:
|
||||
for line in f:
|
||||
m = pat.match(line)
|
||||
if m:
|
||||
return int(m.group(1), 0)
|
||||
sys.exit(f"{header}: #define {name} not found")
|
||||
|
||||
ASSET_MAX = _cdefine("app_overlay.h", "APP_ASSET_MAX")
|
||||
|
||||
class Assets:
|
||||
def __init__(self, guard):
|
||||
self.guard = guard
|
||||
self.texts = [] # (name, bytes)
|
||||
self.data = [] # (name, bytes)
|
||||
self.extra = [] # (name, value) plain constants
|
||||
self.names = set()
|
||||
|
||||
def _name(self, name):
|
||||
if name in self.names or not re.fullmatch(r"[A-Z][A-Z0-9_]*", name):
|
||||
sys.exit(f"asset name {name!r} is invalid or duplicated")
|
||||
self.names.add(name)
|
||||
|
||||
def text(self, name, s):
|
||||
self._name(name)
|
||||
self.texts.append((name, s.encode("ascii") + b"\x00"))
|
||||
|
||||
def table(self, name, strings, stride=None):
|
||||
"""Fixed-stride string table: entry i is at name + i * name_STRIDE."""
|
||||
self._name(name)
|
||||
width = stride or max(len(s) for s in strings) + 1
|
||||
if any(len(s) + 1 > width for s in strings):
|
||||
sys.exit(f"{name}: an entry does not fit the {width} B stride")
|
||||
blob = b"".join(s.encode("ascii").ljust(width, b"\x00") for s in strings)
|
||||
self.texts.append((name, blob))
|
||||
self.const(name + "_STRIDE", width)
|
||||
|
||||
def const(self, name, value):
|
||||
self._name(name)
|
||||
self.extra.append((name, value))
|
||||
|
||||
def raw(self, name, payload):
|
||||
self._name(name)
|
||||
self.data.append((name, bytes(payload)))
|
||||
|
||||
def u8(self, name, values): self.raw(name, struct.pack(f"<{len(values)}B", *values))
|
||||
def i8(self, name, values): self.raw(name, struct.pack(f"<{len(values)}b", *values))
|
||||
def u16(self, name, values): self.raw(name, struct.pack(f"<{len(values)}H", *values))
|
||||
def i16(self, name, values): self.raw(name, struct.pack(f"<{len(values)}h", *values))
|
||||
def u32(self, name, values): self.raw(name, struct.pack(f"<{len(values)}I", *values))
|
||||
|
||||
def build(self):
|
||||
blob = bytearray()
|
||||
lines = []
|
||||
text_max = 1
|
||||
strides = dict(self.extra)
|
||||
for name, payload in self.texts:
|
||||
lines.append((name, len(blob), len(payload)))
|
||||
text_max = max(text_max, strides.get(name + "_STRIDE", len(payload)))
|
||||
blob += payload
|
||||
for name, payload in self.data: # asset_read copies bytes: no alignment needed
|
||||
lines.append((name, len(blob), len(payload)))
|
||||
blob += payload
|
||||
if len(blob) > ASSET_MAX:
|
||||
sys.exit(f"assets {len(blob)} B exceed the {ASSET_MAX} B asset area")
|
||||
return bytes(blob), lines, text_max
|
||||
|
||||
def main(self):
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit("usage: gen_assets.py <out.bin> <out.h>")
|
||||
blob, lines, text_max = self.build()
|
||||
with open(sys.argv[1], "wb") as f:
|
||||
f.write(blob)
|
||||
guard = self.guard.upper() + "_ASSETS_H"
|
||||
out = [f"/* Generated by gen_assets.py - do not edit ({len(blob)} B of assets). */",
|
||||
f"#ifndef {guard}", f"#define {guard}", "",
|
||||
f"#define TEXT_MAX {text_max}u /* longest text incl. NUL */"]
|
||||
for name, off, size in lines:
|
||||
out.append(f"#define {name} {off}u")
|
||||
out.append(f"#define {name}_LEN {size}u")
|
||||
for name, value in self.extra:
|
||||
out.append(f"#define {name} {value}u")
|
||||
out += ["", f"#endif /* {guard} */", ""]
|
||||
with open(sys.argv[2], "w") as f:
|
||||
f.write("\n".join(out))
|
||||
+67
-5
@@ -50,12 +50,17 @@
|
||||
#include "functions.h"
|
||||
#include "frequencies.h"
|
||||
#include "radio.h"
|
||||
#include "scheduler.h"
|
||||
#include "helper/battery.h"
|
||||
#include "settings.h"
|
||||
#include "misc.h" /* dBmCorrTable */
|
||||
|
||||
_Static_assert(sizeof(app_header_t) == 64, "app_header_t must be 64 bytes");
|
||||
_Static_assert(sizeof(app_api_t) <= UINT16_MAX, "app_api_t size field overflow");
|
||||
_Static_assert(APP_ASSET_OFFSET + APP_ASSET_MAX == APP_CODE_OFFSET,
|
||||
"assets must end where the code sector starts");
|
||||
_Static_assert(APP_ASSET_MAX <= APP_OVERLAY_MAX,
|
||||
"assets are CRC-checked through the overlay buffer");
|
||||
|
||||
enum {
|
||||
APP_AVAILABLE_CAPS = 0u
|
||||
@@ -420,6 +425,49 @@ static void app_cfg_save(const uint8_t *buf, uint8_t len)
|
||||
app_cfg_len = len; /* mark dirty; the loader commits after the app returns */
|
||||
}
|
||||
|
||||
_Static_assert(APP_CFG_OFFSET + sizeof(app_cfg_buf) <= APP_ASSET_OFFSET,
|
||||
"config area overlaps the assets");
|
||||
|
||||
/* ---- API level 2: time, randomness, read-only assets ---- */
|
||||
static uint16_t app_asset_size; /* verified asset size of the running app */
|
||||
static uint32_t app_rng_state = 0x2545F491u;
|
||||
|
||||
static uint32_t app_ticks_ms(void)
|
||||
{
|
||||
return SCHEDULER_GetTick10ms() * 10u;
|
||||
}
|
||||
|
||||
static uint32_t app_rand32(void)
|
||||
{
|
||||
uint32_t x = app_rng_state;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
app_rng_state = x;
|
||||
return x;
|
||||
}
|
||||
|
||||
/* Fold fresh entropy into the persistent state: RSSI noise LSBs, the SysTick
|
||||
* phase of the key press that launched the app, and the 10 ms counter. */
|
||||
static void app_rng_mix(void)
|
||||
{
|
||||
app_rng_state ^= ((uint32_t)BK4819_ReadRegister(BK4819_REG_67) << 16) ^
|
||||
SysTick->VAL ^ SCHEDULER_GetTick10ms();
|
||||
if (app_rng_state == 0u)
|
||||
app_rng_state = 0x2545F491u;
|
||||
app_rand32();
|
||||
}
|
||||
|
||||
static uint16_t app_asset_read(uint16_t offset, void *buf, uint16_t len)
|
||||
{
|
||||
if (offset >= app_asset_size)
|
||||
return 0;
|
||||
if (len > app_asset_size - offset)
|
||||
len = app_asset_size - offset;
|
||||
PY25Q16_ReadBuffer(APP_SLOT_BASE(app_run_slot) + APP_ASSET_OFFSET + offset, buf, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
/* ---- v2 battery / backlight ---- */
|
||||
static void app_draw_battery(void)
|
||||
{
|
||||
@@ -517,7 +565,8 @@ uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header)
|
||||
if (h.required_caps & ~APP_AVAILABLE_CAPS) return APP_ERR_CAP;
|
||||
if (h.code_size < 2u || h.code_size > APP_OVERLAY_MAX ||
|
||||
(uint32_t)h.entry_off > h.code_size - 2u || /* leave room for a 2-byte Thumb insn */
|
||||
(h.entry_off & 1u) != 0u) /* entry must be Thumb-aligned (even) */
|
||||
(h.entry_off & 1u) != 0u || /* entry must be Thumb-aligned (even) */
|
||||
h.asset_size > APP_ASSET_MAX)
|
||||
return APP_ERR_SIZE;
|
||||
|
||||
if (out_header)
|
||||
@@ -685,6 +734,9 @@ static const app_api_t app_api = {
|
||||
.beam_rx_poll = app_beam_rx_poll,
|
||||
.beam_draw = app_beam_draw,
|
||||
#endif
|
||||
.ticks_ms = app_ticks_ms,
|
||||
.rand32 = app_rand32,
|
||||
.asset_read = app_asset_read,
|
||||
};
|
||||
|
||||
uint8_t APP_LaunchOverlay(uint8_t slot)
|
||||
@@ -711,12 +763,20 @@ uint8_t APP_LaunchOverlay(uint8_t slot)
|
||||
|
||||
/* Repurpose the sector cache: drop any cached config sector, load the code
|
||||
* straight in (ReadBuffer bypasses the cache), and verify it in RAM before
|
||||
* trusting it. Zeroing first leaves the app's .bss clean. */
|
||||
* trusting it. Zeroing first leaves the app's .bss clean. The assets are
|
||||
* verified first through the same buffer: a slot written by a host that
|
||||
* does not know the asset area (older UV Studio) is refused here instead of
|
||||
* handing the app unprogrammed flash. */
|
||||
PY25Q16_InvalidateCache();
|
||||
bool assets_ok = true;
|
||||
if (h.asset_size) {
|
||||
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot) + APP_ASSET_OFFSET, ws, h.asset_size);
|
||||
assets_ok = (uint16_t)MB_Crc32Bytes(ws, h.asset_size) == h.asset_crc;
|
||||
}
|
||||
memset(ws, 0, APP_OVERLAY_MAX);
|
||||
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot) + APP_CODE_OFFSET, ws, h.code_size);
|
||||
|
||||
if (MB_Crc32Bytes(ws, h.code_size) != h.code_crc32) {
|
||||
if (!assets_ok || MB_Crc32Bytes(ws, h.code_size) != h.code_crc32) {
|
||||
PY25Q16_InvalidateCache();
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
RXTX_LOG_Resume();
|
||||
@@ -728,8 +788,10 @@ uint8_t APP_LaunchOverlay(uint8_t slot)
|
||||
__DSB();
|
||||
__ISB();
|
||||
|
||||
app_run_slot = slot; /* for cfg_load / cfg_save */
|
||||
app_cfg_len = 0;
|
||||
app_run_slot = slot; /* for cfg_load / cfg_save / asset_read */
|
||||
app_asset_size = h.asset_size;
|
||||
app_cfg_len = 0;
|
||||
app_rng_mix();
|
||||
#ifdef ENABLE_FMRADIO
|
||||
app_fm_dirty = false;
|
||||
#endif
|
||||
|
||||
+17
-3
@@ -39,6 +39,13 @@
|
||||
* ... ]
|
||||
* 0x120000 slot 15 ]
|
||||
*
|
||||
* Header sector layout (slot-relative):
|
||||
*
|
||||
* 0x000 app_header_t (64 B)
|
||||
* 0x040 per-app config staged by cfg_save (16 B, reserved up to 0x0FF)
|
||||
* 0x100 read-only assets (API level 2, up to 3840 B, CRC-checked at launch)
|
||||
* 0x1000 code (<= 4 KiB, copied into the overlay RAM)
|
||||
*
|
||||
* Host tooling (APP_SlotErase/Write/Info) touches the external flash only and is
|
||||
* never brick-critical; a bad slot is simply refused at launch by the CRC check.
|
||||
*/
|
||||
@@ -63,6 +70,12 @@
|
||||
* via compile-app.sh; the loader checks each blob's link_vma against it. ---- */
|
||||
#define APP_OVERLAY_MAX 0x00001000u /* 4 KiB */
|
||||
|
||||
/* ---- read-only assets, served by api->asset_read (API level 2) ----
|
||||
* Literal values so pack_app.py can parse them; checked against the slot
|
||||
* geometry by static asserts in app_overlay.c. */
|
||||
#define APP_ASSET_OFFSET 0x00000100u /* assets start in the header sector */
|
||||
#define APP_ASSET_MAX 0x00000F00u /* up to the end of the header sector */
|
||||
|
||||
/* ---- blob header (64 bytes, little-endian; see App/apps/pack_app.py) ---- */
|
||||
#define APP_MAGIC 0x31504146u /* "FAP1" */
|
||||
#define APP_HDR_VERSION 1u
|
||||
@@ -96,7 +109,8 @@ typedef struct __attribute__((packed)) {
|
||||
char version[APP_VERSION_LEN];/* app version string */
|
||||
uint32_t link_vma; /* RAM VMA the code was linked at */
|
||||
uint32_t required_caps; /* APP_CAP_* required by this app */
|
||||
uint8_t reserved[4]; /* pad to 64 bytes */
|
||||
uint16_t asset_size; /* bytes at APP_ASSET_OFFSET, 0 = none */
|
||||
uint16_t asset_crc; /* low 16 bits of the assets' CRC-32 */
|
||||
} app_header_t;
|
||||
|
||||
enum {
|
||||
@@ -105,8 +119,8 @@ enum {
|
||||
APP_ERR_MAGIC, /* no/invalid header */
|
||||
APP_ERR_ABI, /* ABI family/API level mismatch */
|
||||
APP_ERR_NOT_COMMITTED, /* image not marked complete */
|
||||
APP_ERR_SIZE, /* code_size out of range */
|
||||
APP_ERR_CRC, /* code CRC-32 mismatch */
|
||||
APP_ERR_SIZE, /* code_size or asset_size out of range */
|
||||
APP_ERR_CRC, /* code or asset CRC mismatch */
|
||||
APP_ERR_VMA, /* overlay buffer not at the link VMA */
|
||||
APP_ERR_AUTH, /* host write refused: timestamp mismatch */
|
||||
APP_ERR_CAP, /* required firmware capability missing */
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "beacon_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define LCD_WIDTH 128
|
||||
#define TICK_MS 50
|
||||
@@ -56,13 +57,8 @@
|
||||
|
||||
#define CFG_MAGIC 0xF4
|
||||
|
||||
/* Sentinel-prefixed Morse (leading 1 sentinel, then elements MSB-first: 0 dit, 1 dah). */
|
||||
static const uint8_t M_LET[26] = {0x05,0x18,0x1A,0x0C,0x02,0x12,0x0E,0x10,0x04,0x17,0x0D,0x14,0x07,0x06,0x0F,0x16,0x1D,0x0A,0x08,0x03,0x09,0x11,0x0B,0x19,0x1B,0x1C};
|
||||
static const uint8_t M_DIG[10] = {0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E};
|
||||
static const char FOX_TAIL[5] = {'E','I','S','H','5'};
|
||||
static const uint8_t BMP_TX[16] = {0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
|
||||
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
|
||||
/* Texts, fox identifiers, the Morse table and icons are read-only assets
|
||||
* (gen_assets.py); only the displayed item is fetched from external flash. */
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
@@ -75,6 +71,10 @@ static char msg[17];
|
||||
static uint8_t prevKey;
|
||||
static char str[16];
|
||||
|
||||
/* Text from the assets in a shared buffer, valid until the next T() call. */
|
||||
static char tbuf[TEXT_MAX];
|
||||
static char *T(uint16_t off){ A->asset_read(off,tbuf,TEXT_MAX); return tbuf; }
|
||||
|
||||
/* ---- tiny formatting ---- */
|
||||
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
|
||||
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||
@@ -105,26 +105,28 @@ static bool settingKey(uint8_t key,int8_t dir){
|
||||
/* ---- message ---- */
|
||||
static void buildMsg(void){
|
||||
char *o=msg;
|
||||
if(foxFox==FOX_CALL){
|
||||
if(foxCall[0]){ o=put(o,foxCall); o=put(o," MOE"); }
|
||||
else o=put(o,"MOE");
|
||||
} else if(foxFox==FOX_MO){ o=put(o,"MO"); }
|
||||
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
|
||||
uint8_t id=foxFox;
|
||||
if(foxFox==FOX_CALL){ /* "<call> MOE", or "MOE" alone */
|
||||
id=0;
|
||||
if(foxCall[0]){ o=put(o,foxCall); *o++=' '; }
|
||||
}
|
||||
o=put(o,T(FOX_ID+id*FOX_ID_STRIDE));
|
||||
*o='\0';
|
||||
}
|
||||
static void foxLabel(char *out){
|
||||
char *o=put(out,"FOX ");
|
||||
if(foxFox==FOX_CALL) o=put(o,"CALL");
|
||||
else if(foxFox==FOX_MO) o=put(o,"MO");
|
||||
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
|
||||
char *o=put(out,T(T_FOX));
|
||||
o=put(o,T(foxFox==FOX_CALL?T_CALL:FOX_ID+foxFox*FOX_ID_STRIDE));
|
||||
*o='\0';
|
||||
}
|
||||
static uint8_t morseByte(char c){
|
||||
uint8_t i, code;
|
||||
if(c>='a'&&c<='z') c-=32;
|
||||
if(c>='A'&&c<='Z') return M_LET[c-'A'];
|
||||
if(c>='0'&&c<='9') return M_DIG[c-'0'];
|
||||
if(c=='/') return 0x32;
|
||||
return 0;
|
||||
if(c>='A'&&c<='Z') i=(uint8_t)(c-'A');
|
||||
else if(c>='0'&&c<='9') i=(uint8_t)(26+c-'0');
|
||||
else if(c=='/') i=36;
|
||||
else return 0;
|
||||
A->asset_read(MORSE+i,&code,1);
|
||||
return code;
|
||||
}
|
||||
|
||||
/* ---- drawing ---- */
|
||||
@@ -133,10 +135,9 @@ static void tag(const char *s,uint8_t x,uint8_t line){ A->print_inverse(s,x,line
|
||||
static void chrome(void){
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->print_inverse("BEACON",2,0,true,true,26);
|
||||
A->print_inverse(T(T_BEACON),2,0,true,true,26);
|
||||
A->draw_battery();
|
||||
if(foxLocked) cpy(A->status_line+70,FONT_LOCK,sizeof(FONT_LOCK));
|
||||
else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F));
|
||||
if(foxLocked||fArm) A->asset_read(foxLocked?BMP_LOCK:BMP_F,A->status_line+70,BMP_F_LEN);
|
||||
{ char *o=putu(str,A->tx_freq()/100000u); *o++='.'; uint32_t fr=A->tx_freq()%100000u;
|
||||
for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
|
||||
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
|
||||
@@ -144,7 +145,7 @@ static void chrome(void){
|
||||
static void drawTxSeconds(void){
|
||||
uint8_t sec=(uint8_t)((txMsLeft+999u)/1000u);
|
||||
for(uint8_t x=0;x<LCD_WIDTH;x++) A->fb[0][x]=0;
|
||||
A->print_normal("TX",1,0,0);
|
||||
A->print_normal(T(T_TX),1,0,0);
|
||||
char *o=str; put2(&o,sec); *o++='s'; *o='\0';
|
||||
A->print_normal(str,(uint8_t)(127-slen(str)*7),0,0);
|
||||
secShown=sec;
|
||||
@@ -178,18 +179,18 @@ static void updateProgress(uint8_t vis){
|
||||
static void beaconDraw(bool txNow,uint8_t il){
|
||||
chrome();
|
||||
if(txNow){
|
||||
cpy(A->status_line+48,BMP_TX,16);
|
||||
A->asset_read(BMP_TX,A->status_line+48,BMP_TX_LEN);
|
||||
drawTxSeconds();
|
||||
drawMessage(charsSent);
|
||||
} else {
|
||||
A->print_string("IDLE",0,127,1,10);
|
||||
A->print_string(T(T_IDLE),0,127,1,10);
|
||||
char *o=str; put2(&o,il); *o++='s'; *o='\0';
|
||||
A->print_string(str,0,127,3,10);
|
||||
}
|
||||
char *o=put(str,"TX "); o=putu(o,beaconTx); o=put(o,"s"); *o='\0'; tag(str,4,5);
|
||||
o=put(str,"IDLE "); o=putu(o,beaconIdle); o=put(o,"s"); *o='\0'; tag(str,4,6);
|
||||
char *o=put(str,T(T_TX)); *o++=' '; o=putu(o,beaconTx); *o++='s'; *o='\0'; tag(str,4,5);
|
||||
o=put(str,T(T_IDLE)); *o++=' '; o=putu(o,beaconIdle); *o++='s'; *o='\0'; tag(str,4,6);
|
||||
foxLabel(str); tag(str,66,5);
|
||||
{ const char *m=carrier?"CARR":"TONE"; tag(m,(uint8_t)(125-slen(m)*4),5); }
|
||||
{ const char *m=T(carrier?T_CARR:T_TONE); tag(m,(uint8_t)(125-slen(m)*4),5); }
|
||||
}
|
||||
static void blit(void){ A->blit_status(); A->blit_full(); }
|
||||
|
||||
@@ -295,7 +296,7 @@ static void saveConfig(void){ uint8_t c[5]={CFG_MAGIC,beaconIdle,foxFox,beaconTx
|
||||
|
||||
static void txDenied(void){
|
||||
chrome();
|
||||
A->print_string("TX OFF",0,127,1,8);
|
||||
A->print_string(T(T_TX_OFF),0,127,1,8);
|
||||
blit();
|
||||
for(uint8_t i=0;i<20;i++) tickDelay();
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||
APP_NAME="Beacon" # <-- the only per-app line
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||
|
||||
@@ -24,14 +24,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
|
||||
--shortcut beacon >/dev/null
|
||||
--shortcut beacon --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# Beacon read-only assets: texts, fox identifiers, Morse table and icons.
|
||||
#
|
||||
# ./gen_assets.py beacon_assets.bin beacon_assets.h
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
a = Assets("BEACON")
|
||||
a.text("T_BEACON", "BEACON")
|
||||
a.text("T_TX", "TX")
|
||||
a.text("T_IDLE", "IDLE")
|
||||
a.text("T_FOX", "FOX ")
|
||||
a.text("T_CALL", "CALL")
|
||||
a.text("T_CARR", "CARR")
|
||||
a.text("T_TONE", "TONE")
|
||||
a.text("T_TX_OFF", "TX OFF")
|
||||
# Fox identifiers by foxFox (0..FOX_MO); FOX_CALL sends "<call> " + entry 0.
|
||||
a.table("FOX_ID", ["MOE", "MOI", "MOS", "MOH", "MO5", "MO"])
|
||||
# Sentinel-prefixed Morse (leading 1, then elements MSB-first: 0 dit, 1 dah):
|
||||
# A..Z, then 0..9, then '/'.
|
||||
a.u8("MORSE", [0x05,0x18,0x1A,0x0C,0x02,0x12,0x0E,0x10,0x04,0x17,0x0D,0x14,0x07,
|
||||
0x06,0x0F,0x16,0x1D,0x0A,0x08,0x03,0x09,0x11,0x0B,0x19,0x1B,0x1C,
|
||||
0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E,
|
||||
0x32])
|
||||
a.u8("BMP_TX", [0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c])
|
||||
a.u8("BMP_LOCK", [0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c])
|
||||
a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e])
|
||||
a.main()
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "beam_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define PACKET_MAGIC 0xBEA5u
|
||||
#define PACKET_VERSION 2u
|
||||
@@ -37,10 +38,6 @@ typedef struct {
|
||||
_Static_assert(sizeof(beam_payload_t) == 48u,
|
||||
"BEAM v2 wire payload layout changed");
|
||||
|
||||
static const uint16_t OBFUSCATION[8] = {
|
||||
0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9
|
||||
};
|
||||
|
||||
static const app_api_t *A;
|
||||
static uint32_t packet_store[18]; /* 72 bytes, 4-byte aligned for the payload */
|
||||
static uint16_t copiedChannel;
|
||||
@@ -71,23 +68,19 @@ static uint16_t crc16(const void *buffer, uint16_t size)
|
||||
|
||||
static void obfuscate(void)
|
||||
{
|
||||
uint16_t key[8]; /* obfuscation key, from the assets */
|
||||
uint16_t *p = packet();
|
||||
A->asset_read(OBFUSCATION, key, sizeof(key));
|
||||
for (uint8_t i = 0; i < 32u; i++)
|
||||
p[i + 1u] ^= OBFUSCATION[i & 7u];
|
||||
p[i + 1u] ^= key[i & 7u];
|
||||
}
|
||||
|
||||
static void draw(void)
|
||||
{
|
||||
const char *state;
|
||||
switch (status) {
|
||||
case STATUS_TX_WAIT: state = "SENDING"; break;
|
||||
case STATUS_TX_DONE: state = "SENT"; break;
|
||||
case STATUS_RX_WAIT: state = "WAITING"; break;
|
||||
case STATUS_RX_SAVED: state = "RECEIVED"; break;
|
||||
case STATUS_RX_FULL: state = "MEM FULL"; break;
|
||||
case STATUS_ERROR: state = "ERROR"; break;
|
||||
default: state = mode ? "BEAM RX" : "BEAM TX"; break;
|
||||
}
|
||||
/* T_STATE: READY in TX / RX mode, then one entry per later status. */
|
||||
char state[T_STATE_STRIDE];
|
||||
const uint8_t index = status ? (uint8_t)(status + 1u) : mode;
|
||||
A->asset_read(T_STATE + index * T_STATE_STRIDE, state, sizeof(state));
|
||||
A->beam_draw(state);
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
|
||||
@@ -3,8 +3,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")"
|
||||
APP_NAME="Beam"
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280}
|
||||
OUT="${APP_NAME// /}"
|
||||
|
||||
@@ -19,14 +19,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
|
||||
--shortcut beam --require beam >/dev/null
|
||||
--shortcut beam --require beam --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
# Beam read-only assets: status texts and the packet obfuscation key.
|
||||
#
|
||||
# ./gen_assets.py beam_assets.bin beam_assets.h
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
a = Assets("BEAM")
|
||||
# Indexed by beam_app.c: 0/1 = READY in TX/RX mode, then status + 1.
|
||||
a.table("T_STATE", ["BEAM TX", "BEAM RX", "SENDING", "SENT", "WAITING",
|
||||
"RECEIVED", "MEM FULL", "ERROR"])
|
||||
a.u16("OBFUSCATION", [0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9])
|
||||
a.main()
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "breakout_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define LCD_WIDTH 128
|
||||
#define BRICK_NUMBER 18
|
||||
@@ -70,12 +71,13 @@ static void u2str(char *out, const char *label, uint16_t v, uint8_t w) {
|
||||
static const app_api_t *A;
|
||||
|
||||
/* ---- game state (lives in the overlay; re-initialised at entry) ---- */
|
||||
static uint32_t randSeed;
|
||||
static uint8_t blockAnim;
|
||||
static bool isInitialized;
|
||||
static bool isPaused;
|
||||
static bool saverPaused, saverActive;
|
||||
static bool gameOver, redrawRequired;
|
||||
static bool title;
|
||||
static uint8_t titleTick;
|
||||
static uint8_t levelCountBreakout;
|
||||
static uint8_t gameOverLevel;
|
||||
static uint16_t tone;
|
||||
@@ -89,12 +91,11 @@ static Brick brick[BRICK_NUMBER];
|
||||
static Racket racket;
|
||||
static Ball ball;
|
||||
|
||||
static const uint8_t BRICK_ANIM_PATTERNS[4] =
|
||||
{0b00110001, 0b00101001, 0b00100101, 0b00100011};
|
||||
/* Text from the assets in a shared buffer, valid until the next T() call. */
|
||||
static char tbuf[TEXT_MAX];
|
||||
static char *T(uint16_t off) { A->asset_read(off, tbuf, TEXT_MAX); return tbuf; }
|
||||
|
||||
static void srand_custom(uint32_t seed) { randSeed = seed ? seed : 1; }
|
||||
static int rand_custom(void) { randSeed = randSeed * 1103515245u + 12345u; return (randSeed >> 16) & 0x7FFF; }
|
||||
static int randInt(int min, int max) { return min + (rand_custom() % (max - min + 1)); }
|
||||
static int randInt(int min, int max) { return min + (int)(A->rand32() % (uint32_t)(max - min + 1)); }
|
||||
|
||||
static void reset(void) { ballCount = BALL_NUMBER; levelCountBreakout = 1; score = 0; }
|
||||
|
||||
@@ -102,9 +103,9 @@ static void playBeep(uint16_t t) { A->play_tone(t, 100); }
|
||||
|
||||
static void drawScoreValues(uint8_t level, int16_t balls, uint16_t value) {
|
||||
A->status_clear();
|
||||
u2str(str, "Level ", level, 2); A->print_tiny(str, 0, 1, true, true);
|
||||
u2str(str, "Ball ", (balls < 0) ? 0 : (uint16_t)balls, 2); A->print_tiny(str, 45, 1, true, true);
|
||||
u2str(str, "Score ", value, 4); A->print_tiny(str, 88, 1, true, true);
|
||||
u2str(str, T(T_LEVEL), level, 2); A->print_tiny(str, 0, 1, true, true);
|
||||
u2str(str, T(T_BALL), (balls < 0) ? 0 : (uint16_t)balls, 2); A->print_tiny(str, 45, 1, true, true);
|
||||
u2str(str, T(T_SCORE), value, 4); A->print_tiny(str, 88, 1, true, true);
|
||||
}
|
||||
|
||||
static void drawScore(void) { drawScoreValues(levelCountBreakout, ballCount, score); }
|
||||
@@ -154,7 +155,7 @@ static void drawBall(void) {
|
||||
gameOver = true;
|
||||
reset(); initWall(); /* drawWall drawn by main loop */
|
||||
isPaused = true;
|
||||
A->print_bold("GAME OVER", 0, LCD_WIDTH - 1, 4);
|
||||
A->print_bold(T(T_GAME_OVER), 0, LCD_WIDTH - 1, 4);
|
||||
}
|
||||
initRacket(); initBall();
|
||||
}
|
||||
@@ -162,6 +163,8 @@ static void drawBall(void) {
|
||||
}
|
||||
|
||||
static void drawWall(bool checkCollision) {
|
||||
uint8_t anim[BRICK_ANIM_LEN]; /* brick patterns, read once per frame */
|
||||
A->asset_read(BRICK_ANIM, anim, sizeof(anim));
|
||||
for (uint8_t i = 0; i < BRICK_NUMBER; i++) {
|
||||
if (brick[i].destroy) continue;
|
||||
uint8_t *fb_ptr = A->fb[brick[i].y / 8] + brick[i].x;
|
||||
@@ -187,7 +190,7 @@ static void drawWall(bool checkCollision) {
|
||||
}
|
||||
} else {
|
||||
for (uint8_t k = 0; k < 13; k++)
|
||||
fb_ptr[k + 1] = BRICK_ANIM_PATTERNS[(blockAnim + k) % 4];
|
||||
fb_ptr[k + 1] = anim[(blockAnim + k) % 4];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,10 +222,25 @@ static void redrawScene(void) {
|
||||
racket.p = racket.x;
|
||||
renderBall(true);
|
||||
if (isPaused)
|
||||
A->print_bold(gameOver ? "GAME OVER" : "PAUSE", 0, LCD_WIDTH - 1, 4);
|
||||
A->print_bold(T(gameOver ? T_GAME_OVER : T_PAUSE), 0, LCD_WIDTH - 1, 4);
|
||||
}
|
||||
|
||||
/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers,
|
||||
* with a blinking prompt on the blank bottom page. */
|
||||
static void drawTitle(void) {
|
||||
titleTick++;
|
||||
A->asset_read(ART_TITLE, A->status_line, LCD_WIDTH);
|
||||
A->asset_read(ART_TITLE + LCD_WIDTH, A->fb[0], 7u * LCD_WIDTH);
|
||||
if (titleTick & 32u)
|
||||
A->print_bold(T(T_PRESS), 0, LCD_WIDTH - 1, 6);
|
||||
}
|
||||
|
||||
static void OnKeyDown(uint8_t key) {
|
||||
if (title) { /* MENU starts the game (and pauses it later on) */
|
||||
if (key == APP_KEY_MENU) { title = false; redrawRequired = true; }
|
||||
else if (key == APP_KEY_EXIT) isInitialized = false;
|
||||
return;
|
||||
}
|
||||
bool wasPaused = isPaused;
|
||||
switch (key) {
|
||||
case APP_KEY_4:
|
||||
@@ -231,7 +249,7 @@ static void OnKeyDown(uint8_t key) {
|
||||
case APP_KEY_DOWN: if (!isPaused && racket.x < 102) racket.x += 2; isPaused = false; break;
|
||||
case APP_KEY_MENU:
|
||||
isPaused = !isPaused;
|
||||
if (isPaused) A->print_bold("PAUSE", 0, LCD_WIDTH - 1, 4);
|
||||
if (isPaused) A->print_bold(T(T_PAUSE), 0, LCD_WIDTH - 1, 4);
|
||||
break;
|
||||
case APP_KEY_EXIT: isPaused = false; isInitialized = false; break;
|
||||
}
|
||||
@@ -282,7 +300,7 @@ void app_main(const app_api_t *api) {
|
||||
#ifdef APP_POC_HELLO
|
||||
/* Minimal bisection test: prove jump + ABI + return work, no game body. */
|
||||
A->display_clear();
|
||||
A->print_bold("OVL HELLO", 0, LCD_WIDTH - 1, 3);
|
||||
A->print_bold(T(T_HELLO), 0, LCD_WIDTH - 1, 3);
|
||||
A->blit_full();
|
||||
A->delay_ms(2000);
|
||||
return;
|
||||
@@ -295,10 +313,6 @@ void app_main(const app_api_t *api) {
|
||||
kbdPrev = APP_KEY_INVALID; kbdCur = APP_KEY_INVALID;
|
||||
uint8_t swap = 0;
|
||||
|
||||
/* The low BK4819 counter bits vary continuously; mix them with the tuned
|
||||
* frequency instead of carrying a launch-only seed field in every ABI
|
||||
* table. */
|
||||
srand_custom(((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq());
|
||||
A->led(false);
|
||||
A->backlight_on();
|
||||
|
||||
@@ -306,6 +320,7 @@ void app_main(const app_api_t *api) {
|
||||
reset(); initWall(); initRacket(); initBall();
|
||||
A->status_clear();
|
||||
isInitialized = true;
|
||||
title = true;
|
||||
|
||||
while (isInitialized) {
|
||||
handleInput();
|
||||
@@ -316,6 +331,15 @@ void app_main(const app_api_t *api) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (title) { /* the game scene is redrawn when MENU leaves the title */
|
||||
drawTitle();
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
A->backlight_update();
|
||||
A->delay_ms(TICK_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (redrawRequired) {
|
||||
redrawScene();
|
||||
redrawRequired = false;
|
||||
|
||||
@@ -8,8 +8,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||
APP_NAME="Breakout" # <-- the only per-app line
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||
|
||||
@@ -24,13 +24,14 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
# Breakout read-only assets: texts, the brick animation patterns and the
|
||||
# 128x64 title screen.
|
||||
#
|
||||
# ./gen_assets.py breakout_assets.bin breakout_assets.h [preview.pgm]
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
from app_art import Canvas
|
||||
|
||||
BRICK_ANIM = [0b00110001, 0b00101001, 0b00100101, 0b00100011]
|
||||
|
||||
def brick(cv, x, y):
|
||||
"""A 15 px brick as drawn by the game: end caps and the animated fill."""
|
||||
for k in range(15):
|
||||
col = 0b00011110 if k in (0, 14) else BRICK_ANIM[(k - 1) % 4]
|
||||
for bit in range(8):
|
||||
if col >> bit & 1:
|
||||
cv.set(x + k, y + bit)
|
||||
|
||||
def title_screen():
|
||||
"""BREAK / OUT, a few bricks, the racket and the ball with its trail."""
|
||||
cv = Canvas()
|
||||
cv.two_words("BREAK", "OUT")
|
||||
for x in (2, 22, 86, 106):
|
||||
brick(cv, x, 42)
|
||||
for k in range(24): # racket, as renderRacket()
|
||||
cv.set(52 + k, 54)
|
||||
if 0 < k < 23:
|
||||
cv.set(52 + k, 53); cv.set(52 + k, 55)
|
||||
cv.rect(62, 47, 64, 49) # ball, as renderBall()
|
||||
cv.set(61, 48); cv.set(65, 48)
|
||||
for i in range(0, 8, 2): # dotted trail
|
||||
cv.set(58 - i * 2, 47 - i // 2)
|
||||
return cv
|
||||
|
||||
preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None
|
||||
|
||||
a = Assets("BREAKOUT")
|
||||
a.text("T_LEVEL", "Level ")
|
||||
a.text("T_BALL", "Ball ")
|
||||
a.text("T_SCORE", "Score ")
|
||||
a.text("T_GAME_OVER", "GAME OVER")
|
||||
a.text("T_PAUSE", "PAUSE")
|
||||
a.text("T_HELLO", "OVL HELLO") # APP_POC_HELLO bisection build only
|
||||
a.text("T_PRESS", "PRESS MENU")
|
||||
a.u8("BRICK_ANIM", BRICK_ANIM)
|
||||
title = title_screen()
|
||||
a.raw("ART_TITLE", title.pages())
|
||||
a.main()
|
||||
if preview_path:
|
||||
title.preview(preview_path)
|
||||
@@ -8,8 +8,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||
APP_NAME="Cube3D" # <-- the only per-app line
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||
|
||||
@@ -24,13 +24,14 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
@@ -23,17 +23,21 @@
|
||||
* the signed area of its projected polygon shows it facing us).
|
||||
* - WIRE : every edge, with the far hemisphere dotted for a depth cue.
|
||||
* Edges use a self-clipped Bresenham writing the full 64 rows directly, since the
|
||||
* resident pixel helper does not bound-check. The eight solids' faces (with a
|
||||
* uniform outward winding) are generated offline by a convex-hull extractor, so
|
||||
* nothing here has to be hand-wound. Pure compute, no radio.
|
||||
* resident pixel helper does not bound-check. The solids, the sine table and the
|
||||
* speeds are read-only assets (gen_assets.py; faces with a uniform outward
|
||||
* winding from an offline convex-hull extractor). They are copied onto the stack
|
||||
* (the sine once, the displayed solid every frame), so none of them occupies the
|
||||
* 4 KiB overlay and a solid can be added without touching this file.
|
||||
* Pure compute, no radio.
|
||||
*
|
||||
* Keys: UP/DOWN speed · 1-8 shape · STAR next shape · F solid/wire ·
|
||||
* Keys: UP/DOWN speed · 1-9 shape · STAR next shape · F solid/wire ·
|
||||
* MENU pause · EXIT quit.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "../app_api.h"
|
||||
#include "cube3d_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define W 128
|
||||
#define H 64
|
||||
@@ -41,20 +45,10 @@
|
||||
#define CY 32 /* projection centre y */
|
||||
#define DIST 150 /* camera distance along +z (keeps zc > 0) */
|
||||
#define FOCAL 80 /* focal length / field-of-view scale */
|
||||
#define MAXV 12 /* largest vertex count across the solids */
|
||||
#define MAXV SHAPE_MAXV /* largest vertex count across the solids */
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
/* One Q14 sine quadrant. Symmetry recovers the full 256-step wave while saving
|
||||
* 382 bytes for the renderer. */
|
||||
static const int16_t SIN_Q[65] = {
|
||||
0, 402, 804, 1205, 1606, 2006, 2404, 2801, 3196, 3590, 3981,
|
||||
4370, 4756, 5139, 5520, 5897, 6270, 6639, 7005, 7366, 7723, 8076,
|
||||
8423, 8765, 9102, 9434, 9760,10080,10394,10702,11003,11297,11585,
|
||||
11866,12140,12406,12665,12916,13160,13395,13623,13842,14053,14256,
|
||||
14449,14635,14811,14978,15137,15286,15426,15557,15679,15791,15893,
|
||||
15986,16069,16143,16207,16261,16305,16340,16364,16379,16384
|
||||
};
|
||||
static const int16_t *sinq; /* Q14 sine quadrant, on app_main's stack */
|
||||
|
||||
static int sin8(uint8_t angle)
|
||||
{
|
||||
@@ -62,70 +56,13 @@ static int sin8(uint8_t angle)
|
||||
uint8_t i = angle & 63u;
|
||||
if (quadrant & 1u)
|
||||
i = (uint8_t)(64u - i);
|
||||
const int value = SIN_Q[i];
|
||||
const int value = sinq[i];
|
||||
return quadrant >= 2u ? -value : value;
|
||||
}
|
||||
|
||||
/* A face is a polygon of up to 6 vertex indices, wound CCW as seen from outside
|
||||
* (generated offline by the hull extractor, so the signed-area cull sign is the
|
||||
* same for every solid). Unused slots are 0-padded and ignored (n gives length). */
|
||||
typedef struct { uint8_t n; uint8_t v[6]; } face_t;
|
||||
|
||||
static const int8_t CUBE_V[8][3]={{-26,-26,-26},{26,-26,-26},{26,26,-26},{-26,26,-26},{-26,-26,26},{26,-26,26},{26,26,26},{-26,26,26}};
|
||||
static const face_t CUBE_F[6]={{4,{2,1,0,3,0,0}},{4,{4,0,1,5,0,0}},{4,{7,3,0,4,0,0}},{4,{5,1,2,6,0,0}},{4,{6,2,3,7,0,0}},{4,{7,4,5,6,0,0}}};
|
||||
|
||||
static const int8_t OCTAHEDRON_V[6][3]={{38,0,0},{-38,0,0},{0,38,0},{0,-38,0},{0,0,38},{0,0,-38}};
|
||||
static const face_t OCTAHEDRON_F[8]={{3,{4,0,2,0,0,0}},{3,{2,0,5,0,0,0}},{3,{3,0,4,0,0,0}},{3,{5,0,3,0,0,0}},{3,{2,1,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{4,1,3,0,0,0}},{3,{3,1,5,0,0,0}}};
|
||||
|
||||
static const int8_t TETRAHEDRON_V[4][3]={{28,28,28},{28,-28,-28},{-28,28,-28},{-28,-28,28}};
|
||||
static const face_t TETRAHEDRON_F[4]={{3,{2,0,1,0,0,0}},{3,{1,0,3,0,0,0}},{3,{3,0,2,0,0,0}},{3,{2,1,3,0,0,0}}};
|
||||
|
||||
static const int8_t DIAMOND_V[8][3]={{30,0,0},{14,26,0},{-14,26,0},{-30,0,0},{-14,-26,0},{14,-26,0},{0,0,40},{0,0,-40}};
|
||||
static const face_t DIAMOND_F[12]={{3,{6,0,1,0,0,0}},{3,{1,0,7,0,0,0}},{3,{5,0,6,0,0,0}},{3,{7,0,5,0,0,0}},{3,{6,1,2,0,0,0}},{3,{2,1,7,0,0,0}},{3,{6,2,3,0,0,0}},{3,{3,2,7,0,0,0}},{3,{6,3,4,0,0,0}},{3,{4,3,7,0,0,0}},{3,{6,4,5,0,0,0}},{3,{5,4,7,0,0,0}}};
|
||||
|
||||
static const int8_t ICOSAHEDRON_V[12][3]={{0,18,29},{0,18,-29},{0,-18,29},{0,-18,-29},{18,29,0},{18,-29,0},{-18,29,0},{-18,-29,0},{29,0,18},{29,0,-18},{-29,0,18},{-29,0,-18}};
|
||||
static const face_t ICOSAHEDRON_F[20]={{3,{8,0,2,0,0,0}},{3,{2,0,10,0,0,0}},{3,{6,0,4,0,0,0}},{3,{4,0,8,0,0,0}},{3,{10,0,6,0,0,0}},{3,{3,1,9,0,0,0}},{3,{11,1,3,0,0,0}},{3,{4,1,6,0,0,0}},{3,{9,1,4,0,0,0}},{3,{6,1,11,0,0,0}},{3,{5,2,7,0,0,0}},{3,{8,2,5,0,0,0}},{3,{7,2,10,0,0,0}},{3,{7,3,5,0,0,0}},{3,{5,3,9,0,0,0}},{3,{11,3,7,0,0,0}},{3,{9,4,8,0,0,0}},{3,{8,5,9,0,0,0}},{3,{10,6,11,0,0,0}},{3,{11,7,10,0,0,0}}};
|
||||
|
||||
static const int8_t CUBOCTA_V[12][3]={{-24,-24,0},{-24,24,0},{24,-24,0},{24,24,0},{-24,0,-24},{-24,0,24},{24,0,-24},{24,0,24},{0,-24,-24},{0,-24,24},{0,24,-24},{0,24,24}};
|
||||
static const face_t CUBOCTA_F[14]={{4,{4,0,5,1,0,0}},{4,{2,9,0,8,0,0}},{3,{8,0,4,0,0,0}},{3,{5,0,9,0,0,0}},{4,{10,1,11,3,0,0}},{3,{4,1,10,0,0,0}},{3,{11,1,5,0,0,0}},{4,{3,7,2,6,0,0}},{3,{6,2,8,0,0,0}},{3,{9,2,7,0,0,0}},{3,{10,3,6,0,0,0}},{3,{7,3,11,0,0,0}},{4,{8,4,10,6,0,0}},{4,{7,11,5,9,0,0}}};
|
||||
|
||||
static const int8_t HEXPRISM_V[12][3]={{26,0,24},{12,22,24},{-12,22,24},{-26,0,24},{-12,-22,24},{12,-22,24},{26,0,-24},{12,22,-24},{-12,22,-24},{-26,0,-24},{-12,-22,-24},{12,-22,-24}};
|
||||
static const face_t HEXPRISM_F[8]={{6,{4,5,0,1,2,3}},{4,{7,1,0,6,0,0}},{4,{6,0,5,11,0,0}},{4,{8,2,1,7,0,0}},{4,{9,3,2,8,0,0}},{4,{10,4,3,9,0,0}},{4,{11,5,4,10,0,0}},{6,{9,8,7,6,11,10}}};
|
||||
|
||||
static const int8_t PENTAGEM_V[7][3]={{28,0,0},{8,26,0},{-22,16,0},{-22,-16,0},{8,-26,0},{0,0,42},{0,0,-42}};
|
||||
static const face_t PENTAGEM_F[10]={{3,{5,0,1,0,0,0}},{3,{1,0,6,0,0,0}},{3,{4,0,5,0,0,0}},{3,{6,0,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{2,1,6,0,0,0}},{3,{5,2,3,0,0,0}},{3,{3,2,6,0,0,0}},{3,{5,3,4,0,0,0}},{3,{4,3,6,0,0,0}}};
|
||||
|
||||
typedef struct {
|
||||
const int8_t (*v)[3];
|
||||
const face_t *f;
|
||||
uint8_t nv;
|
||||
uint8_t nf;
|
||||
const char *name;
|
||||
uint8_t namelen;
|
||||
} shape_t;
|
||||
|
||||
#define NSHAPE 8
|
||||
static const shape_t SHAPES[NSHAPE] = {
|
||||
{ CUBE_V, CUBE_F, 8, 6, "CUBE", 4 },
|
||||
{ OCTAHEDRON_V, OCTAHEDRON_F, 6, 8, "OCTAHEDRON", 10 },
|
||||
{ TETRAHEDRON_V, TETRAHEDRON_F, 4, 4, "TETRAHEDRON", 11 },
|
||||
{ DIAMOND_V, DIAMOND_F, 8, 12, "DIAMOND", 7 },
|
||||
{ ICOSAHEDRON_V, ICOSAHEDRON_F, 12, 20, "ICOSAHEDRON", 11 },
|
||||
{ CUBOCTA_V, CUBOCTA_F, 12, 14, "CUBOCTA", 7 },
|
||||
{ HEXPRISM_V, HEXPRISM_F, 12, 8, "HEXPRISM", 8 },
|
||||
{ PENTAGEM_V, PENTAGEM_F, 7, 10, "PENTAGEM", 8 },
|
||||
};
|
||||
|
||||
/* Per-frame projected screen coords + rotated depth of each vertex. */
|
||||
static int16_t px[MAXV], py[MAXV], pz[MAXV];
|
||||
|
||||
/* Quarter-half-units per frame. The low end has fractional angular steps;
|
||||
* level 16 reaches the old 16 half-units/frame once divided by four. */
|
||||
static const uint8_t ROT_RATE[16] = {
|
||||
1, 2, 3, 4, 6, 8, 10, 12,
|
||||
16, 20, 24, 30, 36, 44, 52, 64
|
||||
};
|
||||
|
||||
/* Set one pixel across the full 64 rows: 0..7 -> status line, 8..63 -> fb. */
|
||||
static void set_pixel(int x, int y)
|
||||
{
|
||||
@@ -168,13 +105,15 @@ static void clear_screen(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* Signed area of a face's projected polygon (<0 == facing us, calibrated). */
|
||||
static int face_area(const face_t *f)
|
||||
/* Signed area of a packed face's projected polygon (<0 == facing us). */
|
||||
static int face_area(const uint8_t *f)
|
||||
{
|
||||
const uint8_t n = f[0];
|
||||
const uint8_t *v = f + 1;
|
||||
int sa = 0;
|
||||
for (uint8_t k = 0; k < f->n; k++) {
|
||||
const uint8_t a = f->v[k];
|
||||
const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u];
|
||||
for (uint8_t k = 0; k < n; k++) {
|
||||
const uint8_t a = v[k];
|
||||
const uint8_t b = v[(k + 1u == n) ? 0u : k + 1u];
|
||||
sa += (int)px[a] * py[b] - (int)px[b] * py[a];
|
||||
}
|
||||
return sa;
|
||||
@@ -187,6 +126,16 @@ void app_main(const app_api_t *api)
|
||||
A->backlight_on();
|
||||
A->status_clear();
|
||||
|
||||
/* Assets copied to the stack: the sine once, the displayed solid every
|
||||
* frame (nv, nf, name + NUL, vertices, packed faces; see gen_assets.py). */
|
||||
int16_t sin_q[SIN_Q_LEN / 2u];
|
||||
uint8_t rec[SHAPE_REC_MAX];
|
||||
uint8_t nshape;
|
||||
A->asset_read(SIN_Q, sin_q, sizeof(sin_q));
|
||||
sinq = sin_q;
|
||||
if (A->asset_read(SHAPES, &nshape, 1) != 1u || nshape == 0u)
|
||||
return;
|
||||
|
||||
uint16_t ax = 0, ay = 0, az = 0; /* Q2 half-units: 2048 = full turn */
|
||||
uint8_t shape = 0;
|
||||
uint8_t speed = 4; /* 1..16, shared by all three rotation axes */
|
||||
@@ -222,14 +171,15 @@ void app_main(const app_api_t *api)
|
||||
paused = !paused;
|
||||
break;
|
||||
case APP_KEY_STAR:
|
||||
shape = (uint8_t)((shape + 1u) % NSHAPE);
|
||||
shape = (uint8_t)((shape + 1u) % nshape);
|
||||
break;
|
||||
case APP_KEY_F:
|
||||
wire = !wire;
|
||||
break;
|
||||
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3: case APP_KEY_4:
|
||||
case APP_KEY_5: case APP_KEY_6: case APP_KEY_7: case APP_KEY_8:
|
||||
if ((uint8_t)(key - APP_KEY_1) < NSHAPE)
|
||||
case APP_KEY_9:
|
||||
if ((uint8_t)(key - APP_KEY_1) < nshape)
|
||||
shape = (uint8_t)(key - APP_KEY_1);
|
||||
break;
|
||||
default:
|
||||
@@ -240,14 +190,23 @@ void app_main(const app_api_t *api)
|
||||
if (!running)
|
||||
break;
|
||||
|
||||
const shape_t *s = &SHAPES[shape];
|
||||
uint8_t entry[3]; /* directory: u16 record offset, u8 record length */
|
||||
A->asset_read(SHAPES + 1u + 3u * shape, entry, sizeof(entry));
|
||||
const uint8_t reclen = entry[2] < sizeof(rec) ? entry[2] : (uint8_t)sizeof(rec);
|
||||
A->asset_read(SHAPES + (uint16_t)(entry[0] | (entry[1] << 8)), rec, reclen);
|
||||
const uint8_t nv = rec[0] < MAXV ? rec[0] : MAXV;
|
||||
const uint8_t nf = rec[1];
|
||||
uint8_t namelen = 0;
|
||||
while (2u + namelen < reclen && rec[2u + namelen])
|
||||
namelen++;
|
||||
const int8_t (*verts)[3] = (const int8_t (*)[3])&rec[3u + namelen];
|
||||
const uint8_t ia = (uint8_t)(ax >> 3), ib = (uint8_t)(ay >> 3), ic = (uint8_t)(az >> 3);
|
||||
const int cx = sin8((uint8_t)(ia + 64u)), sxr = sin8(ia);
|
||||
const int cy = sin8((uint8_t)(ib + 64u)), syr = sin8(ib);
|
||||
const int cz = sin8((uint8_t)(ic + 64u)), szr = sin8(ic);
|
||||
|
||||
for (uint8_t i = 0; i < s->nv; i++) {
|
||||
int x = s->v[i][0], y = s->v[i][1], z = s->v[i][2];
|
||||
for (uint8_t i = 0; i < nv; i++) {
|
||||
int x = verts[i][0], y = verts[i][1], z = verts[i][2];
|
||||
int ny = (y * cx - z * sxr) >> 14; /* Rx */
|
||||
int nz = (y * sxr + z * cx) >> 14;
|
||||
y = ny; z = nz;
|
||||
@@ -265,29 +224,32 @@ void app_main(const app_api_t *api)
|
||||
}
|
||||
|
||||
clear_screen();
|
||||
for (uint8_t i = 0; i < s->nf; i++) {
|
||||
const face_t *f = &s->f[i];
|
||||
if (!wire && face_area(f) >= 0)
|
||||
continue; /* hidden face culled */
|
||||
for (uint8_t k = 0; k < f->n; k++) {
|
||||
const uint8_t a = f->v[k];
|
||||
const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u];
|
||||
const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */
|
||||
draw_edge(px[a], py[a], px[b], py[b], dotted);
|
||||
const uint8_t *f = &rec[3u + namelen + 3u * rec[0]]; /* packed faces */
|
||||
for (uint8_t i = 0; i < nf; i++) {
|
||||
const uint8_t n = f[0];
|
||||
if (wire || face_area(f) < 0) { /* else hidden face culled */
|
||||
for (uint8_t k = 0; k < n; k++) {
|
||||
const uint8_t a = f[1u + k];
|
||||
const uint8_t b = f[(k + 1u == n) ? 1u : 2u + k];
|
||||
const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */
|
||||
draw_edge(px[a], py[a], px[b], py[b], dotted);
|
||||
}
|
||||
}
|
||||
f += 1u + n;
|
||||
}
|
||||
|
||||
/* Shape name: inverse label, top-left of the status bar (scan-list look). */
|
||||
const uint8_t end = (uint8_t)(2u + 4u * s->namelen);
|
||||
const uint8_t end = (uint8_t)(2u + 4u * namelen);
|
||||
for (uint8_t i = 0; i <= end; i++)
|
||||
A->status_line[i] = 0;
|
||||
A->print_inverse(s->name, 2, 0, true, true, end);
|
||||
A->print_inverse((const char *)&rec[2], 2, 0, true, true, end);
|
||||
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
|
||||
if (!paused) {
|
||||
const uint16_t rate = ROT_RATE[speed - 1u];
|
||||
uint8_t rate;
|
||||
A->asset_read(ROT_RATE + speed - 1u, &rate, 1);
|
||||
ax += rate;
|
||||
ay += (uint16_t)(rate + rate / 2u);
|
||||
az += (uint16_t)((rate + 1u) / 2u);
|
||||
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
# Cube3D read-only assets: the solids, the Q14 sine quadrant and the speeds.
|
||||
#
|
||||
# SHAPES layout (offsets relative to SHAPES):
|
||||
# 0 count N
|
||||
# 1 + 3*k solid k directory entry: u16 record offset, u8 record length
|
||||
# record nv, nf, name + NUL, nv x (x, y, z) int8, then nf packed faces
|
||||
# (n, v0 .. v{n-1}), each wound CCW as seen from outside
|
||||
# The faces were generated offline by a convex-hull extractor, so every solid
|
||||
# shares the same outward winding and the signed-area cull sign. SHAPE_MAXV and
|
||||
# SHAPE_REC_MAX size the app's buffers, so adding a solid needs no C change.
|
||||
#
|
||||
# ./gen_assets.py cube3d_assets.bin cube3d_assets.h
|
||||
import os, struct, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
MAXN = 6 # largest face polygon
|
||||
|
||||
SHAPES = [
|
||||
("CUBE",
|
||||
[(-26,-26,-26),(26,-26,-26),(26,26,-26),(-26,26,-26),(-26,-26,26),(26,-26,26),(26,26,26),(-26,26,26)],
|
||||
[(2,1,0,3),(4,0,1,5),(7,3,0,4),(5,1,2,6),(6,2,3,7),(7,4,5,6)]),
|
||||
("OCTAHEDRON",
|
||||
[(38,0,0),(-38,0,0),(0,38,0),(0,-38,0),(0,0,38),(0,0,-38)],
|
||||
[(4,0,2),(2,0,5),(3,0,4),(5,0,3),(2,1,4),(5,1,2),(4,1,3),(3,1,5)]),
|
||||
("TETRAHEDRON",
|
||||
[(28,28,28),(28,-28,-28),(-28,28,-28),(-28,-28,28)],
|
||||
[(2,0,1),(1,0,3),(3,0,2),(2,1,3)]),
|
||||
("DIAMOND",
|
||||
[(30,0,0),(14,26,0),(-14,26,0),(-30,0,0),(-14,-26,0),(14,-26,0),(0,0,40),(0,0,-40)],
|
||||
[(6,0,1),(1,0,7),(5,0,6),(7,0,5),(6,1,2),(2,1,7),(6,2,3),(3,2,7),(6,3,4),(4,3,7),(6,4,5),(5,4,7)]),
|
||||
("ICOSAHEDRON",
|
||||
[(0,18,29),(0,18,-29),(0,-18,29),(0,-18,-29),(18,29,0),(18,-29,0),(-18,29,0),(-18,-29,0),(29,0,18),(29,0,-18),(-29,0,18),(-29,0,-18)],
|
||||
[(8,0,2),(2,0,10),(6,0,4),(4,0,8),(10,0,6),(3,1,9),(11,1,3),(4,1,6),(9,1,4),(6,1,11),
|
||||
(5,2,7),(8,2,5),(7,2,10),(7,3,5),(5,3,9),(11,3,7),(9,4,8),(8,5,9),(10,6,11),(11,7,10)]),
|
||||
("CUBOCTA",
|
||||
[(-24,-24,0),(-24,24,0),(24,-24,0),(24,24,0),(-24,0,-24),(-24,0,24),(24,0,-24),(24,0,24),(0,-24,-24),(0,-24,24),(0,24,-24),(0,24,24)],
|
||||
[(4,0,5,1),(2,9,0,8),(8,0,4),(5,0,9),(10,1,11,3),(4,1,10),(11,1,5),(3,7,2,6),(6,2,8),(9,2,7),(10,3,6),(7,3,11),(8,4,10,6),(7,11,5,9)]),
|
||||
("HEXPRISM",
|
||||
[(26,0,24),(12,22,24),(-12,22,24),(-26,0,24),(-12,-22,24),(12,-22,24),(26,0,-24),(12,22,-24),(-12,22,-24),(-26,0,-24),(-12,-22,-24),(12,-22,-24)],
|
||||
[(4,5,0,1,2,3),(7,1,0,6),(6,0,5,11),(8,2,1,7),(9,3,2,8),(10,4,3,9),(11,5,4,10),(9,8,7,6,11,10)]),
|
||||
("PENTAGEM",
|
||||
[(28,0,0),(8,26,0),(-22,16,0),(-22,-16,0),(8,-26,0),(0,0,42),(0,0,-42)],
|
||||
[(5,0,1),(1,0,6),(4,0,5),(6,0,4),(5,1,2),(2,1,6),(5,2,3),(3,2,6),(5,3,4),(4,3,6)]),
|
||||
]
|
||||
|
||||
# One Q14 sine quadrant: symmetry recovers the full 256-step wave.
|
||||
SIN_Q = [
|
||||
0, 402, 804, 1205, 1606, 2006, 2404, 2801, 3196, 3590, 3981,
|
||||
4370, 4756, 5139, 5520, 5897, 6270, 6639, 7005, 7366, 7723, 8076,
|
||||
8423, 8765, 9102, 9434, 9760,10080,10394,10702,11003,11297,11585,
|
||||
11866,12140,12406,12665,12916,13160,13395,13623,13842,14053,14256,
|
||||
14449,14635,14811,14978,15137,15286,15426,15557,15679,15791,15893,
|
||||
15986,16069,16143,16207,16261,16305,16340,16364,16379,16384,
|
||||
]
|
||||
|
||||
# Quarter-half-units per frame for speed levels 1..16. The low end has
|
||||
# fractional angular steps; level 16 reaches 16 half-units/frame once divided
|
||||
# by four.
|
||||
ROT_RATE = [1, 2, 3, 4, 6, 8, 10, 12, 16, 20, 24, 30, 36, 44, 52, 64]
|
||||
|
||||
def record(name, verts, faces):
|
||||
out = bytes([len(verts), len(faces)]) + name.encode("ascii") + b"\x00"
|
||||
for v in verts:
|
||||
out += struct.pack("<3b", *v)
|
||||
for f in faces:
|
||||
if not 3 <= len(f) <= MAXN or any(i >= len(verts) for i in f):
|
||||
sys.exit(f"{name}: bad face {f}")
|
||||
out += bytes([len(f), *f])
|
||||
if len(out) > 255:
|
||||
sys.exit(f"{name}: record {len(out)} B exceeds 255 B")
|
||||
return out
|
||||
|
||||
records = [record(*s) for s in SHAPES]
|
||||
base = 1 + 3 * len(records)
|
||||
shapes = bytes([len(records)])
|
||||
body = b""
|
||||
for r in records:
|
||||
shapes += struct.pack("<HB", base + len(body), len(r))
|
||||
body += r
|
||||
|
||||
a = Assets("CUBE3D")
|
||||
a.raw("SHAPES", shapes + body)
|
||||
a.i16("SIN_Q", SIN_Q)
|
||||
a.u8("ROT_RATE", ROT_RATE)
|
||||
a.const("SHAPE_MAXV", max(len(v) for _, v, _ in SHAPES))
|
||||
a.const("SHAPE_REC_MAX", max(len(r) for r in records))
|
||||
a.main()
|
||||
@@ -8,8 +8,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||
APP_NAME="Broadcast FM" # <-- the only per-app line
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||
|
||||
@@ -24,14 +24,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
|
||||
--shortcut fm --require fm --screensaver >/dev/null
|
||||
--shortcut fm --require fm --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
+19
-17
@@ -30,15 +30,12 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "fm_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define STEP 1
|
||||
#define SCAN_SETTLE 100
|
||||
#define CHMAX APP_FM_CH_MAX
|
||||
|
||||
static const char BAND_NAME[4][10] = { "87.5-108M", "76-108M", "76-90M", "64-76M" };
|
||||
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
|
||||
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
|
||||
|
||||
static const app_api_t *A;
|
||||
static uint16_t *ch; /* shared gFM_Channels[48] */
|
||||
static app_fm_state_t st; /* freq_playing, sel_freq, band, is_mr, sel_ch */
|
||||
@@ -57,6 +54,11 @@ static char str[16];
|
||||
static char *putu(char *o, uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
|
||||
static char *put(char *o, const char *s){ while(*s)*o++=*s++; return o; }
|
||||
|
||||
/* Texts, band names and the F icon are read-only assets (gen_assets.py).
|
||||
* T() returns a shared buffer, valid until the next T() call. */
|
||||
static char tbuf[TEXT_MAX];
|
||||
static char *T(uint16_t off){ A->asset_read(off,tbuf,TEXT_MAX); return tbuf; }
|
||||
|
||||
static void bandLimits(void){ lo=A->fm_lo(st.band); hi=A->fm_hi(st.band); }
|
||||
static uint16_t wrap(uint16_t f){ if(f<lo)return hi; if(f>hi)return lo; return f; }
|
||||
static void tune(void){ A->fm_set_freq(st.freq_playing, st.band); }
|
||||
@@ -102,29 +104,29 @@ static void draw(void)
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->draw_battery();
|
||||
if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F)); /* F armed indicator */
|
||||
A->print_string("FM",2,0,0,8);
|
||||
A->print_normal(BAND_NAME[st.band],1,0,6);
|
||||
if(fArm) A->asset_read(BMP_F,A->status_line+70,BMP_F_LEN); /* F armed indicator */
|
||||
A->print_string(T(T_FM),2,0,0,8);
|
||||
A->print_normal(T(BAND_NAME+st.band*BAND_NAME_STRIDE),1,0,6);
|
||||
|
||||
/* ---- status (line 3) ---- */
|
||||
if(askSave) { o=put(b,"SAVE?"); *o='\0'; }
|
||||
else if(askDelete) { o=put(b,"DEL?"); *o='\0'; }
|
||||
if(askSave) { o=put(b,T(T_SAVE)); *o='\0'; }
|
||||
else if(askDelete) { o=put(b,T(T_DEL)); *o='\0'; }
|
||||
else if(scanState) {
|
||||
if(autoScan){ o=put(b,"A-SCAN("); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */
|
||||
else o=put(b,"M-SCAN");
|
||||
if(autoScan){ o=put(b,T(T_ASCAN)); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */
|
||||
else o=put(b,T(T_MSCAN));
|
||||
*o='\0';
|
||||
}
|
||||
else if(st.is_mr) { o=put(b,"MR(CH"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; }
|
||||
else if(st.is_mr) { o=put(b,T(T_MR_CH)); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; }
|
||||
else {
|
||||
o=put(b,"VFO"); *o='\0';
|
||||
for(uint8_t i=0;i<CHMAX;i++) if(ch[i]==st.freq_playing){ o=put(b,"VFO(CH"); o=num2(o,(uint8_t)(i+1)); *o++=')'; *o='\0'; break; }
|
||||
o=put(b,T(T_VFO)); *o='\0';
|
||||
for(uint8_t i=0;i<CHMAX;i++) if(ch[i]==st.freq_playing){ o=put(b,T(T_VFO_CH)); o=num2(o,(uint8_t)(i+1)); *o++=')'; *o='\0'; break; }
|
||||
}
|
||||
A->print_string(b,0,127,3,10);
|
||||
|
||||
/* ---- line 1: input in progress / channel prompt / frequency ---- */
|
||||
if(inIdx>0){
|
||||
if(st.is_mr||askSave){ /* channel number: "CH-XX" */
|
||||
o=put(b,"CH-");
|
||||
o=put(b,T(T_CH));
|
||||
*o++=(char)('0'+inBox[0]);
|
||||
*o++=(inIdx>1)?(char)('0'+inBox[1]):'-';
|
||||
*o='\0';
|
||||
@@ -139,10 +141,10 @@ static void draw(void)
|
||||
A->display_freq(b,36,1,false);
|
||||
}
|
||||
} else if(askSave){ /* pick a save slot */
|
||||
o=put(b,"CH-"); o=num2(o,(uint8_t)(savePos+1)); *o='\0';
|
||||
o=put(b,T(T_CH)); o=num2(o,(uint8_t)(savePos+1)); *o='\0';
|
||||
A->print_string(b,0,127,1,10);
|
||||
} else if(askDelete){
|
||||
o=put(b,"CH-"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0';
|
||||
o=put(b,T(T_CH)); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0';
|
||||
A->print_string(b,0,127,1,10);
|
||||
} else {
|
||||
freqBig(st.freq_playing);
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
# Broadcast FM read-only assets: texts, band names and the F icon.
|
||||
#
|
||||
# ./gen_assets.py fm_assets.bin fm_assets.h
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
a = Assets("FM")
|
||||
a.text("T_FM", "FM")
|
||||
a.text("T_SAVE", "SAVE?")
|
||||
a.text("T_DEL", "DEL?")
|
||||
a.text("T_ASCAN", "A-SCAN(")
|
||||
a.text("T_MSCAN", "M-SCAN")
|
||||
a.text("T_MR_CH", "MR(CH")
|
||||
a.text("T_VFO", "VFO")
|
||||
a.text("T_VFO_CH", "VFO(CH")
|
||||
a.text("T_CH", "CH-")
|
||||
a.table("BAND_NAME", ["87.5-108M", "76-108M", "76-90M", "64-76M"]) # by band 0..3
|
||||
a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e])
|
||||
a.main()
|
||||
@@ -8,8 +8,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||
APP_NAME="FoxHunt" # <-- the only per-app line
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||
|
||||
@@ -24,14 +24,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
|
||||
--shortcut foxhunt >/dev/null
|
||||
--shortcut foxhunt --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "foxhunt_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define LCD_WIDTH 128
|
||||
#define BK_REG_13 0x13
|
||||
@@ -70,21 +71,8 @@
|
||||
|
||||
#define CFG_MAGIC 0xF1 /* config validity marker */
|
||||
|
||||
static const uint16_t ATT_REG13[ATT_COUNT] = { 0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139 };
|
||||
static const uint8_t ATT_DB[ATT_BYP0] = { 0, 6, 15, 27 };
|
||||
|
||||
/* Status-bar / trend icons (bytes copied verbatim from App/bitmaps.c). */
|
||||
static const uint8_t BMP_BARS[11] = {0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f};
|
||||
static const uint8_t BMP_GRAPH[15] = {0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20};
|
||||
static const uint8_t BMP_SIGNAL[10] = {0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||
static const uint8_t BMP_SPEAKER[10] = {0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||
static const uint8_t BMP_UP[11] = {0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20};
|
||||
static const uint8_t BMP_DOWN[11] = {0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01};
|
||||
static const uint8_t BMP_FLAT[11] = {0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12};
|
||||
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
|
||||
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
|
||||
|
||||
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
|
||||
/* Texts, the attenuator ladder and the icons are read-only assets
|
||||
* (gen_assets.py): icons are copied straight into the LCD buffers. */
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
@@ -113,12 +101,17 @@ static char *puti(char *o,int v){
|
||||
}
|
||||
static void i2str(char *out,int v){ *puti(out,v)='\0'; }
|
||||
|
||||
/* Text from the assets in a shared buffer, valid until the next T() call. */
|
||||
static char tbuf[TEXT_MAX];
|
||||
static char *T(uint16_t off){ A->asset_read(off,tbuf,TEXT_MAX); return tbuf; }
|
||||
|
||||
/* ---- radio helpers ---- */
|
||||
static int16_t iabs16(int16_t v){ return v<0?-v:v; }
|
||||
|
||||
static void applyAtt(void){
|
||||
uint16_t reg = A->bk_read(BK_REG_13);
|
||||
reg = (uint16_t)((reg & ~0x03FFu) | ATT_REG13[attStep]);
|
||||
uint16_t reg = A->bk_read(BK_REG_13), gain;
|
||||
A->asset_read(ATT_REG13 + attStep * 2u, &gain, sizeof(gain));
|
||||
reg = (uint16_t)((reg & ~0x03FFu) | gain);
|
||||
A->bk_write(BK_REG_13, reg);
|
||||
}
|
||||
|
||||
@@ -155,9 +148,9 @@ static uint8_t fillCount(int16_t dbm){
|
||||
return (uint8_t)n;
|
||||
}
|
||||
static void buildS(char *out,int16_t dbm){
|
||||
if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,"S9+"); if(o<10)*p++='0'; i2str(p,o); }
|
||||
else if(dbm<-141) put(out,"S0")[0]='\0';
|
||||
else { char *p=put(out,"S"); i2str(p,(int)((uint16_t)(dbm+147)/6u)); }
|
||||
if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,T(T_S9P)); if(o<10)*p++='0'; i2str(p,o); }
|
||||
else if(dbm<-141) put(out,T(T_S0))[0]='\0';
|
||||
else { char *p=put(out,T(T_S)); i2str(p,(int)((uint16_t)(dbm+147)/6u)); }
|
||||
}
|
||||
|
||||
static int16_t div_trunc_pow2(int32_t v, uint8_t shift)
|
||||
@@ -232,33 +225,32 @@ static void draw(void){
|
||||
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->print_inverse("FOX HUNT",2,0,true,true,34);
|
||||
A->print_inverse(T(T_TITLE),2,0,true,true,34);
|
||||
A->draw_battery();
|
||||
|
||||
/* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */
|
||||
if(foxGraphMode==GRAPH_HIST) cpy(A->status_line+38,BMP_GRAPH,15);
|
||||
else cpy(A->status_line+38,BMP_BARS,11);
|
||||
if(foxAudioMode==AUDIO_BEEP) cpy(A->status_line+55,BMP_SIGNAL,10);
|
||||
else if(foxAudioMode==AUDIO_STATION) cpy(A->status_line+55,BMP_SPEAKER,10);
|
||||
if(foxLocked) cpy(A->status_line+70,FONT_LOCK,9);
|
||||
else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F));
|
||||
if(foxGraphMode==GRAPH_HIST) A->asset_read(BMP_GRAPH,A->status_line+38,BMP_GRAPH_LEN);
|
||||
else A->asset_read(BMP_BARS,A->status_line+38,BMP_BARS_LEN);
|
||||
if(foxAudioMode!=AUDIO_OFF)
|
||||
A->asset_read(foxAudioMode==AUDIO_BEEP?BMP_SIGNAL:BMP_SPEAKER,A->status_line+55,BMP_SIGNAL_LEN);
|
||||
if(foxLocked||fArm) A->asset_read(foxLocked?BMP_LOCK:BMP_F,A->status_line+70,BMP_F_LEN);
|
||||
|
||||
i2str(big,curDbm);
|
||||
A->display_freq(big,2,0,false);
|
||||
A->print_normal("dBm",(uint8_t)(slen(big)*13+4),0,1);
|
||||
A->print_normal(T(T_DBM),(uint8_t)(slen(big)*13+4),0,1);
|
||||
|
||||
/* trend arrow (line 0, right) + signed delta (line 1) */
|
||||
cpy(A->fb[0]+115, trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, 11);
|
||||
A->asset_read(trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, A->fb[0]+115, BMP_UP_LEN);
|
||||
if(trendDelta!=0){
|
||||
char *p=str; if(trendDelta>0)*p++='+'; else{*p++='-';}
|
||||
int16_t a=iabs16(trendDelta); if(a<10){*p++='0';} i2str(p,a);
|
||||
A->print_normal("dBm",127-3*7,0,1);
|
||||
A->print_normal(T(T_DBM),127-3*7,0,1);
|
||||
A->print_normal(str,(uint8_t)(127-3*7-2-slen(str)*7),1,1);
|
||||
}
|
||||
|
||||
i2str(put(str,"PK "),peakDbm);
|
||||
i2str(put(str,T(T_PK)),peakDbm);
|
||||
tag(str,4,2);
|
||||
i2str(put(str,"MN "),minDbm);
|
||||
i2str(put(str,T(T_MN)),minDbm);
|
||||
tag(str,(uint8_t)(((uint32_t)LCD_WIDTH-(uint32_t)slen(str)*4u)/2u),2);
|
||||
buildS(sMeter,curDbm);
|
||||
A->print_inverse(sMeter,(uint8_t)(126-slen(sMeter)*4),2,false,true,125);
|
||||
@@ -266,8 +258,11 @@ static void draw(void){
|
||||
if(foxGraphMode==GRAPH_HIST) drawHist();
|
||||
else drawBar();
|
||||
|
||||
if(attStep<ATT_BYP0){ char *o=put(str,"ATT "); o=puti(o,ATT_DB[attStep]); put(o,"dB")[0]='\0'; }
|
||||
else put(str,(attStep==ATT_BYP0)?"BYP":"BYP+")[0]='\0';
|
||||
if(attStep<ATT_BYP0){
|
||||
uint8_t db; A->asset_read(ATT_DB+attStep,&db,1);
|
||||
char *o=put(str,T(T_ATT)); o=puti(o,db); put(o,T(T_DB))[0]='\0';
|
||||
}
|
||||
else put(str,T(attStep==ATT_BYP0?T_BYP:T_BYP_PLUS))[0]='\0';
|
||||
tag(str,4,6);
|
||||
|
||||
{ uint32_t f=A->rx_freq(); char *o=puti(str,(int)(f/100000u)); *o++='.';
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
# FoxHunt read-only assets: texts, attenuator ladder and icons (bytes copied
|
||||
# verbatim from App/bitmaps.c).
|
||||
#
|
||||
# ./gen_assets.py foxhunt_assets.bin foxhunt_assets.h
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
a = Assets("FOXHUNT")
|
||||
a.text("T_TITLE", "FOX HUNT")
|
||||
a.text("T_DBM", "dBm")
|
||||
a.text("T_S9P", "S9+")
|
||||
a.text("T_S0", "S0")
|
||||
a.text("T_S", "S")
|
||||
a.text("T_PK", "PK ")
|
||||
a.text("T_MN", "MN ")
|
||||
a.text("T_ATT", "ATT ")
|
||||
a.text("T_DB", "dB")
|
||||
a.text("T_BYP", "BYP")
|
||||
a.text("T_BYP_PLUS", "BYP+")
|
||||
a.u16("ATT_REG13", [0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139]) # REG_13 gain bits by attStep
|
||||
a.u8("ATT_DB", [0, 6, 15, 27]) # attenuation below the bypass steps
|
||||
a.u8("BMP_BARS", [0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f])
|
||||
a.u8("BMP_GRAPH", [0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20])
|
||||
a.u8("BMP_SIGNAL", [0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c])
|
||||
a.u8("BMP_SPEAKER", [0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c])
|
||||
a.u8("BMP_UP", [0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20])
|
||||
a.u8("BMP_DOWN", [0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01])
|
||||
a.u8("BMP_FLAT", [0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12])
|
||||
a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e])
|
||||
a.u8("BMP_LOCK", [0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c])
|
||||
a.main()
|
||||
+21
-4
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# Pack an overlay-app raw binary into a .app blob: 64-byte header + code.
|
||||
# Pack an overlay-app raw binary into a .app blob: 64-byte header + code
|
||||
# [+ read-only assets]. The host installer writes the code at slot+0x1000 and
|
||||
# the assets (the bytes after code_size) at slot+APP_ASSET_OFFSET.
|
||||
# Header layout mirrors app_overlay.h : app_header_t (little-endian, packed).
|
||||
# The format/version constants (magic, header + ABI/API levels, overlay budget) are
|
||||
# read straight from the C headers the firmware itself compiles, so there is a
|
||||
@@ -9,6 +11,8 @@
|
||||
#
|
||||
# ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout \
|
||||
# --ver 1.0 --vma 0x20000280 --api-min 1
|
||||
# ./pack_app.py game.bin Game.app --name Game --vma 0x20000280 --api-min 2 \
|
||||
# --assets levels.bin # served to the app by api->asset_read()
|
||||
import argparse, os, re, struct, zlib, sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -32,6 +36,8 @@ HDR_VERSION = cdefine("app_overlay.h", "APP_HDR_VERSION")
|
||||
ABI_MAJOR = cdefine("app_api.h", "APP_ABI_MAJOR")
|
||||
API_LEVEL = cdefine("app_api.h", "APP_API_LEVEL")
|
||||
OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget
|
||||
ASSET_MAX = cdefine("app_overlay.h", "APP_ASSET_MAX") # header-sector asset area
|
||||
API_ASSETS = cdefine("app_api.h", "APP_API_ASSETS") # first level serving assets
|
||||
FLAG_COMMITTED = cdefine("app_overlay.h", "APP_FLAG_COMMITTED")
|
||||
FLAG_SCREEN_SAVER = cdefine("app_overlay.h", "APP_FLAG_SCREEN_SAVER")
|
||||
FLAG_SHORTCUT_SHIFT = cdefine("app_overlay.h", "APP_FLAG_SHORTCUT_SHIFT")
|
||||
@@ -68,6 +74,8 @@ def main():
|
||||
help="resident quick action advertised by this app")
|
||||
ap.add_argument("--require", action="append", choices=CAPABILITIES, default=[],
|
||||
help="resident capability required by this app (repeatable)")
|
||||
ap.add_argument("--assets",
|
||||
help=f"read-only asset file served by api->asset_read (<= {ASSET_MAX} B)")
|
||||
a = ap.parse_args()
|
||||
|
||||
if not 1 <= a.api_min <= API_LEVEL:
|
||||
@@ -79,6 +87,13 @@ def main():
|
||||
if len(code) > OVERLAY_MAX:
|
||||
sys.exit(f"code {len(code)} B exceeds overlay budget {OVERLAY_MAX} B")
|
||||
|
||||
assets = open(a.assets, "rb").read() if a.assets else b""
|
||||
if len(assets) > ASSET_MAX:
|
||||
sys.exit(f"assets {len(assets)} B exceed the {ASSET_MAX} B asset area")
|
||||
if assets and a.api_min < API_ASSETS:
|
||||
sys.exit(f"--assets needs --api-min {API_ASSETS} or higher (asset_read)")
|
||||
asset_crc = (zlib.crc32(assets) & 0xFFFF) if assets else 0
|
||||
|
||||
crc = zlib.crc32(code) & 0xFFFFFFFF
|
||||
flags = (FLAG_COMMITTED |
|
||||
(FLAG_SCREEN_SAVER if a.screensaver else 0) |
|
||||
@@ -87,22 +102,24 @@ def main():
|
||||
for capability in a.require:
|
||||
required_caps |= CAPABILITIES[capability]
|
||||
header = struct.pack(
|
||||
"<4sHBBIIHH16s16sII4s",
|
||||
"<4sHBBIIHH16s16sIIHH",
|
||||
MAGIC, HDR_VERSION, ABI_MAJOR, a.api_min,
|
||||
len(code), crc, a.entry, flags,
|
||||
field(a.name, 16), field(a.ver, 16), a.vma,
|
||||
required_caps, b"\x00" * 4,
|
||||
required_caps, len(assets), asset_crc,
|
||||
)
|
||||
assert len(header) == 64, len(header)
|
||||
|
||||
with open(a.outfile, "wb") as f:
|
||||
f.write(header)
|
||||
f.write(code)
|
||||
f.write(assets)
|
||||
|
||||
print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} "
|
||||
f"abi={ABI_MAJOR} api>={a.api_min} caps=0x{required_caps:08x} "
|
||||
f"vma=0x{a.vma:08x} "
|
||||
f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B")
|
||||
f"code={len(code)} B crc32=0x{crc:08x} assets={len(assets)} B "
|
||||
f"-> blob {64 + len(code) + len(assets)} B")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -8,8 +8,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||
APP_NAME="Plasma" # <-- the only per-app line
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||
|
||||
@@ -24,13 +24,14 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# Plasma read-only assets: the sine wave, the ordered-dither matrix and the
|
||||
# pattern presets. The app copies them onto its stack at launch, so the render
|
||||
# loop reads them from RAM at full speed while they stay out of the overlay.
|
||||
#
|
||||
# ./gen_assets.py plasma_assets.bin plasma_assets.h
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
|
||||
# 32 * sin(2*pi*i/256): four of these sum to -128..128.
|
||||
SIN = [
|
||||
0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 9, 10, 11, 12,
|
||||
12, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 21, 22,
|
||||
23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29,
|
||||
30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32,
|
||||
32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 30, 30, 30,
|
||||
30, 29, 29, 29, 28, 28, 27, 27, 27, 26, 26, 25, 25, 24, 24, 23,
|
||||
23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 16, 16, 15, 14, 14, 13,
|
||||
12, 12, 11, 10, 9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1,
|
||||
0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -9, -9, -10, -11, -12,
|
||||
-12, -13, -14, -14, -15, -16, -16, -17, -18, -18, -19, -20, -20, -21, -21, -22,
|
||||
-23, -23, -24, -24, -25, -25, -26, -26, -27, -27, -27, -28, -28, -29, -29, -29,
|
||||
-30, -30, -30, -30, -31, -31, -31, -31, -31, -32, -32, -32, -32, -32, -32, -32,
|
||||
-32, -32, -32, -32, -32, -32, -32, -32, -31, -31, -31, -31, -31, -30, -30, -30,
|
||||
-30, -29, -29, -29, -28, -28, -27, -27, -27, -26, -26, -25, -25, -24, -24, -23,
|
||||
-23, -22, -21, -21, -20, -20, -19, -18, -18, -17, -16, -16, -15, -14, -14, -13,
|
||||
-12, -12, -11, -10, -9, -9, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1,
|
||||
]
|
||||
|
||||
a = Assets("PLASMA")
|
||||
a.i8("SIN", SIN)
|
||||
# 4x4 ordered-dither matrix, flattened: idx = (y&3)*4 + (x&3), values 0..15.
|
||||
a.u8("BAYER", [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5])
|
||||
# Pattern presets: {x scale, y scale, diagonal scale, radial ring shift}.
|
||||
a.u8("VAR", [4, 4, 3, 5, 6, 3, 5, 4, 3, 7, 2, 6, 5, 5, 4, 5, 2, 8, 6, 4])
|
||||
a.const("NVAR", 5)
|
||||
a.main()
|
||||
@@ -30,40 +30,15 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "../app_api.h"
|
||||
#include "plasma_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define W 128
|
||||
#define H 64
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
/* 32 * sin(2*pi*i/256): four of these sum to -128..128. */
|
||||
static const int8_t SIN[256] = {
|
||||
0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 9, 10, 11, 12,
|
||||
12, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 21, 22,
|
||||
23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29,
|
||||
30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32,
|
||||
32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 30, 30, 30,
|
||||
30, 29, 29, 29, 28, 28, 27, 27, 27, 26, 26, 25, 25, 24, 24, 23,
|
||||
23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 16, 16, 15, 14, 14, 13,
|
||||
12, 12, 11, 10, 9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1,
|
||||
0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -9, -9,-10,-11,-12,
|
||||
-12,-13,-14,-14,-15,-16,-16,-17,-18,-18,-19,-20,-20,-21,-21,-22,
|
||||
-23,-23,-24,-24,-25,-25,-26,-26,-27,-27,-27,-28,-28,-29,-29,-29,
|
||||
-30,-30,-30,-30,-31,-31,-31,-31,-31,-32,-32,-32,-32,-32,-32,-32,
|
||||
-32,-32,-32,-32,-32,-32,-32,-32,-31,-31,-31,-31,-31,-30,-30,-30,
|
||||
-30,-29,-29,-29,-28,-28,-27,-27,-27,-26,-26,-25,-25,-24,-24,-23,
|
||||
-23,-22,-21,-21,-20,-20,-19,-18,-18,-17,-16,-16,-15,-14,-14,-13,
|
||||
-12,-12,-11,-10, -9, -9, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1,
|
||||
};
|
||||
|
||||
/* 4x4 ordered-dither matrix, flattened: idx = (y&3)*4 + (x&3), values 0..15. */
|
||||
static const uint8_t BAYER[16] = { 0,8,2,10, 12,4,14,6, 3,11,1,9, 15,7,13,5 };
|
||||
|
||||
/* Pattern presets: {x scale, y scale, diagonal scale, radial ring shift}. */
|
||||
#define NVAR 5
|
||||
static const uint8_t VAR[NVAR][4] = {
|
||||
{4,4,3,5}, {6,3,5,4}, {3,7,2,6}, {5,5,4,5}, {2,8,6,4},
|
||||
};
|
||||
/* The sine wave, the 4x4 ordered-dither matrix and the pattern presets are
|
||||
* read-only assets (gen_assets.py), copied onto app_main's stack at launch. */
|
||||
|
||||
/* Per-frame separable tables. */
|
||||
static int8_t colA[W]; /* horizontal sine, by column x */
|
||||
@@ -81,6 +56,13 @@ void app_main(const app_api_t *api)
|
||||
A->backlight_on();
|
||||
A->status_clear();
|
||||
|
||||
int8_t sine[SIN_LEN]; /* 32 * sin(2*pi*i/256) */
|
||||
uint8_t bayer[BAYER_LEN]; /* idx = (y&3)*4 + (x&3), values 0..15 */
|
||||
uint8_t presets[NVAR][4]; /* x, y, diagonal scales, radial shift */
|
||||
A->asset_read(SIN, sine, sizeof(sine));
|
||||
A->asset_read(BAYER, bayer, sizeof(bayer));
|
||||
A->asset_read(VAR, presets, sizeof(presets));
|
||||
|
||||
uint16_t t1 = 0, t2 = 0, t3 = 0, tr = 0, tc = 0, tc2 = 0;
|
||||
uint16_t autoCtr = 0;
|
||||
uint8_t speed = 3;
|
||||
@@ -125,22 +107,22 @@ void app_main(const app_api_t *api)
|
||||
if (!running)
|
||||
break;
|
||||
|
||||
const uint8_t sx = VAR[var][0], sy = VAR[var][1];
|
||||
const uint8_t sd = VAR[var][2], rsh = VAR[var][3];
|
||||
const uint8_t sx = presets[var][0], sy = presets[var][1];
|
||||
const uint8_t sd = presets[var][2], rsh = presets[var][3];
|
||||
|
||||
/* drifting radial centre (gentle Lissajous), then the separable squares */
|
||||
const int cx = 64 + SIN[(uint8_t)tc]; /* 32..96 */
|
||||
const int cy = 32 + (SIN[(uint8_t)tc2] >> 1);/* 16..48 */
|
||||
const int cx = 64 + sine[(uint8_t)tc]; /* 32..96 */
|
||||
const int cy = 32 + (sine[(uint8_t)tc2] >> 1);/* 16..48 */
|
||||
|
||||
for (uint8_t x = 0; x < W; x++) {
|
||||
colA[x] = SIN[(uint8_t)(x * sx + t1)];
|
||||
colA[x] = sine[(uint8_t)(x * sx + t1)];
|
||||
int dx = (int)x - cx; sqx[x] = (uint16_t)(dx * dx);
|
||||
}
|
||||
for (uint8_t y = 0; y < H; y++) {
|
||||
rowA[y] = SIN[(uint8_t)(y * sy + t2)];
|
||||
rowA[y] = sine[(uint8_t)(y * sy + t2)];
|
||||
int dy = (int)y - cy; sqy[y] = (uint16_t)(dy * dy);
|
||||
}
|
||||
for (uint16_t s = 0; s < W + H; s++) diagA[s] = SIN[(uint8_t)(s * sd + t3)];
|
||||
for (uint16_t s = 0; s < W + H; s++) diagA[s] = sine[(uint8_t)(s * sd + t3)];
|
||||
|
||||
/* render all 8 pages: page 0 -> status line (top), pages 1..7 -> fb[0..6] */
|
||||
for (uint8_t x = 0; x < W; x++) {
|
||||
@@ -152,13 +134,13 @@ void app_main(const app_api_t *api)
|
||||
for (uint8_t b = 0; b < 8u; b++) {
|
||||
const uint8_t y = (uint8_t)(p * 8u + b);
|
||||
int v = cxv + rowA[y] + diagA[x + y]
|
||||
+ SIN[(uint8_t)(((sxv + sqy[y]) >> rsh) + tr)];
|
||||
+ sine[(uint8_t)(((sxv + sqy[y]) >> rsh) + tr)];
|
||||
v += 128; /* 0..256 */
|
||||
bool on;
|
||||
if (bands) on = ((v >> 4) & 1u) != 0u; /* sweeping stripes */
|
||||
else {
|
||||
int lvl = v >> 4; if (lvl > 15) lvl = 15; /* 0..15 stipple */
|
||||
on = lvl > (int)BAYER[((y & 3u) << 2) | bx];
|
||||
on = lvl > (int)bayer[((y & 3u) << 2) | bx];
|
||||
}
|
||||
if (on) byte = (uint8_t)(byte | (1u << b));
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")"
|
||||
APP_NAME="Rapid Roll"
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280}
|
||||
OUT="${APP_NAME// /}"
|
||||
|
||||
@@ -22,14 +22,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \
|
||||
--vma "${APP_VMA}" --screensaver >/dev/null
|
||||
--vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
# Rapid Roll read-only assets: texts, decimal places, the sprite sheet
|
||||
# (column-major, LSB = top row, read by blit() at draw time) and the 128x64
|
||||
# title screen.
|
||||
#
|
||||
# ./gen_assets.py rapidroll_assets.bin rapidroll_assets.h [preview.pgm]
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
from app_art import Canvas
|
||||
|
||||
SPRITES = [
|
||||
("SPR_BALL", [0x3C,0x42,0x8D,0x85,0x81,0x81,0x42,0x3C, # 4 frames: a mark turning
|
||||
0x3C,0x42,0x81,0x81,0x85,0x8D,0x42,0x3C, # as the ball rolls
|
||||
0x3C,0x42,0x81,0x81,0xA1,0xB1,0x42,0x3C,
|
||||
0x3C,0x42,0xB1,0xA1,0x81,0x81,0x42,0x3C]),
|
||||
("SPR_HEART", [0x1E,0x21,0x45,0x82,0x82,0x41,0x21,0x1E]), # 8x8, the size of the ball
|
||||
("SPR_LIFE", [0x06,0x0F,0x1E,0x0F,0x06]),
|
||||
("SPR_BOOM", [0x00,0x52,0x34,0x08,0x34,0x4A,0x08,0x00]),
|
||||
("SPR_TOOTH", [0x03,0x07,0x0F,0x07,0x03,0x01,0x01]),
|
||||
("SPR_SPIKE", [0x0C,0x0E,0x0F,0x0E,0x0C,0x08,0x08]), # 3-row tooth on a 1-row floor, drawn from y - 3
|
||||
("SPR_SQUASH", [0x0C,0x12,0x21,0x21,0x21,0x21,0x12,0x0C]), # 8x6 ball, just landed
|
||||
]
|
||||
|
||||
def pill(cv, x, y, w):
|
||||
"""A safe platform as drawn by the game: hollow 3-row pill, rounded ends."""
|
||||
for k in range(w):
|
||||
v = 0x02 if k in (0, w - 1) else 0x07 if k in (1, w - 2) else 0x05
|
||||
for bit in range(3):
|
||||
if v >> bit & 1:
|
||||
cv.set(x + k, y + bit)
|
||||
|
||||
def title_screen(sprites):
|
||||
"""RAPID / ROLL, the ball on a platform, spikes and a heart to grab."""
|
||||
cv = Canvas()
|
||||
cv.two_words("RAPID", "ROLL")
|
||||
pill(cv, 4, 53, 28)
|
||||
cv.sprite(14, 45, sprites["SPR_BALL"][:8])
|
||||
spike = sprites["SPR_SPIKE"]
|
||||
cv.sprite(48, 50, [spike[k % 7] for k in range(26)]) # spiked floor, from y - 3
|
||||
pill(cv, 92, 49, 28)
|
||||
cv.sprite(100, 41, sprites["SPR_HEART"])
|
||||
return cv
|
||||
|
||||
preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None
|
||||
a = Assets("RAPIDROLL")
|
||||
a.text("T_LV", "LV")
|
||||
a.text("T_GAME_OVER", "GAME OVER")
|
||||
a.text("T_PAUSE", "PAUSE")
|
||||
a.text("T_LEVEL1", "LEVEL 1")
|
||||
a.text("T_PRESS", "PRESS MENU")
|
||||
a.u16("PLACE", [10000, 1000, 100, 10, 1])
|
||||
for name, cols in SPRITES:
|
||||
a.u8(name, cols)
|
||||
title = title_screen(dict(SPRITES))
|
||||
a.raw("ART_TITLE", title.pages())
|
||||
a.main()
|
||||
if preview_path:
|
||||
title.preview(preview_path)
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "rapidroll_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define W 128
|
||||
#define TICK_MS 40u
|
||||
@@ -46,36 +47,17 @@
|
||||
slow LCD but choppier, 1 is smooth */
|
||||
#define NONE 0xFFu
|
||||
|
||||
enum { ST_PLAY, ST_OVER };
|
||||
enum { ST_PLAY, ST_OVER, ST_TITLE };
|
||||
enum { P_FREE, P_SAFE, P_SPIKE, P_CRUMBLE };
|
||||
|
||||
/* Sprite offsets in ART[] (column-major, LSB = top row). */
|
||||
#define SPR_BALL 0u /* 4 frames: a mark turning as the ball rolls */
|
||||
#define SPR_HEART 32u /* 8x8, the size of the ball */
|
||||
#define SPR_LIFE 40u
|
||||
#define SPR_BOOM 45u
|
||||
#define SPR_TOOTH 53u
|
||||
#define SPR_SPIKE 60u /* one tooth period (7 px, as the ceiling), drawn from y - 3 */
|
||||
#define SPR_SQUASH 67u
|
||||
/* Texts, decimal places and sprites (column-major, LSB = top row) are
|
||||
* read-only assets (gen_assets.py), fetched from external flash when drawn. */
|
||||
|
||||
typedef struct { int8_t y; uint8_t x, w, kind, heart; } plat_t; /* heart: crumble timer on P_CRUMBLE */
|
||||
|
||||
static const uint8_t ART[] = {
|
||||
/* ball @ 0 */ 0x3C,0x42,0x8D,0x85,0x81,0x81,0x42,0x3C, /* ring: solid smears */
|
||||
0x3C,0x42,0x81,0x81,0x85,0x8D,0x42,0x3C,
|
||||
0x3C,0x42,0x81,0x81,0xA1,0xB1,0x42,0x3C,
|
||||
0x3C,0x42,0xB1,0xA1,0x81,0x81,0x42,0x3C,
|
||||
/* heart @32 */ 0x1E,0x21,0x45,0x82,0x82,0x41,0x21,0x1E,
|
||||
/* life @40 */ 0x06,0x0F,0x1E,0x0F,0x06,
|
||||
/* boom @45 */ 0x00,0x52,0x34,0x08,0x34,0x4A,0x08,0x00,
|
||||
/* tooth @53 */ 0x03,0x07,0x0F,0x07,0x03,0x01,0x01,
|
||||
/* spike @60 */ 0x0C,0x0E,0x0F,0x0E,0x0C,0x08,0x08, /* 3-row tooth on a 1-row floor */
|
||||
/* squash @67 */ 0x0C,0x12,0x21,0x21,0x21,0x21,0x12,0x0C, /* 8x6 ball, just landed */
|
||||
};
|
||||
|
||||
static const app_api_t *A;
|
||||
static plat_t plats[N_PLAT];
|
||||
static uint32_t randomState, score;
|
||||
static uint32_t score;
|
||||
static int16_t bx, by, boomX, boomY; /* ball and explosion, top-left */
|
||||
static int8_t lastRowY; /* y of the most recently spawned row */
|
||||
static uint8_t lastX, nextX; /* current and planned safe platform x */
|
||||
@@ -83,6 +65,7 @@ static uint8_t on; /* platform under the ball, or NONE */
|
||||
static uint8_t mode, prevKey, lives, level, rows, riseAcc;
|
||||
static uint8_t banner, waitCd, boom, respawnCd, invul, vy, squash;
|
||||
static bool running, paused, saver, alive, spikeAllowed;
|
||||
static uint8_t titleTick;
|
||||
static char text[6];
|
||||
|
||||
void *memset(void *dst, int value, size_t size)
|
||||
@@ -92,10 +75,17 @@ void *memset(void *dst, int value, size_t size)
|
||||
return dst;
|
||||
}
|
||||
|
||||
/* Text from the assets in a shared buffer, valid until the next T() call. */
|
||||
static char tbuf[TEXT_MAX];
|
||||
static char *T(uint16_t off)
|
||||
{
|
||||
A->asset_read(off, tbuf, TEXT_MAX);
|
||||
return tbuf;
|
||||
}
|
||||
|
||||
static uint32_t rnd(void)
|
||||
{
|
||||
randomState = randomState * 1664525u + 1013904223u;
|
||||
return randomState >> 8;
|
||||
return A->rand32() >> 8; /* resident PRNG, seeded by the loader */
|
||||
}
|
||||
|
||||
static void pixel(int16_t x, int16_t y)
|
||||
@@ -111,15 +101,20 @@ static void column(int16_t x, int16_t y, uint8_t v)
|
||||
if (v & 1u) pixel(x, y);
|
||||
}
|
||||
|
||||
/* Draw a column-major sprite (up to 8 rows high, LSB = top). */
|
||||
static void blit(int16_t x, int16_t y, const uint8_t *col, uint8_t w)
|
||||
/* Draw a column-major sprite (up to 8 rows high, LSB = top, w <= 8) read
|
||||
* from the assets at offset art. */
|
||||
static void blit(int16_t x, int16_t y, uint16_t art, uint8_t w)
|
||||
{
|
||||
uint8_t cols[8];
|
||||
const uint8_t *col = cols;
|
||||
A->asset_read(art, cols, w);
|
||||
while (w--) column(x++, y, *col++);
|
||||
}
|
||||
|
||||
static void number(uint32_t value, uint8_t width)
|
||||
{
|
||||
static const uint16_t place[5] = {10000u, 1000u, 100u, 10u, 1u};
|
||||
uint16_t place[5];
|
||||
A->asset_read(PLACE, place, sizeof(place));
|
||||
const uint8_t first = (uint8_t)(5u - width);
|
||||
for (uint8_t i = 0; i < width; i++) {
|
||||
uint8_t digit = 0;
|
||||
@@ -292,7 +287,7 @@ static void draw(void)
|
||||
for (int16_t y = 10; y < 64; y += 2) { pixel(0, y); pixel(W - 1, y); } /* x = 0/127 are tooth-free */
|
||||
/* Ceiling: 7-px tooth period started at x = -5, so the 18 visible teeth
|
||||
* span x = 2..125 and leave the same 2-px margin on both sides. */
|
||||
for (int16_t x = -5; x < W; x += 7) blit(x, 8, &ART[SPR_TOOTH], 7);
|
||||
for (int16_t x = -5; x < W; x += 7) blit(x, 8, SPR_TOOTH, 7);
|
||||
|
||||
for (uint8_t i = 0; i < N_PLAT; i++) {
|
||||
const plat_t *p = &plats[i];
|
||||
@@ -300,7 +295,7 @@ static void draw(void)
|
||||
for (uint8_t k = 0; k < p->w; k++) /* hollow 3-row pill with rounded ends */
|
||||
column(p->x + k, p->y, (k == 0u || k == p->w - 1u) ? 0x02u
|
||||
: (k == 1u || k == p->w - 2u) ? 0x07u : 0x05u);
|
||||
if (p->heart && p->y >= CEIL_Y + 8) blit(p->x + 10, p->y - 8, &ART[SPR_HEART], 8);
|
||||
if (p->heart && p->y >= CEIL_Y + 8) blit(p->x + 10, p->y - 8, SPR_HEART, 8);
|
||||
} else if (p->kind == P_CRUMBLE) {
|
||||
if (!(p->heart & 2u)) /* cracked pill, blinking once it starts to give way */
|
||||
for (uint8_t k = 0; k < p->w; k++)
|
||||
@@ -308,26 +303,39 @@ static void draw(void)
|
||||
: (k == 1u || k == p->w - 2u) ? 0x07u
|
||||
: ((k & 3u) == 3u) ? 0x00u : 0x05u);
|
||||
} else if (p->kind == P_SPIKE) {
|
||||
uint8_t spike[SPR_SPIKE_LEN]; /* one tooth period, read once per row */
|
||||
A->asset_read(SPR_SPIKE, spike, sizeof(spike));
|
||||
for (uint8_t k = 0, j = 0; k < SPIKE_W; k++, j = j == 6u ? 0u : j + 1u)
|
||||
column(p->x + k, p->y - 3, ART[SPR_SPIKE + j]);
|
||||
column(p->x + k, p->y - 3, spike[j]);
|
||||
}
|
||||
}
|
||||
if (alive && !(invul & 2u)) { /* blinks after a respawn; the mark turns every 4 px */
|
||||
if (squash) blit(bx, by + 2, &ART[SPR_SQUASH], 8);
|
||||
else blit(bx, by, &ART[SPR_BALL + ((bx >> 2) & 3u) * 8u], 8);
|
||||
if (squash) blit(bx, by + 2, SPR_SQUASH, 8);
|
||||
else blit(bx, by, SPR_BALL + ((bx >> 2) & 3u) * 8u, 8);
|
||||
}
|
||||
if (boom) blit(boomX, boomY, &ART[SPR_BOOM], 8);
|
||||
if (boom) blit(boomX, boomY, SPR_BOOM, 8);
|
||||
|
||||
for (uint8_t i = 0; i < lives; i++) blit(1 + i * 6, 1, &ART[SPR_LIFE], 5);
|
||||
for (uint8_t i = 0; i < lives; i++) blit(1 + i * 6, 1, SPR_LIFE, 5);
|
||||
if (!(banner & 4u)) { /* blinks for a while after a level change */
|
||||
A->print_tiny("LV", 52, 1, true, true);
|
||||
A->print_tiny(T(T_LV), 52, 1, true, true);
|
||||
number(level, 1); A->print_tiny(text, 62, 1, true, true);
|
||||
}
|
||||
number(score > 99999u ? 99999u : score, 5); A->print_tiny(text, 107, 1, true, true);
|
||||
|
||||
if (mode == ST_OVER) A->print_bold("GAME OVER", 0, 127, 3);
|
||||
else if (paused) A->print_bold("PAUSE", 0, 127, 3);
|
||||
else if (banner > 10u && level == 1u) A->print_bold("LEVEL 1", 0, 127, 2); /* start only: it overwrites its line */
|
||||
if (mode == ST_OVER) A->print_bold(T(T_GAME_OVER), 0, 127, 3);
|
||||
else if (paused) A->print_bold(T(T_PAUSE), 0, 127, 3);
|
||||
else if (banner > 10u && level == 1u) A->print_bold(T(T_LEVEL1), 0, 127, 2); /* start only: it overwrites its line */
|
||||
}
|
||||
|
||||
/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers,
|
||||
* with a blinking prompt on the blank bottom page. */
|
||||
static void draw_title(void)
|
||||
{
|
||||
titleTick++;
|
||||
A->asset_read(ART_TITLE, A->status_line, W);
|
||||
A->asset_read(ART_TITLE + W, A->fb[0], 7u * W);
|
||||
if (titleTick & 16u)
|
||||
A->print_bold(T(T_PRESS), 0, 127, 6);
|
||||
}
|
||||
|
||||
static void poll_key(void)
|
||||
@@ -359,18 +367,18 @@ __attribute__((section(".text.entry"), used))
|
||||
void app_main(const app_api_t *api)
|
||||
{
|
||||
A = api;
|
||||
randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq();
|
||||
running = true; saver = false;
|
||||
prevKey = A->get_key();
|
||||
A->led(false); A->backlight_on();
|
||||
new_game();
|
||||
mode = ST_TITLE; /* MENU starts a game, like after GAME OVER */
|
||||
|
||||
while (running) {
|
||||
poll_key();
|
||||
if (!running) break;
|
||||
if (saver) { A->backlight_update(); A->delay_ms(TICK_MS); continue; }
|
||||
if (!paused) tick();
|
||||
draw(); A->blit_status(); A->blit_full(); A->backlight_update();
|
||||
if (mode == ST_TITLE) draw_title();
|
||||
else { if (!paused) tick(); draw(); }
|
||||
A->blit_status(); A->blit_full(); A->backlight_update();
|
||||
A->delay_ms(TICK_MS);
|
||||
}
|
||||
A->led(false);
|
||||
|
||||
@@ -3,8 +3,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")"
|
||||
APP_NAME="Snake"
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280}
|
||||
OUT="${APP_NAME// /}"
|
||||
|
||||
@@ -22,14 +22,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \
|
||||
--vma "${APP_VMA}" --screensaver >/dev/null
|
||||
--vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
# Snake read-only assets: texts, decimal places, direction steps, the 4x4
|
||||
# sprites (16-bit masks, bit i = pixel (i & 3, i >> 2)) and the 128x64 title
|
||||
# screen. The four sprite sets are contiguous: the app loads them onto its
|
||||
# stack in one read per frame.
|
||||
#
|
||||
# ./gen_assets.py snake_assets.bin snake_assets.h [preview.pgm]
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
from app_art import Canvas
|
||||
|
||||
HEAD = [0x6566, 0x0FD2, 0x66A6, 0x0FB4] # by direction: up, right, down, left
|
||||
HEAD_OPEN = [0x65A9, 0xC35A, 0x95A6, 0x3CA5] # facing the food
|
||||
BODY = [0, 0, 0, 0x0CA6, 0, 0x6426, 0x6AC0, 0, # by neighbour mask:
|
||||
0, 0x0356, 0x0DB0, 0, 0x6530, 0, 0, 0] # up 1, right 2, down 4, left 8
|
||||
TAIL = [0, 0x0466, 0x0CE0, 0, 0x6620, 0, 0, 0,
|
||||
0x0370, 0, 0, 0, 0, 0, 0, 0]
|
||||
FOOD = 0x6996
|
||||
STEP = {"U": (0, -1), "R": (1, 0), "D": (0, 1), "L": (-1, 0)}
|
||||
NEIGHBOUR = {(0, -1): 1, (1, 0): 2, (0, 1): 4, (-1, 0): 8}
|
||||
HEADING = {(0, -1): 0, (1, 0): 1, (0, 1): 2, (-1, 0): 3}
|
||||
|
||||
def cell(cv, mask, gx, gy):
|
||||
"""One 4x4 sprite on the game's board grid (as draw_sprite())."""
|
||||
for i in range(16):
|
||||
if mask >> i & 1:
|
||||
cv.set(2 + gx * 4 + (i & 3), 10 + gy * 4 + (i >> 2))
|
||||
|
||||
def title_screen():
|
||||
"""The logo inside the board frame; a snake at game scale heads for food."""
|
||||
cv = Canvas()
|
||||
for x in range(1, 127): # board frame, as render()
|
||||
cv.set(x, 1); cv.set(x, 54)
|
||||
for y in range(1, 55):
|
||||
cv.set(1, y); cv.set(126, y)
|
||||
cv.word("SNAKE", 6, kx=1.3, ky=1.1)
|
||||
x, y = 2, 8 # tail cell, then the moves
|
||||
cells = [(x, y)]
|
||||
for move, count in (("R", 8), ("U", 2), ("R", 6), ("D", 2), ("R", 8)):
|
||||
for _ in range(count):
|
||||
x, y = x + STEP[move][0], y + STEP[move][1]
|
||||
cells.append((x, y))
|
||||
for k, (cx, cy) in enumerate(cells):
|
||||
if k == len(cells) - 1: # head, mouth open towards the food
|
||||
px, py = cells[k - 1]
|
||||
mask = HEAD_OPEN[HEADING[(cx - px, cy - py)]]
|
||||
elif k == 0:
|
||||
nx, ny = cells[1]
|
||||
mask = TAIL[NEIGHBOUR[(nx - cx, ny - cy)]]
|
||||
else:
|
||||
(px, py), (nx, ny) = cells[k - 1], cells[k + 1]
|
||||
mask = BODY[NEIGHBOUR[(nx - cx, ny - cy)] | NEIGHBOUR[(px - cx, py - cy)]]
|
||||
cell(cv, mask, cx, cy)
|
||||
cell(cv, FOOD, 28, 8)
|
||||
return cv
|
||||
|
||||
preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None
|
||||
a = Assets("SNAKE")
|
||||
a.text("T_SCORE", "SCORE:")
|
||||
a.text("T_BEST", "BEST:")
|
||||
a.text("T_NEW_SCORE", "NEW SCORE")
|
||||
a.text("T_GAME_OVER", "GAME OVER")
|
||||
a.text("T_PAUSE", "PAUSE")
|
||||
a.text("T_PRESS", "PRESS MENU")
|
||||
a.u16("PLACE", [10000, 1000, 100, 10])
|
||||
a.i8("DXY", [0, -1, 1, 0, 0, 1, -1, 0]) # (dx, dy) for up, right, down, left
|
||||
# Direction bit from a neighbour's packed (dx + 2 * dy) & 7 offset.
|
||||
a.u8("DIR_MASK", [0, 2, 4, 0, 0, 0, 1, 8])
|
||||
a.u16("SPR_HEAD", HEAD)
|
||||
a.u16("SPR_HEAD_OPEN", HEAD_OPEN)
|
||||
a.u16("SPR_BODY", BODY)
|
||||
a.u16("SPR_TAIL", TAIL)
|
||||
title = title_screen()
|
||||
a.raw("ART_TITLE", title.pages())
|
||||
a.main()
|
||||
if preview_path:
|
||||
title.preview(preview_path)
|
||||
+68
-39
@@ -28,6 +28,7 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "../app_api.h"
|
||||
#include "snake_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define W 128u
|
||||
|
||||
@@ -59,32 +60,25 @@ typedef struct {
|
||||
static const app_api_t *A;
|
||||
|
||||
static uint32_t board_map[ROWS];
|
||||
static uint32_t randomState, best, initial_best;
|
||||
static uint32_t best, initial_best;
|
||||
static segment_t snake[RING_SIZE];
|
||||
static uint16_t head_idx, tail_idx, repeatMs;
|
||||
static int16_t moveTimer;
|
||||
static uint8_t foodX, foodY, direction, nextDirection, previousKey;
|
||||
static bool paused, gameOver, running, newBest, saverPaused, saverActive;
|
||||
static bool paused, gameOver, running, newBest, saverPaused, saverActive, title;
|
||||
static uint8_t titleTick;
|
||||
static char text[6];
|
||||
|
||||
static const int8_t DX[4] = { 0, 1, 0, -1 };
|
||||
static const int8_t DY[4] = {-1, 0, 1, 0 };
|
||||
/* Texts, decimal places, direction steps and sprites are read-only assets
|
||||
* (gen_assets.py). The sprite sets and the direction map are copied onto the
|
||||
* stack once per frame by draw_snake(). */
|
||||
|
||||
/* --- Branchless Direction Map --- */
|
||||
static const uint8_t DIR_MASK[8] = { [6]=1, [1]=2, [2]=4, [7]=8 };
|
||||
/* Sprite index in draw_snake()'s copy of the SPR_HEAD..SPR_TAIL block. */
|
||||
#define SPR_INDEX(item) (((item) - SPR_HEAD) / 2u)
|
||||
#define SPR_BLOCK_LEN (SPR_TAIL + SPR_TAIL_LEN - SPR_HEAD)
|
||||
|
||||
/* Macro for compact direction evaluation */
|
||||
#define GET_DIR(n_idx, s_x, s_y) DIR_MASK[(((snake[n_idx].x & 0x7F) - (s_x)) + ((snake[n_idx].y - (s_y)) << 1)) & 7]
|
||||
|
||||
/* --- Sprites --- */
|
||||
static const uint16_t HEAD_SPRITE[4] = { 0x6566, 0x0FD2, 0x66A6, 0x0FB4 };
|
||||
static const uint16_t HEAD_OPEN_SPRITE[4] = { 0x65A9, 0xC35A, 0x95A6, 0x3CA5 };
|
||||
static const uint16_t BODY_SPRITE[16] = {
|
||||
0, 0, 0, 0x0CA6, 0, 0x6426, 0x6AC0, 0, 0, 0x0356, 0x0DB0, 0, 0x6530, 0, 0, 0
|
||||
};
|
||||
static const uint16_t TAIL_SPRITE[16] = {
|
||||
0, 0x0466, 0x0CE0, 0, 0x6620, 0, 0, 0, 0x0370, 0, 0, 0, 0, 0, 0, 0
|
||||
};
|
||||
/* Macro for compact direction evaluation (dir_mask: draw_snake()'s stack copy) */
|
||||
#define GET_DIR(n_idx, s_x, s_y) dir_mask[(((snake[n_idx].x & 0x7F) - (s_x)) + ((snake[n_idx].y - (s_y)) << 1)) & 7]
|
||||
|
||||
static void new_game(void);
|
||||
|
||||
@@ -92,16 +86,24 @@ static void new_game(void);
|
||||
/* Utilities & RNG */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static uint32_t random_next(void)
|
||||
/* Text from the assets in a shared buffer, valid until the next T() call. */
|
||||
static char tbuf[TEXT_MAX];
|
||||
static char *T(uint16_t off)
|
||||
{
|
||||
randomState = randomState * 1664525u + 1013904223u;
|
||||
return randomState;
|
||||
A->asset_read(off, tbuf, TEXT_MAX);
|
||||
return tbuf;
|
||||
}
|
||||
|
||||
/* One grid step (dx, dy) for a direction: up, right, down, left. */
|
||||
static void step_of(uint32_t dir, int8_t d[2])
|
||||
{
|
||||
A->asset_read(DXY + dir * 2u, d, 2);
|
||||
}
|
||||
|
||||
static void place_food(void)
|
||||
{
|
||||
for (uint32_t tries = 0; tries < 128u; tries++) {
|
||||
const uint32_t r = random_next();
|
||||
const uint32_t r = A->rand32(); /* resident PRNG, seeded by the loader */
|
||||
const uint32_t x = ((r >> 24) * COLS) >> 8;
|
||||
const uint32_t y = (((r >> 16) & 0xFFu) * ROWS) >> 8;
|
||||
|
||||
@@ -139,6 +141,11 @@ static void draw_sprite(uint32_t bx, uint32_t by, uint32_t sprite)
|
||||
|
||||
static void draw_snake(void)
|
||||
{
|
||||
uint16_t spr[SPR_BLOCK_LEN / 2u];
|
||||
uint8_t dir_mask[DIR_MASK_LEN];
|
||||
A->asset_read(SPR_HEAD, spr, sizeof(spr));
|
||||
A->asset_read(DIR_MASK, dir_mask, sizeof(dir_mask));
|
||||
|
||||
uint32_t idx = head_idx;
|
||||
uint32_t next_idx = 0;
|
||||
uint32_t len = (head_idx - tail_idx + 1u) & RING_MASK;
|
||||
@@ -149,15 +156,17 @@ static void draw_snake(void)
|
||||
uint32_t sprite = 0;
|
||||
|
||||
if (i == 0) {
|
||||
bool open = ((sx + DX[direction]) == foodX && (sy + DY[direction]) == foodY);
|
||||
sprite = open ? HEAD_OPEN_SPRITE[direction] : HEAD_SPRITE[direction];
|
||||
int8_t d[2];
|
||||
step_of(direction, d);
|
||||
bool open = ((sx + d[0]) == foodX && (sy + d[1]) == foodY);
|
||||
sprite = spr[(open ? SPR_INDEX(SPR_HEAD_OPEN) : SPR_INDEX(SPR_HEAD)) + direction];
|
||||
} else if (snake[idx].x & 0x80) {
|
||||
sprite = 0x6FF6;
|
||||
} else if (i == len - 1u) {
|
||||
sprite = TAIL_SPRITE[GET_DIR(next_idx, sx, sy)];
|
||||
sprite = spr[SPR_INDEX(SPR_TAIL) + GET_DIR(next_idx, sx, sy)];
|
||||
} else {
|
||||
uint32_t prev_idx = (idx - 1u) & RING_MASK;
|
||||
sprite = BODY_SPRITE[GET_DIR(next_idx, sx, sy) | GET_DIR(prev_idx, sx, sy)];
|
||||
sprite = spr[SPR_INDEX(SPR_BODY) + (GET_DIR(next_idx, sx, sy) | GET_DIR(prev_idx, sx, sy))];
|
||||
}
|
||||
|
||||
draw_sprite(BOARD_X + (sx << 2), BOARD_Y + (sy << 2), sprite);
|
||||
@@ -168,7 +177,8 @@ static void draw_snake(void)
|
||||
|
||||
static void number_5(uint32_t value)
|
||||
{
|
||||
static const uint16_t place[4] = { 10000u, 1000u, 100u, 10u };
|
||||
uint16_t place[4];
|
||||
A->asset_read(PLACE, place, sizeof(place));
|
||||
if (value > 99999u)
|
||||
value = 99999u;
|
||||
|
||||
@@ -201,18 +211,18 @@ static void render(void)
|
||||
draw_snake();
|
||||
|
||||
/* Score computed dynamically based on current length */
|
||||
A->print_tiny("SCORE:", 2, 1, true, true);
|
||||
A->print_tiny(T(T_SCORE), 2, 1, true, true);
|
||||
number_5((((head_idx - tail_idx + 1u) & RING_MASK) - 3u) * SCORE_PER_FOOD);
|
||||
A->print_tiny(text, 32, 1, true, true);
|
||||
|
||||
A->print_tiny("BEST:", 81, 1, true, true);
|
||||
A->print_tiny(T(T_BEST), 81, 1, true, true);
|
||||
number_5(best);
|
||||
A->print_tiny(text, 107, 1, true, true);
|
||||
|
||||
if (gameOver)
|
||||
A->print_bold(newBest ? "NEW SCORE" : "GAME OVER", 0, W - 1, 3);
|
||||
A->print_bold(T(newBest ? T_NEW_SCORE : T_GAME_OVER), 0, W - 1, 3);
|
||||
else if (paused)
|
||||
A->print_bold("PAUSE", 0, W - 1, 3);
|
||||
A->print_bold(T(T_PAUSE), 0, W - 1, 3);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
@@ -222,8 +232,10 @@ static void render(void)
|
||||
static void move_snake(void)
|
||||
{
|
||||
/* Casting to unsigned handles both upper limit and negative underflow in one instruction */
|
||||
uint32_t nx = (snake[head_idx].x & 0x7F) + DX[nextDirection];
|
||||
uint32_t ny = snake[head_idx].y + DY[nextDirection];
|
||||
int8_t d[2];
|
||||
step_of(nextDirection, d);
|
||||
uint32_t nx = (snake[head_idx].x & 0x7F) + d[0];
|
||||
uint32_t ny = snake[head_idx].y + d[1];
|
||||
|
||||
if (nx >= COLS || ny >= ROWS) {
|
||||
gameOver = true;
|
||||
@@ -271,6 +283,17 @@ static void move_snake(void)
|
||||
/* Input & Loop */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers,
|
||||
* with a blinking prompt on the blank bottom page. */
|
||||
static void draw_title(void)
|
||||
{
|
||||
titleTick++;
|
||||
A->asset_read(ART_TITLE, A->status_line, W);
|
||||
A->asset_read(ART_TITLE + W, A->fb[0], 7u * W);
|
||||
if (titleTick & 32u)
|
||||
A->print_bold(T(T_PRESS), 0, W - 1, 6);
|
||||
}
|
||||
|
||||
static void key_action(uint32_t key, bool repeat)
|
||||
{
|
||||
if (key == APP_KEY_EXIT) {
|
||||
@@ -278,6 +301,14 @@ static void key_action(uint32_t key, bool repeat)
|
||||
return;
|
||||
}
|
||||
|
||||
if (title) { /* the game-over restart keys also leave the title */
|
||||
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) {
|
||||
title = false;
|
||||
new_game();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameOver) {
|
||||
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0))
|
||||
new_game();
|
||||
@@ -380,10 +411,6 @@ void app_main(const app_api_t *api)
|
||||
best = (cfg.magic == CFG_MAGIC && cfg.version == 1u) ? cfg.best : 0u;
|
||||
initial_best = best;
|
||||
|
||||
randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ best ^ 0x534E414Bu;
|
||||
if (!randomState)
|
||||
randomState = 1u;
|
||||
|
||||
previousKey = APP_KEY_INVALID;
|
||||
repeatMs = 0;
|
||||
running = true;
|
||||
@@ -392,6 +419,7 @@ void app_main(const app_api_t *api)
|
||||
A->backlight_on();
|
||||
|
||||
new_game();
|
||||
title = true;
|
||||
|
||||
while (running) {
|
||||
poll_key();
|
||||
@@ -404,7 +432,7 @@ void app_main(const app_api_t *api)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!paused && !gameOver) {
|
||||
if (!title && !paused && !gameOver) {
|
||||
if (moveTimer <= 0) {
|
||||
moveTimer = MOVE_INTERVAL;
|
||||
move_snake();
|
||||
@@ -413,7 +441,8 @@ void app_main(const app_api_t *api)
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
if (title) draw_title();
|
||||
else render();
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
A->backlight_update();
|
||||
|
||||
@@ -4,8 +4,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")"
|
||||
APP_NAME="Space Impact"
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280}
|
||||
OUT="${APP_NAME// /}"
|
||||
|
||||
@@ -20,14 +20,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \
|
||||
--vma "${APP_VMA}" --screensaver >/dev/null
|
||||
--vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# Space Impact read-only assets: texts, tables, the sprite sheet (column-major,
|
||||
# LSB = top row, read by blit() at draw time) and the 128x64 title screen as 8
|
||||
# LCD pages of 128 bytes (page 0 = status line, pages 1..7 = frame buffer;
|
||||
# page 7 stays blank for the blinking prompt).
|
||||
#
|
||||
# ./gen_assets.py spaceimpact_assets.bin spaceimpact_assets.h [preview.pgm]
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
from app_art import Canvas
|
||||
|
||||
SPRITES = [
|
||||
("SPR_SHIP", [0x41,0x6B,0x7F,0x3E,0x3E,0x1C,0x1C,0x1C,0x08,0x08]),
|
||||
("SPR_ENEMY", [0x4C,0x32,0x15,0x21,0x21,0x15,0x32,0x4C, # invader (+ type * 8)
|
||||
0x08,0x14,0x14,0x14,0x22,0x41,0x49,0x77, # jet
|
||||
0x0C,0x56,0x3D,0x15,0x15,0x3D,0x56,0x0C, # saucer
|
||||
0x0E,0x11,0x6D,0x41,0x41,0x6D,0x11,0x0E]), # skull
|
||||
("SPR_BOOM", [0x00,0x00,0x08,0x1C,0x08,0x00,0x00,0x00, # + frame * 8
|
||||
0x41,0x04,0x10,0x40,0x02,0x20,0x08,0x41]),
|
||||
("SPR_BOSS", [0x00,0x78,0x84,0x02,0x32,0x31,0x01,0x01,0x01,0x01,0x31,0x32,0x02,0x84,0x78,0x00, # octopus
|
||||
0x30,0x0C,0x82,0x71,0x0D,0x03,0xE0,0x1E,0x02,0x00,0x03,0x0D,0x31,0xC2,0x0C,0x30,
|
||||
0xC0,0x20,0x10,0xDC,0xD2,0x12,0x1D,0xD1,0xD1,0x1D,0x12,0xD2,0xDC,0x10,0x20,0xC0, # mothership (+32)
|
||||
0x20,0x11,0x0A,0x06,0x02,0x02,0x02,0x3E,0x3E,0x02,0x02,0x02,0x06,0x0A,0x11,0x20]),
|
||||
("SPR_HEART", [0x06,0x0F,0x1E,0x0F,0x06]),
|
||||
("SPR_MISS", [0x1B,0x0E,0x04,0x0E,0x0E,0x0E,0x0E,0x04]), # 8x5, also the HUD stock icon
|
||||
]
|
||||
|
||||
def title_screen(sprites):
|
||||
cv = Canvas()
|
||||
cv.two_words("SPACE", "IMPACT")
|
||||
# Scene under the logo: the ship firing at an invader wave.
|
||||
ship, enemy = sprites["SPR_SHIP"], sprites["SPR_ENEMY"]
|
||||
cv.sprite(6, 46, ship)
|
||||
for x in (18, 26, 34): # gun burst
|
||||
for k in range(4):
|
||||
cv.set(x + k, 49)
|
||||
for i, ex in enumerate((52, 68, 84, 100)):
|
||||
cv.sprite(ex, 45 + (i & 1) * 3, enemy[8 * (i & 3):8 * (i & 3) + 8])
|
||||
return cv
|
||||
|
||||
def main():
|
||||
preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None
|
||||
a = Assets("SPACEIMPACT")
|
||||
a.text("T_GAME_OVER", "GAME OVER")
|
||||
a.text("T_PAUSE", "PAUSE")
|
||||
a.text("T_PRESS", "PRESS MENU")
|
||||
a.text("T_LEVEL", "LEVEL 1") # digit patched at run time
|
||||
a.u16("PLACE", [10000, 1000, 100, 10, 1])
|
||||
a.u8("POINTS", [10, 15, 40, 25]) # score per enemy type
|
||||
a.u8("SPEED", [1, 2, 1, 1]) # pixels per tick per enemy type
|
||||
for name, cols in SPRITES:
|
||||
a.u8(name, cols)
|
||||
cv = title_screen(dict(SPRITES))
|
||||
a.raw("ART_TITLE", cv.pages())
|
||||
a.main()
|
||||
if preview_path:
|
||||
cv.preview(preview_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -15,12 +15,18 @@
|
||||
*
|
||||
* Controls: 2/8 or UP/DOWN move the ship, 5/MENU launches a piercing
|
||||
* missile, F pauses and EXIT quits.
|
||||
*
|
||||
* Texts, tables and artwork live in the app's read-only assets (built by
|
||||
* gen_assets.py), not in the 4 KiB overlay: sprites are fetched from external
|
||||
* flash as they are drawn and the title screen is streamed straight into the
|
||||
* frame buffer.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "spaceimpact_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define W 128
|
||||
#define TICK_MS 40u
|
||||
@@ -33,43 +39,16 @@
|
||||
#define LEVEL_LEN 700u /* ticks of waves before the boss */
|
||||
#define BOOM 10u /* explosion length in ticks */
|
||||
|
||||
enum { ST_PLAY, ST_OVER };
|
||||
|
||||
/* Sprite offsets in ART[] (column-major, LSB = top row). */
|
||||
#define SPR_SHIP 0u
|
||||
#define SPR_ENEMY 10u /* + type * 8 */
|
||||
#define SPR_BOOM 42u /* + frame * 8 */
|
||||
#define SPR_BOSS 58u /* octopus, then mothership (+32) */
|
||||
#define SPR_HEART 122u
|
||||
#define SPR_MISS 127u /* 8x5, also the HUD stock icon */
|
||||
enum { ST_PLAY, ST_OVER, ST_TITLE };
|
||||
|
||||
typedef struct { int16_t x; int8_t y, vx, vy; uint8_t live; } shot_t;
|
||||
/* state: 0 free, 1 alive, >1 exploding while the counter decreases. */
|
||||
typedef struct { int16_t x; int8_t y, base; uint8_t type, hp, state, phase; } enemy_t;
|
||||
|
||||
static const uint8_t ART[] = {
|
||||
/* ship @ 0 */ 0x41,0x6B,0x7F,0x3E,0x3E,0x1C,0x1C,0x1C,0x08,0x08,
|
||||
/* invader @ 10 */ 0x4C,0x32,0x15,0x21,0x21,0x15,0x32,0x4C,
|
||||
/* jet @ 18 */ 0x08,0x14,0x14,0x14,0x22,0x41,0x49,0x77,
|
||||
/* saucer @ 26 */ 0x0C,0x56,0x3D,0x15,0x15,0x3D,0x56,0x0C,
|
||||
/* skull @ 34 */ 0x0E,0x11,0x6D,0x41,0x41,0x6D,0x11,0x0E,
|
||||
/* boom @ 42 */ 0x00,0x00,0x08,0x1C,0x08,0x00,0x00,0x00,
|
||||
0x41,0x04,0x10,0x40,0x02,0x20,0x08,0x41,
|
||||
/* octopus @ 58 */ 0x00,0x78,0x84,0x02,0x32,0x31,0x01,0x01,0x01,0x01,0x31,0x32,0x02,0x84,0x78,0x00,
|
||||
0x30,0x0C,0x82,0x71,0x0D,0x03,0xE0,0x1E,0x02,0x00,0x03,0x0D,0x31,0xC2,0x0C,0x30,
|
||||
/* mother @ 90 */ 0xC0,0x20,0x10,0xDC,0xD2,0x12,0x1D,0xD1,0xD1,0x1D,0x12,0xD2,0xDC,0x10,0x20,0xC0,
|
||||
0x20,0x11,0x0A,0x06,0x02,0x02,0x02,0x3E,0x3E,0x02,0x02,0x02,0x06,0x0A,0x11,0x20,
|
||||
/* heart @122 */ 0x06,0x0F,0x1E,0x0F,0x06,
|
||||
/* missile @127 */ 0x1B,0x0E,0x04,0x0E,0x0E,0x0E,0x0E,0x04,
|
||||
};
|
||||
|
||||
static const uint8_t POINTS[4] = {10u, 15u, 40u, 25u};
|
||||
static const uint8_t SPEED[4] = {1u, 2u, 1u, 1u}; /* pixels per tick */
|
||||
|
||||
static const app_api_t *A;
|
||||
static enemy_t enemies[N_ENEMY];
|
||||
static shot_t shots[N_SHOT], hostile[N_HOSTILE];
|
||||
static uint32_t randomState, score;
|
||||
static uint32_t score;
|
||||
static uint16_t distance;
|
||||
static uint16_t frame; /* 16-bit so the slow star layers wrap without a jump */
|
||||
static uint8_t mode, prevKey, kills;
|
||||
@@ -80,7 +59,6 @@ static int8_t bossY, bossDir;
|
||||
static uint8_t bossHp, bossMax;
|
||||
static bool running, paused, boss, saver;
|
||||
static char text[6];
|
||||
static char levelText[] = "LEVEL 1";
|
||||
|
||||
void *memset(void *dst, int value, size_t size)
|
||||
{
|
||||
@@ -89,10 +67,24 @@ void *memset(void *dst, int value, size_t size)
|
||||
return dst;
|
||||
}
|
||||
|
||||
/* Text from the assets in a shared buffer, valid until the next T() call. */
|
||||
static char tbuf[TEXT_MAX];
|
||||
static char *T(uint16_t off)
|
||||
{
|
||||
A->asset_read(off, tbuf, TEXT_MAX);
|
||||
return tbuf;
|
||||
}
|
||||
|
||||
static uint8_t asset_u8(uint16_t off)
|
||||
{
|
||||
uint8_t v;
|
||||
A->asset_read(off, &v, 1);
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint32_t rnd(void)
|
||||
{
|
||||
randomState = randomState * 1664525u + 1013904223u;
|
||||
return randomState >> 8;
|
||||
return A->rand32() >> 8; /* resident PRNG, seeded by the loader */
|
||||
}
|
||||
|
||||
static void pixel(int16_t x, int16_t y)
|
||||
@@ -102,9 +94,13 @@ static void pixel(int16_t x, int16_t y)
|
||||
*p |= (uint8_t)(1u << (y & 7));
|
||||
}
|
||||
|
||||
/* Draw a column-major sprite (up to 8 rows high, LSB = top). */
|
||||
static void blit(int16_t x, int16_t y, const uint8_t *col, uint8_t w)
|
||||
/* Draw a column-major sprite (up to 8 rows high, LSB = top, w <= 16) read
|
||||
* from the assets at offset art. */
|
||||
static void blit(int16_t x, int16_t y, uint16_t art, uint8_t w)
|
||||
{
|
||||
uint8_t cols[16];
|
||||
const uint8_t *col = cols;
|
||||
A->asset_read(art, cols, w);
|
||||
for (; w; w--, x++) {
|
||||
uint8_t v = *col++;
|
||||
for (int16_t yy = y; v; v >>= 1, yy++)
|
||||
@@ -130,7 +126,8 @@ static int8_t aim(int16_t y)
|
||||
|
||||
static void number(uint32_t value, uint8_t width)
|
||||
{
|
||||
static const uint16_t place[5] = {10000u, 1000u, 100u, 10u, 1u};
|
||||
uint16_t place[5];
|
||||
A->asset_read(PLACE, place, sizeof(place));
|
||||
const uint8_t first = (uint8_t)(5u - width);
|
||||
for (uint8_t i = 0; i < width; i++) {
|
||||
uint8_t digit = 0;
|
||||
@@ -164,7 +161,6 @@ static void boom(int16_t x, int8_t y, uint8_t delay)
|
||||
|
||||
static void start_level(void)
|
||||
{
|
||||
levelText[6] = (char)('0' + level);
|
||||
distance = 0; banner = 50u; spawnCd = 0; boss = false;
|
||||
}
|
||||
|
||||
@@ -192,7 +188,7 @@ static void player_hit(void)
|
||||
static void kill_enemy(enemy_t *e)
|
||||
{
|
||||
e->state = BOOM;
|
||||
score += POINTS[e->type];
|
||||
score += asset_u8(POINTS + e->type);
|
||||
if ((++kills & 15u) == 0u && missiles < 9u) missiles++;
|
||||
}
|
||||
|
||||
@@ -244,7 +240,7 @@ static void update_enemies(void)
|
||||
const uint8_t t = e->type;
|
||||
int16_t y = e->y;
|
||||
e->phase++;
|
||||
if (t != 2u || (frame & 1u)) e->x -= SPEED[t]; /* saucer: half speed */
|
||||
if (t != 2u || (frame & 1u)) e->x -= asset_u8(SPEED + t); /* saucer: half speed */
|
||||
if (t == 0u) {
|
||||
const uint8_t p = (e->phase >> 1) & 31u;
|
||||
y = e->base - 8 + (int16_t)(p < 16u ? p : 31u - p); /* triangle wave */
|
||||
@@ -342,8 +338,8 @@ static void tick(void)
|
||||
|
||||
static void draw_hud(void)
|
||||
{
|
||||
for (uint8_t i = 0; i < lives; i++) blit(1 + i * 6, 1, &ART[SPR_HEART], 5);
|
||||
blit(33, 1, &ART[SPR_MISS], 8);
|
||||
for (uint8_t i = 0; i < lives; i++) blit(1 + i * 6, 1, SPR_HEART, 5);
|
||||
blit(33, 1, SPR_MISS, 8);
|
||||
number(missiles, 1); A->print_tiny(text, 43, 1, true, true);
|
||||
number(score > 99999u ? 99999u : score, 5); A->print_tiny(text, 107, 1, true, true);
|
||||
for (uint8_t x = 0; x < W; x += 2u) pixel(x, 8);
|
||||
@@ -396,20 +392,20 @@ static void draw(void)
|
||||
for (uint8_t i = 0; i < N_ENEMY; i++) {
|
||||
const enemy_t *e = &enemies[i];
|
||||
if (e->state == 1u && !(e->type == 2u && e->hp < 3u && (frame & 2u))) /* hurt saucer blinks */
|
||||
blit(e->x, e->y, &ART[SPR_ENEMY + e->type * 8u], 8);
|
||||
blit(e->x, e->y, SPR_ENEMY + e->type * 8u, 8);
|
||||
else if (e->state > 1u && e->state <= BOOM)
|
||||
blit(e->x, e->y, &ART[SPR_BOOM + (e->state > 5u ? 0u : 8u)], 8);
|
||||
blit(e->x, e->y, SPR_BOOM + (e->state > 5u ? 0u : 8u), 8);
|
||||
}
|
||||
if (boss) {
|
||||
const uint8_t *spr = &ART[SPR_BOSS + ((level & 1u) ? 0u : 32u)]; /* alternate bosses */
|
||||
const uint16_t spr = SPR_BOSS + ((level & 1u) ? 0u : 32u); /* alternate bosses */
|
||||
blit(bossX, bossY, spr, 16);
|
||||
blit(bossX, bossY + 8, spr + 16, 16);
|
||||
blit(bossX, bossY + 8, spr + 16u, 16);
|
||||
}
|
||||
if (mode == ST_PLAY && !(invul & 2u)) blit(PX, py, &ART[SPR_SHIP], 10);
|
||||
if (mode == ST_PLAY && !(invul & 2u)) blit(PX, py, SPR_SHIP, 10);
|
||||
|
||||
for (uint8_t i = 0; i < N_SHOT; i++) {
|
||||
const shot_t *s = &shots[i];
|
||||
if (s->live == 2u) blit(s->x, s->y - 2, &ART[SPR_MISS], 8);
|
||||
if (s->live == 2u) blit(s->x, s->y - 2, SPR_MISS, 8);
|
||||
else if (s->live) for (uint8_t k = 0; k < 4u; k++) pixel(s->x + k, s->y);
|
||||
}
|
||||
for (uint8_t i = 0; i < N_HOSTILE; i++) {
|
||||
@@ -421,14 +417,28 @@ static void draw(void)
|
||||
|
||||
draw_hud();
|
||||
if (mode == ST_OVER) {
|
||||
A->print_bold("GAME OVER", 0, 127, 3);
|
||||
A->print_bold(T(T_GAME_OVER), 0, 127, 3);
|
||||
} else if (paused) {
|
||||
A->print_bold("PAUSE", 0, 127, 3);
|
||||
A->print_bold(T(T_PAUSE), 0, 127, 3);
|
||||
} else if (banner > 10u) {
|
||||
A->print_bold(levelText, 0, 127, 3);
|
||||
char *t = T(T_LEVEL);
|
||||
t[6] = (char)('0' + level); /* "LEVEL n" */
|
||||
A->print_bold(t, 0, 127, 3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers
|
||||
* every frame, the starfield scrolls over it and the prompt blinks. */
|
||||
static void draw_title(void)
|
||||
{
|
||||
frame++;
|
||||
A->asset_read(ART_TITLE, A->status_line, W);
|
||||
A->asset_read(ART_TITLE + W, A->fb[0], 7u * W);
|
||||
draw_stars();
|
||||
if (frame & 16u)
|
||||
A->print_bold(T(T_PRESS), 0, 127, 6);
|
||||
}
|
||||
|
||||
static void move_player(int8_t dy)
|
||||
{
|
||||
int16_t ny = py + dy;
|
||||
@@ -470,18 +480,18 @@ __attribute__((section(".text.entry"), used))
|
||||
void app_main(const app_api_t *api)
|
||||
{
|
||||
A = api;
|
||||
randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq();
|
||||
running = true; paused = false;
|
||||
prevKey = A->get_key();
|
||||
A->led(false); A->backlight_on();
|
||||
new_game();
|
||||
mode = ST_TITLE; /* MENU starts a game, like after GAME OVER */
|
||||
|
||||
while (running) {
|
||||
poll_key();
|
||||
if (!running) break;
|
||||
if (saver) { A->backlight_update(); A->delay_ms(TICK_MS); continue; }
|
||||
if (!paused) tick();
|
||||
draw(); A->blit_status(); A->blit_full(); A->backlight_update();
|
||||
if (mode == ST_TITLE) draw_title();
|
||||
else { if (!paused) tick(); draw(); }
|
||||
A->blit_status(); A->blit_full(); A->backlight_update();
|
||||
A->delay_ms(TICK_MS);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")"
|
||||
APP_NAME="Tetris"
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VER="1.1"
|
||||
APP_API_MIN=2
|
||||
APP_VMA=${APP_VMA:-0x20000280}
|
||||
OUT="${APP_NAME// /}"
|
||||
|
||||
@@ -22,14 +22,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin
|
||||
|
||||
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
|
||||
|
||||
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h"
|
||||
step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||
step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
|
||||
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \
|
||||
--vma "${APP_VMA}" --screensaver >/dev/null
|
||||
--vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null
|
||||
trap - ERR
|
||||
|
||||
BYTES=$(wc -c < "${APP}.bin")
|
||||
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
# Tetris read-only assets: texts, piece masks, scoring, decimal places, the
|
||||
# rotation wall kicks and the 128x64 title screen.
|
||||
#
|
||||
# ./gen_assets.py tetris_assets.bin tetris_assets.h [preview.pgm]
|
||||
import os, sys
|
||||
sys.dont_write_bytecode = True
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from app_assets import Assets
|
||||
from app_art import Canvas
|
||||
|
||||
MASKS = [0x00F0, 0x4444, 0x0F00, 0x2222,
|
||||
0x0066, 0x0066, 0x0066, 0x0066,
|
||||
0x0072, 0x0262, 0x0270, 0x0232,
|
||||
0x0036, 0x0462, 0x0360, 0x0231,
|
||||
0x0063, 0x0264, 0x0630, 0x0132,
|
||||
0x0071, 0x0226, 0x0470, 0x0322,
|
||||
0x0074, 0x0622, 0x0170, 0x0223]
|
||||
|
||||
def title_cell(cv, x, y):
|
||||
"""Bevelled 4x4 block on a 5 px grid: larger than in game, to read at a glance."""
|
||||
for k in range(4):
|
||||
cv.set(x + k, y); cv.set(x + k, y + 3); cv.set(x, y + k); cv.set(x + 3, y + k)
|
||||
cv.rect(x + 1, y + 1, x + 2, y + 2)
|
||||
|
||||
def title_screen():
|
||||
"""Wide logo over a stack with a gap, and a T piece coming down into it."""
|
||||
cv = Canvas()
|
||||
cv.word("TETRIS", 2, kx=1.45, ky=1.2)
|
||||
floor, x0 = 55, 4
|
||||
stack = ["111111111111101111111111", # bottom row first, 5 px cells
|
||||
"111110111111100111111101",
|
||||
"001100011110000011100100"]
|
||||
for r, row in enumerate(stack):
|
||||
for c, cell in enumerate(row):
|
||||
if cell == "1":
|
||||
title_cell(cv, x0 + c * 5, floor - 4 - 5 * r)
|
||||
t_down = 0x0027 # T pointing down, over the gap
|
||||
for i in range(16):
|
||||
if t_down >> i & 1:
|
||||
title_cell(cv, x0 + (12 + (i & 3)) * 5, floor - 4 - 25 + (i >> 2) * 5)
|
||||
return cv
|
||||
|
||||
preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None
|
||||
a = Assets("TETRIS")
|
||||
a.text("T_TETRIS", "TETRIS")
|
||||
a.text("T_NEXT", "NEXT")
|
||||
a.text("T_NEW_BEST", "NEW BEST!")
|
||||
a.text("T_GAME_OVER", "GAME OVER")
|
||||
a.text("T_PAUSE", "PAUSE")
|
||||
a.text("T_SCORE", "SCORE")
|
||||
a.text("T_LINES", "LINES")
|
||||
a.text("T_LEVEL", "LEVEL")
|
||||
a.text("T_BEST", "BEST")
|
||||
a.text("T_PRESS", "PRESS MENU")
|
||||
# Four 4x4 masks (one per rotation) for I, O, T, S, Z, J and L.
|
||||
a.u16("MASK", MASKS)
|
||||
a.u16("LINE_POINTS", [0, 100, 300, 500, 800]) # by lines cleared
|
||||
a.u32("DECIMAL_PLACE", [100000, 10000, 1000, 100, 10, 1])
|
||||
a.i8("KICK", [0, -1, 1, -2, 2]) # rotation x offsets
|
||||
title = title_screen()
|
||||
a.raw("ART_TITLE", title.pages())
|
||||
a.main()
|
||||
if preview_path:
|
||||
title.preview(preview_path)
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
#include "tetris_assets.h" /* generated by gen_assets.py */
|
||||
|
||||
#define W 128u
|
||||
#define H 64u
|
||||
@@ -67,28 +68,18 @@ void *memset(void *dst, int value, size_t size)
|
||||
return dst;
|
||||
}
|
||||
|
||||
/* Four 4x4 masks for I, O, T, S, Z, J and L. */
|
||||
static const uint16_t MASK[7][4] = {
|
||||
{0x00F0, 0x4444, 0x0F00, 0x2222},
|
||||
{0x0066, 0x0066, 0x0066, 0x0066},
|
||||
{0x0072, 0x0262, 0x0270, 0x0232},
|
||||
{0x0036, 0x0462, 0x0360, 0x0231},
|
||||
{0x0063, 0x0264, 0x0630, 0x0132},
|
||||
{0x0071, 0x0226, 0x0470, 0x0322},
|
||||
{0x0074, 0x0622, 0x0170, 0x0223},
|
||||
};
|
||||
|
||||
static const uint16_t LINE_POINTS[5] = {0, 100, 300, 500, 800};
|
||||
static const uint32_t DECIMAL_PLACE[6] = {100000u, 10000u, 1000u, 100u, 10u, 1u};
|
||||
/* Texts, piece masks, scoring, decimal places and wall kicks are read-only
|
||||
* assets (gen_assets.py), fetched from external flash when needed. */
|
||||
|
||||
static uint16_t board[ROWS];
|
||||
static uint8_t bag[7], bagPos;
|
||||
static uint8_t piece, nextPiece, rotation;
|
||||
static int8_t pieceX, pieceY;
|
||||
static uint32_t randomState, score, best;
|
||||
static uint32_t score, best;
|
||||
static uint16_t lines, gravityMs;
|
||||
static uint8_t level;
|
||||
static bool paused, gameOver, running, saverPaused, saverActive, bestDirty, newBest;
|
||||
static bool paused, gameOver, running, saverPaused, saverActive, bestDirty, newBest, title;
|
||||
static uint8_t titleTick;
|
||||
static uint8_t previousKey;
|
||||
static uint16_t repeatMs;
|
||||
static char text[9];
|
||||
@@ -100,10 +91,20 @@ static void new_game(void);
|
||||
/* RNG and the shuffled 7-piece bag */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static uint32_t random_next(void)
|
||||
/* Text from the assets in a shared buffer, valid until the next T() call. */
|
||||
static char tbuf[TEXT_MAX];
|
||||
static char *T(uint16_t off)
|
||||
{
|
||||
randomState = randomState * 1664525u + 1013904223u;
|
||||
return randomState;
|
||||
A->asset_read(off, tbuf, TEXT_MAX);
|
||||
return tbuf;
|
||||
}
|
||||
|
||||
/* 4x4 occupancy mask of a piece in a rotation (bit i = cell (i & 3, i >> 2)). */
|
||||
static uint16_t mask_of(uint8_t type, uint8_t rot)
|
||||
{
|
||||
uint16_t mask;
|
||||
A->asset_read(MASK + (type * 4u + (rot & 3u)) * 2u, &mask, sizeof(mask));
|
||||
return mask;
|
||||
}
|
||||
|
||||
static void refill_bag(void)
|
||||
@@ -113,7 +114,7 @@ static void refill_bag(void)
|
||||
for (uint8_t i = 6u; i > 0u; i--) {
|
||||
uint8_t j;
|
||||
do {
|
||||
j = (uint8_t)(random_next() >> 29);
|
||||
j = (uint8_t)(A->rand32() >> 29); /* resident PRNG, seeded by the loader */
|
||||
} while (j > i);
|
||||
const uint8_t t = bag[i]; bag[i] = bag[j]; bag[j] = t;
|
||||
}
|
||||
@@ -190,7 +191,7 @@ static void cell(int gx, int gy, uint8_t style)
|
||||
|
||||
static bool collision(uint8_t type, uint8_t rot, int8_t px, int8_t py)
|
||||
{
|
||||
const uint16_t mask = MASK[type][rot & 3u];
|
||||
const uint16_t mask = mask_of(type, rot);
|
||||
for (uint8_t i = 0; i < 16u; i++) {
|
||||
if (!(mask & (uint16_t)(1u << i)))
|
||||
continue;
|
||||
@@ -206,7 +207,7 @@ static bool collision(uint8_t type, uint8_t rot, int8_t px, int8_t py)
|
||||
|
||||
static void draw_piece(uint8_t type, uint8_t rot, int8_t px, int8_t py, uint8_t style)
|
||||
{
|
||||
const uint16_t mask = MASK[type][rot & 3u];
|
||||
const uint16_t mask = mask_of(type, rot);
|
||||
for (uint8_t i = 0; i < 16u; i++)
|
||||
if (mask & (uint16_t)(1u << i))
|
||||
cell(px + (i & 3u), py + (i >> 2), style);
|
||||
@@ -216,7 +217,8 @@ static void number(uint32_t value, uint8_t width)
|
||||
{
|
||||
const uint8_t first = (uint8_t)(6u - width);
|
||||
for (uint8_t i = 0; i < width; i++) {
|
||||
const uint32_t place = DECIMAL_PLACE[first + i];
|
||||
uint32_t place;
|
||||
A->asset_read(DECIMAL_PLACE + (first + i) * 4u, &place, sizeof(place));
|
||||
uint8_t digit = 0;
|
||||
while (value >= place) {
|
||||
value -= place;
|
||||
@@ -229,7 +231,7 @@ static void number(uint32_t value, uint8_t width)
|
||||
|
||||
static void preview(void)
|
||||
{
|
||||
const uint16_t mask = MASK[nextPiece][0];
|
||||
const uint16_t mask = mask_of(nextPiece, 0);
|
||||
const int ox = nextPiece < 2u ? PREVIEW_CX - 7 : PREVIEW_CX - 5;
|
||||
const int oy = nextPiece == 0u ? PREVIEW_CY - 5 : PREVIEW_CY - 3;
|
||||
for (uint8_t i = 0; i < 16u; i++) {
|
||||
@@ -255,16 +257,16 @@ static void stat(const char *label, uint8_t y, uint32_t value,
|
||||
|
||||
static void panel(void)
|
||||
{
|
||||
A->print_tiny("NEXT", LABEL_X, 2, false, true);
|
||||
A->print_tiny(T(T_NEXT), LABEL_X, 2, false, true);
|
||||
preview();
|
||||
if (gameOver)
|
||||
A->print_inverse(newBest ? "NEW BEST!" : "GAME OVER", 82, 2, false, true, 118);
|
||||
A->print_inverse(T(newBest ? T_NEW_BEST : T_GAME_OVER), 82, 2, false, true, 118);
|
||||
else if (paused)
|
||||
A->print_inverse("PAUSE", 90, 2, false, true, 110);
|
||||
stat("SCORE", 24, score > 999999u ? 999999u : score, 101, 6);
|
||||
stat("LINES", 32, lines > 999u ? 999u : lines, 113, 3);
|
||||
stat("LEVEL", 40, level, 117, 2);
|
||||
stat("BEST", 48, best > 999999u ? 999999u : best, 101, 6);
|
||||
A->print_inverse(T(T_PAUSE), 90, 2, false, true, 110);
|
||||
stat(T(T_SCORE), 24, score > 999999u ? 999999u : score, 101, 6);
|
||||
stat(T(T_LINES), 32, lines > 999u ? 999u : lines, 113, 3);
|
||||
stat(T(T_LEVEL), 40, level, 117, 2);
|
||||
stat(T(T_BEST), 48, best > 999999u ? 999999u : best, 101, 6);
|
||||
}
|
||||
|
||||
/* effect 0 is normal, 1..8 is the line-clear sweep, 0xFF flashes a lock. */
|
||||
@@ -272,7 +274,7 @@ static void render(bool showPiece, uint32_t clearRows, uint8_t effect)
|
||||
{
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->print_inverse("TETRIS", 74, 0, true, true, 101);
|
||||
A->print_inverse(T(T_TETRIS), 74, 0, true, true, 101);
|
||||
|
||||
container();
|
||||
|
||||
@@ -357,7 +359,7 @@ static void lock_piece(void)
|
||||
render(true, 0, 0xFFu);
|
||||
A->blit_status(); A->blit_full(); A->delay_ms(45);
|
||||
|
||||
const uint16_t mask = MASK[piece][rotation];
|
||||
const uint16_t mask = mask_of(piece, rotation);
|
||||
bool above = false;
|
||||
for (uint8_t i = 0; i < 16u; i++) {
|
||||
if (!(mask & (uint16_t)(1u << i)))
|
||||
@@ -377,7 +379,9 @@ static void lock_piece(void)
|
||||
lines = (uint16_t)(lines + cleared);
|
||||
while (level < 15u && lines >= (uint16_t)level * 10u)
|
||||
level++;
|
||||
score += (uint32_t)LINE_POINTS[cleared] * level;
|
||||
uint16_t points;
|
||||
A->asset_read(LINE_POINTS + cleared * 2u, &points, sizeof(points));
|
||||
score += (uint32_t)points * level;
|
||||
update_best();
|
||||
spawn_piece();
|
||||
}
|
||||
@@ -396,7 +400,8 @@ static bool move_down(bool manual)
|
||||
static void rotate_piece(void)
|
||||
{
|
||||
const uint8_t next = (uint8_t)((rotation + 1u) & 3u);
|
||||
static const int8_t kick[5] = {0, -1, 1, -2, 2};
|
||||
int8_t kick[KICK_LEN];
|
||||
A->asset_read(KICK, kick, sizeof(kick));
|
||||
for (uint8_t i = 0; i < 5u; i++)
|
||||
if (!collision(piece, next, pieceX + kick[i], pieceY)) {
|
||||
pieceX += kick[i]; rotation = next; return;
|
||||
@@ -421,9 +426,27 @@ static void hard_drop(void)
|
||||
/* Input, screensaver handling and the main loop */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers,
|
||||
* with a blinking prompt on the blank bottom page. */
|
||||
static void draw_title(void)
|
||||
{
|
||||
titleTick++;
|
||||
A->asset_read(ART_TITLE, A->status_line, W);
|
||||
A->asset_read(ART_TITLE + W, A->fb[0], 7u * W);
|
||||
if (titleTick & 32u)
|
||||
A->print_bold(T(T_PRESS), 0, W - 1, 6);
|
||||
}
|
||||
|
||||
static void key_action(uint8_t key, bool repeat)
|
||||
{
|
||||
if (key == APP_KEY_EXIT) { running = false; return; }
|
||||
if (title) { /* the game-over restart keys also leave the title */
|
||||
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) {
|
||||
title = false;
|
||||
new_game();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (gameOver) {
|
||||
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0))
|
||||
new_game();
|
||||
@@ -509,14 +532,13 @@ void app_main(const app_api_t *api)
|
||||
best = cfg.magic == CFG_MAGIC && cfg.version == 1u ? cfg.best : 0u;
|
||||
bestDirty = false;
|
||||
|
||||
randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ best;
|
||||
if (!randomState) randomState = 1;
|
||||
previousKey = APP_KEY_INVALID;
|
||||
repeatMs = 0;
|
||||
running = true;
|
||||
A->led(false);
|
||||
A->backlight_on();
|
||||
new_game();
|
||||
title = true;
|
||||
|
||||
while (running) {
|
||||
poll_key();
|
||||
@@ -528,7 +550,7 @@ void app_main(const app_api_t *api)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!paused && !gameOver) {
|
||||
if (!title && !paused && !gameOver) {
|
||||
const uint16_t interval = level >= 15u ? 100u : (uint16_t)(900u - (level - 1u) * 55u);
|
||||
gravityMs = (uint16_t)(gravityMs + TICK_MS);
|
||||
if (gravityMs >= interval) {
|
||||
@@ -537,7 +559,8 @@ void app_main(const app_api_t *api)
|
||||
}
|
||||
}
|
||||
|
||||
render(true, 0, 0);
|
||||
if (title) draw_title();
|
||||
else render(true, 0, 0);
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
A->backlight_update();
|
||||
|
||||
@@ -44,6 +44,13 @@
|
||||
|
||||
static volatile uint32_t gGlobalSysTickCounter;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
uint32_t SCHEDULER_GetTick10ms(void)
|
||||
{
|
||||
return gGlobalSysTickCounter;
|
||||
}
|
||||
#endif
|
||||
|
||||
// we come here every 10ms
|
||||
void SysTick_Handler(void)
|
||||
{
|
||||
|
||||
@@ -28,4 +28,9 @@ static void inline SCHEDULER_Disable()
|
||||
NVIC_DisableIRQ(SysTick_IRQn);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
/* Free-running 10 ms SysTick counter (wraps after ~497 days). */
|
||||
uint32_t SCHEDULER_GetTick10ms(void);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+5
-2
@@ -66,7 +66,8 @@ printf ' VMA %s · budget %d B / %d.00 KiB · out %s/\n\n' \
|
||||
files=(); sizes=(); vmas=(); states=()
|
||||
fail=0
|
||||
|
||||
read_u32le() { od -An -tx1 -j"$2" -N4 "$1" | awk '{printf "0x%s%s%s%s",$4,$3,$2,$1}'; }
|
||||
read_u32le() { od -An -tx1 -j"$2" -N4 "$1" | awk 'NF{printf "0x%s%s%s%s",$4,$3,$2,$1}'; }
|
||||
read_u16le() { od -An -tx1 -j"$2" -N2 "$1" | awk 'NF{printf "0x%s%s",$2,$1}'; }
|
||||
|
||||
for app in "${TARGETS[@]}"; do
|
||||
if docker run --rm ${TTY_ARG:+"$TTY_ARG"} -u "$(id -u):$(id -g)" \
|
||||
@@ -77,9 +78,11 @@ for app in "${TARGETS[@]}"; do
|
||||
if [ -n "$appfile" ] && [ -f "$appfile" ]; then
|
||||
cp -f "$appfile" "$OUT_DIR/"
|
||||
base=$(basename "$appfile")
|
||||
code=$(( $(wc -c < "$appfile") - 64 ))
|
||||
code=$(( $(read_u32le "$appfile" 8) )) # app_header_t.code_size
|
||||
assets=$(( $(read_u16le "$appfile" 60) )) # app_header_t.asset_size
|
||||
vma=$(read_u32le "$appfile" 52)
|
||||
state="✅ OK"; [ "$code" -gt "$OVERLAY_MAX" ] && { state="🚨 OVERFLOW"; fail=1; }
|
||||
[ "$assets" -gt 0 ] && state="$state (+$assets B assets)"
|
||||
else
|
||||
base="$app.app"; code=-1; vma="--"; state="❌ NO BLOB"; fail=1
|
||||
fi
|
||||
|
||||
Reference in new issue
Block a user