ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
翻譯準備中
正在載入完整 README。 以下為已儲存的摘錄。
A simple visual model of watch gears that can work together — a 3D simulation of a
mechanical watch movement (no dial), focused on the minimal set of modules and wheels.
Built with Three.js + Vite. All geometry is generated procedurally in code.檢視完整 README ↗▶ Live demo: https://olegg90.github.io/watch-simulator/
A simple visual model of watch gears that can work together — a 3D simulation of a mechanical watch movement (no dial), focused on the minimal set of modules and wheels.
Built with Three.js + Vite. All geometry is generated procedurally in code.
Explore is what opens: six stops in the order energy flows through the movement — winding, barrel, going train, escapement, time display, power reserve. A stop dims everything but its own parts, flies the camera to it, and shows a card with the key idea, the formula and one control to turn. Each stop leaves a sentence behind; the summary at the end assembles all six into a description of the going.
Two markers keep the cards honest. Green names the test that backs the claim — and a meta-test asserts every referenced test actually exists, so the badge cannot quietly become a lie. Amber marks where the model departs from real horology (the same departures listed under Assumptions below).
No figure in a card is typed: every one is derived from the constants that build the geometry, so a card cannot drift from the mechanism it describes.
Each stop can be seen from the side — a developed section along the chain rather than a projection. An orthographic side view collapses Y and would drop the tourbillon cage onto the differential; here heights are true and arbors sit at their real centre distances, which is why every pinion touches its neighbour exactly at the pitch circles.
Free mode is the movement with every control at once — run/pause, time mode, speed, beat rate, amplitude, per-node visibility, camera presets, winding. It is reachable from the header and from the end of the route, and has its own way back.
The escapement model is a separate exhibit rather than a third view of the same movement. Inside the movement the escapement runs at movement scale and at movement speed, where lock, unlock, impulse and drop blur into one flip; here one lever escapement stands alone, large, with its own play/pause, a step that advances to the middle of the next stage, a way back to the home view, and a readout naming the current phase and the pallet doing the work.
It runs on contact, not on a schedule. The balance is the input; its pin drives the lever through the notch; the lever's travel is what the pallets were traced from, so the wheel's angle follows from the faces rather than from an eased window. Who pushes whom changes at the release, and so does the flank of the notch in contact — the balance drives the lever off the lock, then the wheel drives the lever, which runs ahead and is held back by the other flank, and that is how the impulse reaches the balance.
What holds the lever between beats is the guard pin, not the draw alone: knocked off its banking the lever butts the safety roller and stops, well before the lock could come off, and the crescent cut into that roller is the one place it may pass — facing the lever exactly while the impulse pin is in the notch. A control in the panel pushes the lever so you can watch it be caught; it is live only between beats, because that is the only time anything is holding the lever that a push could argue with.
The pallets are the conjugate profiles of that action: for every lever angle in a phase, the acting tooth's toe is put where the phase says the wheel stands, and the run of those places is the face. So a beat advances the wheel by exactly half a pitch — a result of the pallets spanning two and a half teeth, written down nowhere — the draw comes out at the 12° it was asked for, and the lift at the 50° the pin's orbit was solved for. Two figures are magnified four times, because a real lock and a real drop would be invisible at this size: the panel says so. It shares no code with the movement's escapement — see Source layout.
The interface is Ukrainian and English, including the 3D labels; wheel names follow horological usage rather than literal translation.
Notation: $m$ — module, $z$ — tooth count, $s = 2\pi/z$ — angular tooth pitch, $r = mz/2$ — pitch radius, $\omega$ — angular velocity (sign = direction).
$$\varphi_B = \theta + \pi - \frac{s_B}{2} + \frac{z_A}{z_B}\big((\theta-\varphi_A)\bmod s_A\big)$$
$$u = \frac{(\theta - R_A)\bmod s_A}{s_A},\qquad v = \frac{(\theta+\pi-\tfrac{s_B}{2}-R_B)\bmod s_B}{s_B},\qquad u+v \equiv 0 \pmod 1$$
Measured: error 0 on every mesh at arbitrary drive angles.
Barrel wheel 48 teeth ($m=0.35$, $r=8.4$), 5 spoke windows; open drum (wall + floor, $R=7.2$), steel core $r=0.85$. Speed reference for the train: $\omega_0 = 1$.
| # | Node | Pinion $z_p$ | Wheel $z_w$ | $\omega$ vs barrel |
|---|---|---|---|---|
| 0 | Barrel | — | 48 | $1$ |
| 1 | Centre wheel | 12 | 40 | $-4$ |
| 2 | Third wheel | 12 | 36 | $+40/3 \approx 13.33$ |
| 3 | Fourth (seconds) wheel | 12 | 32 | $-40$ |
| 4 | Escape-wheel arbor | 12 | 15 (escape) | $+320/3 \approx 106.67$ |
$$\omega_k = -,\omega_{k-1}\cdot\frac{z_{w,k-1}}{z_{p,k}}$$
Verified: measured angle increments $=[1,,-4,,+13.333,,-40,,+106.667]$ — exact.
15 ratchet-shaped teeth (steep locking face + sloped back), pitch $s_E = 24°$. Advance per balance beat is half a tooth pitch: $\Delta E = \pi/15$; wheel angle $E(u) = \frac{\pi}{15}(n-1+ss)$ with $n=\mathrm{round}(u)$ and $ss$ a smoothstep inside the flip window $|u-n|\le 0.12$. Verified: stationary between beats, exactly $\pi/15$ per beat.
Pallets at $\pm30°$ from the line of centers (span 2.5 teeth), fork throw $F_{max}=0.14$ rad; $F(u) = -F_{max},\sigma(n),(2,ss-1)$, $\sigma(n)=(-1)^n$. Verified: alternates $-0.14/+0.14/-0.14$ rad at rest.
balanceR(cageR) because the developed section draws from the same number.The escapement is a socket holding exactly one module at a time. Whatever is in it, the train is driven by the same angle — the escape arbor's — so swapping the module changes no timing and no figure on the station card. That is the point of the thing: one function, units of wildly different complexity.
The beat maths lives once, in escapement/beat.js, and a module only maps that phase onto its own geometry. β is therefore physically one, and identical timing cannot be broken by accident.
A module hands the socket { rotating, fixed, update() → β, nodes } and nothing else. nodes are its own controls for the free-mode panel — the lever offers escape wheel, pallet fork and balance; the tourbillon offers fixed wheel, balance, cage opacity and top plate. The panel asks the socket for the installed module's controls and never names a variant, so it cannot show one module's parts beside another's.
| parts | of them moving | nested rotations | footprint (r × h) | |
|---|---|---|---|---|
| Lever escapement | 26 | 24 | 1 | 4.73 × 2.95 |
| Tourbillon | 50 | 48 | 2 | 4.32 × 5.20 |
| Double-axis tourbillon | 47 | 45 | 3 | 4.45 × 6.82 |
Every figure is measured by walking the module's own scene graph (escapement/metrics.js), never typed. Read the part counts for what they are — a count of the meshes each module is drawn with, so they compare the models rather than the mechanisms; the figures that carry the comparison are the nesting and the height, and both rise with every step. Note that the lever's footprint is wider — its balance is carried out to one side, while the cage keeps everything inside its rim. The cost of complexity shows in part count, in how much of it moves, in nesting and in height, not in width.
What the model does not show is what a tourbillon is for: a scripted kinematic has no positional error to average away (see assumption 12).
The escape wheel sits directly on the escape arbor (arbor4), so it turns by exactly $\beta$ — once per 12 s at 2.5 beats/s. The fork and balance stand on the plate and turn about their own axes. The balance sits 2.6 from the arbor with radius 1.95 — deliberately the same position and size the tourbillon gives it, so that a swap moves only the construction (assumption 11).
The entire escapement (escape wheel + pallet fork + balance + hairspring) lives inside a rotating cage. The cage IS the escape arbor (arbor4): it carries the pinion (12) that meshes the seconds wheel and turns as the cage. The escape pinion ($Z_p$) rolls around a fixed wheel ($Z_f$) at the cage center.
Polyline of 200 points: $\alpha(f) = \theta_b(1-f) + f\Phi - \Phi + \lambda$, $r(f) = r_0 + (r_1-r_0)f$, $\Phi = 2\pi\cdot 4.5$. Outer end ($f{=}1$) pinned to the stud, inner end ($f{=}0$) rotates with the balance — the "breathing" effect.
movement.bounds exposes $minX/maxX/minY/maxY$, center and $R_{plate}$ for checks.lil-gui panel — run/pause, time mode, speed, beat rate,
amplitude, wireframe, per-node visibility, cage opacity and top plate, camera
presets, and "wind the mainspring".npm install
npm run dev
Then open the URL Vite prints (default http://localhost:5173).
Build for production:
npm run build
npm run preview
npm run build produces a self-contained dist/ (one index.html + one bundle),
so it can be served by any static HTTP server — no Node needed.
One-shot build + serve:
npm run serve # builds, then http://localhost:8642 (also on your LAN)
Or serve an existing dist/ with any static server:
python -m http.server 8642 --directory dist
One file per mechanism module, mirroring Mechanism elements above. movement.js is a
composer, not a monolith. The escapement is the exception: it is a socket rather than a
single module, so it gets a folder — the shared beat maths and one file per variant.
src/
main.js scene, render loop, lil-gui, time modes
movement.js composer: layout → bounds → module assembly → kinematics §13
common.js shared constants and helpers (meshPhase, makeAxle, makeHandAssembly)
gear.js procedural geometry: gears, bevel gears, hands, spirals
train.js going train (TRAIN table, MESH_ANGLES) §3
barrel.js barrel and mainspring §1, §2
escapement/ the escapement socket — one module installed at a time §4–§7
index.js the registry, install(), and "only the installed one runs"
beat.js the beat phase, shared by every module
hairspring.js the balance spring, shared by every module
metrics.js cost of complexity, measured by walking the graph
lever.js Swiss lever escapement
tourbillon.js cage: escape wheel, fork, balance, hairspring
motionWorks.js motion works, central seconds, hands §8–§10
winding.js ratchet, crown wheel, bevel pair, click, stem §11
powerReserve.js bevel differential and sector scale §12
ui.js node labels and camera presets
i18n.js interface strings, Ukrainian and English
settings.js the one table of running parameters: range, step, format, label
lesson/ the guided layer (see below)
showcase/ the escapement exhibit (see below)
The lesson layer is separate from the mechanism and never reaches into it:
src/lesson/
panel.js header, rail, card, chain footer - knows nothing of three.js
stations.js the six stops: focus, highlight set, content keys, backing test
content.js card copy in both languages
readouts.js every live figure, derived from the mechanism constants
highlight.js dims all but a station's parts (swaps in dimmed material clones,
because materials are shared across dozens of meshes)
section.js the developed section as data - a composer over module profiles
sectionView.js and as SVG
variantsModal.js the escapement comparison: silhouettes, the cost table, the swap
cardText.js one resolver for card text, shared with the tests
draw.js el, svgEl and one linear projector, shared by all three diagrams
lesson.css the interface; fonts.css + fonts/ ship the faces (OFL 1.1)
The escapement exhibit is not a module of the movement and imports nothing from it:
src/showcase/
design.js the escapement itself: the geometry and the contact. The pallets
are traced from the action, so the shapes and the poses are one
solution; no meshes here and no time
leverModel.js the meshes: the wheel, the lever cut as one piece, the balance
with its roller and impulse pin, the banking bridge, the stand
motion.js the running: the balance drives the lever, the lever the wheel
spring.js its hairspring
bar.js the controls, inside the scene (showcase CSS hides all else)
The isolation is structural, not an agreement: a text test in test/showcase.test.js
fails if any file there imports from movement, escapement, lesson, settings or
motionWorks. The cost is that a couple of numbers exist twice — deliberately, because
the alternative was contact kinematics inside the socket, which every variant would then
have to satisfy.
There is no escapement.js — the escapement is a socket, escapement/, holding one
interchangeable module at a time (§The escapement socket). Nothing outside it names a
variant: the socket is the only door.
Every module is three-phase:
layout…() returns positions and its own extents (centre + radius pairs). It runs
before any mesh exists, so movement.js can size the main plate from the whole mechanism
while no module needs to know about its neighbours.build…() creates the meshes and returns its own update().profile() returns the module's own figures — centre distances, z-spans, radii, label
keys — for the station cards and the developed section. No mesh needed, and no outsider
re-derives them: the same centre-distance expression used to be typed in three places at
once, in the module, in lesson/readouts.js and in lesson/section.js.Keep that split: it is what lets a module move or grow without touching the rest. A module's interface is those three functions — not its constant table.
The running parameters follow the same rule from the other side. settings.js holds the one
table — range, step, formatter, label key — and both the free-mode panel and the station cards
are adapters over it. Two adapters, so the seam is real: a control declared in one place and
not the other cannot drift, and a card naming a parameter that does not exist now throws
instead of moving a slider that changes nothing.
Two cautions for anyone refactoring the kinematics:
npm test
Headless Vitest suite (no browser needed — three.js builds geometry in Node). It locks in every formula from Mechanism elements: mesh invariants on all pairs, train/motion-works/central-seconds ratios, escapement stepping and balance phase, real-time hand angles, the winding charge model, bevel-pair tangency (analytic point-to-circle), spiral/gear/hand geometry, and a layout collision scan.
The exhibit has its own suite: the tooth profile and the stones' solved seats, the arbors on the line of centres, the measured lock pose, the phase machine's stepping, and the import isolation above. Three more guards read source text rather than objects — no GUI label may bypass t(), and src/ may hold no Cyrillic outside the two dictionaries that are the Ukrainian interface copy.
The Explore layer is covered too. test/panel.test.js runs under jsdom (declared per file, so the geometry suites keep the faster Node environment): it mounts the real panel — over the real movement, in the real index.html grid — and asserts that the rail, the card, the chain and the developed section always agree with each other. test/lessonHarness.js is the rig; it is not a suite, so the runner does not collect it.
MIT © OlegG90. See CHANGELOG.md for release notes.