Spaces:
Running
Running
| <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> | |