From ee8a2d97ed18e87187a8f6a2e3fe116020bfbacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PROFERIS=20-=20Mi=C2=B3osz=20Stocki?= Date: Mon, 6 Jul 2026 15:23:35 +0200 Subject: [PATCH] added proper kinematics --- TODO.md | 1 + gemini.md | 457 ++++++++++++++++++++++++++++++++ src/main.cpp | 97 ++++++- web/js/kinematics.js | 225 ++++++++++------ web/js/sections/stepper-test.js | 130 +++++++-- 5 files changed, 810 insertions(+), 100 deletions(-) create mode 100644 TODO.md create mode 100644 gemini.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..65dc642 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +You didn't finish writing home for web. Analyse it properly as it was quota reached. I can see that there is definitely wron position on map at the start. I do not know what else but you should properly finish homing sequence it was done for mcu already \ No newline at end of file diff --git a/gemini.md b/gemini.md new file mode 100644 index 0000000..0748218 --- /dev/null +++ b/gemini.md @@ -0,0 +1,457 @@ +# WijiBoard – Project Context for AI Assistants + +> **Purpose**: This file is the authoritative project memory. Read it in full before making +> any changes. It captures hardware, architecture, current state, open work, and every +> important design decision made so far. + +--- + +## 1. Project Overview + +WijiBoard is a **5-bar parallel linkage SCARA robot** (spirit-board / planchette mover) controlled +wirelessly from a browser via **Web Bluetooth**. There is **no mobile app, no WiFi, no server** — +only BLE between the ESP32 firmware and a local web SPA. + +- **Inspiration / upstream**: `nerd-sniped/WijiBoard` on GitHub. We took *only* the kinematics + and lookup tables from that project. Everything else (frontend, BLE protocol, SPA shell, + section architecture) was written from scratch. + +--- + +## 2. Hardware + +| Component | Detail | +|---|---| +| **MCU** | Waveshare ESP32-S3-Zero | +| **Framework** | PlatformIO + Arduino core | +| **Motors** | 2× 28BYJ-48 (unipolar stepper via ULN2003 driver) | +| **Motor mode** | `AccelStepper::FULL4WIRE` — 2048 steps/rev | +| **Motor 1 pins** | IN1=4, IN2=5, IN3=6, IN4=7 | +| **Motor 2 pins** | IN1=8, IN2=9, IN3=10, IN4=11 | +| **USB Serial** | Native USB CDC (`-D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1`) | +| **Default speed** | 600 steps/s, accel 100 steps/s² | + +### Mechanism geometry (critical — do NOT change without hardware verification) + +``` + END EFFECTOR + / \ + l2 (110 mm) l2 (110 mm) + / \ + ELBOW1 ELBOW2 + \ / + l1 (85 mm) l1 (85 mm) + \ / + MOTOR1 (+12.9,0) MOTOR2 (-12.9,0) + | | + [===BASE (25.8 mm)===] +``` + +**⚠️ CRITICAL MOTOR CONVENTION** (source of many past bugs): + +`PositionControl.cpp` from the original project uses: +- `xmd = x - d2` → Motor 1 pivot is at **(+d2, 0) = (+12.9, 0)** — the RIGHT side +- `xpd = x + d2` → Motor 2 pivot is at **(-d2, 0) = (-12.9, 0)** — the LEFT side + +This is counter-intuitive. Motor 1 is physically on the right. **Every piece of code that +computes elbow or motor-base positions must use M1 at +12.9 and M2 at -12.9 or the arms +will appear visually crossed even for valid target positions.** + +### Arm constants (from `PositionControl.cpp`) + +``` +d = 25.8 mm (full motor separation) +d2 = 12.9 mm (half separation) +l1 = 85.0 mm (proximal link) +l2 = 110.0 mm (distal link) +STEPS_PER_REV = 2048 +STEP_ANGLE = 360/2048 ≈ 0.17578125 °/step +``` + +--- + +## 3. BLE Protocol + +### UUIDs (must be identical in firmware AND `web/js/ble.js`) + +``` +Service: a0b1c2d3-e4f5-6789-abcd-ef0123456700 +Command: a0b1c2d3-e4f5-6789-abcd-ef0123456701 (WRITE | WRITE_NR) +Status: a0b1c2d3-e4f5-6789-abcd-ef0123456702 (NOTIFY) +Name: WijiBoard +``` + +### Command format (ASCII strings, write-without-response) + +| Command | Meaning | +|---|---| +| `S1+` | Move motor 1 CW by n steps | +| `S1-` | Move motor 1 CCW by n steps | +| `S2+` | Move motor 2 CW by n steps | +| `S2-` | Move motor 2 CCW by n steps | +| `SPD:` | Set max speed (steps/sec) for both motors | +| `ACC:` | Set acceleration (steps/sec²) for both motors | +| `HOME1` | Zero motor 1 step counter | +| `HOME2` | Zero motor 2 step counter | +| `POS` | Request current positions (triggers NOTIFY) | + +### Status notifications (firmware → browser) + +`P:,` — current step positions for motor 1 and 2, sent every 200 ms while moving. + +--- + +## 4. Project Structure + +``` +esp32s3-wiji/ +├── index.html # SPA shell — ES module bootstrap, sidebar, routing +├── platformio.ini # PlatformIO config (env: esp32-s3-devkitc-1) +├── fivebarIKGame.js # REFERENCE ONLY — original site's kinematics (do not import) +├── src/ +│ └── main.cpp # ESP32 firmware (BLE + AccelStepper) +└── web/ + ├── styles/ + │ ├── base.css # CSS variables, reset, typography, layout primitives + │ └── components.css # Cards, buttons, inputs, sidebar, nav items + └── js/ + ├── ble.js # Web Bluetooth singleton + ├── kinematics.js # 5-bar IK/FK (PositionControl.cpp port) + ├── router.js # Hash-based SPA router + ├── ui.js # UI helpers (log panel, toast, BLE status badge) + └── sections/ + ├── home.js # Home/landing section (placeholder) + └── stepper-test.js # Stepper test section — SVG arm visualiser + jog + IK +``` + +### Module loading (index.html) + +All ES modules are loaded with a **cache-buster** to prevent stale code on refresh: + +```javascript +const V = Date.now(); +const { default: Router } = await import(`./web/js/router.js?v=${V}`); +const { default: BLE } = await import(`./web/js/ble.js?v=${V}`); +const { default: UI } = await import(`./web/js/ui.js?v=${V}`); +const { default: HomeSection } = await import(`./web/js/sections/home.js?v=${V}`); +const { default: StepperSection } = await import(`./web/js/sections/stepper-test.js?v=${V}`); +``` + +> The `?v=${V}` suffix on ES module imports is mandatory. Without it, browsers aggressively +> cache modules and you will see stale code running despite file changes. + +--- + +## 5. SPA Architecture + +### Router (`web/js/router.js`) + +Hash-based SPA router. Sections are mounted/unmounted as the user navigates. + +```javascript +Router.register('home', HomeSection); +Router.register('stepper-test', StepperSection); +Router.init('#view', '#home'); +``` + +Each section module must export `{ mount(containerEl), unmount() }`. + +### Section lifecycle + +- `mount(el)` — called when navigating TO this section; injects HTML, sets up event listeners +- `unmount()` — called when navigating AWAY; removes event listeners from `eventCleanup[]` + +Pattern used in every section: + +```javascript +let eventCleanup = []; + +export default { + async mount(container) { + container.innerHTML = buildHTML(); + // grab refs... + const handler = (e) => { ... }; + el.addEventListener('click', handler); + eventCleanup.push(() => el.removeEventListener('click', handler)); + renderArmFromSteps(); + }, + unmount() { + eventCleanup.forEach(fn => fn()); + eventCleanup = []; + }, +}; +``` + +### Sidebar + +The sidebar is in `index.html` (not in any section). It is a collapsible panel opened by a +hamburger (`☰`) button. It contains ` + +
@@ -619,7 +703,7 @@ const StepperTestSection = { ghost2Dist = document.getElementById('ghost-2d'); ghostElbow1 = document.getElementById('ghost-e1'); ghostElbow2 = document.getElementById('ghost-e2'); - ghostEnd = document.getElementById('ghost-end'); + ghostEnd = document.getElementById('ghost-e1'); ghostLabel = document.getElementById('ghost-label'); targetMarker= document.getElementById('target-marker'); @@ -734,6 +818,11 @@ const StepperTestSection = { // ── BLE NOTIFY position feedback ────────────────────────────── function onBLEStatus(e) { const msg = e.detail; + if (msg === 'HOMED') { + // Firmware completed homing — sync browser counters + onHomingComplete(); + return; + } if (msg.startsWith('P:')) { const [s1, s2] = msg.slice(2).split(',').map(Number); steps1 = s1; steps2 = s2; @@ -742,6 +831,9 @@ const StepperTestSection = { } document.addEventListener('ble:status', onBLEStatus); + // ── Find Home button ────────────────────────────────────── + document.getElementById('btn-find-home').addEventListener('click', startHoming); + eventCleanup = [ () => BLE.off('connected', updateBadge), () => BLE.off('disconnected', updateBadge),