Created
June 17, 2025 20:12
-
-
Save CharStiles/4ea96386f230075650b8713d82f6a104 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>WebSerial Movement Controller</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
margin: 20px; | |
} | |
button { | |
padding: 10px; | |
margin: 5px; | |
font-size: 14px; | |
} | |
input, select { | |
padding: 5px; | |
margin: 5px; | |
} | |
#console { | |
border: 1px solid #ccc; | |
padding: 10px; | |
height: 150px; | |
overflow-y: auto; | |
font-family: monospace; | |
background-color: #f9f9f9; | |
} | |
.section { | |
border: 1px solid #ddd; | |
padding: 15px; | |
margin: 10px 0; | |
} | |
.movement-grid { | |
display: inline-block; | |
} | |
.movement-grid table { | |
border-collapse: collapse; | |
} | |
.movement-grid td { | |
text-align: center; | |
padding: 5px; | |
} | |
.status { | |
padding: 10px; | |
margin: 10px 0; | |
font-weight: bold; | |
} | |
.connected { | |
background-color: #d4edda; | |
color: #155724; | |
} | |
.disconnected { | |
background-color: #f8d7da; | |
color: #721c24; | |
} | |
.coordinate-toggle { | |
margin: 10px 0; | |
padding: 10px; | |
background-color: #e9ecef; | |
border-radius: 4px; | |
} | |
.coordinate-toggle label { | |
margin-right: 15px; | |
} | |
#polarInputs, #cartesianInputs { | |
margin: 10px 0; | |
padding: 10px; | |
background-color: #f8f9fa; | |
border-radius: 4px; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>WebSerial Movement Controller</h1> | |
<div class="section"> | |
<h3>Connection</h3> | |
<label>Baud Rate: </label> | |
<select id="baudRate"> | |
<option value="9600" selected>9600</option> | |
<option value="115200">115200</option> | |
<option value="57600">57600</option> | |
<option value="19200">19200</option> | |
</select> | |
<br><br> | |
<label>Flow Control: </label> | |
<select id="flowControl"> | |
<option value="hardware">Hardware (RTS/CTS)</option> | |
<option value="none">None</option> | |
</select> | |
<br><br> | |
<button id="connectBtn">Connect Device</button> | |
<button id="disconnectBtn" disabled>Disconnect</button> | |
<div id="status" class="status disconnected">Status: Disconnected</div> | |
</div> | |
<div class="section"> | |
<h3>Movement</h3> | |
<label>Step Size: </label> | |
<input type="number" id="stepSize" value="10" min="1" max="1000" style="width: 60px;"> | |
<br><br> | |
<div class="coordinate-toggle"> | |
<label> | |
<input type="radio" name="coordinateSystem" value="cartesian" checked> Cartesian | |
</label> | |
<label> | |
<input type="radio" name="coordinateSystem" value="polar"> Polar | |
</label> | |
</div> | |
<br> | |
<div id="cartesianInputs"> | |
<label>X: </label> | |
<input type="number" id="xPos" value="0" style="width: 60px;"> | |
<label>Y: </label> | |
<input type="number" id="yPos" value="0" style="width: 60px;"> | |
</div> | |
<div id="polarInputs" style="display: none;"> | |
<label>Radius: </label> | |
<input type="number" id="radiusInput" value="0" min="0" style="width: 60px;"> | |
<label>Angle (degrees): </label> | |
<input type="number" id="angleInput" value="0" min="0" max="360" style="width: 60px;"> | |
</div> | |
<br> | |
<div class="movement-grid"> | |
<table> | |
<tr> | |
<td><button id="upLeft" disabled>↖</button></td> | |
<td><button id="up" disabled>↑</button></td> | |
<td><button id="upRight" disabled>↗</button></td> | |
</tr> | |
<tr> | |
<td><button id="left" disabled>←</button></td> | |
<td><button id="home" disabled>Home</button></td> | |
<td><button id="right" disabled>→</button></td> | |
</tr> | |
<tr> | |
<td><button id="downLeft" disabled>↙</button></td> | |
<td><button id="down" disabled>↓</button></td> | |
<td><button id="downRight" disabled>↘</button></td> | |
</tr> | |
</table> | |
</div> | |
<br> | |
<button id="moveToBtn" disabled>Move To</button> | |
</div> | |
<div class="section"> | |
<h3>Custom Command</h3> | |
<input type="text" id="customCommand" placeholder="G0 X10 Y20" style="width: 200px;"> | |
<button id="sendCustomBtn" disabled>Send</button> | |
</div> | |
<div class="section"> | |
<h3>MIDI Connection</h3> | |
<button id="setupMIDIBtn">Setup MIDI Connection</button> | |
<div id="midiStatus" class="status disconnected">MIDI Status: Not Connected</div> | |
</div> | |
<div class="section"> | |
<h3>Console</h3> | |
<div id="console"></div> | |
<button id="clearConsole">Clear</button> | |
</div> | |
<div class="section"> | |
<h3>Spiral Simulation</h3> | |
<div id="spiralSim"></div> | |
<div style="font-size:12px; color:#888;">This canvas simulates the spiral and head movement in real time.</div> | |
</div> | |
<label>Laser Power (%):</label> | |
<input type="number" id="laserPowerInput" value="10" min="0" max="100" style="width: 60px;"> | |
<label>Cut Speed (mm/s):</label> | |
<input type="number" id="velocityInput" value="15" min="1" max="1000" style="width: 60px;"> | |
<label>Jog Speed (mm/s):</label> | |
<input type="number" id="jogInput" value="100" min="1" max="1000" style="width: 60px;"> | |
<label>Thickness (mm):</label> | |
<input type="number" id="thicknessInput" value="0.5" min="0" max="100" style="width: 60px;"> | |
<label>Origin X (mm):</label> | |
<input type="number" id="oxInput" value="0" min="0" max="1000" style="width: 60px;"> | |
<label>Origin Y (mm):</label> | |
<input type="number" id="oyInput" value="0" min="0" max="1000" style="width: 60px;"> | |
<div class="section"> | |
<button id="endBtn">End</button> | |
<button id="startSpiralBtn">Start Spiral</button> | |
<div style="margin-top:10px;"> | |
<label>Z Axis: </label> | |
<button id="zDownBtn">Down</button> | |
<span id="zValue">0.00</span> mm | |
<button id="zUpBtn">Up</button> | |
</div> | |
<div style="margin-top:10px;"> | |
<label>Set Z Height: </label> | |
<input type="number" id="setZInput" value="0.00" step="0.01" style="width: 80px;"> | |
<button id="setZBtn">Set Z</button> | |
</div> | |
</div> | |
<script src="https://unpkg.com/@strudel/[email protected]"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script> | |
<script> | |
// Simple Perlin noise implementation | |
// Adapted from https://github.com/joeiddon/perlin/blob/master/perlin.js | |
const Perlin = (() => { | |
let perm = new Uint8Array(512); | |
let grad3 = [ | |
[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0], | |
[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1], | |
[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1] | |
]; | |
function seed(s) { | |
for (let i = 0; i < 256; ++i) perm[i] = i; | |
for (let i = 0; i < 256; ++i) { | |
let j = (s * (i + 1) + 31) % 256; | |
[perm[i], perm[j]] = [perm[j], perm[i]]; | |
} | |
for (let i = 0; i < 256; ++i) perm[256 + i] = perm[i]; | |
} | |
function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } | |
function lerp(a, b, t) { return (1 - t) * a + t * b; } | |
function grad(hash, x, y, z) { | |
let h = hash & 15; | |
let u = h < 8 ? x : y; | |
let v = h < 4 ? y : h === 12 || h === 14 ? x : z; | |
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v); | |
} | |
function noise(x, y = 0, z = 0) { | |
let X = Math.floor(x) & 255, Y = Math.floor(y) & 255, Z = Math.floor(z) & 255; | |
x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z); | |
let u = fade(x), v = fade(y), w = fade(z); | |
let A = perm[X] + Y, AA = perm[A] + Z, AB = perm[A + 1] + Z; | |
let B = perm[X + 1] + Y, BA = perm[B] + Z, BB = perm[B + 1] + Z; | |
return lerp( | |
lerp( | |
lerp(grad(perm[AA], x, y, z), grad(perm[BA], x - 1, y, z), u), | |
lerp(grad(perm[AB], x, y - 1, z), grad(perm[BB], x - 1, y - 1, z), u), | |
v | |
), | |
lerp( | |
lerp(grad(perm[AA + 1], x, y, z - 1), grad(perm[BA + 1], x - 1, y, z - 1), u), | |
lerp(grad(perm[AB + 1], x, y - 1, z - 1), grad(perm[BB + 1], x - 1, y - 1, z - 1), u), | |
v | |
), | |
w | |
); | |
} | |
seed(42); | |
return { noise }; | |
})(); | |
let port = null; | |
let reader = null; | |
let currentX = 0; | |
let currentY = 0; | |
let isReadingPort = false; | |
const connectBtn = document.getElementById('connectBtn'); | |
const disconnectBtn = document.getElementById('disconnectBtn'); | |
const status = document.getElementById('status'); | |
const consoleDiv = document.getElementById('console'); | |
const baudRate = document.getElementById('baudRate'); | |
const flowControl = document.getElementById('flowControl'); | |
const stepSize = document.getElementById('stepSize'); | |
const xPos = document.getElementById('xPos'); | |
const yPos = document.getElementById('yPos'); | |
const customCommand = document.getElementById('customCommand'); | |
const movementButtons = [ | |
'up', 'down', 'left', 'right', 'upLeft', 'upRight', | |
'downLeft', 'downRight', 'home' | |
]; | |
function log(message) { | |
const time = new Date().toLocaleTimeString(); | |
consoleDiv.innerHTML += `[${time}] ${message}<br>`; | |
consoleDiv.scrollTop = consoleDiv.scrollHeight; | |
} | |
async function connectDevice() { | |
try { | |
port = await navigator.serial.requestPort(); | |
await port.open({ | |
baudRate: parseInt(baudRate.value), | |
flowControl: flowControl.value | |
}); | |
log(`Connected at ${baudRate.value} baud`); | |
updateStatus(true); | |
readFromPort(); | |
} catch (error) { | |
log(`Connection failed: ${error.message}`); | |
} | |
} | |
async function disconnectDevice() { | |
try { | |
if (reader) { | |
await reader.cancel(); | |
reader.releaseLock(); | |
reader = null; | |
} | |
if (port) { | |
await port.close(); | |
port = null; | |
} | |
log('Disconnected'); | |
updateStatus(false); | |
} catch (error) { | |
log(`Disconnect error: ${error.message}`); | |
} | |
} | |
async function readFromPort() { | |
if (isReadingPort) return; | |
isReadingPort = true; | |
try { | |
reader = port.readable.getReader(); | |
while (port && port.readable) { | |
const { value, done } = await reader.read(); | |
if (done) break; | |
if (value) { | |
const text = new TextDecoder().decode(value); | |
log(`Received: ${text}`); | |
if (text.includes('ok')) { | |
readyToSend = true; | |
} | |
} | |
} | |
} catch (error) { | |
if (error.name !== 'AbortError') { | |
log(`Read error: ${error.message}`); | |
} | |
} | |
isReadingPort = false; | |
} | |
async function sendCommand(command) { | |
if (!port || !port.writable) { | |
log('Error: Not connected'); | |
return; | |
} | |
try { | |
const writer = port.writable.getWriter(); | |
const data = new TextEncoder().encode(command + '\n'); | |
await writer.write(data); | |
writer.releaseLock(); | |
log(`Sent: ${command}`); | |
} catch (error) { | |
log(`Send error: ${error.message}`); | |
} | |
} | |
function updateStatus(connected) { | |
if (connected) { | |
status.textContent = 'Status: Connected'; | |
status.className = 'status connected'; | |
connectBtn.disabled = true; | |
disconnectBtn.disabled = false; | |
movementButtons.forEach(id => { | |
document.getElementById(id).disabled = false; | |
}); | |
document.getElementById('moveToBtn').disabled = false; | |
document.getElementById('sendCustomBtn').disabled = false; | |
} else { | |
status.textContent = 'Status: Disconnected'; | |
status.className = 'status disconnected'; | |
connectBtn.disabled = false; | |
disconnectBtn.disabled = true; | |
movementButtons.forEach(id => { | |
document.getElementById(id).disabled = true; | |
}); | |
document.getElementById('moveToBtn').disabled = true; | |
document.getElementById('sendCustomBtn').disabled = true; | |
} | |
} | |
let usePolarCoordinates = false; | |
let currentRadius = 0; | |
let currentAngle = 0; | |
// Add coordinate system toggle handler | |
document.getElementsByName('coordinateSystem').forEach(radio => { | |
radio.addEventListener('change', (e) => { | |
usePolarCoordinates = e.target.value === 'polar'; | |
document.getElementById('cartesianInputs').style.display = usePolarCoordinates ? 'none' : 'block'; | |
document.getElementById('polarInputs').style.display = usePolarCoordinates ? 'block' : 'none'; | |
}); | |
}); | |
// Update move function to handle polar coordinates | |
async function move(deltaX, deltaY) { | |
const step = parseInt(stepSize.value); | |
if (usePolarCoordinates) { | |
// Convert current position to polar | |
currentRadius = Math.sqrt(currentX * currentX + currentY * currentY); | |
currentAngle = Math.atan2(currentY, currentX) * 180 / Math.PI; | |
// Update polar coordinates | |
currentRadius += step; | |
currentAngle += deltaX * 45; // Rotate by 45 degrees for diagonal movements | |
// Convert back to cartesian | |
currentX = currentRadius * Math.cos(currentAngle * Math.PI / 180); | |
currentY = currentRadius * Math.sin(currentAngle * Math.PI / 180); | |
// Update polar input fields | |
radiusInput.value = currentRadius; | |
angleInput.value = currentAngle; | |
} else { | |
currentX += deltaX * step; | |
currentY += deltaY * step; | |
} | |
const command = `G0 X${currentX} Y${currentY}`; | |
await sendCommand(command); | |
xPos.value = currentX; | |
yPos.value = currentY; | |
} | |
// Update moveToPosition function to handle both coordinate systems | |
async function moveToPosition() { | |
if (usePolarCoordinates) { | |
currentRadius = parseInt(radiusInput.value) || 0; | |
currentAngle = parseInt(angleInput.value) || 0; | |
currentX = currentRadius * Math.cos(currentAngle * Math.PI / 180); | |
currentY = currentRadius * Math.sin(currentAngle * Math.PI / 180); | |
const command = `G0 X${currentX} Y${currentY}`; | |
await sendCommand(command); | |
} else { | |
currentX = parseInt(xPos.value) || 0; | |
currentY = parseInt(yPos.value) || 0; | |
const command = `G0 X${currentX} Y${currentY}`; | |
await sendCommand(command); | |
} | |
} | |
// Update goHome function to reset both coordinate systems | |
async function goHome() { | |
currentX = 0; | |
currentY = 0; | |
currentRadius = 0; | |
currentAngle = 0; | |
await sendCommand('G28'); | |
xPos.value = 0; | |
yPos.value = 0; | |
radiusInput.value = 0; | |
angleInput.value = 0; | |
} | |
// Event listeners | |
connectBtn.addEventListener('click', async () => { | |
await connectDevice(); | |
if (!isReadingPort) { | |
readFromPort(); | |
} | |
}); | |
disconnectBtn.addEventListener('click', disconnectDevice); | |
document.getElementById('up').addEventListener('click', () => move(0, 1)); | |
document.getElementById('down').addEventListener('click', () => move(0, -1)); | |
document.getElementById('left').addEventListener('click', () => move(-1, 0)); | |
document.getElementById('right').addEventListener('click', () => move(1, 0)); | |
document.getElementById('upLeft').addEventListener('click', () => move(-1, 1)); | |
document.getElementById('upRight').addEventListener('click', () => move(1, 1)); | |
document.getElementById('downLeft').addEventListener('click', () => move(-1, -1)); | |
document.getElementById('downRight').addEventListener('click', () => move(1, -1)); | |
document.getElementById('home').addEventListener('click', goHome); | |
document.getElementById('moveToBtn').addEventListener('click', moveToPosition); | |
document.getElementById('sendCustomBtn').addEventListener('click', async () => { | |
const command = customCommand.value.trim(); | |
if (command) { | |
await sendCommand(command); | |
customCommand.value = ''; | |
} | |
}); | |
document.getElementById('clearConsole').addEventListener('click', () => { | |
consoleDiv.innerHTML = ''; | |
}); | |
customCommand.addEventListener('keypress', (e) => { | |
if (e.key === 'Enter') { | |
document.getElementById('sendCustomBtn').click(); | |
} | |
}); | |
if (!('serial' in navigator)) { | |
log('WebSerial not supported in this browser'); | |
connectBtn.disabled = true; | |
} else { | |
log('Ready to connect'); | |
} | |
// Spiral parameters | |
const spiralTurns = 5; | |
const spiralSize = 76.2; // 3 inches in mm | |
// For commercial laser cutters (e.g., xTool S1), typical command rates are 50-100+ per second. | |
// Use 500 steps and 10ms interval for fast, smooth, and responsive motion. | |
const spiralSteps = 1000; // Fewer steps for faster completion | |
let spiralIndex = 0; | |
let spiralTimer = null; | |
let spiralInterval = 50; // ms (20 commands/sec, slower) | |
let spiralPath = []; | |
let simHead = { x: 0, y: 0 }; | |
let simCanvasSize = spiralSize; | |
// MIDI state | |
let midiNoteOn = false; | |
let midiPitch = 60; // Default middle C | |
let midiVelocity = 0; | |
let perlinZ = 0; // For noise cycling | |
// Perlin noise parameters | |
const baseNoiseAmount = 10; // How much the noise perturbs the radius when no note is on | |
const midiNoiseAmount = 40; // Max perturbation when note is on | |
const midiNoiseFreqMin = 0.005; // Slowest noise freq (low pitch) | |
const midiNoiseFreqMax = 0.1; // Fastest noise freq (high pitch) | |
const midiPitchMin = 36; // C2 | |
const midiPitchMax = 96; // C7 | |
let currentNoiseAmount = 0; // For smooth ramping | |
const noiseRampSpeed = 0.15; // 0..1, higher = faster ramp (per step) | |
let laserPowerPercent = parseFloat(document.getElementById('laserPowerInput').value); | |
let laserPower = 1000 * laserPowerPercent / 100; | |
let laserWasOn = false; // Track previous laser state | |
let velocity = parseFloat(document.getElementById('velocityInput').value) * 60; // mm/min | |
let jog = parseFloat(document.getElementById('jogInput').value) * 60; // mm/min | |
let thickness = parseFloat(document.getElementById('thicknessInput').value); | |
let ox = parseFloat(document.getElementById('oxInput').value); | |
let oy = parseFloat(document.getElementById('oyInput').value); | |
let readyToSend = true; // Only send next command after 'ok' | |
// Z focus base value (distance from bed to lens focal point) | |
let zFocusBase = 48.68; // mm, adjustable if needed | |
// Helper: get spiral position and tangent at index | |
function getSpiralInfo(turns, size, steps, index) { | |
const centerX = size / 2; | |
const centerY = size / 2; | |
const offsetX = 0; | |
const offsetY = 0; | |
const maxTheta = 2 * Math.PI * turns; | |
const t = index / (steps - 1); | |
const theta = t * maxTheta; | |
const r = t * (size / 2); | |
const x = offsetX + centerX + r * Math.cos(theta); | |
const y = offsetY + centerY + r * Math.sin(theta); | |
// Tangent (dx/dt, dy/dt) | |
const dtheta_dt = maxTheta; | |
const dr_dt = size / 2; | |
const dx_dt = dr_dt * Math.cos(theta) - r * Math.sin(theta) * dtheta_dt; | |
const dy_dt = dr_dt * Math.sin(theta) + r * Math.cos(theta) * dtheta_dt; | |
// Normalize tangent | |
const mag = Math.sqrt(dx_dt * dx_dt + dy_dt * dy_dt); | |
const tx = dx_dt / mag; | |
const ty = dy_dt / mag; | |
// Perpendicular (normal) direction | |
const nx = -ty; | |
const ny = tx; | |
return { x, y, r, theta, nx, ny }; | |
} | |
// Spiral stepping function | |
async function spiralStep() { | |
//if (!readyToSend) return; | |
if (spiralIndex >= spiralSteps) { | |
stopSpiralTimer(); | |
log('Spiral complete.'); | |
return; | |
} | |
readyToSend = false; | |
// Get spiral info | |
const info = getSpiralInfo(spiralTurns, spiralSize, spiralSteps, spiralIndex); | |
// Perlin noise input | |
let noiseFreq, targetNoiseAmount; | |
if (midiNoteOn) { | |
// If this is the first note-on (transition from off to on), reset spiral | |
if (!laserWasOn) { | |
spiralIndex = 0; | |
spiralPath = []; | |
} | |
// Map pitch to noise speed and amount | |
const pitchNorm = Math.max(0, Math.min(1, (midiPitch - midiPitchMin) / (midiPitchMax - midiPitchMin))); | |
noiseFreq = midiNoiseFreqMin + (midiNoiseFreqMax - midiNoiseFreqMin) * pitchNorm; | |
targetNoiseAmount = baseNoiseAmount + (midiNoiseAmount - baseNoiseAmount) * pitchNorm; | |
} else { | |
noiseFreq = midiNoiseFreqMin; | |
targetNoiseAmount = 0; // No perturbation when no MIDI | |
} | |
// Smoothly ramp currentNoiseAmount toward targetNoiseAmount | |
currentNoiseAmount += (targetNoiseAmount - currentNoiseAmount) * noiseRampSpeed; | |
// Perlin noise value (smooth, -1 to 1) | |
const noiseVal = Perlin.noise(spiralIndex * noiseFreq, 0, 0); | |
// Perturb radius | |
const perturb = noiseVal * currentNoiseAmount; | |
const px = info.x + info.nx * perturb; | |
const py = info.y + info.ny * perturb; | |
// Send move command | |
let gcode; | |
if (midiNoteOn) { | |
if (!laserWasOn) { | |
gcode = `G1 X${Math.round(px)} Y${Math.round(py)} S${laserPower.toFixed(2)}`; | |
} else { | |
gcode = `G1 X${Math.round(px)} Y${Math.round(py)}`; | |
} | |
} else { | |
gcode = `G0 X${Math.round(px)} Y${Math.round(py)}`; | |
} | |
await sendCommand(gcode); | |
xPos.value = Math.round(px); | |
yPos.value = Math.round(py); | |
spiralIndex = (spiralIndex + 1); | |
log(`Spiral step sent: ${gcode} (noise: ${noiseVal.toFixed(2)})`); | |
simHead = { x: px, y: py }; | |
spiralPath.push({ x: px, y: py }); | |
if (spiralPath.length > spiralSteps) spiralPath.shift(); | |
laserWasOn = midiNoteOn; | |
} | |
// Start/stop spiral timer | |
function startSpiralTimer() { | |
if (spiralTimer) clearInterval(spiralTimer); | |
spiralTimer = setInterval(spiralStep, spiralInterval); | |
} | |
function stopSpiralTimer() { | |
if (spiralTimer) clearInterval(spiralTimer); | |
spiralTimer = null; | |
} | |
// MIDI input setup (update note state) | |
let midiAccess = null; | |
let midiInput = null; | |
async function setupMIDI() { | |
try { | |
log('Requesting MIDI access...'); | |
midiAccess = await navigator.requestMIDIAccess({ sysex: true }); | |
log('MIDI Access granted'); | |
const inputs = Array.from(midiAccess.inputs.values()); | |
log(`Found ${inputs.length} MIDI inputs:`); | |
inputs.forEach(input => { | |
log(`- ${input.name} (${input.manufacturer})`); | |
}); | |
midiInput = inputs.find(input => input.name === "IAC Driver Bus 1"); | |
if (midiInput) { | |
log(`Connected to MIDI input: ${midiInput.name}`); | |
document.getElementById('midiStatus').textContent = `MIDI Status: Connected to ${midiInput.name}`; | |
document.getElementById('midiStatus').className = 'status connected'; | |
midiInput.onmidimessage = (event) => { | |
const [status, note, velocity] = event.data; | |
log(`MIDI received - Status: ${status}, Note: ${note}, Velocity: ${velocity}`); | |
if (status === 144 && velocity > 0) { // Note on | |
midiNoteOn = true; | |
midiPitch = note; | |
midiVelocity = velocity; | |
} else if (status === 128 || (status === 144 && velocity === 0)) { // Note off | |
midiNoteOn = false; | |
} | |
}; | |
} else { | |
log('Could not find "IAC Driver Bus 1" MIDI input'); | |
document.getElementById('midiStatus').textContent = 'MIDI Status: Network CHAR/IAC not found'; | |
document.getElementById('midiStatus').className = 'status disconnected'; | |
} | |
} catch (error) { | |
log(`MIDI setup error: ${error.message}`); | |
document.getElementById('midiStatus').textContent = `MIDI Status: Error - ${error.message}`; | |
document.getElementById('midiStatus').className = 'status disconnected'; | |
} | |
} | |
// Add event listener for MIDI setup button | |
document.getElementById('setupMIDIBtn').addEventListener('click', setupMIDI); | |
// Initialize MIDI when the page loads | |
setupMIDI(); | |
// --- p5.js Spiral Simulation --- | |
function setup() { | |
let cnv = createCanvas(simCanvasSize, simCanvasSize); | |
cnv.parent('spiralSim'); | |
background(255); | |
} | |
function draw() { | |
background(255); | |
// Draw spiral path | |
noFill(); | |
stroke(200, 200, 255); | |
strokeWeight(1); | |
beginShape(); | |
for (let pt of spiralPath) { | |
vertex(pt.x, pt.y); | |
} | |
endShape(); | |
// Draw head | |
if (spiralPath.length > 0) { | |
let last = spiralPath[spiralPath.length - 1]; | |
fill(255, 0, 0); | |
noStroke(); | |
ellipse(last.x, last.y, 10, 10); | |
} | |
} | |
// --- Startup G-code sequence --- | |
async function sendStartupGCode() { | |
let str = "M110 X1 Y1 Z1\n"; | |
str += "M109 S1\n"; | |
str += "M223 X498 Y319\n"; | |
str += "M7S1\n"; | |
str += "M96 S0\n"; | |
str += "G90\n"; // absolute | |
str += "G92 U0\n"; | |
str += "G0F" + jog.toFixed(2) + "\n"; // set jog velocity | |
str += "G1F" + velocity.toFixed(2) + "\n"; // set cut velocity | |
str += "G0Z0\n"; // raise Z | |
str += "G0X" + ox.toFixed(2) + "Y" + oy.toFixed(2) + "\n"; // move to origin | |
str += "G0Z" + (zFocusBase - thickness).toFixed(2) + "\n"; // lower Z | |
await sendCommand(str); | |
} | |
// --- End G-code sequence --- | |
async function sendEndGCode() { | |
let str = "G0Z0\n"; // raise Z | |
str += "G0X15Y15\n"; // move to origin | |
str += "G0 U0\n"; | |
str += "M6\n"; | |
str += "M97 S0\n"; | |
await sendCommand(str); | |
} | |
// Start Spiral button event | |
document.getElementById('startSpiralBtn').addEventListener('click', async () => { | |
await sendStartupGCode(); | |
spiralIndex = 0; | |
spiralPath = []; | |
readyToSend = true; | |
startSpiralTimer(); | |
}); | |
// End button event | |
document.getElementById('endBtn').addEventListener('click', sendEndGCode); | |
// --- Z Axis Jog Controls --- | |
// Persistent Z value | |
function getStoredZ() { | |
return parseFloat(localStorage.getItem('zValue')) || 0.00; | |
} | |
function setStoredZ(val) { | |
localStorage.setItem('zValue', val.toFixed(2)); | |
} | |
let zValue = getStoredZ(); | |
document.getElementById('zValue').textContent = zValue.toFixed(2); | |
document.getElementById('setZInput').value = zValue.toFixed(2); | |
document.getElementById('zUpBtn').addEventListener('click', async () => { | |
zValue += 0.5; | |
setStoredZ(zValue); | |
document.getElementById('zValue').textContent = zValue.toFixed(2); | |
document.getElementById('setZInput').value = zValue.toFixed(2); | |
let str = `G0Z${zValue.toFixed(2)}\n`; | |
await sendCommand(str); | |
log(`Sent: ${str.trim()}`); | |
}); | |
document.getElementById('zDownBtn').addEventListener('click', async () => { | |
zValue -= 0.5; | |
setStoredZ(zValue); | |
document.getElementById('zValue').textContent = zValue.toFixed(2); | |
document.getElementById('setZInput').value = zValue.toFixed(2); | |
let str = `G0Z${zValue.toFixed(2)}\n`; | |
await sendCommand(str); | |
log(`Sent: ${str.trim()}`); | |
}); | |
document.getElementById('setZBtn').addEventListener('click', async () => { | |
zValue = parseFloat(document.getElementById('setZInput').value); | |
setStoredZ(zValue); | |
document.getElementById('zValue').textContent = zValue.toFixed(2); | |
let str = `G0Z${zValue.toFixed(2)}\n`; | |
await sendCommand(str); | |
log(`Sent: ${str.trim()}`); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment