Python interface for GQ GMC Geiger counters using the GQ-RFC1201 protocol
Project description
GQ Terminal
A Python library and command-line interface for GQ GMC Geiger counters, implementing the GQ-RFC1201 protocol. GQ publishes the protocol and encourages third-party software.
⚠️ Not a certified instrument. GQ GMC counters are hobbyist devices and this library is unaffiliated with GQ Electronics. Do not use values from this software for safety-, regulatory-, or medical-decision-making. The MIT license disclaims all warranty; the same applies to readings. See DISCLAIMERS.md for the full safety, hardware-coverage, and protocol-compatibility notes.
Installation
pip install gq-terminal
For the interactive terminal dashboard, install the optional tui extra
(pulls in Textual):
pip install 'gq-terminal[tui]'
Quick start
Talk to a counter from Python:
from gq_terminal import GMCInterface
with GMCInterface('/dev/ttyUSB0', baudrate=115200) as gmc:
print(gmc.get_version()) # e.g. "GMC-600 2.42"
print(gmc.get_serial_number()) # 7-byte serial as hex
print(f"{gmc.get_battery_voltage():.2f} V")
print(f"CPM: {gmc.get_cpm()}")
gmc.start_heartbeat()
for _ in range(10):
cps = gmc.read_heartbeat()
if cps is not None:
print(f"CPS: {cps}")
gmc.stop_heartbeat()
The context manager raises GMCError on connection failure; outside a with
block, call gmc.connect() and check the boolean return.
Or from the shell:
gq-terminal info --port /dev/ttyUSB0 # device info + current readings
gq-terminal monitor --port /dev/ttyUSB0 # live readings in the terminal
gq-terminal tui --port /dev/ttyUSB0 # interactive dashboard (needs the 'tui' extra)
--port is optional — omit it to auto-detect (see Port discovery).
The full set of subcommands is below.
Command line
gq-terminal ports # list serial ports; likely-GMC ones flagged
gq-terminal info --port /dev/ttyUSB0
gq-terminal info --port /dev/ttyUSB0 --verbose
gq-terminal monitor --port /dev/ttyUSB0 --duration 60
gq-terminal tui --port /dev/ttyUSB0 # interactive dashboard (needs the 'tui' extra)
gq-terminal log --port /dev/ttyUSB0 --interval 60 --output radiation.csv
gq-terminal config read --port /dev/ttyUSB0
gq-terminal history --port /dev/ttyUSB0 --address 0 --length 1024
gq-terminal set-time --port /dev/ttyUSB0 # sync device clock to the computer
gq-terminal raw 'GETVER' --port /dev/ttyUSB0 # send a raw protocol command
gq-terminal --help
gq-terminal monitor --help
You can also invoke the CLI as a module: python -m gq_terminal info ....
Terminal dashboard
gq-terminal tui opens a live, full-screen dashboard built on
Textual — large CPS, CPM, µSv/h, and
mR/h readouts, a sparkline graph over time, battery, device
temperature/clock/gyroscope (where supported), and running statistics
(sample count, average/max/min CPS, total counts). The layout reflows: it
scrolls on a short terminal and the graph grows to fill a tall one. Keys: the
update interval is adjustable with + / -, the graph toggles between CPS and
CPM with g, ? opens a glossary, a opens an about box, and q quits. It
requires the optional tui extra (pip install 'gq-terminal[tui]'); without
it the command prints an install hint and exits.
The dashboard is for live viewing; for durable, long-running capture use
gq-terminal log, which streams readings to a CSV file.
gq-terminal tui # auto-detect the port
gq-terminal tui --port /dev/ttyUSB0 --interval 0.5
gq-terminal tui --usv-per-cpm 0.0065 # supply a calibration for the dose readouts
Dose rate (µSv/h, mR/h)
The protocol has no "read dose rate" command — the counter derives µSv/h from
CPM using a tube-specific calibration. gq-terminal mirrors this: info,
monitor, and tui show µSv/h and mR/h only when a calibration is
available. By default it reads the calibration stored on the device; pass
--usv-per-cpm (µSv/h per CPM, e.g. 0.0065) to override it. In Python:
from gq_terminal import GMCInterface, Calibration
# Use the device's stored calibration:
with GMCInterface("/dev/ttyUSB0") as gmc:
print(gmc.get_dose_usv()) # µSv/h, or None if no calibration
print(gmc.get_dose_mr()) # mR/h
# Or supply your own calibration explicitly:
with GMCInterface("/dev/ttyUSB0", calibration=Calibration.from_factor(0.0065)) as gmc:
print(gmc.get_dose_usv())
⚠️ Dose rates are derived, not measured. They depend entirely on the calibration and the tube, and the device-config offsets used to read the stored calibration are reverse-engineered (verify with
tools/diagnose.pyagainst your counter's on-screen calibration menu). This is not a certified dose — see the safety disclaimer at the top.
Port discovery
--port is optional on every command. Omit it and the device is auto-detected:
the first serial port whose USB chip matches a GMC counter and that answers a
GETVER probe. If detection fails, the discovered ports are listed so you can
pass --port explicitly, and gq-terminal ports lists everything it sees.
gq-terminal info # auto-detect the port
The same discovery is available in Python via discover_ports() and
find_gmc_port():
from gq_terminal import GMCInterface, find_gmc_port
port = find_gmc_port() # None if no GMC is found
if port:
with GMCInterface(port) as gmc:
print(gmc.get_version())
Supported commands
Implements all 26 commands defined by GQ-RFC1201:
| Category | Methods |
|---|---|
| Basic | get_version, get_cpm, get_battery_voltage, get_serial_number |
| Dose rate (derived) | get_calibration, get_dose_usv, get_dose_mr (+ Calibration, parse_calibration) |
| Real-time | start_heartbeat, stop_heartbeat, read_heartbeat |
| Environment | get_temperature, get_gyroscope (GMC-320 Re.3.01+) |
| Real-time clock | get_datetime, set_datetime (GMC-280/300 Re.3.00+) |
| Memory | get_history_data, get_config, write_config, erase_config, update_config |
| Device control | send_key, power_off, power_on, reboot, factory_reset |
| Diagnostics | send_raw, wrap_command (exercise the protocol directly) |
Serial configuration
| Setting | Default |
|---|---|
| Baud rate | 115200 (use 57600 for GMC-300 V3.xx and earlier) |
| Data bits | 8 |
| Parity | None |
| Stop bits | 1 |
| Flow control | None |
Troubleshooting
Linux: Permission denied: /dev/ttyUSB0 — your user needs access to the
serial device. The portable fix is to add yourself to the dialout group
(sudo usermod -a -G dialout $USER) and log out / back in. Auto-detection
(gq-terminal info with no --port, or find_gmc_port()) hits the same
permission wall, so fix the group membership first.
gq-terminal ports lists my device but auto-detect doesn't find it — the
counter has to answer a GETVER probe to be auto-selected. Check the baud
rate (--baudrate 57600 for older GMC-300 firmware) and that nothing else
holds the port open; or just pass --port explicitly.
Short read: expected N bytes, got 0 — usually wrong baud rate (try 57600
for older firmware), wrong port, or the device is off. With heartbeat mode
running, do not interleave heartbeat reads with normal commands without first
calling stop_heartbeat().
Windows: which COM port? — open Device Manager → Ports (COM & LPT) with the device plugged in.
Development
This project uses uv for dependency management
and a Makefile for common tasks.
git clone https://github.com/jmcmeen/gq-terminal
cd gq-terminal
make dev # uv sync --extra dev
make test # run the pytest suite
make check # lint + typecheck + test (everything CI runs)
make build # build sdist + wheel into dist/
make help # list all targets
The test suite uses a fake serial backend, so no hardware is required.
If you don't have uv and don't want it, plain pip works too:
pip install -e ".[dev,tui]"
pytest
See CONTRIBUTING.md for conventions (protocol invariants, testing the wire bytes, style) before opening a PR.
Citing
If you use this software in research, please cite it. The concept DOI 10.5281/zenodo.20129915 always resolves to the latest release; each tagged release also gets its own version-specific DOI on the same Zenodo page.
BibTeX:
@software{mcmeen_gq_terminal,
author = {McMeen, John},
title = {GQ Terminal: a Python interface for GQ GMC Geiger counters},
year = {2026},
publisher = {Zenodo},
doi = {10.5281/zenodo.20129915},
url = {https://github.com/jmcmeen/gq-terminal},
orcid = {0009-0003-8141-567X}
}
A machine-readable CITATION.cff is included in the repository.
GitHub renders a "Cite this repository" button from it.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gq_terminal-0.2.0.tar.gz.
File metadata
- Download URL: gq_terminal-0.2.0.tar.gz
- Upload date:
- Size: 40.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4fd8d9cbb609c2b5fd444b6ca85fb815190c7c521c8ea5727ca2459587633a2
|
|
| MD5 |
ca7baccb3812cb1ef6d321593952cf11
|
|
| BLAKE2b-256 |
0fced0c8d8d8992b82d631c5566f0d8f21b462076acc49c93ad97ba16d3e90a8
|
Provenance
The following attestation bundles were made for gq_terminal-0.2.0.tar.gz:
Publisher:
publish.yml on jmcmeen/gq-terminal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gq_terminal-0.2.0.tar.gz -
Subject digest:
f4fd8d9cbb609c2b5fd444b6ca85fb815190c7c521c8ea5727ca2459587633a2 - Sigstore transparency entry: 1707354849
- Sigstore integration time:
-
Permalink:
jmcmeen/gq-terminal@963b47940e542609d9512bbb84e4837c0357ff28 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/jmcmeen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@963b47940e542609d9512bbb84e4837c0357ff28 -
Trigger Event:
release
-
Statement type:
File details
Details for the file gq_terminal-0.2.0-py3-none-any.whl.
File metadata
- Download URL: gq_terminal-0.2.0-py3-none-any.whl
- Upload date:
- Size: 27.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6a82c6f2e8a01c75b6d7de61b0a65efe750dca09dd3b3fab323d4fe2d3a6374
|
|
| MD5 |
806c84ef1c861cda608a02a03174f028
|
|
| BLAKE2b-256 |
dfa6b96d3ffacde78908d78be556b31583df96624bf92aca08a3bb34f4b1d8d2
|
Provenance
The following attestation bundles were made for gq_terminal-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on jmcmeen/gq-terminal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gq_terminal-0.2.0-py3-none-any.whl -
Subject digest:
f6a82c6f2e8a01c75b6d7de61b0a65efe750dca09dd3b3fab323d4fe2d3a6374 - Sigstore transparency entry: 1707354863
- Sigstore integration time:
-
Permalink:
jmcmeen/gq-terminal@963b47940e542609d9512bbb84e4837c0357ff28 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/jmcmeen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@963b47940e542609d9512bbb84e4837c0357ff28 -
Trigger Event:
release
-
Statement type: