Sample programs
Complete, working programs to read, run and take apart. Every one of them ships on the
device in SDK\ - open it in Studio, press Ctrl-B to build and
Ctrl-R to run. Each sample earns its place by teaching something the one before it didn't.
| Sample | Language | What it teaches |
|---|---|---|
| SAMPLE.C / SAMPLE.PY | UnoC / Python | The app skeleton: the vtable / the uno.App class, drawing, keys, ticks. |
| TIMER.C | UnoC | Real timekeeping, number formatting without printf, sound, deliberate repainting. |
| LIFE.C | UnoC | Arrays and computation, randomness, pacing a simulation. |
| TODO.PY | Python | Files that survive reboots, both kinds of key, the idle idiom. |
| CHART.PY | Python | Parsing a file, scaling to the canvas, graceful fallbacks. |
| GOODNITE.PY | Python | Automation under the permission model - three calls at three tiers. |
| DOSTRIS.C | UnoC | A complete shipped game, unchanged - the big worked example. |
| bench_snapshot.py | Python, on your PC | A genuinely scheduled task: your PC's scheduler drives the box over the remote link. |
TIMER.C - a kitchen timer
The natural second program after SAMPLE.C, because it forces the three habits a real UnoC app needs.
The frame is the timebase - count your own ticks. tick() arrives once per shell
frame, ~60 a second, and the timer counts those calls; pausing works by simply not counting. It
deliberately does not use TickCount() - on pc64 that is a
call counter shared by every app, not a clock.
Repaint deliberately: a tick-driven app calls repaint_all(), and only when the
displayed state changed - once a second here, not sixty times. And numbers without printf:
UnoC has no varargs, so put2() composes the MM:SS readout and
fmt_u() does general unsigned decimal.
Also in there: one-shot notes with music_open_chan() + music_note_on() for
the alarm, and a countdown that rounds up (one remaining tick still reads 0:01) while the
stopwatch rounds down - the small honesty every clock UI owes its user.
win_title./* TIMER.C - a kitchen timer and stopwatch for the UnoDOS desktop.
*
* Space starts and pauses. + and - (or the Up/Down arrows) dial the
* countdown a minute at a time, M switches between countdown and
* stopwatch, N resets. When the countdown reaches zero the window
* flashes and beeps until you press any key.
*
* What it demonstrates, beyond SAMPLE.C: keeping time by counting your
* own tick() calls (the shell ticks you ~60 times a second - and that,
* not TickCount(), is the platform's timebase: TickCount() counts CALLS,
* shared by every app, so pace on your own frames), formatting numbers
* with put2() - there is no printf - playing notes with music_note_on(),
* and the repaint_all() redraw idiom. Build with Ctrl-B, run with
* Ctrl-R.
*/
#include "UNO.H"
#define BAR_W 240
#define MAX_MIN 95
#define START_T 18000 /* 5 minutes, in 60 Hz frames */
#define CAP_T 359940 /* put2 shows 2 digits: 99:59 */
static long set_frames = START_T; /* the dialled countdown duration */
static long left = START_T; /* countdown frames remaining */
static long elapsed; /* stopwatch frames accumulated */
static Boolean running = false;
static Boolean stopwatch = false; /* false = countdown mode */
static long ring_frames; /* nonzero while the alarm rings */
static long beep_in; /* frames until the next beep */
static long shown = -1; /* last second painted */
static Boolean flash;
static const GameRGB kFace = { 24, 26, 40, C_BLUE };
static const GameRGB kRing = { 200, 40, 40, C_MAG };
static const GameRGB kBarOn = { 90, 210, 120, C_CYAN };
static const GameRGB kBarOff = { 46, 50, 72, C_BLUE };
static void timer_draw(UnoWin *w)
{
Rect r;
char t[8];
short x0 = w->bounds.left + 14;
short y0 = w->bounds.top + TBAR_H + 10;
long frames_now = stopwatch ? elapsed : left;
/* a countdown rounds up (1 frame left still reads 0:01); a stopwatch
* rounds down, like every stopwatch you have ever held */
long secs = stopwatch ? frames_now / 60 : (frames_now + 59) / 60;
short fill;
SetRect(&r, x0 - 4, y0 - 4, x0 + BAR_W + 4, y0 + 96);
fill_rgb(&r, (ring_frames && flash) ? &kRing : &kFace);
uno_box(&r, C_WHITE);
text_at(x0, y0 + 6, stopwatch ? "STOPWATCH" : "COUNTDOWN",
C_CYAN, C_BLUE, false);
put2(secs / 60, t);
t[2] = ':';
put2(secs % 60, t + 3);
text_at(x0 + 170, y0 + 6, t, C_WHITE, C_BLUE, false);
/* the bar drains as the countdown runs */
SetRect(&r, x0, y0 + 28, x0 + BAR_W, y0 + 44);
fill_rgb(&r, &kBarOff);
if (!stopwatch && set_frames > 0) {
fill = (short)((long)BAR_W * (left / 60) / (set_frames / 60));
if (fill > 0) {
SetRect(&r, x0, y0 + 28, x0 + fill, y0 + 44);
fill_rgb(&r, &kBarOn);
}
}
text_at(x0, y0 + 56, running ? "Space: pause" : "Space: start",
C_WHITE, C_BLUE, false);
text_at(x0 + 110, y0 + 56, "M: mode N: reset", C_MAG, C_BLUE, false);
text_at(x0, y0 + 74, "+ / - or Up / Down: dial the minutes",
C_CYAN, C_BLUE, false);
}
static void timer_reset(void)
{
running = false;
left = set_frames;
elapsed = 0;
shown = -1;
}
static Boolean timer_key(char ch, short code, Boolean cmd)
{
if (cmd) return false;
if (ring_frames) { /* any key silences the alarm */
ring_frames = 0;
music_quiet();
repaint_all();
return true;
}
if (ch == ' ') {
running = !running;
repaint_all();
return true;
}
if (ch == 'n' || ch == 'N') {
timer_reset();
repaint_all();
return true;
}
if (ch == 'm' || ch == 'M') {
stopwatch = !stopwatch;
timer_reset();
repaint_all();
return true;
}
if (ch == '+' || ch == '=' || ch == 0x1E) { /* Up arrow */
if (set_frames < (long)MAX_MIN * 60 * 60)
set_frames += 60 * 60;
if (!running) left = set_frames;
repaint_all();
return true;
}
if (ch == '-' || ch == 0x1F) { /* Down arrow */
if (set_frames > 60 * 60)
set_frames -= 60 * 60;
if (!running) left = set_frames;
repaint_all();
return true;
}
(void)code;
return false;
}
/* tick() arrives once per shell frame, ~60/s: the frame IS the clock.
* Pausing works by not counting - a paused timer simply lets frames by. */
static void timer_tick(void)
{
long secs;
if (ring_frames) {
ring_frames--;
if (!ring_frames) { /* rang long enough on its own */
music_quiet();
repaint_all();
return;
}
if (--beep_in <= 0) {
music_note_on(96, 9);
beep_in = 24;
}
if (((ring_frames / 12) & 1) != (long)flash) {
flash = (Boolean)((ring_frames / 12) & 1);
repaint_all();
}
return;
}
if (!running) return;
if (stopwatch) {
if (elapsed < CAP_T) elapsed++;
secs = elapsed / 60;
} else {
left--;
if (left <= 0) {
left = 0;
running = false;
ring_frames = 300; /* ring for five seconds */
beep_in = 1;
repaint_all();
return;
}
secs = (left + 59) / 60;
}
if (secs != shown) { /* repaint once a second, not 60x */
shown = secs;
repaint_all();
}
}
static void timer_opened(void)
{
music_open_chan();
}
static const AppInterface kIface = {
timer_draw, timer_key, 0, timer_tick, timer_opened, 0,
"Timer", { 40, 40, 328, 194 }
};
const AppInterface *uno_app_main(const KernelApi *k)
{
gK = k;
return &kIface;
}
LIFE.C - Conway's Game of Life
Real computation in UnoC: 2-D unsigned char arrays, a double buffer flipped with one
memcpy, and a wrapping neighbourhood via modular arithmetic. Three details worth stealing:
the simulation is paced (a generation every 6 frames, counted by the app itself - the shell stays
responsive and the motion reads as motion); the draw paints only what is alive over one background
fill; and the board seeds
defensively, stirring the cell coordinates into Random() so even a weak generator
yields a live board. Note opened() can run again after a close and reopen - the board seeds
only once.
/* LIFE.C - Conway's Game of Life.
*
* The classic cellular automaton on a wrapping grid. Space pauses,
* S steps one generation while paused, N reseeds the board, C clears it.
* Cells that survive a while cool from green to blue.
*
* What it demonstrates: 2-D arrays and real computation in UnoC, seeding
* randomness with Random(), pacing a simulation by counting tick() calls
* (the shell's ~60 Hz frame is the platform's timebase), and painting
* only what is alive instead of every cell. Build with Ctrl-B, run with
* Ctrl-R.
*/
#include "UNO.H"
#define COLS 44
#define ROWS 30
#define CELL 6
static unsigned char g[ROWS][COLS]; /* 0 dead, else generations alive */
static unsigned char nx[ROWS][COLS];
static Boolean running = true;
static Boolean seeded = false;
static long gen;
static long pop;
static short acc; /* frames since the last step */
static const GameRGB kBack = { 12, 14, 24, C_BLUE };
static const GameRGB kYoung = { 120, 220, 140, C_CYAN };
static const GameRGB kOld = { 70, 140, 220, C_CYAN };
static void life_seed(void)
{
short r, c;
pop = 0;
for (r = 0; r < ROWS; r++)
for (c = 0; c < COLS; c++) {
/* stir the coordinates in so even a weak Random() seeds life */
short v = (short)(((Random() + r * 31 + c * 17) & 0x7FFF) % 100);
g[r][c] = (unsigned char)(v < 30 ? 1 : 0);
if (g[r][c]) pop++;
}
gen = 0;
}
static short life_neigh(short r, short c)
{
short dr, dc, n = 0;
for (dr = -1; dr <= 1; dr++)
for (dc = -1; dc <= 1; dc++) {
short rr, cc;
if (dr == 0 && dc == 0) continue;
rr = (short)((r + dr + ROWS) % ROWS); /* the grid wraps */
cc = (short)((c + dc + COLS) % COLS);
if (g[rr][cc]) n++;
}
return n;
}
static void life_step(void)
{
short r, c, n;
pop = 0;
for (r = 0; r < ROWS; r++)
for (c = 0; c < COLS; c++) {
n = life_neigh(r, c);
if (g[r][c])
nx[r][c] = (unsigned char)
((n == 2 || n == 3) ? (g[r][c] < 200 ? g[r][c] + 1 : 200) : 0);
else
nx[r][c] = (unsigned char)(n == 3 ? 1 : 0);
if (nx[r][c]) pop++;
}
memcpy(g, nx, sizeof(g));
gen++;
}
static void life_draw(UnoWin *w)
{
Rect f, q;
short r, c, x, y;
char num[16];
short x0 = w->bounds.left + 8;
short y0 = w->bounds.top + TBAR_H + 6;
short by = (short)(y0 + ROWS * CELL);
SetRect(&f, x0 - 2, y0 - 2, x0 + COLS * CELL + 2, by + 2);
uno_box(&f, C_WHITE);
SetRect(&f, x0, y0, x0 + COLS * CELL, by);
fill_rgb(&f, &kBack);
for (r = 0; r < ROWS; r++)
for (c = 0; c < COLS; c++)
if (g[r][c]) {
x = (short)(x0 + c * CELL);
y = (short)(y0 + r * CELL);
SetRect(&q, x, y, x + CELL - 1, y + CELL - 1);
fill_rgb(&q, g[r][c] > 4 ? &kOld : &kYoung);
}
/* Below the field the background is the window's own light chrome, so
* these draw in BLUE - C_WHITE is invisible off the dark playfield. */
text_at(x0, by + 8, "Gen:", C_CYAN, C_BLUE, false);
fmt_u(gen, num);
text_at(x0 + 38, by + 8, num, C_BLUE, C_BLUE, false);
text_at(x0 + 100, by + 8, "Pop:", C_CYAN, C_BLUE, false);
fmt_u(pop, num);
text_at(x0 + 138, by + 8, num, C_BLUE, C_BLUE, false);
text_at(x0, by + 24,
running ? "Space: pause N: reseed C: clear"
: "PAUSED - Space runs, S steps once",
C_MAG, C_BLUE, false);
}
static Boolean life_key(char ch, short code, Boolean cmd)
{
if (cmd) return false;
if (ch == ' ') {
running = !running;
repaint_all();
return true;
}
if (ch == 's' || ch == 'S') {
running = false;
life_step();
repaint_all();
return true;
}
if (ch == 'n' || ch == 'N') {
life_seed();
repaint_all();
return true;
}
if (ch == 'c' || ch == 'C') {
memset(g, 0, sizeof(g));
gen = 0;
pop = 0;
running = false;
repaint_all();
return true;
}
(void)code;
return false;
}
/* tick() arrives once per shell frame, ~60/s - count frames yourself
* rather than trusting TickCount(), which counts calls, not time. */
static void life_tick(void)
{
if (!running) return;
if (++acc < 6) return; /* ~10 generations a second */
acc = 0;
life_step();
repaint_all();
}
static void life_opened(void)
{
if (!seeded) { /* opened() can run again after a
* close/reopen - keep the board */
life_seed();
seeded = true;
}
}
static const AppInterface kIface = {
life_draw, life_key, 0, life_tick, life_opened, 0,
"Life", { 30, 30, 320, 280 }
};
const AppInterface *uno_app_main(const KernelApi *k)
{
gK = k;
return &kIface;
}
TODO.PY - a to-do list that survives reboots
Persistence is two calls: uno.read() returns None if the file doesn't exist
(a fine first run), and uno.write() returns False on a read-only volume - the
app shows that state instead of hiding it. The format is plain text ([x] task /
[ ] task), so Notepad can edit the same file. The key() handler routes both
kinds of key - printable characters arrive in uni, special keys in scan - and
tick() returns False whenever nothing changed, so a static window costs the
machine nothing.
# TODO.PY - a to-do list that survives reboots.
#
# Type a task and press Enter to add it. Up/Down pick a task, Tab checks
# it off, Delete removes it, Esc clears what you were typing. The list is
# written to TODO.TXT on the boot volume after every change and loaded
# back the next time the app opens - it is plain text, so Notepad can read
# and edit the same file.
#
# What it demonstrates, beyond SAMPLE.PY: persistence with uno.read() and
# uno.write(), telling printable keys (uni) from special keys (scan) apart,
# and the idle idiom - tick() returns False whenever nothing changed, so
# the shell doesn't repaint a static window 60 times a second.
import uno
FILE = "TODO.TXT"
BG = uno.rgb(20, 22, 36)
INK = uno.rgb(214, 218, 232)
DIM = uno.rgb(120, 126, 150)
HEAD = uno.rgb(120, 200, 255)
GOOD = uno.rgb(110, 210, 130)
BAD = uno.rgb(240, 120, 120)
SELB = uno.rgb(44, 50, 80)
class Todo(uno.App):
def build(self, cv):
self.items = [] # [done, text] pairs
self.sel = 0
self.edit = ""
self.saved = True
self.dirty = True
data = uno.read(FILE) # None if the file doesn't exist yet
if data:
for ln in data.decode("latin1").split("\n"):
ln = ln.strip()
if ln.startswith("[x] "):
self.items.append([True, ln[4:]])
elif ln.startswith("[ ] "):
self.items.append([False, ln[4:]])
def save(self):
out = ""
for done, text in self.items:
out += ("[x] " if done else "[ ] ") + text + "\n"
# write() returns False on a read-only volume - show it, don't hide it
self.saved = uno.write(FILE, out.encode())
def key(self, uni, scan, ctrl):
if scan == 1 and self.sel > 0: # Up
self.sel -= 1
elif scan == 2 and self.sel + 1 < len(self.items): # Down
self.sel += 1
elif scan == 8 and self.items: # Delete: remove
self.items.pop(self.sel)
if self.sel and self.sel >= len(self.items):
self.sel -= 1
self.save()
elif scan == 0x17: # Esc: drop the entry
self.edit = ""
elif uni == 13 and self.edit: # Enter: add
self.items.append([False, self.edit])
self.sel = len(self.items) - 1
self.edit = ""
self.save()
elif uni == 9 and self.items: # Tab: toggle done
self.items[self.sel][0] = not self.items[self.sel][0]
self.save()
elif uni == 8: # Backspace
self.edit = self.edit[:-1]
elif 32 <= uni < 127 and len(self.edit) < 48: # typing
self.edit += chr(uni)
else:
return False
self.dirty = True
return True
def tick(self):
if self.dirty:
self.dirty = False
return None # repaint this frame...
return False # ...and sleep the rest of the time
def draw(self, cv):
cv.clear(BG)
done = 0
for it in self.items:
if it[0]:
done += 1
cv.text(10, 8, "TO-DO", HEAD)
cv.text(cv.width() - 110, 8, "%d of %d done" % (done, len(self.items)), DIM)
cv.hline(10, 24, cv.width() - 20, DIM)
# the list (the last 14 entries fit; older ones scroll away)
shown = self.items[-14:]
base = len(self.items) - len(shown)
y = 32
for i in range(len(shown)):
if base + i == self.sel:
cv.fill_rect(6, y - 2, cv.width() - 12, 15, SELB)
mark, text = shown[i][0], shown[i][1]
cv.text(10, y, "[x]" if mark else "[ ]", GOOD if mark else DIM)
cv.text(42, y, text, DIM if mark else INK)
y += 16
if not self.items:
cv.text(42, 40, "nothing yet - type a task, press Enter", DIM)
# the entry line and the save state
y = cv.height() - 44
cv.hline(10, y, cv.width() - 20, DIM)
cv.text(10, y + 6, "> " + self.edit + "_", INK)
cv.text(10, y + 22, "Enter add Tab done Del remove", DIM)
cv.text(10, y + 34,
"saved to " + FILE if self.saved else "NOT SAVED - volume is read-only",
GOOD if self.saved else BAD)
app = Todo()
CHART.PY - a bar chart from a file
A small, genuinely useful tool: drop a DATA.CSV on the boot volume - one
label,value per line - and it draws the chart, scaled to fit, with the biggest bar called
out. Press R after editing the file in Notepad. It parses without a csv module (there
isn't one): split(","), strip(), and a try/except ValueError
that makes header lines and junk simply vanish. Everything is scaled from
cv.width()/cv.height() with integer math - never hardcode the canvas size,
the shell picks your window. With no file it charts built-in demo data and says so, so the first launch
teaches you what to do next.
# CHART.PY - a bar chart drawn from a text file.
#
# Put a DATA.CSV on the boot volume - one "label,value" line each, like
#
# MON,12
# TUE,19
# WED,7
#
# and this app charts it, scaled to fit the window, with the biggest bar
# called out. Edit the file in Notepad and press R here to reload it.
# With no file it charts built-in demo data so you can see the shape first.
#
# What it demonstrates: reading and parsing a file with uno.read(), integer
# scaling to a variable-size canvas (read cv.width()/height(), never
# hardcode - the shell picks your window size), and a static app that only
# repaints when its data changes.
import uno
FILE = "DATA.CSV"
BG = uno.rgb(18, 20, 34)
INK = uno.rgb(214, 218, 232)
DIM = uno.rgb(110, 116, 140)
BAR = uno.rgb(96, 170, 255)
TOP = uno.rgb(255, 170, 90)
DEMO = [["MON", 12], ["TUE", 19], ["WED", 7], ["THU", 23],
["FRI", 17], ["SAT", 4], ["SUN", 9]]
class Chart(uno.App):
def build(self, cv):
self.load()
def load(self):
self.rows = []
self.demo = False
data = uno.read(FILE)
if data:
for ln in data.decode("latin1").split("\n"):
parts = ln.strip().split(",")
if len(parts) < 2:
continue
try:
v = int(parts[1].strip())
except ValueError:
continue # a header line, or junk - skip it
self.rows.append([parts[0].strip(), v if v > 0 else 0])
if len(self.rows) == 16: # more bars than that won't fit
break
if not self.rows:
self.rows = DEMO
self.demo = True
self.dirty = True
def key(self, uni, scan, ctrl):
if uni in (114, 82): # r / R: reload the file
self.load()
return True
return False
def tick(self):
if self.dirty:
self.dirty = False
return None
return False
def draw(self, cv):
cv.clear(BG)
head = ("%s not found - demo data (R reloads)" % FILE) if self.demo else FILE
cv.text(10, 8, head, DIM if self.demo else INK)
x0, y0 = 34, 30 # plot origin (top-left)
ph = cv.height() - y0 - 34 # plot height
pw = cv.width() - x0 - 10
base = y0 + ph # the x axis
mx = 1
for r in self.rows:
if r[1] > mx:
mx = r[1]
cv.vline(x0, y0, ph, DIM)
cv.hline(x0, base, pw, DIM)
cv.text(4, y0 - 4, "%d" % mx, DIM)
cv.text(4, base - 10, "0", DIM)
n = len(self.rows)
step = pw // n
bw = step - 6 if step > 12 else step - 2
for i in range(n):
label, v = self.rows[i][0], self.rows[i][1]
h = v * ph // mx
x = x0 + 4 + i * step
cv.fill_rect(x, base - h, bw, h, TOP if v == mx else BAR)
cv.text(x, base - h - 12, "%d" % v, INK)
cv.text(x, base + 6, label[:5], DIM)
app = Chart()
GOODNITE.PY - an end-of-day automation app
The bridge from apps to automation: one launch journals what you were working on, then offers to shut the machine down. It is the permission model made concrete - three calls at three tiers:
| Call | Capability | Tier |
|---|---|---|
u.ui.screen() - read the window tree | ui.read | 0, ambient |
u.fs.write("journal/...") - into your home | fs.user |
1 |
u.sys.power(0) - shut down | power | 2, consent |
Two things to take away. Denial is not an error state: denied calls raise OSError
(or u.request() answers False), the app handles both, and the same script runs
everywhere - it just does less on a machine that trusts it less. And the generator/tick pattern is
load-bearing: an injected action lands on the next shell frame, so an automation script yields
between step and check, pacing those yields on unoauto.uptime() - the wall clock in
milliseconds, and the only real clock an app can read. For unattended runs, a signed manifest
(.MFT sidecar) grants the declared capabilities at launch, no
prompts.
# GOODNITE.PY - an end-of-day automation app.
#
# One launch journals what you were working on, then offers to shut the
# machine down. It is a worked example of `unoscript`, the production
# automation surface, doing real work under the permission model:
#
# - reading the window tree u.ui.screen() tier 0, ambient
# - writing into your home u.fs.write() tier 1, fs.user
# - powering the machine off u.sys.power(0) tier 2, `power` -
# granted only through an explicit consent
# (or a role, or a signed manifest)
#
# A capability you don't hold never crashes the app: denied calls raise
# OSError, and u.request() simply answers False. Handle both and the same
# script runs everywhere - it just does less on a machine that trusts it
# less.
#
# The steps run from a generator paced by tick() on unoauto.uptime(),
# the wall clock in milliseconds (ungated, real time in every build).
# That pattern is load-bearing for every automation app: an injected
# action lands on the NEXT shell frame, so a script must yield before it
# looks at the result.
import uno
import unoauto
import unoscript as u
JOURNAL = "journal/GOODNITE.LOG"
KEEP = 3000 # stay under unoscript's 4 KB read cap
BG = uno.rgb(16, 18, 34)
HEAD = uno.rgb(120, 200, 255)
INK = uno.rgb(214, 218, 232)
WARN = uno.rgb(255, 170, 90)
class GoodNight(uno.App):
def build(self, cv):
self.lines = []
self.armed = False
self.until = 0
self.gen = self.script()
self.dirty = True
def say(self, s):
self.lines.append(s)
self.dirty = True
# ---- the workflow; each yield is a pause in milliseconds -------------
def script(self):
uid = u.whoami() # 0xFFFFFFFF when nobody is signed in
self.say("Good night." if uid in (-1, 0xFFFFFFFF)
else "Good night, user %d." % uid)
yield 500
# 1. what is open right now? (ui.read - ambient, tier 0)
try:
wins = [ln.strip() for ln in u.ui.screen().split("\n") if ln.strip()]
except OSError:
wins = []
self.say("open right now: %d window(s)" % len(wins))
for w in wins[:6]:
self.say(" " + w[:44])
yield 1000
# 2. journal it into your home (fs.user, tier 1)
entry = "-- session, uptime %d min, %d window(s)\n" % (
unoauto.uptime() // 60000, len(wins))
for w in wins[:6]:
entry += " " + w[:60] + "\n"
journaled = False
try:
try:
old = u.fs.read(JOURNAL).decode("latin1")
except OSError:
old = "" # first run: no journal yet
u.fs.write(JOURNAL, (old + entry)[-KEEP:].encode())
self.say("journaled to home:" + JOURNAL)
journaled = True
except (OSError, NotImplementedError):
self.say("journal skipped - fs.user not granted")
yield 500
# 3. ask permission to power off (power, tier 2)
if u.request("power"):
self.say("")
self.say("Enter shuts the machine down. Esc keeps it on.")
self.armed = True
else:
self.say("")
self.say("`power` not granted - leaving the machine on.")
if journaled:
self.say("(journal is written; close me when you're done)")
def tick(self):
if self.gen:
now = unoauto.uptime() # wall-clock milliseconds
if now >= self.until:
try:
self.until = now + next(self.gen)
except StopIteration:
self.gen = None
if self.dirty:
self.dirty = False
return None
return False
def key(self, uni, scan, ctrl):
if self.armed and uni == 13: # Enter: do it
self.say("shutting down...")
u.sys.power(0) # does not return when granted
return True
if self.armed and scan == 0x17: # Esc: stand down
self.armed = False
self.say("cancelled - staying on.")
return True
return False
def draw(self, cv):
cv.clear(BG)
cv.text(10, 8, "GOODNITE - end-of-day automation", HEAD)
cv.hline(10, 24, cv.width() - 20, uno.rgb(60, 66, 96))
y = 32
for s in self.lines[-14:]:
cv.text(10, y, s, WARN if self.armed and s.startswith("Enter") else INK)
y += 16
app = GoodNight()
bench_snapshot.py - a genuinely scheduled task
There is no cron on the device - deliberately: pc64 has no preemptive scheduler, and background scripts are a reserved capability for a facility that doesn't exist yet. The honest scheduled-task story is your PC schedules, the box answers. Task Scheduler or cron runs this script nightly; it waits for the box to dial in over the remote link, authenticates with the Remote-control code, appends one CSV row of uptime/heap/fs/net counters, saves a screenshot, and exits. A month later the folder answers "when did that start?" for free.
Arm only Watch on the device - the script only observes, so give it only that - and put the
6-digit code in UNO_PIN on the PC. The standing caveat applies: the link is plaintext and
LAN-only by intent; the code proves who may drive the box, it does not encrypt the wire.
#!/usr/bin/env python3
"""bench_snapshot.py - a scheduled health snapshot of a UnoDOS machine.
Run this from your PC's scheduler each night. It waits for the box to
dial in over the URC link, authenticates, and files one snapshot: a
screenshot, plus a CSV row of uptime, heap, filesystem and network
counters. A month later you have a folder that answers "when did that
start" for free.
Everything it needs ships with UnoDOS: tools/unoauto_remote.py is the
client library - keep this script next to it, or point PYTHONPATH at it.
On the machine: Control Panel -> Remote control..., tick Watch (and
nothing else - this script only observes), note the 6-digit code, and
choose discover so the box finds this PC by broadcast. Put the code in
the UNO_PIN environment variable on the PC. Debug builds skip the code.
Schedule it:
Windows: schtasks /Create /TN UnoSnapshot /SC DAILY /ST 23:00 ^
/TR "py C:\\unodos\\pc64\\tools\\bench_snapshot.py"
Linux: 0 23 * * * python3 /home/you/unodos/pc64/tools/bench_snapshot.py
"""
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from unoauto_remote import UnoAutoLink
PIN = os.environ.get("UNO_PIN", "")
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "snapshots")
CSV = os.path.join(OUT, "health.csv")
def main():
os.makedirs(OUT, exist_ok=True)
stamp = time.strftime("%Y%m%d-%H%M%S")
link = UnoAutoLink(port=5099)
link.listen() # the box dials us
if not link.wait_connected(timeout=300):
print("no box dialled in - is it on, and is discover armed?")
return 1
link.wait_hello(timeout=30) # booted and draining its link
if PIN: # production builds authenticate
link.command("auth", PIN)
# one CSV row of the numbers that drift
row = {"uptime_s": link.uptime() // 1000}
for p in link.probe(): # kind 2 = subsystem rows
if p["kind"] == 2 and p["name"] in ("heap", "fs", "net"):
row[p["name"] + "_v1"] = p["v1"]
row[p["name"] + "_v2"] = p["v2"]
new = not os.path.exists(CSV)
keys = sorted(row)
with open(CSV, "a") as f:
if new:
f.write("stamp," + ",".join(keys) + "\n")
f.write(stamp + "," + ",".join(str(row[k]) for k in keys) + "\n")
# and one screenshot, as a portable PPM
w, h, rgba = link.screen_grab()
shot = os.path.join(OUT, stamp + ".ppm")
rgb = bytearray()
for i in range(0, len(rgba), 4):
rgb += rgba[i:i + 3]
with open(shot, "wb") as f:
f.write(b"P6\n%d %d\n255\n" % (w, h))
f.write(bytes(rgb))
link.close()
print("snapshot %s: %dx%d screen, %s" % (stamp, w, h,
", ".join("%s=%s" % (k, row[k]) for k in keys)))
return 0
if __name__ == "__main__":
sys.exit(main())
schtasks /Create /TN UnoSnapshot /SC DAILY /ST 23:00 /TR "py ...\bench_snapshot.py" on Windows, or 0 23 * * * python3 .../bench_snapshot.py in a crontab.DOSTRIS.C and the shipped sources
SDK\DOSTRIS.C is the shipped game, ported to UnoC unchanged - the big worked example for
game state, a piece table in nested initializers, line clearing and a looping gm_start
song. SDK\DUUM.PY is the whole Doom-style engine in Python, streaming a real WAD with
uno.read_at. Both reward reading with the C and
Python references open beside them.
Next: Writing apps for the app model these all share, and the UnoC SDK reference / Python SDK reference for every call they make.