Skip to content
chamnitPublic

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

67 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gcgo

An interactive GRBL g-code streamer for the terminal. Streams using GRBL's character-counting (buffer-fill) protocol to keep the planner fed on fast jobs, with a live status line, real-time overrides, an MDI mode, and configurable display — all in a single small package with one dependency (pyserial).

Install

The recommended way is pipx (or uv tool), which installs into an isolated environment and puts a gcgo command on your PATH — this also avoids the "externally-managed-environment" error on Raspberry Pi OS and other recent distros:

pipx install git+https://github.com/chamnit/gcgo
# or, once published to PyPI:
pipx install gcgo

From source (development):

git clone https://github.com/chamnit/gcgo
cd gcgo
python -m gcgo /dev/ttyACM0      # or: pip install -e .

Linux serial permissions: you must be in the dialout group to open the port, otherwise you'll get a permission error:

sudo usermod -aG dialout $USER   # then log out and back in

Usage

gcgo                 # auto-detect the port (prompts if there's more than one)
gcgo /dev/ttyACM0    # specify the port
gcgo -b 115200 ...   # override the baud rate (default 115200)

At the gcgo> prompt:

load <file>   Load a g-code file
run           Stream the loaded file
mdi           Enter MDI mode (send g-code/$-commands directly to GRBL)
settings      Show GRBL $$ settings in readable form
              (caPy: settings GROUP for one group, settings all for every setting)
params        Show GRBL $# coordinate parameters
unlock        Unlock GRBL alarm state ($X)
home          Run homing cycle ($H)
check         Toggle check mode ($C)
config        Configure status fields, poll rate, units, and stream keys
reset         Soft-reset GRBL (Ctrl-X); restores overrides to 100%
status        Query GRBL status (?)
ports         List available serial ports
ls / cd       List files / change directory
help          Show the command list
quit / exit   Exit

Tab completes commands and file paths. Command history persists between runs.

Streaming

load a file, then run. While streaming, a live status line at the bottom shows state, position, feed, spindle, overrides, and pins, while sent g-code and GRBL messages scroll above. Single keypresses send real-time commands — by default:

!  feed hold      ~  cycle start / resume
+  feed +10%      -  feed -10%      0  feed reset 100%      q  stop

q (or Ctrl-C) performs a true stop: it feed-holds, then soft-resets to halt motion and flush GRBL's buffers — stopping only the stream would leave GRBL running its already-buffered moves.

Configuration

config manages everything, persisted to ~/.config/gcgo/config.json (~/Library/Application Support/gcgo/ on macOS):

config                      Show all settings
config fields <name> on|off Toggle a status-line field
config rate <seconds>       Status query interval (0 disables)
config units <mm|inch>      Display units for positions and feed
config after <keep|clear>   Keep or unload the file after a completed run
config keys <action> <char> Bind a streaming real-time key (or 'off' to disable)

Units note: gcgo keeps the controller reporting in mm (it sends $13=0 on connect and re-asserts it if you change $13 in MDI) and converts to the configured display units itself, so displayed values always match their labels. This also works on firmware with no $13, such as caPy, which reports mm only.

Architecture

gcgo is split so the GRBL "brain" is shared across platforms:

  • gcgo/core/ — platform-agnostic logic (protocol/streaming pump, status, config, gcode, clock). No platform imports; runs on CPython and MicroPython.
  • gcgo/ports/ — adapter contracts (e.g. the byte-level Transport).
  • gcgo/desktop/ — CPython adapters: pyserial transport, terminal display, termios/readline keyboard, paths.
  • gcgo/frontends/ — front-ends that drive the core (terminal today).
  • gcgo/micropython/ — machine.UART transport + a minimal serial-console front-end.

Streaming is a single-threaded non-blocking pump(), so the same core drops into a desktop loop or an MCU loop unchanged.

Jogging

Continuous jogging uses caPy's jog mode (bare $J, then U/V/S/T lines; see caPy's doc/jog/README.md), or short $J= lines on GRBL (below). gcgo/core/jog.py's JogSession is the only code that talks to the controller; input devices never write to the wire. An input module reads its device and calls one of:

