Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 47 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ A web-based G-code visualizer and job management tool for pen plotters.
- Line color based on Z height (lower Z = darker color)
- Bounding box display around drawing area
- Manual jogging controls for X/Y/Z
- Job sending functionality (stub for now)
- Job sending functionality to GRBL-compatible plotters
- Current tool position shown as a red dot

## Setup and Installation
Expand All @@ -28,45 +28,82 @@ A web-based G-code visualizer and job management tool for pen plotters.
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Install the dependencies:
```
```bash
pip install -r requirements.txt
```
4. Run the application:
```
This will install Flask, pyserial, and other necessary packages.
4. Configure Plotter Connection (Optional):
If you intend to connect to a GRBL plotter, you might need to update the serial port and baud rate settings in `app.py`. See the "Plotter Backend > Configuration" section for details.
5. Run the application:
```bash
python app.py
```
5. Open your browser and navigate to `http://127.0.0.1:5000`
6. Open your browser and navigate to `http://127.0.0.1:5000` (or the host/port shown in the terminal, e.g., `http://0.0.0.0:5000`).

## Usage

- Paste G-code into the text area or upload a .gcode file
- Click "Visualize" to render the G-code on the canvas
- Use jogging controls to move the virtual tool
- Click "Send Job" to send the job to the plotter (currently just logs to console)
- Use jogging controls to move the virtual tool (visualization only)
- Click "Send Job" to send the G-code to your configured GRBL plotter.

## Project Structure

- `app.py` - Flask application entry point
- `app.py` - Flask application entry point, includes GRBL communication logic.
- `grbl_communicator.py` - Module for handling serial communication with GRBL devices.
- `requirements.txt` - Python package dependencies.
- `templates/` - HTML templates
- `static/` - Static assets
- `css/style.css` - Main stylesheet
- `js/gcode-parser.js` - G-code parsing logic
- `js/gcode-renderer.js` - Canvas rendering logic
- `js/visualizer.js` - Main application logic
- `test_grbl_communicator.py` - Unit tests for `grbl_communicator.py`.

## Technical Details

- Built as a Flask web application
- Frontend uses HTML5 Canvas for visualization
- Backend uses `pyserial` for GRBL communication
- Modular JavaScript architecture:
- GCodeParser class for parsing G-code
- GCodeRenderer class for canvas rendering
- Modern JavaScript (ES6+)
- Responsive design

## Plotter Backend

This application now supports sending G-code jobs directly to GRBL-compatible pen plotters. The backend uses the `pyserial` library to communicate with the plotter over a serial connection, managed by the `GRBLCommunicator` class found in `grbl_communicator.py`.

### Configuration

To connect to your plotter, you may need to configure the serial port and baud rate:

- **Location**: These settings are defined as constants at the top of the `app.py` file:
- `DEFAULT_SERIAL_PORT`: The serial port your plotter is connected to.
- `DEFAULT_BAUDRATE`: The baud rate your plotter uses for communication.
- **Default Values**:
- `DEFAULT_SERIAL_PORT = "/dev/ttyUSB0"`
- `DEFAULT_BAUDRATE = 115200`
- **Finding Your Serial Port**:
- **Linux**: Check common device paths like `/dev/ttyUSB*` or `/dev/ttyACM*`. You can use the command `dmesg | grep -i "ttyS\\|ttyUSB\\|ttyACM"` (run as root or with sudo if needed) shortly after connecting your device to see which port it's assigned.
- **Windows**: Open Device Manager (search for "Device Manager" in the Start Menu). Look under "Ports (COM & LPT)". Your plotter will likely appear as a "USB Serial Port" or similar, listed with a `COMx` number (e.g., `COM3`).
- **macOS**: Check devices starting with `/dev/tty.*`. In the terminal, run `ls /dev/tty.*`. Common names include `/dev/tty.usbmodemXXXX` or `/dev/tty.usbserial-XXXX`.

### Dependencies

Serial communication with the plotter requires the `pyserial` library. This library is listed in the `requirements.txt` file.

To ensure all dependencies, including `pyserial`, are installed, run the following command in your activated virtual environment (as mentioned in the "Setup and Installation" section):
```bash
pip install -r requirements.txt
```

## Future Enhancements

- Real plotter communication via serial port
- Real-time job progress and status feedback from the plotter
- Job queue management
- G-code generation from SVG files
- More advanced visualization features
- More advanced visualization features (e.g., toolpath simulation)
- Emergency stop / pause / resume functionality for plotter jobs
- Web-based configuration for serial port/baud rate
Binary file added __pycache__/grbl_communicator.cpython-310.pyc
Binary file not shown.
Binary file not shown.
67 changes: 59 additions & 8 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
from flask import Flask, render_template, request, jsonify
import os
import logging
from grbl_communicator import GRBLCommunicator

