Read weight, price and status from DIGI scales over RS-232.
Project description
PyDIGI — read DIGI scales from Python
pydigi reads weight, price and status from DIGI retail scales over RS-232.
- Supported models: DIGI DS-781 (Type B / Standard command protocol).
- Python: 3.8+ (see DESIGN.md §8 for why 2.x is out of scope).
- Dependency: pyserial (only for real ports).
- Layered so more models, protocol types, and transports slot in without rewrites — see Extending and DESIGN.md.
Install
pip install pydigi
That pulls in pyserial. For development — running the tests or the hardware-in-the-loop tooling — install from a checkout with the extras:
pip install -e '.[test]' # + pytest, PyYAML (run the suite)
pip install -e '.[hil]' # + PyYAML (the HIL recorder)
Quick start
from pydigi import DigiDS781
with DigiDS781.open("/dev/ttyUSB0") as scale: # COM3 on Windows
r = scale.read()
print(r.weight_net_kg, "kg", "stable" if r.is_stable else "moving")
DigiDS781.open(port, baudrate=..., parity=..., stopbits=..., timeout=..., rtscts=...) — extra keywords pass through to the serial config. open()
returns a ready scale and is also a context manager.
What a reading contains
scale.read() returns an immutable ScaleReading:
| Field | Meaning |
|---|---|
weight_net_kg / weight_tare_kg |
net (post-tare) and stored tare weight |
weight_gross_kg |
derived net + tare (always consistent) |
unit_price / total_price |
price per price_base, and net × unit |
price_base |
PriceBase enum ($/kg, $/100g, $/lb, $/(1/4)lb) |
is_stable is_net is_zero is_negative |
condition booleans |
weight_overflow weight_underflow total_price_overflow |
range flags |
raw_hex |
the source frame, for diagnostics |
A weight the scale can't report (overflow / underflow) comes back as None,
never a fake 0.0. Prices are None only when the field is blank (e.g. total
price during an overflow); with no PLU the scale sends real zeros, so they read
0.0. reading.as_dict() gives a JSON-friendly view.
Continuous reading & change-watch
for reading in scale.stream(interval=0.2): # every poll
print(reading.weight_gross_kg)
watch() yields only when a field you care about changes — you tick the
fields with a ChangeFilter:
from pydigi import ChangeFilter, Field
watch_weight = ChangeFilter.weight(min_delta_kg=0.005, stable_only=True) # the default
for reading in scale.watch(interval=0.2, change_filter=watch_weight):
print("weight ->", reading.weight_gross_kg, "kg")
ChangeFilter([Field.NET, Field.TARE, Field.UNIT_PRICE]) # several fields
ChangeFilter.everything() # any field, flags included
For long-running loops, stream(..., ignore_errors=True, on_error=cb) keeps
going through transient timeouts instead of raising.
Runnable examples: single_reading.py, continuous_reading.py, and offline_demo.py (no hardware).
Errors
All exceptions derive from PyDigiError:
PyDigiError
├── TransportError # port open/read/write failed
│ └── ScaleTimeout # no response within timeout / after retries
└── FrameError # a frame arrived but was malformed
├── ShortFrameError · HeaderError · FieldParseError · ParityError
Catch PyDigiError broadly, ScaleTimeout to retry, or FrameError to
log-and-skip a garbled frame in a stream without tearing down the port.
Testing without hardware
The serial layer is a pluggable Transport. Swap in LoopbackTransport (which
replays canned bytes) and bind() to a scale — no port required:
from pydigi import DigiDS781, LoopbackTransport
with DigiDS781.bind(LoopbackTransport(my_frame_bytes)) as scale:
print(scale.read().weight_net_kg)
This is exactly how the test suite runs. A complete, runnable version (which synthesises frames) is examples/offline_demo.py; the testing workflow is in TESTING.md.
Command line
pydigi --port /dev/ttyUSB0 read # one reading
pydigi --port /dev/ttyUSB0 read --json # machine-readable
pydigi --port /dev/ttyUSB0 stream --interval 0.5 --count 10
pydigi --port /dev/ttyUSB0 watch --stable-only # weight changes only
pydigi --port /dev/ttyUSB0 watch --field tare --field unit-price
pydigi --port /dev/ttyUSB0 watch --all-fields
pydigi --model ds781 --port COM3 read # -v / -vv for logging
pydigi list-models
Extending
The library is three seams; a new device touches only one and needs no changes to existing code:
-
New model (same protocol, different defaults) — subclass
ScaleModel, set the class attributes,@register:from pydigi import ScaleModel, register from pydigi import TypeBProtocol @register class DigiDS782(ScaleModel): name = "ds782" protocol_class = TypeBProtocol default_baudrate = 9600 max_weight_kg = 15.0
DigiDS782.open(port)andpydigi --model ds782work immediately. -
New protocol (Type A/C, another vendor) — subclass
Protocol(poll_request/read_frame/parse) and point a model at it. -
New transport (TCP bridge, USB HID) — subclass
Transportand pass it toModel.bind(transport).
Details and rationale in DESIGN.md.
Scaleis not thread-safe — a serial port is a single shared resource. Use oneScaleper thread, or guard it with your own lock.
Development
make test # hardware-free test suite
make build # sdist + wheel into ./dist
make docker-build # same, isolated in Docker
Testing (hardware-free and on a real scale) is documented in TESTING.md; architecture in DESIGN.md.
Vibe-code disclosure
This library is written heavily with Claude. The design is decided upon by the initial human-author. All hands-on tests are conducted by a human-operator.
The human-conducted code review (HCCR) is an optimisation of a sort. It will be done in the right time.
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 pydigi-1.0.1.tar.gz.
File metadata
- Download URL: pydigi-1.0.1.tar.gz
- Upload date:
- Size: 29.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b801ea3bc64420c11574afbe4b9d853f23c1f49cd188148a41c7cdb2d0e297e
|
|
| MD5 |
6af982769ad464fceacc24a6a154302f
|
|
| BLAKE2b-256 |
4df4a2a03413a8e746ff27cf403c050a8f9ab6c17f86f0e4b5d4c0d3daa2ec29
|
Provenance
The following attestation bundles were made for pydigi-1.0.1.tar.gz:
Publisher:
release.yml on for-digi/pydigi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydigi-1.0.1.tar.gz -
Subject digest:
8b801ea3bc64420c11574afbe4b9d853f23c1f49cd188148a41c7cdb2d0e297e - Sigstore transparency entry: 2064303434
- Sigstore integration time:
-
Permalink:
for-digi/pydigi@7d863617c89757275129f946e4e7c01aeab07120 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/for-digi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7d863617c89757275129f946e4e7c01aeab07120 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pydigi-1.0.1-py3-none-any.whl.
File metadata
- Download URL: pydigi-1.0.1-py3-none-any.whl
- Upload date:
- Size: 24.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 |
31cdf5fbdf75b8ead87bc68fc3648ceed54b8e5ff146505267ab92df7f3421f0
|
|
| MD5 |
eb6bc244e786939b74cef37fcec92980
|
|
| BLAKE2b-256 |
391cdcd3dc7661d2e0e28d9f8f0c9845299bc948293048d134a28e63026298c8
|
Provenance
The following attestation bundles were made for pydigi-1.0.1-py3-none-any.whl:
Publisher:
release.yml on for-digi/pydigi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydigi-1.0.1-py3-none-any.whl -
Subject digest:
31cdf5fbdf75b8ead87bc68fc3648ceed54b8e5ff146505267ab92df7f3421f0 - Sigstore transparency entry: 2064303459
- Sigstore integration time:
-
Permalink:
for-digi/pydigi@7d863617c89757275129f946e4e7c01aeab07120 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/for-digi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7d863617c89757275129f946e4e7c01aeab07120 -
Trigger Event:
push
-
Statement type: