Low level command system for stock lihuiyu controllers
lihuiyu plotter will be can be installed with pip
pip install lihuiyuplotter
- pyusb
The primary goal of this project is to make for easy interactions with the lihuiyu-controller board. Interactions that can be low-level enough to exactly allow the user to send exactly the data they want, or high level enough to allow the user to quickly implement their code and send it to the laser without needing to know anything about how that was done.
Currently, the M2 Nano, M3 Nano are supported as well as other boards produced by Lihuiyu-Studios and running LHYMICRO-GL code.
Like most laser cutters, there are two classes of commands: realtime and sequential. The realtime commands usually perform actions like pause, resume, abort. This differs from controller to controller but this same general distinction holds true. There are two different classes of commands: ones that should be executed sequentially in order and the ones that should be executed immediately.
There are two primary threads, main and spooler. The spooler thread will execute a series of jobs in sequential order. Realtime control over this execution cannot occur within that thread, since many of these commands take time to execute and the thread for executing the job cannot correctly modulate the execution of that job, while writing the job. Some jobs may also be infinite meaning that the code used to create them will not natively terminate, this also prevents us from pre-caching of the job (you can't cache an infinite job).
There are 4 primary components used with lihuiyuplotter: controller, connection, spooler, and job.
The controller serves as the bridge between your software and the lihuiyu controller board. It facilitates communication by translating internally used command codes into function calls. The controller houses all the realtime and sequential commands available for interaction. Sequential commands can be easily added to an active list until they are closed and sent to the laser.
The controller has three general states. init when the controller exists and things can be done with it. shutting down and shutdown when shutdown() is called all components should go ahead and stop what they are doing as quick as they can. This includes aborting any operations occurring in the laser (with an abort() command). If the laser should finish, rather than shutdown one of the wait_xx commands should be called.
The connection component provides a low-level interface for raw command communication. The primary commands are open(), close(), write() and read().
In most cases, the connection will connect automatically when we need to send data.
There are three primary connections, usb_connection which connects to the laser via usb (requires pyusb), tcp_connection which sends the data over TCP, and mock_connection which just pretends to connect to something but prints all the relevant debug data.
The connection has 5 primary states.
init: Connection is not opened. We have never connected.opening: Connection is establishing.opened: Connection is open. Laser has responded and appears correct.closed: Connection was opened, but has since been closed.aborted: Connection could not be established after reasonable attempts. Disconnect is required to clear the aborted state.
Unlike other parts of the system, the spooler is optional, and the spooler does not start automatically. It starts only when jobs are submitted. It continues until the queue has completed. Pausing will block the spooler from starting the next job, or re-entering the current job.
The spooler serves to help facilitate sequential interactions, and free up the current thread for manipulating the laser. While compact mode packets are the primary method of sending a series of sequential commands to the lihuiyu-controller, this does not cover all the potential workflows. Sometimes a series of small jobs is required, or an infinite job followed by another infinite job (each requiring an explicit cancel to be issued from the realtime thread), and a lot of other potential workflows not otherwise explicitly stated.
Jobs consist of a function to be called. This function should return True if the function was fully-processed. Otherwise, it will be executed repeatedly by the spooler until it returns True (which never happen). Between executions the spooler can be paused, aborted, or the job may be removed.
There are three laser configurations.
initial: We are only sending realtime commands and no sequential command lists are being sent.vector: We should be sending vector packets the laser should be ready to fire.raster: We should only be sending raster packets.
Note: To send the buffer written in the vector or raster configuration you must return to initial configuration.
There are context managers for the controller.vector() and controller.raster() commands. These are shortcuts for setting the controller.vector_configuration() and then restoring this to controller.initial_configuration() when finished. And likewise for the raster() command.
controller = LihuiyuController(settings_file="<my_settings>.json")
with controller.vector() as c:
c.goto(2000, 2000)
c.dwell(100)
controller.wait_for_machine_idle()This would, for example, fire the laser for 100ms, at position 2in 2in from the initial start location. During the use of the vector() context, our commands are executed in the controller.vector_configuration() it's restored to the initial_configuration on exit which will execute any list commands in the buffer.
The plotlike commands are used to send positional data to the laser.
.goto(x,y)move to a location with the laser off..line(x,y)firing the laser with the given parameters to the given location..quad(cx, cy, x, y)quad to the location x,y using control points cx, cy..cubic(c0x, c0y, c1x, c1y, x, y)cubic to the location x,y using control points c0x, c0y, c1x, c1y..arc(cx, cy, x, y)arc to the location x,y using control points cx, cy..dwell(time_in_ms)fires the laser at the current position for the time specified..wait(time_in_ms)waits without the laser firing at the current position for the time specified.
There are two different speed settings for these plotlikes. The marking values for line, quad, cubic and arc, use mark_speed, and goto uses the travel_speed which is either None for using the default rapid speed for the device, or a specific speed.
The reason that we accept line, quad, cubic, and arc is because these will be executed in pixel perfect algorithms. We do not linearize a quad into small lines but actually plot it directly and perfectly to the laser through the use of Zingl-Bresenham algorithms.
In many cases we want the current thread to block until some event has occurred.
wait_for_spooler_job_sent(job)blocks until the specified job is sent.wait_for_machine_idle()blocks until machine is idle, spooler must have fully sent, and laser must give a finished status.wait_for_spooler_send()blocks until all jobs in the spooler are sent (they may still be buffered in the laser)wait_finished()blocks until the controller is finished.
Note: if you sent an infinite job. And you call wait_for_spooler_job_sent() or wait_for_machine_idle() you may end up livelocking the main thread, as those states are unreachable. It may, however, terminate if the connection were broken.
See https://github.com/meerk40t/lihuiyuplotter/tree/main/examples for example scripts.
- Scorch - Did the initial work for reverse engineering the format for the laser.
- Paiin - Did considerable work in explaining how the controller boards worked, and huge amounts of testing.