# Configure basic logging for the app
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Default GRBL connection parameters
DEFAULT_SERIAL_PORT = "/dev/ttyUSB0" # Common for Linux, use "COMx" for Windows if needed
DEFAULT_BAUDRATE = 115200

app = Flask(__name__)

Expand All @@ -9,14 +18,56 @@ def index():

@app.route('/api/send_job', methods=['POST'])
def send_job():
# This is a stub function that would send the job to the plotter
gcode = request.json.get('gcode', '')
# For now, just return success and the first few lines for confirmation
return jsonify({
'status': 'success',
'message': 'Job sent to plotter',
'preview': '\n'.join(gcode.split('\n')[:5]) + '...' if gcode else 'No G-code provided'
})

if not gcode:
logging.warning("send_job called with no G-code.")
return jsonify({'status': 'error', 'message': 'No G-code provided.'}), 400

logging.info(f"Received G-code job. First 50 chars: {gcode[:50]}...")

communicator = None # Initialize communicator to None for the finally block
try:
communicator = GRBLCommunicator(port=DEFAULT_SERIAL_PORT, baudrate=DEFAULT_BAUDRATE)
logging.info(f"Attempting to connect to GRBL on {DEFAULT_SERIAL_PORT} at {DEFAULT_BAUDRATE} baud.")
communicator.connect()
logging.info("Successfully connected to GRBL.")

gcode_lines = gcode.strip().split('\n')
total_lines = len(gcode_lines)
logging.info(f"Starting to send {total_lines} G-code lines.")

for i, line in enumerate(gcode_lines):
line = line.strip()
if not line or line.startswith(';') or line.startswith('('): # Ignore empty lines and comments
logging.info(f"Skipping empty line or comment: {line}")
continue

logging.info(f"Sending line {i+1}/{total_lines}: {line}")
communicator.send_command(line)
logging.info(f"Successfully sent line: {line}")

logging.info("All G-code commands sent successfully.")
return jsonify({'status': 'success', 'message': 'Job sent to plotter successfully.'})

except ConnectionError as e:
logging.error(f"Connection failed: {e}")
return jsonify({'status': 'error', 'message': f'Connection failed: {str(e)}'}), 500
except RuntimeError as e: # GRBL reported error
logging.error(f"GRBL error: {e}")
return jsonify({'status': 'error', 'message': f'GRBL error: {str(e)}'}), 400
except TimeoutError as e:
logging.error(f"GRBL timeout: {e}")
return jsonify({'status': 'error', 'message': f'GRBL timeout: {str(e)}'}), 400
except Exception as e: # Catch any other unexpected errors
logging.error(f"An unexpected error occurred: {e}", exc_info=True) # Log stack trace
return jsonify({'status': 'error', 'message': f'An unexpected error occurred: {str(e)}'}), 500
finally:
if communicator:
logging.info("Closing GRBL connection.")
communicator.close()

if __name__ == '__main__':
app.run(debug=True)
# Use host='0.0.0.0' to make it accessible from the network if needed for testing
# debug=True is useful for development but should be False in production
app.run(host='0.0.0.0', port=5000, debug=True)
170 changes: 170 additions & 0 deletions grbl_communicator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import serial
import time
import logging

# Configure basic logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class GRBLCommunicator:
def __init__(self, port, baudrate):
self.port = port
self.baudrate = baudrate
self.ser = None
logging.info(f"GRBLCommunicator initialized with port={self.port}, baudrate={self.baudrate}")

def connect(self):
logging.info(f"Attempting to connect to GRBL on {self.port} at {self.baudrate} baud.")
try:
self.ser = serial.Serial(self.port, self.baudrate, timeout=2)
logging.info("Serial port opened. Waiting for GRBL to initialize...")
time.sleep(2) # Wait for GRBL to initialize

# Wake up GRBL and clear buffer
# A soft reset (Ctrl-X) or a newline can be used.
# Ctrl-X is often \x18 in bytes.
# Sending a newline might be safer initially.
self.ser.flushInput() # Clear input buffer before sending commands

# Sending a newline character to ensure GRBL is awake and to clear any partial commands.
self.ser.write(b'\n')
# Alternative: self.ser.write(b'\x18') # Soft reset

# Read and discard startup messages.
# GRBL usually sends a welcome message like "Grbl 1.1h ['$' for help]"
# We'll read for a short period or until no more data comes.
startup_message = ""
start_time = time.time()
while time.time() - start_time < 2: # Read for up to 2 seconds
if self.ser.in_waiting > 0:
line = self.ser.readline().decode('utf-8', errors='ignore').strip()
if line:
startup_message += line + "\n"
logging.info(f"GRBL Startup: {line}")
else:
time.sleep(0.05) # Small delay to avoid busy waiting

