Files
wadamesh/deploy/site/tanmatsu.html
T
Kaj SchittecatandClaude Opus 5 b15d422bdc touch: beta_68 — Lua SDK gains the map, lists, packet delivery and discovery
The four capabilities that kept third-party apps a sketch of a built-in one:

* wada.map      the firmware's own tiles, projection and cache inside an app
                page, with its own capped pool so it never evicts the Map tab's
* wada.ui.list  the missing "pick one of N" widget; rows are real buttons, so
                keyboard and trackball nav walk them for free
* on_packet     each frame delivered once instead of polling a 16-deep ring,
                which sampled rather than observed
* wada.mesh.discover  the active zero-hop probe, behind its own permission
                because it spends every neighbour's airtime, not just ours

Plus the surface those need to be useful: packet identity reported only where
the frame actually carries it, exact micro-degree coordinates (Lua is built
LUA_32BITS, so its floats were quietly costing a metre), altitude and satellite
time, wada.geo, wada.ui.input, named and one-shot timers, http_post, windowed
fs.read, and wada.sys.env on a hardware gate rather than the memory one.

Fixes:
* Both ESP32-P4 targets could not link. g_wifi_last_disc_reason was defined in
  src/main.cpp, which the IDF builds never compile, so all nine S3 envs stayed
  green while Tanmatsu and T-Display P4 were dead.
* Map zoom level was invisible in +/- buttons mode; the readout was hidden with
  the slider it was anchored to.
* Hungarian and Dutch held each other's "No SD card" translation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:50:01 +02:00

