# Streaming
# Streaming from any source
stream() consumes commands lazily from any iterable: a list, a generator,
lines arriving from a network socket. The application decides where the G-code
comes from:
from pygrbl_streamer import GrblStreamer
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'
with GrblStreamer("/dev/ttyUSB0") as laser:
laser.stream(square(), total=7)
send_file(path, **kwargs) streams a file with the same options, reading it
line by line; it never loads the whole file into memory.
Chain chunks back-to-back without stopping the machine between them:
laser.stream(chunk_1, wait_for_idle=False)
laser.stream(chunk_2, wait_for_idle=False)
laser.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:
- Source-provided: if the iterable exposes a
percent()method returning 0 to 100, it is the authority.send_file()uses this internally (bytes read against file size). total: pass the command count tostream()for an exact 0 to 100%.- Heartbeat: with neither, the callback fires every 100 acknowledged
commands with
percent=-1.
# Flow control and completion
stream() strips comments and blank lines, and keeps track of commands
awaiting ok or error responses. It limits the bytes in flight to
rx_buffer_size - 1. Status reports are handled separately from
acknowledgements, and status is not polled while commands are in flight.
ack_timeout=30 bounds each acknowledgement wait. With the default
wait_for_idle=True, the streamer also polls for Idle after acknowledgements
have drained; completion_timeout=600 bounds that final wait. Acknowledgement
means the controller accepted a command, not that motion ended.
wait_for_idle=False is useful for consecutive chunks, but returning True
then does not confirm physical completion. The final chunk should wait for Idle.
Oversized commands produce an error event. By default they are skipped;
stop_on_error=True aborts on an oversized command and on errors received
while waiting for room in the receive buffer or draining the final
acknowledgements. It returns False without waiting for Idle or emitting the
completed progress event. Commands already buffered by the controller may
still run; this policy does not send a feed hold or reset. Applications should
also consume error_callback; with the default stop_on_error=False, the
boolean return alone does not prove that every command was accepted.
Invalid streaming state raises RuntimeError; stream failures can return
False. Python errors from a command source can propagate. Always close the
session in finally, and decide explicitly how to handle a failed job.
# Job control
Streaming is blocking. Run it in a worker thread if the application must remain responsive. While the worker streams, user actions can call:
laser.pause() # feed hold (!) while streaming
laser.resume() # cycle start (~) while paused
laser.stop() # abort: feed hold followed by soft reset
Use one job worker per connection. Do not run command(), home(), sync() or
another stream concurrently with that worker; acknowledgements have one owner.
Callbacks run on a separate dispatcher thread and should not take over the
connection. Pause, resume and stop are the intended concurrent controls.
A stop flushes the controller's buffers and leaves the position untrusted: establish position again before another job. A stopped job cannot be resumed (see Limitations).