cnc-gcode-auditor / index.html
BreyAIrev's picture
Launch CNC G-Code Auditor - Fanuc/Siemens/Heidenhain
b474310
Raw
History Blame Contribute Delete
19 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CNC G-Code Auditor β€” BreyAIrev</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0e1a;
--surface: #111827;
--surface2: #1a2235;
--border: #2a3550;
--text: #e8edf5;
--text-dim: #8892a8;
--accent: #3b82f6;
--accent2: #8b5cf6;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--gradient: linear-gradient(135deg, #3b82f6, #8b5cf6);
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.header {
background: linear-gradient(180deg, rgba(59,130,246,0.08) 0%, transparent 100%);
border-bottom: 1px solid var(--border);
padding: 1.5rem 2rem;
text-align: center;
}
.header h1 {
font-size: 1.8rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.3rem;
}
.header p {
color: var(--text-dim);
font-size: 0.9rem;
}
.badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 600;
margin: 0.3rem 0.15rem;
}
.badge-fanuc { background: rgba(239,68,68,0.15); color: #f87171; border: 1px solid rgba(239,68,68,0.3); }
.badge-siemens { background: rgba(59,130,246,0.15); color: #60a5fa; border: 1px solid rgba(59,130,246,0.3); }
.badge-heidenhain { background: rgba(16,185,129,0.15); color: #34d399; border: 1px solid rgba(16,185,129,0.3); }
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1.5rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 768px) {
.container { grid-template-columns: 1fr; }
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.panel-header {
padding: 0.8rem 1.2rem;
background: var(--surface2);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header h3 {
font-size: 0.85rem;
font-weight: 600;
color: var(--text);
}
.panel-body { padding: 1rem; }
textarea {
width: 100%;
min-height: 350px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
padding: 1rem;
resize: vertical;
outline: none;
line-height: 1.6;
transition: border-color 0.2s;
}
textarea:focus { border-color: var(--accent); }
.controls {
display: flex;
gap: 0.5rem;
margin-top: 0.8rem;
flex-wrap: wrap;
}
select {
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text);
padding: 0.5rem 0.8rem;
border-radius: 6px;
font-family: 'Inter', sans-serif;
font-size: 0.82rem;
cursor: pointer;
}
button {
padding: 0.55rem 1.5rem;
border: none;
border-radius: 6px;
font-family: 'Inter', sans-serif;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--gradient);
color: white;
flex: 1;
}
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-secondary {
background: var(--surface2);
color: var(--text-dim);
border: 1px solid var(--border);
}
.results {
min-height: 350px;
max-height: 500px;
overflow-y: auto;
}
.result-item {
display: flex;
gap: 0.6rem;
padding: 0.6rem 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
font-size: 0.82rem;
align-items: flex-start;
}
.result-icon {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
flex-shrink: 0;
margin-top: 2px;
}
.icon-error { background: rgba(239,68,68,0.2); color: #f87171; }
.icon-warn { background: rgba(245,158,11,0.2); color: #fbbf24; }
.icon-ok { background: rgba(16,185,129,0.2); color: #34d399; }
.icon-info { background: rgba(59,130,246,0.2); color: #60a5fa; }
.result-line { color: var(--text-dim); font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; }
.score-bar {
margin-top: 1rem;
padding: 1rem;
background: var(--surface2);
border-radius: 8px;
text-align: center;
}
.score-value {
font-size: 2.5rem;
font-weight: 700;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.score-label { color: var(--text-dim); font-size: 0.75rem; margin-top: 0.2rem; }
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-top: 0.8rem;
}
.stat {
text-align: center;
padding: 0.5rem;
background: var(--bg);
border-radius: 6px;
}
.stat-num { font-size: 1.1rem; font-weight: 700; }
.stat-label { font-size: 0.65rem; color: var(--text-dim); }
.stat-num.errors { color: #f87171; }
.stat-num.warnings { color: #fbbf24; }
.stat-num.good { color: #34d399; }
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: var(--text-dim);
}
.empty-state .icon { font-size: 3rem; margin-bottom: 1rem; opacity: 0.3; }
.empty-state p { font-size: 0.85rem; }
.footer {
text-align: center;
padding: 1.5rem;
color: var(--text-dim);
font-size: 0.75rem;
}
.footer a { color: var(--accent); text-decoration: none; }
</style>
</head>
<body>
<div class="header">
<h1>πŸ”§ CNC G-Code Auditor</h1>
<p>Professional G-code validation for Fanuc, Siemens & Heidenhain</p>
<div>
<span class="badge badge-fanuc">FANUC</span>
<span class="badge badge-siemens">SINUMERIK</span>
<span class="badge badge-heidenhain">HEIDENHAIN</span>
</div>
</div>
<div class="container">
<div class="panel">
<div class="panel-header">
<h3>πŸ“ G-Code Input</h3>
<span style="color:var(--text-dim); font-size:0.72rem;" id="lineCount">0 lines</span>
</div>
<div class="panel-body">
<textarea id="gcodeInput" placeholder="Paste your G-code here...
Example:
O0001
G90 G54
T1 M6
S2000 M3
G43 H1 Z50.
G81 X10. Y10. Z-5. R2. F200
G80
M5
M30"></textarea>
<div class="controls">
<select id="dialect">
<option value="fanuc">Fanuc</option>
<option value="siemens">Siemens SINUMERIK</option>
<option value="heidenhain">Heidenhain TNC</option>
</select>
<button class="btn-primary" onclick="auditCode()">⚑ Audit G-Code</button>
<button class="btn-secondary" onclick="loadExample()">πŸ“‹ Example</button>
</div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<h3>πŸ“Š Audit Results</h3>
</div>
<div class="panel-body">
<div id="results" class="results">
<div class="empty-state">
<div class="icon">πŸ”</div>
<p>Paste G-code and click Audit</p>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
Built by <a href="https://huggingface.co/BreyAIrev">BreyAIrev</a> β€”
Expert CNC & IoT datasets available on <a href="https://huggingface.co/BreyAIrev">HuggingFace</a>
</div>
<script>
const gcodeInput = document.getElementById('gcodeInput');
const resultsDiv = document.getElementById('results');
const lineCountEl = document.getElementById('lineCount');
gcodeInput.addEventListener('input', () => {
const lines = gcodeInput.value.split('\n').filter(l => l.trim()).length;
lineCountEl.textContent = `${lines} lines`;
});
function loadExample() {
const dialect = document.getElementById('dialect').value;
const examples = {
fanuc: `O0001 (POCKET MILLING)
G90 G54
T1 M6
S2000 M3
G43 H1 Z50.
G0 X0 Y0
G1 Z-5. F100
G1 X50. F300
G1 Y30.
G1 X0
G1 Y0
G0 Z50
G80
M5
G28 G91 Z0
M30`,
siemens: `; POCKET CYCLE
G90 G54
T1 D1
M6
S2000 M3
G0 X0 Y0 Z50
CYCLE81(50, 0, 1, -15)
G0 Z50
M5
M30`,
heidenhain: `0 BEGIN PGM POCKET MM
1 BLK FORM 0.1 Z X+0 Y+0 Z-30
2 BLK FORM 0.2 X+100 Y+80 Z+0
3 TOOL CALL 1 Z S2000
4 L Z+50 R0 FMAX M3
5 L X+50 Y+40 R0 FMAX
6 CYCL DEF 200 DRILLING
Q200=2 SET-UP CLEARANCE
Q201=-15 DEPTH
Q206=150 FEED RATE
7 CYCL CALL
8 L Z+50 R0 FMAX M5
9 END PGM POCKET MM`
};
gcodeInput.value = examples[dialect];
gcodeInput.dispatchEvent(new Event('input'));
}
function auditCode() {
const code = gcodeInput.value.trim();
if (!code) return;
const dialect = document.getElementById('dialect').value;
const lines = code.split('\n');
const findings = [];
let errors = 0, warnings = 0, good = 0, info = 0;
// === SAFETY CHECKS ===
const fullCode = code.toUpperCase();
// Spindle start
if (dialect === 'fanuc' || dialect === 'siemens') {
if (!fullCode.includes('M3') && !fullCode.includes('M4')) {
findings.push({ type: 'error', msg: 'Missing spindle start (M3/M4)', line: '-' });
errors++;
} else { good++; findings.push({ type: 'ok', msg: 'Spindle start command found', line: '-' }); }
}
// Spindle stop
if (!fullCode.includes('M5')) {
findings.push({ type: 'error', msg: 'Missing spindle stop (M5) β€” safety hazard!', line: '-' });
errors++;
} else { good++; }
// Program end
if (dialect === 'fanuc' && !fullCode.includes('M30') && !fullCode.includes('M2')) {
findings.push({ type: 'error', msg: 'Missing program end (M30/M2)', line: '-' });
errors++;
} else if (dialect === 'fanuc') { good++; findings.push({ type: 'ok', msg: 'Program end (M30) present', line: '-' }); }
// Work offset
if (dialect !== 'heidenhain') {
if (!fullCode.match(/G5[4-9]/)) {
findings.push({ type: 'warn', msg: 'No work offset (G54-G59) β€” machine may use wrong zero', line: '-' });
warnings++;
} else { good++; findings.push({ type: 'ok', msg: 'Work coordinate system defined', line: '-' }); }
}
// Tool call
if (dialect === 'fanuc' && !fullCode.includes('M6') && !fullCode.includes('M06')) {
findings.push({ type: 'warn', msg: 'No tool change (M6) detected', line: '-' });
warnings++;
}
// Tool length compensation
if (dialect === 'fanuc') {
if (!fullCode.includes('G43') && !fullCode.includes('G44')) {
findings.push({ type: 'warn', msg: 'No tool length compensation (G43/G44) β€” risk of crash', line: '-' });
warnings++;
} else { good++; findings.push({ type: 'ok', msg: 'Tool length compensation active', line: '-' }); }
}
// Canned cycle cancel
if (fullCode.match(/G8[1-9]/) && !fullCode.includes('G80')) {
findings.push({ type: 'error', msg: 'Canned cycle started but never cancelled (G80 missing)', line: '-' });
errors++;
}
// === LINE-BY-LINE CHECKS ===
let hasRapid = false;
let lastZ = null;
let feedRateSet = false;
lines.forEach((line, i) => {
const ln = line.trim().toUpperCase();
const lineNum = i + 1;
// Empty or comment
if (!ln || ln.startsWith('(') || ln.startsWith(';') || ln.startsWith('%')) return;
// Feed rate check
const feedMatch = ln.match(/F(\d+\.?\d*)/);
if (feedMatch) {
const feed = parseFloat(feedMatch[1]);
feedRateSet = true;
if (feed > 5000) {
findings.push({ type: 'warn', msg: `Very high feed rate F${feed} β€” verify for material`, line: lineNum });
warnings++;
}
if (feed < 1) {
findings.push({ type: 'warn', msg: `Extremely low feed rate F${feed}`, line: lineNum });
warnings++;
}
}
// G1 without feed
if (ln.includes('G1 ') || ln.includes('G01')) {
if (!feedRateSet && !ln.includes('F')) {
findings.push({ type: 'warn', msg: 'G1 linear move without feed rate set', line: lineNum });
warnings++;
}
}
// Spindle speed
const speedMatch = ln.match(/S(\d+)/);
if (speedMatch) {
const rpm = parseInt(speedMatch[1]);
if (rpm > 15000) {
findings.push({ type: 'info', msg: `High spindle speed S${rpm} β€” HSM operation`, line: lineNum });
info++;
}
if (rpm < 100 && rpm > 0) {
findings.push({ type: 'warn', msg: `Very low spindle speed S${rpm}`, line: lineNum });
warnings++;
}
}
// Rapid moves
if (ln.includes('G0 ') || ln.includes('G00')) {
hasRapid = true;
// Check rapid into material
const zMatch = ln.match(/Z(-?\d+\.?\d*)/);
if (zMatch && parseFloat(zMatch[1]) < 0) {
findings.push({ type: 'error', msg: `Rapid move (G0) to negative Z${zMatch[1]} β€” CRASH RISK!`, line: lineNum });
errors++;
}
}
// Absolute vs incremental
if (ln.includes('G91') && ln.includes('G90')) {
findings.push({ type: 'warn', msg: 'Both G90 and G91 on same line β€” ambiguous', line: lineNum });
warnings++;
}
// Duplicate M codes
const mCodes = ln.match(/M\d+/g);
if (mCodes && mCodes.length > 2) {
findings.push({ type: 'warn', msg: `Multiple M-codes on one line (${mCodes.join(', ')})`, line: lineNum });
warnings++;
}
});
// Safe retract
if (dialect === 'fanuc' && !fullCode.includes('G28') && !fullCode.includes('G30')) {
findings.push({ type: 'info', msg: 'No safe retract (G28/G30) β€” consider adding before M30', line: '-' });
info++;
}
// Coolant
if (!fullCode.includes('M8') && !fullCode.includes('M7')) {
findings.push({ type: 'info', msg: 'No coolant command (M7/M8) β€” dry machining?', line: '-' });
info++;
}
// === RENDER RESULTS ===
const score = Math.max(0, Math.min(100, 100 - (errors * 20) - (warnings * 5)));
let html = `
<div class="score-bar">
<div class="score-value">${score}/100</div>
<div class="score-label">${score >= 80 ? 'βœ… Good β€” minor improvements possible' : score >= 50 ? '⚠️ Needs attention' : '❌ Critical issues found'}</div>
</div>
<div class="stats">
<div class="stat"><div class="stat-num errors">${errors}</div><div class="stat-label">Errors</div></div>
<div class="stat"><div class="stat-num warnings">${warnings}</div><div class="stat-label">Warnings</div></div>
<div class="stat"><div class="stat-num good">${good}</div><div class="stat-label">Passed</div></div>
<div class="stat"><div class="stat-num" style="color:var(--accent)">${info}</div><div class="stat-label">Info</div></div>
</div>
`;
// Sort: errors first, then warnings, then info, then ok
const order = { error: 0, warn: 1, info: 2, ok: 3 };
findings.sort((a, b) => order[a.type] - order[b.type]);
findings.forEach(f => {
const iconClass = f.type === 'error' ? 'icon-error' : f.type === 'warn' ? 'icon-warn' : f.type === 'ok' ? 'icon-ok' : 'icon-info';
const icon = f.type === 'error' ? 'βœ•' : f.type === 'warn' ? '!' : f.type === 'ok' ? 'βœ“' : 'i';
html += `
<div class="result-item">
<div class="result-icon ${iconClass}">${icon}</div>
<div>
<div>${f.msg}</div>
${f.line !== '-' ? `<span class="result-line">Line ${f.line}</span>` : ''}
</div>
</div>
`;
});
resultsDiv.innerHTML = html;
}
</script>
</body>
</html>