249 lines
19 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WADAMESH — Sideloading on the Tanmatsu</title>
<meta name="description" content="Write your own apps for WADAMESH in Lua. The wada.* API reference: ui, input, mesh, net, store, sys and timer, plus the app format and how to publish to the Store.">
<link rel="icon" href="wadamesh-badge.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#ffffff; --bg2:#f4f6f2; --ink:#15181e; --mut:#5d6770;
--line:#e5e8e2; --teal:#15b6a6; --teal-ink:#0e8d80;
color-scheme:light;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{font:16px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Inter,sans-serif; background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased}
a{color:var(--teal-ink); text-decoration:none}
.top{position:sticky; top:0; z-index:30; display:flex; align-items:center; gap:14px; padding:12px 22px;
background:rgba(255,255,255,.88); backdrop-filter:saturate(1.2) blur(8px); border-bottom:1px solid var(--line)}
.top .word{font-weight:700; letter-spacing:2px; font-size:1.05rem; color:var(--ink)}
.top .word .t{color:var(--teal)}
.top .tag{font-family:'JetBrains Mono',monospace; font-size:.66rem; letter-spacing:.18em; text-transform:uppercase; color:var(--mut); border:1px solid var(--line); border-radius:100px; padding:4px 9px}
.top .sp{flex:1}
.top .home{display:inline-flex; align-items:center; gap:7px; color:var(--mut); font-size:.86rem; border:1px solid var(--line); border-radius:100px; padding:6px 13px; background:#fff; transition:background .15s,color .15s,border-color .15s}
.top .home:hover{color:var(--teal-ink); background:var(--bg2); border-color:#bfe6e0}
.top .home svg{width:16px;height:16px}
.shell{max-width:1200px; margin:0 auto; display:grid; grid-template-columns:220px 1fr; gap:34px; padding:0 22px}
nav.toc{position:sticky; top:64px; align-self:start; height:calc(100vh - 64px); overflow:auto; padding:26px 0 40px}
nav.toc .grp{font-family:'JetBrains Mono',monospace; font-size:.68rem; letter-spacing:.13em; text-transform:uppercase; color:var(--mut); margin:20px 0 7px}
nav.toc .grp:first-child{margin-top:0}
nav.toc a{display:block; padding:6px 12px; border-radius:9px; color:#46525c; font-size:.92rem; border-left:2px solid transparent}
nav.toc a:hover{background:var(--bg2); color:var(--ink)}
nav.toc a.on{color:var(--teal-ink); background:rgba(21,182,166,.09); border-left-color:var(--teal); font-weight:600}
main{padding:30px 0 90px; min-width:0}
.hero h1{font-size:clamp(1.9rem,4.4vw,2.6rem); letter-spacing:-.5px; font-weight:750}
.hero p{color:var(--mut); max-width:64ch; margin-top:10px; font-size:1.05rem}
.hero .meta{display:flex; gap:8px; flex-wrap:wrap; margin-top:16px}
.chip{display:inline-flex; align-items:center; gap:6px; font-size:.8rem; color:#3a444d; border:1px solid var(--line); border-radius:100px; padding:5px 11px; background:#fff}
.chip svg{width:14px;height:14px;color:var(--teal)}
section.doc{padding:36px 0; border-top:1px solid var(--line); scroll-margin-top:74px}
.doc h2{font-size:1.5rem; font-weight:730; letter-spacing:-.3px; display:flex; align-items:center; gap:10px}
.doc h2 .ico{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;flex:none; background:rgba(21,182,166,.12);color:var(--teal)}
.doc h2 .ico svg{width:18px;height:18px}
.row{display:grid; grid-template-columns:minmax(0,1fr) 372px; gap:34px; align-items:start; margin-top:16px}
@media(max-width:980px){.row{grid-template-columns:1fr}}
.prose{color:#3c454d; min-width:0}
.prose p{margin:0 0 12px}
.prose h3{font-size:1.02rem; margin:18px 0 6px; color:var(--ink)}
.prose ul{margin:4px 0 12px; padding-left:18px}
.prose li{margin:4px 0}
.prose b{color:var(--ink)}
.prose code{font-family:'JetBrains Mono',monospace; font-size:.85em; background:var(--bg2); border:1px solid var(--line); border-radius:6px; padding:1px 6px}
.shot{position:sticky; top:74px}
.cap{text-align:center; font-family:'JetBrains Mono',monospace; font-size:.68rem; color:var(--mut); margin-top:10px; letter-spacing:.04em}
/* T-Deck frame: the real screenshot + a generated QWERTY keyboard (added in JS) */
.device{width:344px; margin:0 auto; background:#16191d; border-radius:16px; padding:12px 12px 9px; border:1px solid #e9ece7; box-shadow:0 24px 60px -30px rgba(21,24,30,.5)}
.device img.screen{display:block; width:320px; height:240px; border-radius:8px}
.kbd{margin:9px auto 2px; display:flex; flex-direction:column; gap:4px; width:312px}
.kr{display:flex; gap:4px; justify-content:center}
.key{flex:1; max-width:28px; height:15px; background:#23282e; border-radius:4px; display:grid; place-items:center; font:8px/1 system-ui; color:#7d8893}
.key.wide{max-width:140px; flex:4}
.gallery{display:grid; grid-template-columns:repeat(auto-fill,minmax(232px,1fr)); gap:20px; margin-top:18px}
.mini{background:#16191d; border-radius:12px; padding:7px; border:1px solid #e9ece7}
.mini img{display:block; width:100%; height:auto; border-radius:6px}
.mini .cap{margin-top:6px}
.webgrid{display:grid; grid-template-columns:repeat(auto-fill,minmax(178px,1fr)); gap:18px; margin-top:22px}
figure.phone{margin:0; background:#16191d; border-radius:20px; padding:8px; border:1px solid #e9ece7; box-shadow:0 18px 44px -26px rgba(21,24,30,.5)}
figure.phone img{display:block; width:100%; height:auto; border-radius:13px}
figure.phone figcaption{text-align:center; font-family:'JetBrains Mono',monospace; font-size:.66rem; color:var(--mut); margin-top:8px; letter-spacing:.04em}
.subh{font-size:1.06rem; font-weight:700; color:var(--ink); margin:30px 0 2px}
/* Settings-page detail cards: real screenshot + what the page does + its controls */
.sgrp{font-family:'JetBrains Mono',monospace; font-size:.7rem; letter-spacing:.14em; text-transform:uppercase; color:var(--mut); margin:30px 0 0; padding-top:4px}
.scards{display:grid; grid-template-columns:repeat(auto-fill,minmax(290px,1fr)); gap:20px; margin-top:13px}
.scard{border:1px solid var(--line); border-radius:14px; background:#fff; padding:13px; display:flex; flex-direction:column}
.scard .thumb{background:#16191d; border-radius:9px; padding:6px}
.scard .thumb img{display:block; width:100%; height:auto; border-radius:5px}
.scard .thumb.ph{aspect-ratio:4/3; display:grid; place-items:center; border:1px dashed #39424b; padding:0}
.scard .thumb.ph span{font-family:'JetBrains Mono',monospace; font-size:.6rem; letter-spacing:.12em; color:#7d8893; text-transform:uppercase}
.scard h3{font-size:1.04rem; margin:12px 0 2px; color:var(--ink); display:flex; align-items:center; gap:8px; flex-wrap:wrap}
.scard h3 .tag{font-family:'JetBrains Mono',monospace; font-size:.58rem; letter-spacing:.05em; text-transform:uppercase; color:var(--teal-ink); background:rgba(21,182,166,.1); border-radius:5px; padding:2px 6px; font-weight:600}
.scard .lede{color:var(--mut); font-size:.9rem; margin:2px 0 9px}
.scard ul{margin:0; padding-left:17px}
.scard li{margin:4px 0; font-size:.89rem; color:#3c454d; line-height:1.5}
.scard li b{color:var(--ink); font-weight:600}
/* Caution callout (amber) */
.callout{display:flex; gap:11px; margin-top:18px; padding:13px 15px; border-radius:11px; background:#fff7ed; border:1px solid #f6dcb8}
.callout .cico{flex:none; width:20px; height:20px; color:#c2740b; margin-top:1px}
.callout .cico svg{width:20px; height:20px}
.callout b{display:block; color:#7c4a03; margin-bottom:3px; font-size:.94rem}
.callout p{margin:0; color:#7a5320; font-size:.9rem; line-height:1.55}
footer{border-top:1px solid var(--line); padding:26px 22px; text-align:center; color:var(--mut); font-size:.85rem}
@media(max-width:980px){
.shell{grid-template-columns:1fr; gap:0}
nav.toc{position:static; height:auto; border-bottom:1px solid var(--line); padding:14px 0; display:flex; gap:6px; overflow:auto; white-space:nowrap}
nav.toc .grp{display:none} nav.toc a{border-left:0; flex:none}
.shot{position:static; margin:0 auto 6px}
}
.prose pre{background:var(--bg2);border:1px solid var(--line);border-radius:10px;padding:14px 16px;overflow-x:auto;margin:14px 0;font:13px/1.6 "JetBrains Mono",ui-monospace,Menlo,Consolas,monospace}
.prose pre code{background:none;padding:0;font-size:inherit}
.api{width:100%;border-collapse:collapse;margin:12px 0 18px;font-size:.92rem;display:block;overflow-x:auto}
.api th,.api td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top}
.api th{font-weight:700;white-space:nowrap}
.api td:first-child{white-space:nowrap;font-family:"JetBrains Mono",ui-monospace,Menlo,Consolas,monospace;font-size:.86rem;color:var(--teal-ink)}
.note{background:var(--bg2);border-left:3px solid var(--teal);border-radius:0 8px 8px 0;padding:12px 16px;margin:16px 0}
.note p{margin:0}
</style>
</head>
<body>
<div class="top">
<a class="word" href="index.html">WADA<span class="t">MESH</span></a>
<span class="tag">Lua SDK</span>
<span class="sp"></span>
<a class="home" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg> Back to site</a>
</div>
<div class="shell">
<nav class="toc" id="toc">
<div class="grp">Tanmatsu</div>
<a href="#model">How it works</a>
<a href="#setup">One-time setup</a>
<a href="#build">Build</a>
<a href="#install">Install</a>
<a href="#verify">Verify</a>
<a href="#trouble">When it goes wrong</a>
<div class="grp">Elsewhere</div>
<a href="docs.html">User guide</a>
<a href="sdk.html">Lua app SDK</a>
</nav>
<main>
<div class="hero">
<h1>Sideloading on the Tanmatsu</h1>
<p>For developers building wadamesh from source and running it on a Tanmatsu. If you just want to use it, install <b>WadaMesh</b> from the launcher's own app store instead.</p>
<div class="meta"><span class="chip">ESP32-P4</span> <span class="chip">ESP-IDF 5.5.1</span> <span class="chip">AppFS</span></div>
</div>
<section class="doc" id="model">
<h2>How it works, and the one way to break it</h2>
<div class="row"><div class="prose">
<p>wadamesh is <b>not</b> standalone firmware on this board. The Tanmatsu boots badge.team's launcher, and wadamesh runs as an <b>AppFS app</b> underneath it, exactly like the launcher's own apps.</p>
<p>So you never flash the whole device. You write <b>one app image and one metadata sector</b> into the AppFS partition, and leave the bootloader, the partition table and the OTA slots alone.</p>
<div class="note"><p><b>Never run <code>idf.py flash</code> on a Tanmatsu.</b> It writes the bootloader, partition table and OTA data, which replaces the launcher OS itself. Recovering means reflashing the launcher from badge.team. Everything on this page touches only the AppFS partition.</p></div>
<p>The board has three processors and only one of them is yours: the <b>ESP32-P4</b> runs the launcher and your app, an <b>ESP32-C6</b> handles Wi-Fi, Bluetooth and LoRa over esp-hosted, and a <b>CH32</b> runs the keyboard and power. You flash the P4.</p>
</div></div>
</section>
<section class="doc" id="setup">
<h2>One-time setup</h2>
<div class="row"><div class="prose">
<div class="note"><p><b>The tools live on <code>main</code>.</b> <code>tanmatsu/tools/</code> was added on 21 August 2026, shortly <i>after</i> <code>beta_67</code> was tagged, so a release tarball or a <code>stable</code> checkout does not contain it and neither does a clone made before that date. Get it with <code>git fetch origin &amp;&amp; git checkout main &amp;&amp; git pull</code>, or browse the four scripts directly at <a href="https://github.com/ALLFATHER-BV/wadamesh/tree/main/tanmatsu/tools">github.com/ALLFATHER-BV/wadamesh/tree/main/tanmatsu/tools</a>. They will be in the next beta as a matter of course.</p></div>
<p>You need ESP-IDF 5.5.1. The build uses a project-local copy in <code>tanmatsu/esp-idf</code> (gitignored, around 3&nbsp;GB); <code>make prepare</code> from <code>tanmatsu/</code> fetches it. Then two one-off steps:</p>
<pre><code>tanmatsu/tools/fetch-appfs.sh</code></pre>
<p>Clones badge.team's <code>esp32-component-appfs</code>, whose <code>appfs.py</code> reads and rewrites the AppFS image. It is deliberately not vendored into the wadamesh repo: it is their code, and the copy in circulation carries no licence header.</p>
<pre><code>tanmatsu/tools/dump-pristine.sh /dev/cu.usbmodemXXXX</code></pre>
<p>Dumps <b>your</b> device's AppFS partition as the baseline that later deploys diff against. Do it while the launcher's own apps are installed and wadamesh is not, so the baseline is the device as it ships.</p>
<div class="note"><p>Use your own dump, never someone else's. The deploy writes only the sectors that <i>differ</i> from this file, so a baseline describing a different device can overwrite apps you actually have.</p></div>
</div></div>
</section>
<section class="doc" id="build">
<h2>Build</h2>
<div class="row"><div class="prose">
<pre><code>cd tanmatsu &amp;&amp; ./build.sh build</code></pre>
<p>Use <code>build.sh</code> rather than <code>idf.py</code> directly: it applies two build-time patches to <code>managed_components</code> (esp-hosted Wi-Fi-init tolerance, and an Arduino BLE stub) that the build needs.</p>
<div class="note"><p><b>The build ends with an error, and that is expected.</b></p>
<pre><code>Generated .../application.bin
Error: All app partitions are too small</code></pre>
<p>The ~2.9&nbsp;MB app is being size-checked against the 2&nbsp;MB <code>ota_0</code> slot, but it does not live there: it lives in the 8&nbsp;MB <code>appfs</code> partition. Judge success by <code>Generated ... application.bin</code> appearing with no <code>error:</code> or <code>undefined reference</code> above it.</p></div>
</div></div>
</section>
<section class="doc" id="install">
<h2>Install</h2>
<div class="row"><div class="prose">
<pre><code>tanmatsu/tools/tan_flash.sh</code></pre>
<p>With no port argument it probes each USB serial device and picks the one answering as an ESP32-P4. The Tanmatsu exposes <b>two</b> ports, the P4 and the C6, and which name each gets changes between replugs, so detecting by chip beats guessing. Pass a port explicitly if you prefer.</p>
<table class="api">
<tr><th>Step</th><th>Why in this order</th></tr>
<tr><td>Build the image, diff against the baseline</td><td>Only the 64&nbsp;KB sectors that actually changed get written.</td></tr>
<tr><td>Write the <b>app data</b>, no reset</td><td>The bytes land before anything points at them.</td></tr>
<tr><td>Write the <b>metadata sector</b>, then reset</td><td>That sector is what makes the app visible to the launcher. Writing it last means an interrupted flash leaves the previous app intact rather than a half-written one marked complete.</td></tr>
<tr><td>Read the metadata back and compare</td><td>The commit write is the one that can silently drop. Without this check the launcher would quietly keep showing the old build.</td></tr>
</table>
<p>Expect <code>metadata verified — installed</code>. Anything else means run it again.</p>
<p>The version number increments on every deploy, and it has to: the launcher keys updates on name plus version, so republishing the same number leaves you on the old binary with no error anywhere.</p>
</div></div>
</section>
<section class="doc" id="verify">
<h2>Verify</h2>
<div class="row"><div class="prose">
<p>Launch <b>WadaMesh</b> from the launcher menu. An empty serial log straight after flashing is normal: the launcher is a silent GUI OS on USB-CDC, and the boot log only appears once the app itself starts.</p>
<pre><code>python3 tanmatsu/tools/sermon.py /dev/cu.usbmodemXXXX /tmp/wada.log</code></pre>
<div class="note"><p><b>Do not attach a monitor that asserts DTR/RTS.</b> Resetting the P4 also desynchronises the C6, which has no reset line of its own, and the launcher then hangs on "Initializing radio" until the board is fully power-cycled. <code>sermon.py</code> does not reset, and survives the USB re-enumeration when an app launches.</p></div>
</div></div>
</section>
<section class="doc" id="trouble">
<h2>When it goes wrong</h2>
<div class="row"><div class="prose">
<table class="api">
<tr><th>Symptom</th><th>Cause and fix</th></tr>
<tr><td>No serial ports at all</td><td>The C6 has wedged. <b>Fully power-cycle</b> the board, off then on. A reset is not enough.</td></tr>
<tr><td>"No ESP32-P4 found", ports present</td><td>Something else is holding the port, usually a monitor still running. Close it and retry.</td></tr>
<tr><td>"metadata DIFFERS"</td><td>The commit write did not land. Re-run. Nothing is broken; the device still has the previous app.</td></tr>
<tr><td>Launcher stuck on "Initializing radio"</td><td>esp-hosted desync, almost always from a resetting monitor. Full power cycle.</td></tr>
<tr><td>A crash to decode</td><td><code>riscv32-esp-elf-addr2line -e tanmatsu/build/tanmatsu/application.elf -fpC &lt;MEPC&gt; &lt;RA&gt; &lt;stack RAs&gt;</code></td></tr>
</table>
<p><b>Before you ship anything:</b> the Tanmatsu shares one <code>src/</code> with the ESP32-S3 boards, so a change here can break them without you noticing. Build at least the T-Deck and Heltec V4 environments as well.</p>
<p>Full text of this guide, kept with the code: <code>TANMATSU_SIDELOAD.md</code> in the repo. Port status and outstanding work: <code>TANMATSU_PORT.md</code>.</p>
</div></div>
</section>
</main>
</div>
<script>
// Highlight the section currently in view, same behaviour as the docs page.
(function () {
var links = [].slice.call(document.querySelectorAll('#toc a'));
var secs = links.map(function (a) { return document.querySelector(a.getAttribute('href')); });
function sync() {
var y = window.scrollY + 120, cur = 0;
for (var i = 0; i < secs.length; i++) if (secs[i] && secs[i].offsetTop <= y) cur = i;
links.forEach(function (a, i) { a.classList.toggle('on', i === cur); });
}
window.addEventListener('scroll', sync, { passive: true });
sync();
})();
</script>
</body>
</html>