Working BLE on S3

This commit is contained in:
osiu97
2026-07-06 19:26:50 +02:00
parent 2e3780cb0b
commit ea4530a5e4
7 changed files with 327 additions and 239 deletions
+27
View File
@@ -0,0 +1,27 @@
# PlatformIO
.pio/
.pioenvs/
.piolibdeps/
lib/readme.txt
# Python Virtual Environments
venv/
env/
.venv/
__pycache__/
*.pyc
# IDEs and Editors
.vscode/
.idea/
*.swp
*.swo
# OS Generated Files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
+7 -16
View File
@@ -75,9 +75,9 @@ STEP_ANGLE = 360/2048 ≈ 0.17578125 °/step
### UUIDs (must be identical in firmware AND `web/js/ble.js`) ### UUIDs (must be identical in firmware AND `web/js/ble.js`)
``` ```
Service: a0b1c2d3-e4f5-6789-abcd-ef0123456700 Service: 18f3b235-9831-4c75-8ec0-210469b820a0
Command: a0b1c2d3-e4f5-6789-abcd-ef0123456701 (WRITE | WRITE_NR) Command: cd083b06-4447-4cf3-a7c3-322ecf802ce4 (WRITE)
Status: a0b1c2d3-e4f5-6789-abcd-ef0123456702 (NOTIFY) Status: 82e38c5b-d3ab-41d1-861c-b84dc6bb1e03 (NOTIFY)
Name: WijiBoard Name: WijiBoard
``` ```
@@ -127,19 +127,8 @@ esp32s3-wiji/
### Module loading (index.html) ### Module loading (index.html)
All ES modules are loaded with a **cache-buster** to prevent stale code on refresh: All ES modules are loaded with static imports in `index.html`.
**CRITICAL**: Do NOT use dynamic cache-busting imports (e.g. `import('./ble.js?v=123')`) for the BLE module. Doing so causes the browser to instantiate multiple copies of the `BLE` singleton, breaking the event emitter (UI will not update when BLE connects).
```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.
--- ---
@@ -414,6 +403,8 @@ To add a new section:
| **M1 at +d2, M2 at -d2** | This is the PositionControl.cpp convention. Reversing it causes visual arm crossing. Verified by tracing `xmd = x - d2` (offset from +d2 pivot) and `xpd = x + d2` (offset from -d2 pivot). | | **M1 at +d2, M2 at -d2** | This is the PositionControl.cpp convention. Reversing it causes visual arm crossing. Verified by tracing `xmd = x - d2` (offset from +d2 pivot) and `xpd = x + d2` (offset from -d2 pivot). |
| **`forward()` picks upward EE** | Two circle-circle intersections exist; the mechanism always operates in the "above" configuration. The lower solution is physically blocked by the board. | | **`forward()` picks upward EE** | Two circle-circle intersections exist; the mechanism always operates in the "above" configuration. The lower solution is physically blocked by the board. |
| **`armsCrossed()` guard** | Prevents IK moves that would drive an elbow into the mechanism housing from the wrong side. Tunable via `LIMITS.ELBOW_BOX_X_INNER` and `ELBOW_BOX_Y_MAX`. | | **`armsCrossed()` guard** | Prevents IK moves that would drive an elbow into the mechanism housing from the wrong side. Tunable via `LIMITS.ELBOW_BOX_X_INNER` and `ELBOW_BOX_Y_MAX`. |
| **Linux/BlueZ stability** | `pAdv->setMaxPreferred(0x0C)` is mandatory in firmware. Without it, Linux/ChromeOS will drop the connection immediately after the handshake. |
| **No dynamic BLE imports**| `index.html` must import `ble.js` via static `import`. Dynamic cache-busters create multiple instances of the BLE singleton, isolating UI event listeners! |
| **`fivebarIKGame.js` is reference only** | The original site's game uses a different angle convention (absolute degrees, different home, with `ikElbowSigns` tracking). We use the C++ IK formula instead because it directly produces step deltas from home. `fivebarIKGame.js` is kept in the repo for reference and understanding, not imported. | | **`fivebarIKGame.js` is reference only** | The original site's game uses a different angle convention (absolute degrees, different home, with `ikElbowSigns` tracking). We use the C++ IK formula instead because it directly produces step deltas from home. `fivebarIKGame.js` is kept in the repo for reference and understanding, not imported. |
| **Hash-based routing** | Keeps the SPA working from `file://` and simple static servers without needing a history API setup | | **Hash-based routing** | Keeps the SPA working from `file://` and simple static servers without needing a history API setup |
+8 -8
View File
@@ -121,12 +121,11 @@
<!-- ── App bootstrap (ES module entry point) ─────────────────── --> <!-- ── App bootstrap (ES module entry point) ─────────────────── -->
<script type="module"> <script type="module">
const V = Date.now(); // cache-buster ensures fresh module load on every page refresh import Router from './web/js/router.js';
const { default: Router } = await import(`./web/js/router.js?v=${V}`); import BLE from './web/js/ble.js';
const { default: BLE } = await import(`./web/js/ble.js?v=${V}`); import UI from './web/js/ui.js';
const { default: UI } = await import(`./web/js/ui.js?v=${V}`); import HomeSection from './web/js/sections/home.js';
const { default: HomeSection } = await import(`./web/js/sections/home.js?v=${V}`); import StepperSection from './web/js/sections/stepper-test.js';
const { default: StepperSection } = await import(`./web/js/sections/stepper-test.js?v=${V}`);
// ── Register routes ──────────────────────────────────────────── // ── Register routes ────────────────────────────────────────────
Router.register('home', HomeSection); Router.register('home', HomeSection);
@@ -177,8 +176,9 @@
BLE.disconnect(); BLE.disconnect();
} else { } else {
BLE.connect().catch(e => { BLE.connect().catch(e => {
if (!e.message?.includes('cancelled') && !e.name?.includes('NotFound')) { console.error('[BLE Connect Error]', e);
UI.log(`Connect error: ${e.message}`, 'error'); if (!e.message?.includes('cancelled')) {
UI.log(`Connect error: ${e.message} (${e.name})`, 'error');
} }
}); });
} }
+3
View File
@@ -12,6 +12,9 @@ build_flags =
-D ARDUINO_USB_MODE=1 -D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_CDC_ON_BOOT=1
board_upload.flash_size = 4MB
board_build.partitions = default.csv
; Adjust to your COM port ; Adjust to your COM port
monitor_speed = 115200 monitor_speed = 115200
upload_protocol = esptool upload_protocol = esptool
+202 -203
View File
@@ -26,12 +26,12 @@
* Status : a0b1c2d3-e4f5-6789-abcd-ef0123456702 * Status : a0b1c2d3-e4f5-6789-abcd-ef0123456702
*/ */
#include <Arduino.h>
#include <AccelStepper.h> #include <AccelStepper.h>
#include <Arduino.h>
#include <BLE2902.h>
#include <BLEDevice.h> #include <BLEDevice.h>
#include <BLEServer.h> #include <BLEServer.h>
#include <BLEUtils.h> #include <BLEUtils.h>
#include <BLE2902.h>
// ─── Motor pin mapping (28BYJ-48 / ULN2003) ────────────────────── // ─── Motor pin mapping (28BYJ-48 / ULN2003) ──────────────────────
// Motor 1 Shoulder // Motor 1 Shoulder
@@ -47,32 +47,32 @@
#define M2_IN4 11 #define M2_IN4 11
// ─── Stepper constants ──────────────────────────────────────────── // ─── Stepper constants ────────────────────────────────────────────
#define STEPS_PER_REV 2048 #define STEPS_PER_REV 2048
#define DEFAULT_SPEED 600.0f // steps/sec #define DEFAULT_SPEED 600.0f // steps/sec
#define DEFAULT_ACCEL 100.0f // steps/sec² #define DEFAULT_ACCEL 100.0f // steps/sec²
// ─── BLE UUIDs ──────────────────────────────────────────────────── // ─── BLE UUIDs ────────────────────────────────────────────────────
#define SERVICE_UUID "a0b1c2d3-e4f5-6789-abcd-ef0123456700" #define SERVICE_UUID "18f3b235-9831-4c75-8ec0-210469b820a0"
#define CMD_UUID "a0b1c2d3-e4f5-6789-abcd-ef0123456701" #define CMD_UUID "cd083b06-4447-4cf3-a7c3-322ecf802ce4"
#define STATUS_UUID "a0b1c2d3-e4f5-6789-abcd-ef0123456702" #define STATUS_UUID "82e38c5b-d3ab-41d1-861c-b84dc6bb1e03"
#define DEVICE_NAME "WijiBoard" #define DEVICE_NAME "WijiBoard"
// ─── Stepper objects ────────────────────────────────────────────── // ─── Stepper objects ──────────────────────────────────────────────
AccelStepper stepper1(AccelStepper::FULL4WIRE, M1_IN1, M1_IN2, M1_IN3, M1_IN4); AccelStepper stepper1(AccelStepper::FULL4WIRE, M1_IN1, M1_IN2, M1_IN3, M1_IN4);
AccelStepper stepper2(AccelStepper::FULL4WIRE, M2_IN1, M2_IN2, M2_IN3, M2_IN4); AccelStepper stepper2(AccelStepper::FULL4WIRE, M2_IN1, M2_IN2, M2_IN3, M2_IN4);
// ─── BLE globals ───────────────────────────────────────────────── // ─── BLE globals ─────────────────────────────────────────────────
BLEServer* pServer = nullptr; BLEServer *pServer = nullptr;
BLECharacteristic* pCmdChar = nullptr; BLECharacteristic *pCmdChar = nullptr;
BLECharacteristic* pStatusChar = nullptr; BLECharacteristic *pStatusChar = nullptr;
bool bleConnected = false; bool bleConnected = false;
// ─── Status notify throttle ─────────────────────────────────────── // ─── Status notify throttle ───────────────────────────────────────
unsigned long lastNotify = 0; unsigned long lastNotify = 0;
const unsigned long NOTIFY_INTERVAL_MS = 200; const unsigned long NOTIFY_INTERVAL_MS = 200;
// ─── Forward declarations ───────────────────────────────────────── // ─── Forward declarations ─────────────────────────────────────────
void parseCommand(const String& cmd); void parseCommand(const String &cmd);
void sendPosition(); void sendPosition();
void performHomingAll(); void performHomingAll();
void performHoming1(); void performHoming1();
@@ -80,241 +80,240 @@ void performHoming2();
// ─── BLE Server Callbacks ───────────────────────────────────────── // ─── BLE Server Callbacks ─────────────────────────────────────────
class ServerCallbacks : public BLEServerCallbacks { class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer*) override { void onConnect(BLEServer *) override {
bleConnected = true; bleConnected = true;
Serial.println("[BLE] Client connected"); Serial.println("[BLE] Client connected");
} }
void onDisconnect(BLEServer*) override { void onDisconnect(BLEServer *) override {
bleConnected = false; bleConnected = false;
Serial.println("[BLE] Client disconnected restarting advertising"); Serial.println("[BLE] Client disconnected restarting advertising");
BLEDevice::startAdvertising(); BLEDevice::startAdvertising();
} }
}; };
// ─── Command Characteristic Callbacks ──────────────────────────── // ─── Command Characteristic Callbacks ────────────────────────────
class CmdCallbacks : public BLECharacteristicCallbacks { class CmdCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* pChar) override { void onWrite(BLECharacteristic *pChar) override {
String val = pChar->getValue().c_str(); String val = pChar->getValue().c_str();
val.trim(); val.trim();
if (val.length() == 0) return; if (val.length() == 0)
Serial.printf("[CMD] Received: '%s'\n", val.c_str()); return;
parseCommand(val); Serial.printf("[CMD] Received: '%s'\n", val.c_str());
} parseCommand(val);
}
}; };
// ─── Command parser ─────────────────────────────────────────────── // ─── Command parser ───────────────────────────────────────────────
void parseCommand(const String& cmd) { void parseCommand(const String &cmd) {
// S1+<n> or S1-<n> // S1+<n> or S1-<n>
if (cmd.startsWith("S1") && cmd.length() > 2) { if (cmd.startsWith("S1") && cmd.length() > 2) {
long steps = cmd.substring(2).toInt(); // '+' or '-' prefix handled by toInt() long steps =
stepper1.move(steps); cmd.substring(2).toInt(); // '+' or '-' prefix handled by toInt()
Serial.printf("[S1] Move %+ld steps\n", steps); stepper1.move(steps);
return; Serial.printf("[S1] Move %+ld steps\n", steps);
} return;
// S2+<n> or S2-<n> }
if (cmd.startsWith("S2") && cmd.length() > 2) { // S2+<n> or S2-<n>
long steps = cmd.substring(2).toInt(); if (cmd.startsWith("S2") && cmd.length() > 2) {
stepper2.move(steps); long steps = cmd.substring(2).toInt();
Serial.printf("[S2] Move %+ld steps\n", steps); stepper2.move(steps);
return; Serial.printf("[S2] Move %+ld steps\n", steps);
} return;
// SPD:<n> }
if (cmd.startsWith("SPD:")) { // SPD:<n>
float spd = cmd.substring(4).toFloat(); if (cmd.startsWith("SPD:")) {
stepper1.setMaxSpeed(spd); float spd = cmd.substring(4).toFloat();
stepper2.setMaxSpeed(spd); stepper1.setMaxSpeed(spd);
Serial.printf("[CFG] Max speed → %.0f steps/sec\n", spd); stepper2.setMaxSpeed(spd);
return; Serial.printf("[CFG] Max speed → %.0f steps/sec\n", spd);
} return;
// ACC:<n> }
if (cmd.startsWith("ACC:")) { // ACC:<n>
float acc = cmd.substring(4).toFloat(); if (cmd.startsWith("ACC:")) {
stepper1.setAcceleration(acc); float acc = cmd.substring(4).toFloat();
stepper2.setAcceleration(acc); stepper1.setAcceleration(acc);
Serial.printf("[CFG] Acceleration → %.0f steps/sec²\n", acc); stepper2.setAcceleration(acc);
return; Serial.printf("[CFG] Acceleration → %.0f steps/sec²\n", acc);
} return;
// HOME1 }
if (cmd == "HOME1") { // HOME1
Serial.println("[S1] Homing Motor 1..."); if (cmd == "HOME1") {
performHoming1(); Serial.println("[S1] Homing Motor 1...");
sendPosition(); performHoming1();
return; sendPosition();
} return;
// HOME2 }
if (cmd == "HOME2") { // HOME2
Serial.println("[S2] Homing Motor 2..."); if (cmd == "HOME2") {
performHoming2(); Serial.println("[S2] Homing Motor 2...");
sendPosition(); performHoming2();
return; sendPosition();
} return;
// HOMEALL }
if (cmd == "HOMEALL") { // HOMEALL
Serial.println("[SYS] Homing both motors..."); if (cmd == "HOMEALL") {
performHomingAll(); Serial.println("[SYS] Homing both motors...");
sendPosition(); performHomingAll();
return; sendPosition();
} return;
// POS explicit position request }
if (cmd == "POS") { // POS explicit position request
sendPosition(); if (cmd == "POS") {
return; sendPosition();
} return;
Serial.printf("[CMD] Unknown command: '%s'\n", cmd.c_str()); }
Serial.printf("[CMD] Unknown command: '%s'\n", cmd.c_str());
} }
// ─── Send current positions via BLE NOTIFY ──────────────────────── // ─── Send current positions via BLE NOTIFY ────────────────────────
void sendPosition() { void sendPosition() {
if (!bleConnected || !pStatusChar) return; if (!bleConnected || !pStatusChar)
String pos = "P:" + String(stepper1.currentPosition()) + return;
"," + String(stepper2.currentPosition()); String pos = "P:" + String(stepper1.currentPosition()) + "," +
pStatusChar->setValue(pos.c_str()); String(stepper2.currentPosition());
pStatusChar->notify(); pStatusChar->setValue(pos.c_str());
Serial.printf("[POS] %s\n", pos.c_str()); pStatusChar->notify();
Serial.printf("[POS] %s\n", pos.c_str());
} }
// ─── Setup ──────────────────────────────────────────────────────── // ─── Setup ────────────────────────────────────────────────────────
void setup() { void setup() {
// Fix: zero TX buffer to eliminate CDC post-write stall Serial.setTxTimeoutMs(0);
Serial.setTxBufferSize(0); Serial.begin(115200);
Serial.begin(115200); delay(1000); // Wait for USB CDC to enumerate
delay(500); Serial.println("\n[BOOT] WijiBoard Stepper Controller");
Serial.println("\n[BOOT] WijiBoard Stepper Controller");
// ── Init steppers ────────────────────────────────────────── // ── Init steppers ──────────────────────────────────────────
stepper1.setMaxSpeed(DEFAULT_SPEED); stepper1.setMaxSpeed(DEFAULT_SPEED);
stepper1.setAcceleration(DEFAULT_ACCEL); stepper1.setAcceleration(DEFAULT_ACCEL);
stepper1.setCurrentPosition(0); stepper1.setCurrentPosition(0);
stepper2.setMaxSpeed(DEFAULT_SPEED); stepper2.setMaxSpeed(DEFAULT_SPEED);
stepper2.setAcceleration(DEFAULT_ACCEL); stepper2.setAcceleration(DEFAULT_ACCEL);
stepper2.setCurrentPosition(0); stepper2.setCurrentPosition(0);
Serial.println("[STEP] Steppers initialized"); Serial.println("[STEP] Steppers initialized");
// ── Init BLE ─────────────────────────────────────────────── // ── Init BLE ───────────────────────────────────────────────
BLEDevice::init(DEVICE_NAME); BLEDevice::init(DEVICE_NAME);
pServer = BLEDevice::createServer(); pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks()); pServer->setCallbacks(new ServerCallbacks());
// Service // Service
BLEService* pService = pServer->createService(SERVICE_UUID); BLEService *pService = pServer->createService(SERVICE_UUID);
// Command characteristic (Write without response) // Command characteristic (Write without response)
pCmdChar = pService->createCharacteristic( pCmdChar = pService->createCharacteristic(CMD_UUID,
CMD_UUID, BLECharacteristic::PROPERTY_WRITE);
BLECharacteristic::PROPERTY_WRITE | pCmdChar->setCallbacks(new CmdCallbacks());
BLECharacteristic::PROPERTY_WRITE_NR
);
pCmdChar->setCallbacks(new CmdCallbacks());
// Status characteristic (Notify) // Status characteristic (Notify)
pStatusChar = pService->createCharacteristic( pStatusChar = pService->createCharacteristic(
STATUS_UUID, STATUS_UUID, BLECharacteristic::PROPERTY_NOTIFY);
BLECharacteristic::PROPERTY_NOTIFY // pStatusChar->addDescriptor(new BLE2902()); // Removed due to
); // deprecation/NimBLE conflicts
pStatusChar->addDescriptor(new BLE2902());
pService->start(); pService->start();
// Advertising // Advertising
BLEAdvertising* pAdv = BLEDevice::getAdvertising(); BLEAdvertising *pAdv = BLEDevice::getAdvertising();
pAdv->addServiceUUID(SERVICE_UUID); pAdv->addServiceUUID(SERVICE_UUID);
pAdv->setScanResponse(true); pAdv->setScanResponse(true);
pAdv->setMinPreferred(0x06); pAdv->setMinPreferred(0x06); // 7.5ms
BLEDevice::startAdvertising(); pAdv->setMaxPreferred(0x0C); // 15ms - CRITICAL for Linux/BlueZ stability
BLEDevice::startAdvertising();
Serial.printf("[BLE] Advertising as '%s' ready!\n", DEVICE_NAME); Serial.printf("[BLE] Advertising as '%s' ready!\n", DEVICE_NAME);
} }
// ─── Loop ───────────────────────────────────────────────────────── // ─── Loop ─────────────────────────────────────────────────────────
void loop() { void loop() {
// Run steppers (non-blocking AccelStepper) // Run steppers (non-blocking AccelStepper)
stepper1.run(); stepper1.run();
stepper2.run(); stepper2.run();
// Periodic position notify while motors are moving // Periodic position notify while motors are moving
if (bleConnected) { if (bleConnected) {
unsigned long now = millis(); unsigned long now = millis();
bool moving = stepper1.isRunning() || stepper2.isRunning(); bool moving = stepper1.isRunning() || stepper2.isRunning();
if (moving && (now - lastNotify >= NOTIFY_INTERVAL_MS)) { if (moving && (now - lastNotify >= NOTIFY_INTERVAL_MS)) {
lastNotify = now; lastNotify = now;
sendPosition(); sendPosition();
}
} }
}
} }
// ─── Homing Routines ────────────────────────────────────────────── // ─── Homing Routines ──────────────────────────────────────────────
void performHomingAll() { void performHomingAll() {
// 1. Move against limits (M1 CCW, M2 CCW) // 1. Move against limits (M1 CCW, M2 CCW)
stepper2.moveTo(1024); stepper2.moveTo(1024);
stepper1.moveTo(2048); stepper1.moveTo(2048);
while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) { while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) {
stepper1.run(); stepper1.run();
stepper2.run(); stepper2.run();
} }
stepper1.setCurrentPosition(0); stepper1.setCurrentPosition(0);
// 2. Move to negative limits (CW) // 2. Move to negative limits (CW)
stepper2.moveTo(-1050); stepper2.moveTo(-1050);
stepper1.move(-1300); // equivalent to moveTo(-1300) since pos is 0 stepper1.move(-1300); // equivalent to moveTo(-1300) since pos is 0
while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) { while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) {
stepper1.run(); stepper1.run();
stepper2.run(); stepper2.run();
} }
stepper2.setCurrentPosition(0); stepper2.setCurrentPosition(0);
// 3. Move to final home position // 3. Move to final home position
stepper2.moveTo(550); stepper2.moveTo(550);
stepper1.moveTo(-530); stepper1.moveTo(-530);
while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) { while (stepper1.distanceToGo() != 0 || stepper2.distanceToGo() != 0) {
stepper1.run(); stepper1.run();
stepper2.run(); stepper2.run();
} }
// 4. Set origin relative to this final position
stepper2.setCurrentPosition(-1024);
stepper1.setCurrentPosition(0);
stepper1.disableOutputs(); // 4. Set origin relative to this final position
stepper2.disableOutputs(); stepper2.setCurrentPosition(-1024);
Serial.println("[SYS] Homing all complete."); stepper1.setCurrentPosition(0);
stepper1.disableOutputs();
stepper2.disableOutputs();
Serial.println("[SYS] Homing all complete.");
} }
void performHoming1() { void performHoming1() {
// M1 (Right) stalls going positive (CCW) // M1 (Right) stalls going positive (CCW)
stepper1.move(2048); stepper1.move(2048);
while (stepper1.distanceToGo() != 0) { while (stepper1.distanceToGo() != 0) {
stepper1.run(); stepper1.run();
} }
stepper1.setCurrentPosition(0); stepper1.setCurrentPosition(0);
// Move to final home position (CW) // Move to final home position (CW)
stepper1.moveTo(-530); stepper1.moveTo(-530);
while (stepper1.distanceToGo() != 0) { while (stepper1.distanceToGo() != 0) {
stepper1.run(); stepper1.run();
} }
stepper1.setCurrentPosition(0); stepper1.setCurrentPosition(0);
stepper1.disableOutputs(); stepper1.disableOutputs();
Serial.println("[S1] Homing complete."); Serial.println("[S1] Homing complete.");
} }
void performHoming2() { void performHoming2() {
// M2 (Left) stalls going negative (CW) // M2 (Left) stalls going negative (CW)
stepper2.move(-2048); stepper2.move(-2048);
while (stepper2.distanceToGo() != 0) { while (stepper2.distanceToGo() != 0) {
stepper2.run(); stepper2.run();
} }
stepper2.setCurrentPosition(0); stepper2.setCurrentPosition(0);
// Move to final home position (CCW) // Move to final home position (CCW)
stepper2.moveTo(550); stepper2.moveTo(550);
while (stepper2.distanceToGo() != 0) { while (stepper2.distanceToGo() != 0) {
stepper2.run(); stepper2.run();
} }
stepper2.setCurrentPosition(-1024); stepper2.setCurrentPosition(-1024);
stepper2.disableOutputs(); stepper2.disableOutputs();
Serial.println("[S2] Homing complete."); Serial.println("[S2] Homing complete.");
} }
+55
View File
@@ -0,0 +1,55 @@
import asyncio
import sys
try:
from bleak import BleakScanner, BleakClient
except ImportError:
print("Please install bleak first: pip install bleak")
sys.exit(1)
DEVICE_NAME = "WijiBoard"
SERVICE_UUID = "18f3b235-9831-4c75-8ec0-210469b820a0"
CMD_UUID = "cd083b06-4447-4cf3-a7c3-322ecf802ce4"
STATUS_UUID = "82e38c5b-d3ab-41d1-861c-b84dc6bb1e03"
async def main():
print(f"Scanning for {DEVICE_NAME}...")
devices = await BleakScanner.discover(timeout=5.0)
target_device = None
for d in devices:
if d.name == DEVICE_NAME:
target_device = d
break
if not target_device:
print(f"Could not find {DEVICE_NAME}. Make sure it is powered on and advertising.")
return
print(f"Found {DEVICE_NAME} at {target_device.address}. Connecting...")
async with BleakClient(target_device.address) as client:
print(f"Connected: {client.is_connected}")
# Setup notifications
def notification_handler(sender, data):
print(f"[STATUS UPDATE] {data.decode('utf-8')}")
print("Subscribing to status characteristic...")
try:
await client.start_notify(STATUS_UUID, notification_handler)
print("Successfully subscribed!")
except Exception as e:
print(f"Failed to subscribe: {e}")
# Send a test command
test_cmd = "S1+100"
print(f"Sending test command: '{test_cmd}'...")
await client.write_gatt_char(CMD_UUID, test_cmd.encode('utf-8'), response=False)
print("Waiting 5 seconds to receive any status updates...")
await asyncio.sleep(5.0)
print("Disconnecting...")
if __name__ == "__main__":
asyncio.run(main())
+25 -12
View File
@@ -25,16 +25,16 @@
*/ */
// ── UUIDs must match src/main.cpp exactly ────────────────────── // ── UUIDs must match src/main.cpp exactly ──────────────────────
const SERVICE_UUID = 'a0b1c2d3-e4f5-6789-abcd-ef0123456700'; const SERVICE_UUID = '18f3b235-9831-4c75-8ec0-210469b820a0';
const CMD_UUID = 'a0b1c2d3-e4f5-6789-abcd-ef0123456701'; const CMD_UUID = 'cd083b06-4447-4cf3-a7c3-322ecf802ce4';
const STATUS_UUID = 'a0b1c2d3-e4f5-6789-abcd-ef0123456702'; const STATUS_UUID = '82e38c5b-d3ab-41d1-861c-b84dc6bb1e03';
const DEVICE_NAME = 'WijiBoard'; const DEVICE_NAME = 'WijiBoard';
const BLE = (() => { const BLE = (() => {
// ── Internal state ────────────────────────────────────────── // ── Internal state ──────────────────────────────────────────
let device = null; let device = null;
let server = null; let server = null;
let cmdChar = null; let cmdChar = null;
let statusChar = null; let statusChar = null;
const encoder = new TextEncoder(); const encoder = new TextEncoder();
@@ -60,9 +60,9 @@ const BLE = (() => {
// ── Disconnect handler ────────────────────────────────────── // ── Disconnect handler ──────────────────────────────────────
function onDisconnected() { function onDisconnected() {
console.log('[BLE] Device disconnected'); console.log('[BLE] Device disconnected');
device = null; device = null;
server = null; server = null;
cmdChar = null; cmdChar = null;
statusChar = null; statusChar = null;
emit('disconnected'); emit('disconnected');
} }
@@ -75,34 +75,47 @@ const BLE = (() => {
// ── connect() ─────────────────────────────────────────────── // ── connect() ───────────────────────────────────────────────
async function connect() { async function connect() {
console.log('[BLE] connect() called');
if (!navigator.bluetooth) { if (!navigator.bluetooth) {
throw new Error('Web Bluetooth API not available. Use Chrome/Edge over HTTPS or localhost.'); throw new Error('Web Bluetooth API not available. Use Chrome/Edge over HTTPS or localhost.');
} }
emit('connecting'); emit('connecting');
console.log('[BLE] Requesting device...');
device = await navigator.bluetooth.requestDevice({ device = await navigator.bluetooth.requestDevice({
filters: [{ name: DEVICE_NAME }], filters: [{ name: DEVICE_NAME }],
optionalServices: [SERVICE_UUID], optionalServices: [SERVICE_UUID],
}); });
console.log('[BLE] Device selected:', device.name);
device.addEventListener('gattserverdisconnected', onDisconnected); device.addEventListener('gattserverdisconnected', onDisconnected);
console.log('[BLE] Connecting to GATT server...');
server = await device.gatt.connect(); server = await device.gatt.connect();
console.log('[BLE] GATT server connected');
console.log('[BLE] Getting primary service:', SERVICE_UUID);
const service = await server.getPrimaryService(SERVICE_UUID); const service = await server.getPrimaryService(SERVICE_UUID);
console.log('[BLE] Primary service found');
console.log('[BLE] Getting command characteristic:', CMD_UUID);
cmdChar = await service.getCharacteristic(CMD_UUID); cmdChar = await service.getCharacteristic(CMD_UUID);
console.log('[BLE] Command characteristic found');
// Status notifications (optional firmware may not have this char yet) // Status notifications (optional firmware may not have this char yet)
try { try {
console.log('[BLE] Getting status characteristic:', STATUS_UUID);
statusChar = await service.getCharacteristic(STATUS_UUID); statusChar = await service.getCharacteristic(STATUS_UUID);
console.log('[BLE] Starting notifications...');
await statusChar.startNotifications(); await statusChar.startNotifications();
statusChar.addEventListener('characteristicvaluechanged', onStatusNotification); statusChar.addEventListener('characteristicvaluechanged', onStatusNotification);
} catch { console.log('[BLE] Notifications started');
console.warn('[BLE] Status characteristic not available notifications disabled'); } catch (err) {
console.warn('[BLE] Status characteristic not available notifications disabled', err);
} }
console.log('[BLE] Connection fully established, emitting connected');
emit('connected', device.name); emit('connected', device.name);
} }