jog.vel("gamepad", vx, vy, vz)    # mm/s, every time the device is read
jog.step("Z", 0.1, feed=300)      # handwheel detents, step buttons
jog.target(x=0, y=0, feed=3000)   # go-to (machine coordinates)
jog.target(x=px, vx=pv)           # a moving target (follower), every reading
jog.release("gamepad")            # device gone

and the driver loop calls jog.tick() each pass. The session enters jog mode when asked (begin()) and leaves on end(). It sends only the newest velocity, as a U line naming every moving axis, and repeats it inside caPy's per-axis dead-man (read from $jog.keepalive and $jog.cover on entry). So a stick never disturbs a step or target on another axis: a stick on X/Y and a handwheel on Z run together. Each input owns the axes it pushes, and an input or moving target that stops refreshing is zeroed after its TTL. On a caPy without U (before 907d191) it falls back to V, and steps and targets wait until no velocity is live.

On GRBL, which has no jog mode, the bare $J is answered error:3 and the session switches itself to short $J= lines; the inputs don't change:

  • a velocity goes out as $J=G91 blocks of 50 ms of motion each, with at most 200 ms queued, so GRBL can only reach the speed it can stop within that queue (roughly 0.4 × acceleration, in mm/s)
  • letting go, an axis reversing, or an input that goes quiet sends 0x85, which brakes and flushes GRBL's planner
  • steps are $J=G91 lines, and targets (moving ones included) are $J=G53 go-tos to the newest position, paced like a velocity
  • GRBL accepts and then discards a $J= that arrives while a cancel is braking, so after a 0x85 nothing goes out until fresh status reports show it out of Jog; a line still unanswered when the 0x85 went out gets a second 0x85 once it is answered
  • R (rate) scales speeds in gcgo, and C (coordinated) doesn't apply

Following. gcgo/core/follow.py's Follower makes the machine follow a point that an input moves:

f = Follower(jog)                  # axes="XY" by default
f.start(status.mpos)               # take hold: offsets are from here
f.move(dx, dy, feed=1000)          # every reading; it is also the heartbeat
f.end(feed=1000)                   # let go: a last go-to to the final point

It estimates the point's velocity from the readings (smoothed over 50 ms, on the input's own clock when the reading carries one) and sends moving targets, so caPy extrapolates between readings instead of chasing the last one. A point at rest becomes a single go-to; a follower that stops reading is braked by the moving target's TTL.

gcgo/core/jogmap.py holds the shared feel: stick_to_velocity (radial deadband, square-law magnitude, direction kept) and a shuttle-ring table. The web UI has two jog inputs:

  • Virtual sticks (X/Y and Z): speed grows with the drag, up to FEED; let go to stop.
  • Follow pad: press and drag, and the machine follows the pointer in X/Y (a Follower), up to FEED, and stops where you let go. The pad edge is 10 × STEP mm from where the drag began, and a ring shows where the machine is.

The first drag enters jog mode, and the server leaves jog mode after 3 s at rest or when MDI or a run needs the machine. Both work on caPy and GRBL.

Gamepad (Linux, desktop web server): python -m gcgo.desktop.webmain <port> --gamepad [DEV] [--gamepad-feed 3000]. The bindings are in gcgo/core/gamepad.py (GamepadJog, no I/O), and the reader is in gcgo/desktop/gamepad_linux.py (the kernel joystick API, standard library only, replug-safe). Xbox-style layout:

control does
left stick X/Y velocity: direction kept, speed by deflection (square law)
right stick Z velocity, at half the X/Y feed
d-pad / Y, A step X/Y / Z+, Z- by the step size
X cycle the step size (0.01, 0.1, 1, 10 mm)
LB / RB slower / faster speed range (5, 20, 50, 100 % of the feed)
B stop: brake and leave jog mode (the sticks need centring before they jog again)

Local UI wheel (OLED pendant): each encoder detent is a jog.step(), on caPy and GRBL alike. Detents add up and land exactly, and a wheel spun faster than the axis can follow drops detents once the axis is about 2 detents (at least 2 mm) behind, so the axis stops when the hand stops.

