Skip to main content

Thread-safe, fault-tolerant G-code streamer for GRBL controllers.

Project description

PyGrbl_Streamer 0.0.1

PyPI

Robust, source-agnostic G-code streamer for GRBL controllers over serial.

Part of the pygrbl family, a set of libraries to manage GRBL. Companion to pygrbl_build

v0.0.1 — Complete rewrite. The API is not compatible with previous internal versions and may change before 0.1.0.

Features

  • Stream from any source: lists, generators, files, network — stream() accepts any iterable of commands
  • Constant memory and instant start: files of any size are read lazily in a single pass — no preloading, no counting pass
  • Zero-cost progress: file progress is derived from bytes consumed vs file size, accurate to within a few commands
  • Character-counting streaming protocol against GRBL's 128-byte RX buffer
  • Clean connect/disconnect lifecycle — threads are joined, nothing hangs
  • Physical disconnection detection with automatic reconnect support
  • Real-time job control: pause, resume, stop
  • Event callbacks for progress, state changes, alarms, errors, raw I/O, and internal diagnostics
  • Every blocking wait is bounded by a timeout
  • Lightweight: runs multiple machines concurrently on a Raspberry Pi

Installation

pip install pygrbl-streamer

Requires Python 3.10+ and pyserial.

Quick start

from pygrbl_streamer import GrblStreamer

g = GrblStreamer(port='/dev/ttyUSB0')  # 'COM3' on Windows
g.progress_callback = lambda pct, cmd: print(f'{pct}%')

g.connect()
g.send_file('job.gcode')   # any size, constant memory, starts instantly
g.disconnect()

Streaming from any source

stream() consumes commands lazily from any iterable. Your application decides where the G-code comes from:

def square(size=10, power=300, feed=1000):
    yield 'G90 G21'
    yield f'M4 S{power}'
    yield f'G1 X{size} F{feed}'
    yield f'G1 Y{size}'
    yield 'G1 X0'
    yield 'G1 Y0'
    yield 'M5'

g.stream(square(), total=7)

Chain chunks back-to-back without stopping the machine between them:

g.stream(chunk_1, wait_for_idle=False)
g.stream(chunk_2, wait_for_idle=False)
g.stream(final_chunk)   # only the last chunk waits for Idle

Progress reporting

progress_callback(percent, command) fires on acknowledged commands. The percentage source, in order of precedence:

  1. Source-provided — if your iterable exposes a percent() method returning 0–100, it is the authority. send_file() uses this internally (bytes read vs file size).
  2. total — pass the command count to stream() for exact 0–100%.
  3. Heartbeat — with neither, the callback fires every 100 acked commands with percent=-1.

Job control

Streaming calls are blocking. Run them in a thread to control the job from elsewhere:

import threading

threading.Thread(target=g.send_file, args=('job.gcode',)).start()

g.pause()   # immediate feed hold (!)
g.resume()  # cycle start (~)
g.stop()    # abort: feed hold + soft reset

API overview

Method Description
connect() / disconnect() open/close the session; safe to call repeatedly
stream(commands, total=None, ...) stream any iterable of commands
send_file(path, ...) stream a file lazily; same options as stream()
command(cmd) send one command interactively, wait for ok/error
pause() / resume() / stop() real-time job control
unlock() / home() $X / $H
reconnect(retries, delay) retry loop after a physical disconnect

Callbacks

Assign as attributes or override in a subclass. All callbacks run on a dedicated thread and can never block serial communication. If one of your callbacks raises, the exception is reported through log_callback instead of being silently swallowed.

Callback Signature Fires on
progress_callback (percent, command) acknowledged command progress (-1 for unbounded streams)
state_callback (state) state machine transitions
alarm_callback (line) GRBL ALARM:n
error_callback (line) GRBL error:n or internal errors
send_callback / receive_callback (data) raw serial traffic
disconnect_callback (reason) physical disconnection
log_callback (level, message) internal diagnostics ('debug'/'info'/'warning')

Logging integration

The library imposes no logging framework. Wire the callbacks to Python's standard logging in your application:

import logging
log = logging.getLogger('laser1')

g.log_callback = lambda lv, m: getattr(log, lv)(m)
g.error_callback = lambda l: log.warning('GRBL error: %s', l)
g.alarm_callback = lambda l: log.error('ALARM: %s', l)
g.disconnect_callback = lambda r: log.critical('disconnected: %s', r)
g.receive_callback = lambda l: log.debug('<< %s', l)
g.send_callback = lambda d: log.debug('>> %s', d.strip())

States

DISCONNECTED → CONNECTING → IDLE ⇄ STREAMING ⇄ PAUSED, plus ALARM.

An alarm aborts the running job and is never cleared automatically — call unlock() explicitly. After stop(), machine position is untrusted: run home() before the next job.

Compatibility

Works with any GRBL 1.1 (or compatible, e.g. grblHAL) controller: diode laser engravers, CNC routers, pen plotters, drag-knife cutters.

Not supported: Ruida-based CO2 lasers, galvo fiber lasers (EZCad/BJJCZ controllers — entirely different protocol), and Marlin-based machines (no character-counting buffer or real-time commands).

I use this library daily in production, driving several lasers concurrently from a Raspberry Pi 4. Tested so far on:

  • Acmer P1S
  • Acmer P2
  • Longer Ray5 20W
  • AtomStack A24 Pro
  • AtomStack Atelier — a diode galvo running GRBL (unlike the gantry machines above)

The Atelier streams identically to the rest, with one current caveat: the machine ships locked and, for now, has to be connected once through LightBurn or lasergrbl to get unlocked before pygrbl_streamer can connect and drive it normally — the same unlock step lasergrbl performs on its own first connection. Reproducing that unlock handshake directly from the library is a work in progress.

Reports of it working (or not) on other machines are welcome via issues.

Safety notes

  • Laser users: verify $32=1 (laser mode) so the beam is disabled during feed hold.
  • Commands longer than GRBL's RX buffer (127 chars) are skipped with an error event instead of deadlocking the stream.
  • This library streams G-code; it does not validate it. Garbage in, garbage out.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pygrbl_streamer-0.1.0.tar.gz (17.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pygrbl_streamer-0.1.0-py3-none-any.whl (15.2 kB view details)

Uploaded Python 3

File details

Details for the file pygrbl_streamer-0.1.0.tar.gz.

File metadata

  • Download URL: pygrbl_streamer-0.1.0.tar.gz
  • Upload date:
  • Size: 17.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for pygrbl_streamer-0.1.0.tar.gz
Algorithm Hash digest
SHA256 32f0c37b33ae6e85698c98895d52c3c2a74e05917762f72840feb1f72bc1b0e7
MD5 322d99a80b99d10cb266a6a9b7966eeb
BLAKE2b-256 e32ec9937adca6a3261941b94081ec5f0440c48a0321a6e2794e6d18ed97eb58

See more details on using hashes here.

File details

Details for the file pygrbl_streamer-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pygrbl_streamer-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0fc7981c54604051065275d9f8c59ec2e1d346f196064c46f706934f3b8d9978
MD5 05724fc643a319c4fb2e23b347314d87
BLAKE2b-256 751aa3f9c5b0b42630dd65cce688558d32d0db4fe386c91855883356a9ffe019

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page