Skip to main content

Read weight, price and status from DIGI scales over RS-232.

Project description

PyDIGI — read DIGI scales from Python

PyPI Python versions License: MIT

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) and pydigi --model ds782 work 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 Transport and pass it to Model.bind(transport).

Details and rationale in DESIGN.md.

Scale is not thread-safe — a serial port is a single shared resource. Use one Scale per 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydigi-1.0.1.tar.gz (29.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydigi-1.0.1-py3-none-any.whl (24.7 kB view details)

Uploaded Python 3

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

Hashes for pydigi-1.0.1.tar.gz
Algorithm Hash digest
SHA256 8b801ea3bc64420c11574afbe4b9d853f23c1f49cd188148a41c7cdb2d0e297e
MD5 6af982769ad464fceacc24a6a154302f
BLAKE2b-256 4df4a2a03413a8e746ff27cf403c050a8f9ab6c17f86f0e4b5d4c0d3daa2ec29

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydigi-1.0.1.tar.gz:

Publisher: release.yml on for-digi/pydigi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pydigi-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 31cdf5fbdf75b8ead87bc68fc3648ceed54b8e5ff146505267ab92df7f3421f0
MD5 eb6bc244e786939b74cef37fcec92980
BLAKE2b-256 391cdcd3dc7661d2e0e28d9f8f0c9845299bc948293048d134a28e63026298c8

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydigi-1.0.1-py3-none-any.whl:

Publisher: release.yml on for-digi/pydigi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page