Every input has the same shape: an object whose poll(jog) is called on every driver pass (serve(..., inputs=[...])). A different device needs only a reader that returns the same snapshot (axes, buttons, pressed).

Keeping in step with caPy

caPy publishes its wire definitions as CSV (doc/csv in the caPy repo). After pulling caPy, run

python3 tools/sync_capy.py            # reads ../caPy/doc/csv

It regenerates the error/alarm text gcgo shows (gcgo/core/codes.py for the terminal, static/codes.json for the web console) and what each setting is (static/settings.json: caPy's kind, range, default and meaning, and GRBL's $N names, for the web settings editor, which reads the values themselves from the controller), and fails, naming them, if caPy now sends a machine state or status-report field gcgo doesn't handle.

Tests

python3 -m pytest

The tests run the real Streamer over a fake serial port, with a clock the tests advance by hand, so they need no hardware and don't sleep. The jog tests cover both caPy's jog mode and the GRBL fallback. The Linux gamepad reader tests use a virtual pad through /dev/uinput and skip themselves where that isn't writable.

MicroPython (experimental)

gcgo's core runs on MicroPython 1.2x. A board can act as a standalone GRBL sender: it talks to the GRBL controller over a UART and gives you a serial console.

Deploy and run (using mpremote):

mpremote connect /dev/ttyACM0 cp -r gcgo :        # copy the package to the board
mpremote connect /dev/ttyACM0 exec "from gcgo.micropython.main import start; start(uart_id=1, baud=115200)"

uart_id/pins are board-specific — pass what your board needs (e.g. start(1, tx=4, rx=5) on many ESP32s). Wire the board's UART TX/RX/GND to the GRBL controller's serial pins.

G-code is streamed from a file on demand (nothing is loaded into RAM), so file size isn't limited by memory — put jobs on an SD card or flash.

Headless web pendant (Pico W)

A WiFi board can serve a browser UI with no display or keyboard: live DRO, jog pad with selectable step/feed, work-zero, all feed/rapid/spindle overrides and coolant toggles, a sent/received console with MDI, file management (browse, make folders, upload, download, run/stop, delete), and a separate settings page (units, poll rate, after-run, which override groups to show, and an editor for the controller's own settings: caPy's $settings, grouped, with dropdowns for choices and ranges checked before sending, or GRBL's $$; caPy changes are kept with Save to flash, $save). The whole stack is a dependency-free uasyncio HTTP + WebSocket server sharing the same core. Verified on a Raspberry Pi Pico W with GRBL on UART0 (GP16/17) and a hardware reset line on GP18, through level shifters:

mpremote connect /dev/ttyACM0 cp -r gcgo :
# put WIFI_SSID / WIFI_PASS in a secrets.py on the board, then:
mpremote connect /dev/ttyACM0 exec "from gcgo.micropython.webmain import start; start()"

It prints the board's IP; open http://<board-ip>/. The desktop build serves the identical UI via python -m gcgo.desktop.webmain <port>.

Wiring note: on an ATmega Arduino, the USB port and pins 0/1 are the same UART — don't drive the board from USB and the Pico's UART at once (bus contention). Use one at a time.

Boards: developed and verified on the Pico W (RP2040). The Pico 2 W (RP2350) is pin-compatible and uses the same CYW43439 WiFi chip, so the wiring and the deploy steps above are unchanged — just flash the Pico 2 W MicroPython build. It has more SRAM (≈520 KB vs 264 KB), so even more headroom. The code uses no pulled-down GPIO inputs, so the RP2350 input-pulldown erratum (E9) doesn't apply to this wiring. Any MicroPython board with WiFi, a UART, and asyncio (e.g. ESP32) should work; pass board-specific UART/pins to start().

Requirements

  • Python 3.11+ and pyserial (desktop), or MicroPython 1.2x (board)
  • For the web pendant: a WiFi MicroPython board (Pico W or Pico 2 W; ESP32 also works)
  • A GRBL 1.1 controller (e.g. an Arduino running GRBL) or a GRBL-compatible one such as caPy

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages