Created
October 11, 2025 19:20
-
-
Save Frank-Buss/167694087d29b3d14e832edd4674c3a5 to your computer and use it in GitHub Desktop.
Converts commas in a gcode nc file to dots for the decimal separator
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
#!/usr/bin/env python3 | |
""" | |
Proper G-code parser that fixes comma decimal separators. | |
G-code structure: | |
- Words: Letter followed by a number (e.g., G0, X123.456, Y-78.9) | |
- Comments: Text in parentheses () | |
- Lines can have multiple words, with or without spaces | |
""" | |
import re | |
import sys | |
from pathlib import Path | |
class GCodeParser: | |
"""Parser for G-code files.""" | |
def __init__(self): | |
# Pattern for G-code words: Letter followed by optional +/- and number | |
# Number can have comma or dot as decimal separator | |
self.word_pattern = re.compile(r'([A-Z])([+-]?[\d]+[,\.]?[\d]*)') | |
def parse_line(self, line): | |
""" | |
Parse a G-code line into components. | |
Returns: (commands, comment, original_structure) | |
- commands: list of (letter, number_string) tuples | |
- comment: comment string including parentheses, or None | |
- original_structure: to preserve spacing | |
""" | |
# Extract comment if present | |
comment_match = re.search(r'\([^)]*\)', line) | |
if comment_match: | |
comment = comment_match.group() | |
comment_start = comment_match.start() | |
comment_end = comment_match.end() | |
# Get the code before and after comment | |
code_part = line[:comment_start] + line[comment_end:] | |
else: | |
comment = None | |
code_part = line | |
# Parse the code part into words | |
commands = [] | |
last_end = 0 | |
for match in self.word_pattern.finditer(code_part): | |
letter = match.group(1) | |
number = match.group(2) | |
# Store the prefix (spaces, etc.) before this word | |
prefix = code_part[last_end:match.start()] | |
commands.append((prefix, letter, number)) | |
last_end = match.end() | |
# Get any trailing characters | |
trailing = code_part[last_end:] | |
return commands, comment, trailing | |
def fix_line(self, line): | |
"""Fix decimal separators in a G-code line.""" | |
# Handle empty lines | |
if not line.strip(): | |
return line | |
commands, comment, trailing = self.parse_line(line) | |
# Reconstruct the line with fixed decimals | |
result = "" | |
# Add commands with fixed numbers | |
for prefix, letter, number in commands: | |
result += prefix | |
result += letter | |
# Replace comma with dot in number | |
fixed_number = number.replace(',', '.') | |
result += fixed_number | |
result += trailing | |
# Add comment back if it exists (preserve as-is, don't modify comments) | |
if comment: | |
result += comment | |
return result | |
def fix_file(self, content): | |
"""Fix decimal separators in entire G-code file.""" | |
lines = content.split('\n') | |
fixed_lines = [self.fix_line(line) for line in lines] | |
return '\n'.join(fixed_lines) | |
def validate_line(self, line): | |
""" | |
Validate that a G-code line looks correct after conversion. | |
Returns (is_valid, message) | |
""" | |
# Skip empty lines and pure comments | |
stripped = line.strip() | |
if not stripped or (stripped.startswith('(') and stripped.endswith(')')): | |
return True, "" | |
# Check for common errors | |
if '..' in line: | |
return False, "Multiple consecutive dots" | |
# Check for letter followed immediately by dot (should have digit first) | |
if re.search(r'[A-Z]\.(?!\d)', line): | |
# Allow cases like "X.5" which is valid G-code | |
if not re.search(r'[A-Z]\.[0-9]', line): | |
return False, "Letter followed by dot without valid number" | |
# Check for numbers that still have commas (should all be converted) | |
if re.search(r'\d,\d', line): | |
return False, "Comma still present between digits" | |
return True, "" | |
def main(): | |
if len(sys.argv) < 2: | |
print("Usage: gcode_parser.py <input_file> [output_file]") | |
print("Properly parses and fixes comma decimal separators in G-code") | |
sys.exit(1) | |
input_file = Path(sys.argv[1]) | |
if not input_file.exists(): | |
print(f"Error: File '{input_file}' not found") | |
sys.exit(1) | |
# Determine output file | |
if len(sys.argv) >= 3: | |
output_file = Path(sys.argv[2]) | |
else: | |
output_file = input_file.with_suffix(input_file.suffix + '.fixed') | |
# Read input | |
print(f"Reading: {input_file}") | |
with open(input_file, 'r', encoding='utf-8') as f: | |
content = f.read() | |
# Parse and fix | |
parser = GCodeParser() | |
print("Parsing and fixing G-code...") | |
fixed_content = parser.fix_file(content) | |
# Validate | |
print("Validating output...") | |
errors = [] | |
for line_num, line in enumerate(fixed_content.split('\n'), 1): | |
is_valid, message = parser.validate_line(line) | |
if not is_valid: | |
errors.append(f"Line {line_num}: {message} - {line[:60]}") | |
if errors: | |
print("\nERROR: Validation failed!") | |
for error in errors[:5]: | |
print(f" {error}") | |
if len(errors) > 5: | |
print(f" ... and {len(errors) - 5} more errors") | |
print("\nNOT writing output file due to errors.") | |
sys.exit(1) | |
# Count changes | |
orig_commas = sum(1 for line in content.split('\n') | |
for match in re.finditer(r'\d,\d', line)) | |
fixed_commas = sum(1 for line in fixed_content.split('\n') | |
for match in re.finditer(r'\d,\d', line)) | |
changes = orig_commas - fixed_commas | |
# Write output | |
print(f"Writing: {output_file}") | |
with open(output_file, 'w', encoding='utf-8') as f: | |
f.write(fixed_content) | |
print(f"\n✓ Fixed {changes} decimal separators") | |
print("✓ Validation passed - file is safe to use") | |
# Show examples | |
print("\nExample conversions:") | |
orig_lines = content.split('\n') | |
fixed_lines = fixed_content.split('\n') | |
shown = 0 | |
for i, (orig, fixed) in enumerate(zip(orig_lines, fixed_lines), 1): | |
if orig != fixed and shown < 5: | |
print(f" Line {i}:") | |
print(f" Before: {orig}") | |
print(f" After: {fixed}") | |
shown += 1 | |
print("\nDone!") | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment