"""Restricted G17/G90/G94 LinuxCNC-style G81/G82/G83 interpreter surrogate. This is NOT LinuxCNC, a firmware emulator, or a measured power simulator. Only supports the explicit subset generated by generate_drilling.py. Fails closed on unsupported G-codes, offset modes, coordinates or parameter combinations. For each canned cycle a source G-code block maps to multiple internal motions. """ from __future__ import annotations import math,re from dataclasses import dataclass WORDS=re.compile(r'([A-Z])\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))',re.I) PAREN=re.compile(r'\([^)]*\)|;[^\n]*') COMPONENTS=['electronics','axis_hold','axis_x','axis_y','axis_z','spindle_idle','cutting_load','coolant'] MATERIALS={'AL6061':(.00270,1.45),'C45':(.00785,3.70),'POM':(.00141,.70)} def motion_time(length,mm_min,accel): if length<1e-12:return 0. v=mm_min/60.;d=v*v/accel return 2*math.sqrt(length/accel) if length=0') op='G4';dwell(float(words['P'])) elif state.cycle and (('X' in words) or ('Y' in words) or (any(g in (81,82,83) for g in gs))): op=state.cycle # G-code cycling: X/Y modal, R and Z modal. G98 uses old Z; G99 uses R. xx=words.get('X',state.x);yy=words.get('Y',state.y) if 'R' in words:state.r=float(words['R']) if 'Z' in words:state.bottom=float(words['Z']) if 'Q' in words:state.q=float(words['Q']) if 'P' in words:state.p=float(words['P']) if state.r is None or state.bottom is None or state.bottom>=state.r or state.bottom>=ztop:raise ValueError('invalid R/Z in drilling cycle') if op=='G83' and (state.q is None or state.q<=0):raise ValueError('G83 requires Q>0') if op=='G82' and (state.p is None or state.p<0):raise ValueError('G82 requires P>=0') if not state.spindle:raise ValueError('Drilling without spindle') # XY traverse at current Z; lift to R if needed. if state.zstate.r:seg(state.z-state.r,'z',True);state.z=state.r target=state.bottom depth=ztop-target volume=math.pi/4*diam*diam*depth if op in ('G81','G82'): seg(state.r-target,'z',False,True);state.z=target if op=='G82':dwell(state.p) else: previous_depth=state.r while previous_depth>target+1e-9: next_z=max(target,previous_depth-state.q) if previous_depth==state.r: seg(state.r-next_z,'z',False,True) else: approach=min(state.r,previous_depth+clearance) seg(state.r-approach,'z',True) seg(approach-next_z,'z',False,True) previous_depth=next_z;pecks+=1 if previous_depth>target+1e-9: seg(state.r-previous_depth,'z',True) state.z=target end_z=state.old_z if state.retract=='G98' and state.old_z is not None and state.old_z>state.r else state.r seg(end_z-state.z,'z',True);state.z=end_z # Drill removes one cylinder per location; each hole is unique in this generated subset. comps['cutting_load']+=k*cutting_factor*volume/3600. elif state.cycle and ('Z' in words or 'R' in words): raise ValueError('Modal cycle must provide hole position') elif any(g in (0,1) for g in gs) or (any(a in words for a in 'XYZ') and state.cycle is None): op=state.motion x,y,z=(words.get('X',state.x),words.get('Y',state.y),words.get('Z',state.z)) seg(math.hypot(x-state.x,y-state.y),'x',state.motion=='G0') seg(abs(z-state.z),'z',state.motion=='G0') state.x,state.y,state.z=x,y,z if 'M3' in stripped or 'M4' in stripped:op='M3' if 'M5' in stripped:op='M5' if 'M8' in stripped:op='M8' if 'M9' in stripped:op='M9' if 'M2' in stripped:op='M2' result.append({'gcode':stripped,'command':op,'time_s':total_time,'energy_Wh':sum(comps.values()), 'material_removed_mm3':volume,'material_g':volume*rho,'energy_components':comps, 'motion_length_mm':length_total,'feed_motion_mm':feed_length, 'segments':segments,'peck_count':pecks,'dwell_s':cycle_dwell, 'spindle_rpm':state.rpm,'feed_mm_min':state.rate, 'tool_diameter_mm':diam,'depth_mm':(-state.bottom) if state.bottom is not None else 0., 'x_after_mm':state.x,'y_after_mm':state.y,'z_after_mm':state.z, 'spindle_on':int(state.spindle),'coolant_on':int(state.coolant), 'cycle_type':op if op in ('G81','G82','G83') else ''}) return result