Add read-only assets to overlay apps and improve apps

This commit is contained in:
Armel FAUVEAU committed 2026-09-24 22:32:00 +02:00
1 parent f64406277c
commit 9d5b3af7a3
43 files changed
+1478 -514

No files matched your search

+1
View File
@@ -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
View File
@@ -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). */
+140
View File
@@ -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)
+107
View File
@@ -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
View File
@@ -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
View File
@@ -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 */
+31 -30
View File
@@ -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 -7
View File
@@ -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")
+30
View File
@@ -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()
+8 -15
View File
@@ -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();
+8 -7
View File
@@ -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")
+15
View File
@@ -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()
+42 -18
View File
@@ -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 -7
View File
@@ -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")
+53
View File
@@ -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 -7
View File
@@ -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")
+58 -96
View File
@@ -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);
+90
View File
@@ -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 -7
View File
@@ -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
View File
@@ -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);
+22
View File
@@ -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 -7
View File
@@ -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")
+29 -34
View File
@@ -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++='.';
+34
View File
@@ -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
View File
@@ -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 -7
View File
@@ -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")
+39
View File
@@ -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()
+19 -37
View File
@@ -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));
}
+8 -7
View File
@@ -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")
+60
View File
@@ -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)
+51 -43
View File
@@ -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);
+8 -7
View File
@@ -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")
+79
View File
@@ -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
View File
@@ -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();
+8 -7
View File
@@ -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")
+62
View File
@@ -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()
+63 -53
View File
@@ -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);
}
+8 -7
View File
@@ -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")
+65
View File
@@ -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)
+61 -38
View File
@@ -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();
+7
View File
@@ -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)
{
+5
View File
@@ -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
View File
@@ -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