Skip to content
Snippets Groups Projects
Commit fd7d6eef authored by q3k's avatar q3k
Browse files

sim: init

This implements a basic Python simulator for the badge. It currently
can't do much other than just displaying the main selection screen, as
it implements no inputs.
parent eb578558
No related branches found
No related tags found
No related merge requests found
...@@ -20,6 +20,9 @@ in with nixpkgs; pkgs.mkShell { ...@@ -20,6 +20,9 @@ in with nixpkgs; pkgs.mkShell {
cmake ninja cmake ninja
ncurses5 ncurses5
(python3.withPackages (ps: with ps; [ pygame wasmer wasmer-compiler-cranelift ]))
emscripten
]; ];
shellHook = '' shellHook = ''
# For esp.py openocd integration. # For esp.py openocd integration.
......
badg23 simulator
===
This is a little simulator that allows quicker development iteration on Python code.
It's a (C)Python application which sets up its environment so that it appears similar enough to the Badge's micropython environment, loading scripts from `python_payload` and `python_modules` in the main project directory.
All C-implemented functions are implemented (or maybe just stubbed out) by 'fakes' in the fakes directory. Please try to keep this in sync with the real usermodule implementation.
Of particular interest is how we provide a `ctx`-compatible API: we compile it using emscripten to a WebAssembly bundle, which we then execute using wasmer.
Setting up
---
If not using nix-shell, you'll need Python3 with the following libraries:
- pygame
- wasmer
- wasmer-compiler-cranelift
All of these should be available in PyPI.
Running
---
From the main badge23-firmware checkout:
```
python3 sim/run.py
```
Known Issues
---
No support for input of any kind yet (captouch, three-way buttons).
No support for audio yet.
No support for LEDs yet.
Hacking
---
A precompiled WASM bundle for ctx is checked into git. If you wish to rebuild it, run `build.sh` in `sim/wasm`.
sim/background.png

373 KiB