if not startup_message:
logging.warning("No startup message received from GRBL. This might be okay.")

# A more robust check would be to send a status command '?' and expect 'ok'
# For now, we assume connection is successful if no serial exception occurred.
logging.info("GRBL connection established.")

except serial.SerialException as e:
logging.error(f"Failed to connect to GRBL: {e}")
self.ser = None # Ensure ser is None if connection failed
raise ConnectionError(f"Failed to connect to GRBL on {self.port}: {e}")
except Exception as e: # Catch any other unexpected errors during connection
logging.error(f"An unexpected error occurred during connection: {e}")
if self.ser and self.ser.is_open:
self.ser.close()
self.ser = None
raise ConnectionError(f"An unexpected error occurred on {self.port}: {e}")


def send_command(self, gcode_line, command_timeout=10.0):
if not self.ser or not self.ser.is_open:
logging.error("Serial port not open. Cannot send command.")
raise ConnectionError("Serial port not open. Connect first.")

try:
command = gcode_line.strip() + '\n'
logging.info(f"Sending G-code command: {command.strip()}")
self.ser.write(command.encode('utf-8'))

response_buffer = ""
start_time = time.time()

while time.time() - start_time < command_timeout:
if self.ser.in_waiting > 0:
char = self.ser.read().decode('utf-8', errors='ignore')
response_buffer += char
if response_buffer.endswith('ok\r\n'):
logging.info(f"Received 'ok' for command: {command.strip()}")
return True
if response_buffer.strip().startswith('error:'):
# Try to read the rest of the error line until a newline
# or timeout to capture the full error message.
while not response_buffer.endswith('\n') and (time.time() - start_time < command_timeout):
if self.ser.in_waiting > 0:
char_more = self.ser.read().decode('utf-8', errors='ignore')
response_buffer += char_more
else:
time.sleep(0.005) # Shorter sleep while actively fetching rest of error

error_message = response_buffer.strip()
logging.error(f"GRBL error for command '{command.strip()}': {error_message}")
raise RuntimeError(f"GRBL error: {error_message}")
else:
time.sleep(0.01) # Short sleep to prevent busy-waiting

logging.error(f"Timeout waiting for response to command: {command.strip()}")
raise TimeoutError(f"Timeout waiting for 'ok' or 'error' from GRBL for command: {command.strip()}")

except serial.SerialException as e:
logging.error(f"Serial communication error during send_command: {e}")
self.close() # Attempt to close port on error
raise ConnectionError(f"Serial communication error: {e}")
except RuntimeError: # Re-raise RuntimeError from GRBL error
raise
except TimeoutError: # Re-raise TimeoutError
raise
except Exception as e: # Catch any other unexpected errors
logging.error(f"An unexpected error occurred during send_command: {e}")
self.close()
raise RuntimeError(f"An unexpected error occurred sending command: {e}")

def close(self):
if self.ser and self.ser.is_open:
try:
self.ser.close()
logging.info("Serial port closed.")
except serial.SerialException as e:
logging.error(f"Error closing serial port: {e}")
else:
logging.info("Serial port was not open or already closed.")
self.ser = None

if __name__ == '__main__':
# This is example usage, not part of the class itself.
# It's good for basic testing if you have a GRBL device connected.
# For actual use, this would be imported into another script.

# Replace with your actual port and baudrate
# On Linux, it might be /dev/ttyUSB0 or /dev/ttyACM0
# On Windows, it might be COM3, COM4, etc.
GRBL_PORT = "/dev/ttyUSB0" # Example, change this!
GRBL_BAUDRATE = 115200

# Example of how to use the GRBLCommunicator
# Note: This will try to connect to a real device.
# If you don't have one, this part will fail.

# communicator = None
# try:
# communicator = GRBLCommunicator(port=GRBL_PORT, baudrate=GRBL_BAUDRATE)
# communicator.connect()

# # Example commands
# # Ensure GRBL is in a known state (e.g., by homing or unlocking if needed)
# # For testing, simple status commands are safest.
# if communicator.send_command("$#"): # View G-code parameters
# print("Successfully sent '$#' and got ok")

# if communicator.send_command("G0 X1 F100"): # Simple move command
# print("Successfully sent 'G0 X1 F100' and got ok")

# except ConnectionError as e:
# print(f"Connection Error: {e}")
# except RuntimeError as e:
# print(f"Runtime Error: {e}")
# except TimeoutError as e:
# print(f"Timeout Error: {e}")
# except Exception as e:
# print(f"An unexpected error occurred: {e}")
# finally:
# if communicator:
# communicator.close()

print("GRBLCommunicator class defined. Example usage (commented out) requires a GRBL device.")
print("To use this class, import it into your main application script.")
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
flask==2.3.3
flask==2.3.3
pyserial
Loading