Skip to content

Instantly share code, notes, and snippets.

@CharStiles
Created June 17, 2025 20:12
Show Gist options
  • Save CharStiles/4ea96386f230075650b8713d82f6a104 to your computer and use it in GitHub Desktop.
Save CharStiles/4ea96386f230075650b8713d82f6a104 to your computer and use it in GitHub Desktop.
<!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