On the PicoCalc, every QR code is braille, one module a dot

Braille was a fallback for codes too big for half blocks; on the
PicoCalc it is now the grid every code is drawn on, eight modules to a
cell, share screens and page fences alike. A channel share is 23 cells
by 12 rows there, a contact card 29 by 15, each with its link under it.
The regular platform keeps half blocks, its fonts' braille being dotted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-09-30 22:55:01 -04:00
1 parent edacbd0740
commit 6840138eb4
4 files changed
+62 -61

No files matched your search

+8 -8
View File
@@ -241,14 +241,14 @@ deliberately, one at a time, and say why in the code.
encodes, sat a little above centre; Esc leaves, as everywhere. The code **fits itself
to the frame every paint**, width and rows both: a lighter error level, then a
narrower quiet zone, before it would ever be cut (a contact card is 57 cells and 29
rows at the standard fit; the PicoCalc is 53 across and a regular terminal 24 tall).
Where the font draws braille solid (`Platform.solid_braille`, the PicoCalc's built
fonts) those fits are then tried again in **braille**, two modules a cell and four a
row (`qr._GRIDS`): half the module, the one step that costs a camera anything, so it
comes after the free ones — and it is what puts the contact card on the 53×26 panel
whole, standard fit and URL together. When code and URL still can't share the frame
the code is whole at the top and the URL a page down. The one exception is a ` ```qr ` fence in a written page, where the code
is an illustration drawn in the prose where the fence is.
rows at the standard fit in half blocks; a regular terminal is 24 tall), and when code
and URL can't share the frame the code is whole at the top and the URL a page down.
The one exception is a ` ```qr ` fence in a written page, where the code is an
illustration drawn in the prose where the fence is. Where the font draws braille solid
(`Platform.solid_braille`, the PicoCalc's built fonts) every code — share screen and
page fence alike — is drawn in **braille**, one module a dot, eight a cell
(`qr._GRID`), so the contact card is 29 cells by 15 rows there and shares the 53×26
panel with its URL at the standard fit.
### Written pages
+3 -3
View File
@@ -90,9 +90,9 @@ class Platform:
solid_braille: Whether the console font draws a braille cell's eight dots as solid
tiles with no gap between them or between neighbouring cells — the PicoCalc's
built fonts (``scripts/picocalc``) do — so braille is a true 2×4 pixel grid,
fine enough for what must stay contiguous. It is what lets a QR code step down
to braille when the half-block code does not fit (:mod:`~meshterm.ui.qr`). A
desktop font's braille is dotted, and a code drawn in it scans as nothing.
fine enough for what must stay contiguous. It is what lets every QR code be
drawn in braille, one module a dot (:mod:`~meshterm.ui.qr`). A desktop font's
braille is dotted, and a code drawn in it scans as nothing.
effects: Whether animated/decorative rendering runs at all — the braille spinner,
which drops to a ``LINE`` fallback. Cheaper on a console where a repaint is
dear. The header battery gauge is deliberately *not* behind it: its sweep is
+38 -42
View File
@@ -1,13 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
"""Render a string as a scannable QR code in the terminal, using half-block characters.
"""Render a string as a scannable QR code in the terminal, in half blocks or in braille.
Each character cell stacks two vertical modules — ``▀`` (upper only), ``▄`` (lower only),
``█`` (both), space (neither) — so a code renders at half the row height of a full-block
one. Where the console font's braille is solid (:attr:`~meshterm.platforms.Platform.
solid_braille`, the PicoCalc), a code too big for its frame in half blocks steps down to
**braille**, two modules across and four down in every cell: half the module, a quarter
of the area, and still every module touching its neighbours, which is what a camera
needs of it. Modules are always drawn **white on black**: the code's dark modules as white ink,
one. Where the console font draws braille solid (:attr:`~meshterm.platforms.Platform.
solid_braille`, the PicoCalc), every code is drawn in **braille** instead: one module a
dot, eight a cell, two across and four down — a quarter of the area, with every module
still touching its neighbours, which is what a camera needs of it. Modules are always
drawn **white on black**: the code's dark modules as white ink,
its light modules and quiet zone as a black field, whatever colour theme the terminal is
running. A phone camera reads contrast, and pure white on pure black is the most of it a
screen has; a light-on-dark code is what every scanner made this decade expects to meet
@@ -63,8 +63,8 @@ _STYLE = "qr"
#: The fits a code tries, in order, until one is small enough for the frame drawing it:
#: the standard code first, then a lighter error level (a screen is never smudged, so the
#: redundancy buys nothing a camera needs), then the narrower quiet zone. A contact card
#: — a name, a 64-hex key — is 57 cells across and 29 rows tall at the standard fit; the
#: PicoCalc panel is 53 across, and a regular terminal is 24 rows.
#: — a name, a 64-hex key — is 57 cells across and 29 rows tall at the standard fit in half
#: blocks (29 by 15 in braille); a regular terminal is 24 rows.
_FITS: tuple[tuple[str, int], ...] = (("m", 4), ("l", 4), ("m", 2), ("l", 2))
@@ -105,20 +105,20 @@ class _Grid:
#: One module a cell, two a row — the code every terminal can draw.
_HALF_BLOCK = _Grid(across=1, down=2, glyph=lambda block: _GLYPH[(block[0][0], block[1][0])])
#: Two modules a cell, four a row — half the module, so only where braille is solid.
#: One module a dot: two a cell, four a row — only where the font draws braille solid.
_BRAILLE = _Grid(across=2, down=4, glyph=_braille)
#: The grids a fitted code may be drawn on, largest module first: a code tries every one
#: of :data:`_FITS` on a grid before it shrinks to the next. Bound per platform — the
#: braille grid is only offered where the font draws braille solid.
_GRIDS: tuple[_Grid, ...] = (_HALF_BLOCK,)
#: The grid every code on this platform is drawn on. Bound per platform: braille where the
#: font tiles it, since a desktop font's braille is dotted and a code drawn in it scans as
#: nothing.
_GRID = _HALF_BLOCK
@on_platform
def _bind(platform: Platform) -> None:
"""Offer the braille grid where the console font tiles it (now and on switches)."""
global _GRIDS
_GRIDS = (_HALF_BLOCK, _BRAILLE) if platform.solid_braille else (_HALF_BLOCK,)
"""Draw codes in braille where the console font tiles it (now and on switches)."""
global _GRID
_GRID = _BRAILLE if platform.solid_braille else _HALF_BLOCK
@lru_cache(maxsize=8)
@@ -129,8 +129,8 @@ def _make(data: str, error: str) -> segno.QRCode:
return segno.make(data, error=error)
def _draw(code: segno.QRCode, border: int, grid: _Grid = _HALF_BLOCK, *, indent: int = 0) -> Text:
"""Draw a segno code on ``grid``, ``border`` light modules around it.
def _draw(code: segno.QRCode, border: int, *, indent: int = 0) -> Text:
"""Draw a segno code on the platform's grid, ``border`` light modules around it.
``indent`` is the one way a code is ever moved across a line: the same run of bare
cells in front of *every* row. A code must never be justified — Rich's centring
@@ -138,6 +138,7 @@ def _draw(code: segno.QRCode, border: int, grid: _Grid = _HALF_BLOCK, *, indent:
modules loses cells and lands a column off its neighbours, and a finder square one
row skewed is a code no camera can lock on to.
"""
grid = _GRID
rows = [[bool(v) for v in row] for row in code.matrix_iter(border=border)]
cols, _ = grid.cells(len(rows))
# Pad the right and the bottom out to whole cells with light modules, so the last
@@ -162,7 +163,7 @@ def _draw(code: segno.QRCode, border: int, grid: _Grid = _HALF_BLOCK, *, indent:
def qr_text(data: str, *, error: str = "m", border: int = _BORDER) -> Text:
"""Render ``data`` as a QR code built from half-block characters.
"""Render ``data`` as a QR code, in half blocks or — where braille is solid — braille.
Args:
data: The string to encode (e.g. a ``meshcore://channel/add`` share URL).
@@ -176,31 +177,27 @@ def qr_text(data: str, *, error: str = "m", border: int = _BORDER) -> Text:
return _draw(_make(data, error), border)
def _fit(data: str, width: int, height: int | None) -> tuple[segno.QRCode, int, _Grid] | None:
"""The first fit within ``width`` cells and ``height`` rows, as (code, border, grid).
def _fit(data: str, width: int, height: int | None) -> tuple[segno.QRCode, int] | None:
"""The first of :data:`_FITS` within ``width`` cells and ``height`` rows, as (code, border).
Every one of :data:`_FITS` on the largest grid, then on the next of :data:`_GRIDS`: on
a screen a lighter error level or a narrower quiet zone costs a camera nothing it
needs, and a module half the size is the one step that does. ``None`` when no fit is
that small — the caller decides what to relax.
``None`` when no fit is that small — the caller decides what to relax.
"""
for grid in _GRIDS:
for error, border in _FITS:
code = _make(data, error)
cols, rows = grid.cells(code.symbol_size(border=border)[0])
if cols <= width and (height is None or rows <= height):
return code, border, grid
for error, border in _FITS:
code = _make(data, error)
cols, rows = _GRID.cells(code.symbol_size(border=border)[0])
if cols <= width and (height is None or rows <= height):
return code, border
return None
def _smallest(data: str) -> tuple[segno.QRCode, int, _Grid]:
"""The last of :data:`_FITS` on the last of :data:`_GRIDS` — for a frame too small for any."""
def _smallest(data: str) -> tuple[segno.QRCode, int]:
"""The last of :data:`_FITS` — what a frame too small for any fit gets anyway."""
error, border = _FITS[-1]
return _make(data, error), border, _GRIDS[-1]
return _make(data, error), border
def fit_qr(data: str, width: int, height: int | None = None) -> Text:
"""The code for ``data`` at the first fit that fits the space given (see :func:`_fit`).
"""The code for ``data`` at the first of :data:`_FITS` that fits the space given.
A code that fits at no fit is drawn at the smallest anyway: a cut code is not
scannable, but neither is no code, and a larger terminal is one resize away.
@@ -211,11 +208,10 @@ def fit_qr(data: str, width: int, height: int | None = None) -> Text:
height: Rows available, or ``None`` to fit the width alone.
Returns:
The code, white on black, in half blocks or — where the platform's braille is
solid and half blocks do not fit — in braille.
The code, as :func:`qr_text` draws it.
"""
code, border, grid = _fit(data, width, height) or _smallest(data)
return _draw(code, border, grid)
code, border = _fit(data, width, height) or _smallest(data)
return _draw(code, border)
class QrScreen(ScrollScreen):
@@ -255,16 +251,16 @@ class QrScreen(ScrollScreen):
rows = self._scroll_viewport
link = Text(self.url, style="accent", justify="center")
link_rows = len(render_lines(link, width))
code, border, grid = (
code, border = (
_fit(self.url, width, rows - 1 - link_rows)
or _fit(self.url, width, rows)
or _fit(self.url, width, None)
or _smallest(self.url)
)
# Centred as a block — one indent for every row (see _draw), never justified.
cols, _ = grid.cells(code.symbol_size(border=border)[0])
cols, _ = _GRID.cells(code.symbol_size(border=border)[0])
indent = max(0, (width - cols) // 2)
self.replace_content(Group(_draw(code, border, grid, indent=indent), Text(""), link))
self.replace_content(Group(_draw(code, border, indent=indent), Text(""), link))
return super().render_body(width)
+13 -8
View File
@@ -275,23 +275,28 @@ def test_the_share_screen_refits_its_code_to_the_frame_every_paint() -> None:
assert len(code_rows(53, 26)) <= 26 # the PicoCalc: likewise
def test_where_braille_is_solid_a_code_too_big_for_half_blocks_steps_down_to_it() -> None:
"""On the PicoCalc a contact card keeps its standard fit, in braille, over its link.
def test_where_braille_is_solid_every_code_is_drawn_in_it() -> None:
"""On the PicoCalc a code is braille, one module a dot — eight to a cell.
In half blocks the card fits the 53x26 panel only by sending its link a page down;
braille's two-by-four cell holds the whole standard code in 29 cells by 15 rows. Every
module has to survive the packing, so the braille is decoded back and held against the
code's own matrix. A desktop font's braille is dotted, so the regular platform never
steps down to it.
So the contact card is 29 cells by 15 rows at the standard fit and shares the 53x26
panel with its link, where half blocks sent the link a page down; and a page's code
is braille too. Every module has to survive the packing, so the braille is decoded
back and held against the code's own matrix. A desktop font's braille is dotted, so
the regular platform never draws a code in it.
"""
import segno
from meshterm.platforms import PICOCALC, set_platform
def is_braille(text: str) -> bool:
return any("⠀" <= ch <= "⣿" for ch in text)
url = "meshcore://contact/add?name=YUL-Cartierville&public_key=" + "ab" * 32 + "&type=2"
assert not any("⠀" <= ch <= "⣿" for ch in fit_qr(url, 53, 22).plain)
channel = "meshcore://channel/add?name=Test&secret=" + "ab" * 16
assert not is_braille(fit_qr(url, 53, 22).plain) and not is_braille(qr_text(channel).plain)
set_platform(PICOCALC)
assert is_braille(qr_text(channel).plain) and "█" not in qr_text(channel).plain
screen = QrScreen(url, title="Share YUL-Cartierville")
screen.note_viewport(26)
lines = [_ANSI.sub("", line) for line in screen.render_body(53)]