"""
ctx.py implements a subset of uctx that is backed by a WebAssembly-compiled
ctx. The interface between our uctx fake and the underlying ctx is the
serialized ctx protocol as described in [1].
[1] - https://ctx.graphics/protocol/
"""
import os
import wasmer
import wasmer_compiler_cranelift
class Wasm:
"""
Wasm wraps access to WebAssembly functions, converting to/from Python types
as needed. It's intended to be used as a singleton.
"""
def __init__(self):
store = wasmer.Store(wasmer.engine.JIT(wasmer_compiler_cranelift.Compiler))
simpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
wasmpath = os.path.join(simpath, 'wasm', 'ctx.wasm')
module = wasmer.Module(store, open(wasmpath, 'rb').read())
wasi_version = wasmer.wasi.get_version(module, strict=False)
wasi_env = wasmer.wasi.StateBuilder('badge23sim').finalize()
import_object = wasi_env.generate_import_object(store, wasi_version)
instance = wasmer.Instance(module, import_object)
self._i = instance
def malloc(self, n):
return self._i.exports.malloc(n)
def free(self, p):
self._i.exports.free(p)
def ctx_parse(self, ctx, s):
s = s.encode('utf-8')
slen = len(s) + 1
p = self.malloc(slen)
mem = self._i.exports.memory.uint8_view(p)
mem[0:slen] = s
mem[slen-1] = 0
self._i.exports.ctx_parse(ctx, p)
self.free(p)
def ctx_new_for_framebuffer(self, width, height):
"""
Call ctx_new_for_framebuffer, but also first allocate the underlying
framebuffer and return it alongside the Ctx*.
"""
fb = self.malloc(width * height * 2)
print('fb', hex(fb))
return fb, self._i.exports.ctx_new_for_framebuffer(fb, width, height, width * 2, 7)
def ctx_apply_transform(self, ctx, *args):
args = [float(a) for a in args]
return self._i.exports.ctx_apply_transform(ctx, *args)
_wasm = Wasm()
class Ctx:
"""
Ctx implements a subset of uctx [1]. It should be extended as needed as we
make use of more and more uctx features in the badge code.
[1] - https://ctx.graphics/uctx/
"""
LEFT = 'left'
RIGHT = 'right'
CENTER = 'center'
END = 'end'
MIDDLE = 'middle'
def __init__(self):
self._fb, self._ctx = _wasm.ctx_new_for_framebuffer(240, 240)
# Place (0, 0) in the center of the screen, mathing what the real badge
# software does.
_wasm.ctx_apply_transform(self._ctx, 1, 0, 120, 0, 1, 120, 0, 0, 1)
self.text_align = 'start'
self.text_baseline = 'alphabetic'
def _get_fb(self):
return _wasm._i.exports.memory.uint8_view(self._fb)
def _emit(self, text):
_wasm.ctx_parse(self._ctx, text)
def move_to(self, x, y):
self._emit(f"moveTo {x} {x}")
return self
def rgb(self, r, g, b):
self._emit(f"rgb {r/255} {g/255} {b/255}")
return self
def text(self, s):
self._emit(f"textAlign {self.text_align}")
self._emit(f"textBaseline {self.text_baseline}")
self._emit(f"text \"{s}\"")
return self
def round_rectangle(self, x, y, width, height, radius):
self._emit(f"roundRectangle {x} {y} {width} {height} {radius}")
return self
def rectangle(self, x, y, width, height):
self._emit(f"rectangle {x} {y} {width} {height}")
return self
def fill(self):
self._emit(f"fill")
return self
import pygame
import math
import os
pygame.init()
screen_w = 814
screen_h = 854
screen = pygame.display.set_mode(size=(screen_w, screen_h))
simpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
bgpath = os.path.join(simpath, 'background.png')
background = pygame.image.load(bgpath)
_deprecated_notified = set()
def _deprecated(f):
def wrapper(*args, **kwargs):
if f not in _deprecated_notified:
print(f'{f.__name__} is deprecated!')
_deprecated_notified.add(f)
return f(*args, **kwargs)
return wrapper
def init_done():
return True
def captouch_autocalib():
pass
def captouch_calibration_active():
return False
_global_ctx = None
def get_ctx():
global _global_ctx
import ctx
if _global_ctx is None:
_global_ctx = ctx.Ctx()
return _global_ctx
@_deprecated
def display_fill(color):
"""
display_fill is deprecated as it doesn't work well with ctx's framebuffer
ownership / state diffing. Instead, callers should use plain ctx functions
to fill the screen.
"""
r = (color >> 11) & 0b11111
g = (color >> 5 ) & 0b111111
b = color & 0b11111
get_ctx().rgb(r << 2, g << 3, b <<2).rectangle(-120, -120, 240, 240).fill()
def display_update():
fb = get_ctx()._get_fb()
full = pygame.Surface((screen_w, screen_h), flags=pygame.SRCALPHA)
full.blit(background, (0, 0))
center_x = 408
center_y = 426
off_x = center_x - (240 // 2)
off_y = center_y - (240 // 2)
oled = pygame.Surface((240, 240), flags=pygame.SRCALPHA)
oled_buf = oled.get_buffer()
for y in range(240):
rgba = bytearray()
for x in range(240):
dx = (x - 120)
dy = (y - 120)
dist = math.sqrt(dx**2 + dy**2)
fbh = fb[y * 240 * 2 + x * 2]
fbl = fb[y * 240 * 2 + x * 2 + 1]
fbv = (fbh << 8) | fbl
r = (fbv >> 11) & 0b11111
g = (fbv >> 5) & 0b111111
b = (fbv >> 0) & 0b11111
if dist > 120:
rgba += bytes([255, 255, 255, 0])
else:
rgba += bytes([b << 3, g << 2, r << 3, 0xff])
oled_buf.write(bytes(rgba), y * 240 * 4)
del oled_buf
full.blit(oled, (off_x, off_y))
screen.blit(full, (0,0))
pygame.display.flip()
def set_led_rgb(a, b, c, d):
pass
def update_leds():
pass
def set_global_volume_dB(a):
pass
def get_button(a):
return 0
def get_captouch(a):
return 0
class tinysynth:
def __init__(self, a, b):
pass
def decay(self, a):
pass
def waveform(self, a):
pass
def sleep_ms(ms):
import _time
_time.sleep(ms * 0.001)
import importlib
import importlib.machinery
from importlib.machinery import PathFinder, BuiltinImporter
import importlib.util
import os
import sys
projectpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
class Hook:
"""
Hook implements a importlib.abc.Finder which overwrites resolution order to
more closely match the resolution order on the badge's Micropython
environment.
"""
def find_spec(self, fullname, path, target=None):
# Attempt to load from python_payload, then python_modules then
# sim/fakes. Afterwards, the normal import resolution order kicks in.
paths = [
os.path.join(projectpath, 'python_payload', fullname+'.py'),
os.path.join(projectpath, 'python_payload', fullname),
os.path.join(projectpath, 'python_modules', fullname+'.py'),
os.path.join(projectpath, 'python_modules', fullname),
os.path.join(projectpath, 'sim', 'fakes', fullname+'.py'),
]
for p in paths:
if os.path.exists(p):
root = os.path.split(p)[:-1]
return PathFinder.find_spec(fullname, root)
# As we provide our own micropython-compatible time library, allow
# resolving the original CPython time through _time
if fullname == '_time':
return BuiltinImporter.find_spec('time')
return None
sys.meta_path.insert(0, Hook())
sys.path_importer_cache.clear()
# Clean up whatever might have already been imported as `time`.
import time
importlib.reload(time)
import main
ctx.wasm binary
#!/usr/bin/env bash
set -e -x
# Work around Nix badness.
# See: https://github.com/NixOS/nixpkgs/issues/139943
emscriptenpath="$(dirname $(dirname $(which emcc)))"
if [[ "${emscriptenpath}" = /nix/store/* ]]; then
if [ ! -d ~/.emscripten_cache ]; then
cp -rv "$emscriptenpath/share/emscripten/cache" ~/.emscripten_cache
chmod u+rwX -R ~/.emscripten_cache
fi
export EM_CACHE=~/.emscripten_cache
fi
emcc ctx.c \
-I ../../usermodule/uctx/uctx/ \
-I ../../usermodule/uctx/fonts/ \
-s EXPORTED_FUNCTIONS=_ctx_new_for_framebuffer,_ctx_parse,_ctx_apply_transform,_malloc,_free \
--no-entry -flto -O3 \
-o ctx.wasm
#include <stdint.h>
#include "ctx_config.h"
#define CTX_IMPLEMENTATION
#include "ctx.h"
File added
#pragma once
// Keep in sync with defines in usermodule/uctx/uctx.c.
#define CTX_TINYVG 1
#define CTX_TVG_STDIO 0
#define CTX_DITHER 1
#define CTX_PDF 0
#define CTX_PROTOCOL_U8_COLOR 1
#define CTX_AVOID_CLIPPED_SUBDIVISION 0
#define CTX_LIMIT_FORMATS 1
#define CTX_ENABLE_FLOAT 0
#define CTX_32BIT_SEGMENTS 0
#define CTX_ENABLE_RGBA8 1
#define CTX_ENABLE_RGB332 1
#define CTX_ENABLE_GRAY1 1
#define CTX_ENABLE_GRAY2 1
#define CTX_ENABLE_GRAY4 1
#define CTX_ENABLE_RGB565 1
#define CTX_ENABLE_RGB565_BYTESWAPPED 1
#define CTX_ENABLE_CBRLE 0
#define CTX_BITPACK_PACKER 0
#define CTX_COMPOSITING_GROUPS 0
#define CTX_RENDERSTREAM_STATIC 0
#define CTX_GRADIENT_CACHE 1
#define CTX_ENABLE_CLIP 1
#define CTX_MIN_JOURNAL_SIZE 512 // grows dynamically
#define CTX_MIN_EDGE_LIST_SIZE 512 // is also max and limits complexity
// of paths that can be filled
#define CTX_STATIC_OPAQUE 1
#define CTX_MAX_SCANLINE_LENGTH 512
#define CTX_1BIT_CLIP 1
#define CTX_MAX_DASHES 10
#define CTX_MAX_GRADIENT_STOPS 10
#define CTX_CM 0
#define CTX_SHAPE_CACHE 0
#define CTX_SHAPE_CACHE_DEFAULT 0
#define CTX_RASTERIZER_MAX_CIRCLE_SEGMENTS 128
#define CTX_NATIVE_GRAYA8 0
#define CTX_ENABLE_SHADOW_BLUR 0
#define CTX_FONTS_FROM_FILE 0
#define CTX_MAX_KEYDB 16
#define CTX_FRAGMENT_SPECIALIZE 1
#define CTX_FAST_FILL_RECT 1
#define CTX_MAX_TEXTURES 1
#define CTX_PARSER_MAXLEN 512
#define CTX_PARSER_FIXED_TEMP 1
#define CTX_CURRENT_PATH 1
#define CTX_BLENDING_AND_COMPOSITING 1
#define CTX_STRINGPOOL_SIZE 256
#define CTX_AUDIO 0
#define CTX_CLIENTS 0
#define CTX_RAW_KB_EVENTS 0
#define CTX_MATH 0
#define CTX_TERMINAL_EVENTS 0 // gets rid of posix bits and bobs
#define CTX_THREADS 0
#define CTX_TILED 0
#define CTX_FORMATTER 0 // we want these eventually
#define CTX_PARSER 0 // enabled
#define CTX_BRAILLE_TEXT 0
#define CTX_BAREMETAL 1
#define CTX_EVENTS 1
#define CTX_MAX_DEVICES 1
#define CTX_MAX_KEYBINDINGS 16
#define CTX_RASTERIZER 1
#define CTX_MAX_STATES 5
#define CTX_MAX_EDGES 127
#define CTX_MAX_PENDING 64
#define CTX_MAX_CBS 8
#define CTX_MAX_LISTEN_FDS 1
#define CTX_ONE_FONT_ENGINE 1
#define CTX_STATIC_FONT(font) \
ctx_load_font_ctx(ctx_font_##font##_name, \
ctx_font_##font, \
sizeof (ctx_font_##font))
#define CTX_MAX_FONTS 10
#include "Arimo-Regular.h"
#include "Arimo-Bold.h"
#include "Arimo-Italic.h"
#include "Arimo-BoldItalic.h"
#define CTX_FONT_0 CTX_STATIC_FONT(Arimo_Regular)
#define CTX_FONT_1 CTX_STATIC_FONT(Arimo_Bold)
#define CTX_FONT_2 CTX_STATIC_FONT(Arimo_Italic)
#define CTX_FONT_3 CTX_STATIC_FONT(Arimo_BoldItalic)
#include "Tinos-Regular.h"
#define CTX_FONT_13 CTX_STATIC_FONT(Tinos_Regular)
#include "Cousine-Regular.h"
#include "Cousine-Bold.h"
#define CTX_FONT_21 CTX_STATIC_FONT(Cousine_Regular)
#define CTX_FONT_22 CTX_STATIC_FONT(Cousine_Bold)
...@@ -27717,7 +27717,7 @@ static int is_in_ctx (void) ...@@ -27717,7 +27717,7 @@ static int is_in_ctx (void)
} }
#endif #endif
   
#if EMSCRIPTEN #if CTX_WASM_ORIG // Renamed from upstream, as we don't want this code to be compiled in, even though we're using Emscripten.
   
CTX_EXPORT Ctx * CTX_EXPORT Ctx *
ctx_wasm_get_context (int flags); ctx_wasm_get_context (int flags);
...@@ -40644,7 +40644,7 @@ Ctx *ctx_new_tft (TFT_eSPI *tft, ...@@ -40644,7 +40644,7 @@ Ctx *ctx_new_tft (TFT_eSPI *tft,
} }
#endif #endif
   
#ifdef EMSCRIPTEN #ifdef CTX_WASM_ORIG // Renamed from upstream, as we don't want this code to be compiled in, even though we're using Emscripten.
#include "emscripten.h" #include "emscripten.h"
   
#include <unistd.h> #include <unistd.h>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment