From 334e8d33c2b91b7fdf92bc2b6711dec3f8a124a0 Mon Sep 17 00:00:00 2001 From: JongChern Date: Sat, 11 Jul 2026 22:51:45 +0800 Subject: [PATCH 1/2] update --- README.md | 2 + ShimmerAPI/shimmer3r.js | 35 +- dj-gyro/index.html | 1769 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 1805 insertions(+), 1 deletion(-) create mode 100644 dj-gyro/index.html diff --git a/README.md b/README.md index 468bd83..8e390ed 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Migration work has already started on the [`copilot/restructure-repo-for-api-sup [Spell Caster — Gyro Demo](https://shimmerresearch.github.io/webBLEDemos/spell-gyro/) +[DJ Gyro — 3D Turntable Scratch Demo](https://shimmerresearch.github.io/webBLEDemos/dj-gyro/) + [Shimmer Capture](https://shimmerresearch.github.io/webBLEDemos/ShimmerCapture/) [Shimmer3R Chrome Extension](https://github.com/ShimmerEngineering/webBLEDemos/tree/main/shimmer-extension) source code only, will require loading this locally via Chrome diff --git a/ShimmerAPI/shimmer3r.js b/ShimmerAPI/shimmer3r.js index a83c3fc..84fcd85 100644 --- a/ShimmerAPI/shimmer3r.js +++ b/ShimmerAPI/shimmer3r.js @@ -3,7 +3,7 @@ // Your note: "timestamp is u24, so layout is: 0x00 [u24 timestamp] " // This build defaults to u24, and you can also pass { timestampFmt: 'u24' } in the constructor. -const OPCODES = { DATA: 0x00, INQUIRY_CMD: 0x01, INQUIRY_RSP: 0x02, START_STREAM: 0x07, STOP_STREAM: 0x20, ACK: 0xFF, SAMPLING_RATE: 0x05, SET_SENSORS_CMD: 0x08, SET_GSR_RANGE: 0x21, SET_INTERNAL_EXP_POWER_ENABLE_CMD: 0x5E, START_BT_STEAM_SD_Logging: 0x70, STOP_BT_STEAM_SD_Logging: 0x97}; +const OPCODES = { DATA: 0x00, INQUIRY_CMD: 0x01, INQUIRY_RSP: 0x02, START_STREAM: 0x07, STOP_STREAM: 0x20, ACK: 0xFF, SAMPLING_RATE: 0x05, SET_SENSORS_CMD: 0x08, SET_GSR_RANGE: 0x21, SET_GYRO_RANGE: 0x49, SET_INTERNAL_EXP_POWER_ENABLE_CMD: 0x5E, START_BT_STEAM_SD_Logging: 0x70, STOP_BT_STEAM_SD_Logging: 0x97}; const DEFAULTS = { SERVICE_UUID: '65333333-a115-11e2-9e9a-0800200ca100', @@ -292,6 +292,39 @@ export class Shimmer3RClient { return { gsrRange, ackRemainder }; } + /** + * Set the gyro range (SET_MPU9150_GYRO_RANGE_COMMAND, 0x49). + * Shimmer3R: 0 = ±125dps, 1 = ±250dps, 2 = ±500dps, 3 = ±1000dps, 4 = ±2000dps, 5 = ±4000dps. + * (Shimmer3 only supports 0–3: ±250/500/1000/2000dps.) + */ + async setGyroRange(gyroRange) { + if (!Number.isInteger(gyroRange) || gyroRange < 0 || gyroRange > 5) { + throw new Error('gyro range must be a 0-5 value'); + } + if (!this.rx) throw new Error('Not connected (RX missing)'); + + const cmd = new Uint8Array([ + OPCODES.SET_GYRO_RANGE, + gyroRange & 0xFF, + ]); + + this._emitStatus( + `SET_GYRO_RANGE → waiting for ACK…` + ); + + const ackRemainder = await this._writeExpectingAck(cmd, 1500); + + this._emitStatus( + `SET_GYRO_RANGE (ACK received).` + ); + this.gyroRangeSetting = gyroRange; + return { gyroRange, ackRemainder }; + } + + getGyroRange() { + return this.gyroRangeSetting; + } + getInternalExpPower(){ return this.ExpPower; } diff --git a/dj-gyro/index.html b/dj-gyro/index.html new file mode 100644 index 0000000..50e7f61 --- /dev/null +++ b/dj-gyro/index.html @@ -0,0 +1,1769 @@ + + + + + + + + + + DJ Gyro — Shimmer3R Turntable + + + +
+ + +
+
+

🎧 DJ GYRO

+
A 3D turntable driven by your Shimmer3R's Z-axis gyroscope
+
    +
  • Connect: pick a gyro range, then hit 🔍 Scan & Play — it connects, configures and starts streaming in one go.
  • +
  • Hold the Shimmer3R flat (like a record) and twist it around its Z-axis — the vinyl follows your hand 1:1. Twist fast to scratch, hold still to stop the record.
  • +
  • Motor: 🎚️ Motor 33⅓ keeps the record grooving at 33⅓ RPM whenever your hand is off it.
  • +
  • No device? Drag the record with your mouse/finger, or hit 🤖 Auto-DJ to watch it scratch itself.
  • +
  • The crate: three original tracks — funk, boom bap and house — all synthesized in your browser, 100% royalty-free. Swap records with the 💿 selector.
  • +
  • Record your set: ⏺ Record captures everything you hear and saves it as a WAV when you stop.
  • +
+
+ Requirements: Shimmer3R · Firmware ≥ v1.0.22 · Chrome/Edge (Web Bluetooth) +
+
+ + +
+ +
+
+ + +
+ + + + + + + + +

No device selected

+
+ + +
+
Deck A · Shimmer Funk Break
+
Platter0.0RPM
+
Pitch%
+
Gyro Z0°/s
+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+ +

+  
🖱️ Drag the record to scratch · Click the deck's START/STOP button · Space = motor · Drag empty space to orbit
+
+ + + + + + From 584266e973d234520a5fa91381f10ff8046334e8 Mon Sep 17 00:00:00 2001 From: JongChern Date: Fri, 17 Jul 2026 13:18:28 +0800 Subject: [PATCH 2/2] Update index.html --- dj-gyro/index.html | 57 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/dj-gyro/index.html b/dj-gyro/index.html index 50e7f61..ffa2bca 100644 --- a/dj-gyro/index.html +++ b/dj-gyro/index.html @@ -347,6 +347,10 @@

🎧 DJ GYRO

+
+ + +
@@ -382,7 +386,15 @@

🎧 DJ GYRO

// ===================================================================== const NOMINAL_RPM = 100 / 3; // 33⅓ RPM const NOMINAL_REVS = NOMINAL_RPM / 60; // rev/s at pitch 0 - const GYRO_DEADZONE_DPS = 6; // below this the "hand" is off the record + // Hand-on/off hysteresis: the hand "grabs" the record above the grab + // threshold (HUD slider, ui.grabDps) and only "lets go" after staying + // below 40% of it for a short hold — micro-tremor can't grab the record + // or pin it near zero. + const GYRO_HAND_OFF_RATIO = 0.4; + const GYRO_HAND_OFF_MS = 90; + // Post-scratch recovery: the motor never stopped, so the platter grabs + // the record through the slipmat — constant accel, lands exactly on pitch 0 + const MOTOR_RECOVERY_ACCEL = 2.5; // rev/s² (~0.22 s from a dead stop) // Shimmer3R (LSM6DSV) gyro ranges — index = SET_GYRO_RANGE value, // sensitivities from the Shimmer C# API default calibration matrices @@ -408,6 +420,8 @@

🎧 DJ GYRO

biasSamples: [], calibrated: false, lastGyroAt: 0, // ms timestamp of last frame (stale detection) + handOn: false, // hysteresis: is the hand currently on the record? + handOffSince: 0, // ms timestamp when gyro first dropped below the OFF threshold // pointer scratch pointerOn: false, pointerVel: 0, // rev/s @@ -428,6 +442,7 @@

🎧 DJ GYRO

sensitivity: 1, invert: false, volume: 0.8, + grabDps: 30, // gyro °/s above which the hand grabs the record }; // ===================================================================== @@ -1406,6 +1421,9 @@

🎧 DJ GYRO

if (state.motorOn) ensureAudio(); } + let prevScratching = false; // was a hand (gyro or pointer) on the record last frame? + let motorRecovering = false; // post-scratch slipmat grab in progress + function physicsStep(dt, nowS) { // pick the driving hand let gyroDps = state.gyroDps; @@ -1417,8 +1435,26 @@

🎧 DJ GYRO

// stale stream guard: if no gyro frame for 400 ms, treat as hands-off const gyroFresh = state.streaming && (performance.now() - state.lastGyroAt < 400); - const gyroActive = (gyroFresh || (state.autoDj && !state.streaming)) && - Math.abs(gyroDps) > GYRO_DEADZONE_DPS; + const gyroDriving = gyroFresh || (state.autoDj && !state.streaming); + + // hand-on/off hysteresis + if (!gyroDriving) { + state.handOn = false; + } else if (!state.handOn) { + if (Math.abs(gyroDps) > ui.grabDps) { state.handOn = true; state.handOffSince = 0; } + } else if (Math.abs(gyroDps) < ui.grabDps * GYRO_HAND_OFF_RATIO) { + if (!state.handOffSince) state.handOffSince = performance.now(); + else if (performance.now() - state.handOffSince > GYRO_HAND_OFF_MS) state.handOn = false; + } else { + state.handOffSince = 0; + } + const gyroActive = gyroDriving && state.handOn; + + // hand released with the motor on → slipmat grabs the record back to speed + const scratching = state.pointerOn || gyroActive; + if (prevScratching && !scratching && state.motorOn) motorRecovering = true; + if (scratching || !state.motorOn) motorRecovering = false; + prevScratching = scratching; let target, slew; if (state.swapping) { @@ -1434,7 +1470,16 @@

🎧 DJ GYRO

slew = 28; } else if (state.motorOn) { target = NOMINAL_REVS; - slew = 5; // motor spin-up feel + if (motorRecovering) { + // constant accel so it lands crisply on pitch 0 instead of creeping up + slew = 0; + const dv = target - state.platterVel; + const step = MOTOR_RECOVERY_ACCEL * dt; + if (Math.abs(dv) <= step) { state.platterVel = target; motorRecovering = false; } + else state.platterVel += Math.sign(dv) * step; + } else { + slew = 5; // cold-start spin-up feel (Technics power-on) + } } else { target = 0; slew = 1.6; // free-wheeling spin-down @@ -1500,6 +1545,10 @@

🎧 DJ GYRO

ui.sensitivity = parseFloat(e.target.value); document.getElementById('sensVal').textContent = ui.sensitivity.toFixed(2).replace(/0$/, '') + '×'; }); + document.getElementById('grabSlider').addEventListener('input', (e) => { + ui.grabDps = parseFloat(e.target.value); + document.getElementById('grabVal').textContent = ui.grabDps.toFixed(0) + ' °/s'; + }); document.getElementById('volSlider').addEventListener('input', (e) => { ui.volume = e.target.value / 100; document.getElementById('volVal').textContent = e.target.value + '%';