serial-scale-hx711 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- serial_scale_hx711-2.1.0/.gitignore +142 -0
- serial_scale_hx711-2.1.0/CITATION.cff +20 -0
- serial_scale_hx711-2.1.0/LICENSE +29 -0
- serial_scale_hx711-2.1.0/PKG-INFO +118 -0
- serial_scale_hx711-2.1.0/README.md +69 -0
- serial_scale_hx711-2.1.0/VERSION +1 -0
- serial_scale_hx711-2.1.0/pyproject.toml +106 -0
- serial_scale_hx711-2.1.0/src/serial_scale_hx711/__init__.py +35 -0
- serial_scale_hx711-2.1.0/src/serial_scale_hx711/_version.py +24 -0
- serial_scale_hx711-2.1.0/src/serial_scale_hx711/connection.py +167 -0
- serial_scale_hx711-2.1.0/src/serial_scale_hx711/py.typed +0 -0
- serial_scale_hx711-2.1.0/src/serial_scale_hx711/scale.py +154 -0
- serial_scale_hx711-2.1.0/tests/data/data.placeholder.txt +0 -0
- serial_scale_hx711-2.1.0/tests/minimal_usage.py +55 -0
- serial_scale_hx711-2.1.0/tests/tests/test_integration/test_placeholder.py +30 -0
- serial_scale_hx711-2.1.0/tests/tests/test_unit/test_unit.py +52 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# IDE files
|
|
2
|
+
.idea/
|
|
3
|
+
.vscode/
|
|
4
|
+
|
|
5
|
+
# Ignored file types
|
|
6
|
+
.DS_Store
|
|
7
|
+
|
|
8
|
+
# Custom config files
|
|
9
|
+
*.conf.custom
|
|
10
|
+
|
|
11
|
+
# Byte-compiled / optimized / DLL files
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.py[cod]
|
|
14
|
+
*$py.class
|
|
15
|
+
|
|
16
|
+
# C extensions
|
|
17
|
+
*.so
|
|
18
|
+
|
|
19
|
+
# Distribution / packaging
|
|
20
|
+
.Python
|
|
21
|
+
build/
|
|
22
|
+
develop-eggs/
|
|
23
|
+
dist/
|
|
24
|
+
downloads/
|
|
25
|
+
eggs/
|
|
26
|
+
.eggs/
|
|
27
|
+
lib/
|
|
28
|
+
lib64/
|
|
29
|
+
parts/
|
|
30
|
+
sdist/
|
|
31
|
+
var/
|
|
32
|
+
wheels/
|
|
33
|
+
pip-wheel-metadata/
|
|
34
|
+
share/python-wheels/
|
|
35
|
+
*.egg-info/
|
|
36
|
+
.installed.cfg
|
|
37
|
+
*.egg
|
|
38
|
+
MANIFEST
|
|
39
|
+
|
|
40
|
+
# PyInstaller
|
|
41
|
+
# Usually these files are written by a python script from a template
|
|
42
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
43
|
+
*.manifest
|
|
44
|
+
*.spec
|
|
45
|
+
|
|
46
|
+
# Installer logs
|
|
47
|
+
pip-log.txt
|
|
48
|
+
pip-delete-this-directory.txt
|
|
49
|
+
|
|
50
|
+
# Unit test / coverage reports
|
|
51
|
+
htmlcov/
|
|
52
|
+
.tox/
|
|
53
|
+
.nox/
|
|
54
|
+
.coverage
|
|
55
|
+
.coverage.*
|
|
56
|
+
.cache
|
|
57
|
+
nosetests.xml
|
|
58
|
+
coverage.xml
|
|
59
|
+
*.cover
|
|
60
|
+
*.py,cover
|
|
61
|
+
.hypothesis/
|
|
62
|
+
.pytest_cache/
|
|
63
|
+
|
|
64
|
+
# Translations
|
|
65
|
+
*.mo
|
|
66
|
+
*.pot
|
|
67
|
+
|
|
68
|
+
# Django stuff:
|
|
69
|
+
*.log
|
|
70
|
+
local_settings.py
|
|
71
|
+
db.sqlite3
|
|
72
|
+
db.sqlite3-journal
|
|
73
|
+
|
|
74
|
+
# Flask stuff:
|
|
75
|
+
instance/
|
|
76
|
+
.webassets-cache
|
|
77
|
+
|
|
78
|
+
# Scrapy stuff:
|
|
79
|
+
.scrapy
|
|
80
|
+
|
|
81
|
+
# Sphinx documentation
|
|
82
|
+
docs/_build/
|
|
83
|
+
|
|
84
|
+
# PyBuilder
|
|
85
|
+
target/
|
|
86
|
+
|
|
87
|
+
# Jupyter Notebook
|
|
88
|
+
.ipynb_checkpoints
|
|
89
|
+
|
|
90
|
+
# IPython
|
|
91
|
+
profile_default/
|
|
92
|
+
ipython_config.py
|
|
93
|
+
|
|
94
|
+
# pyenv
|
|
95
|
+
.python-version
|
|
96
|
+
|
|
97
|
+
# pipenv
|
|
98
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
99
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
100
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
101
|
+
# install all needed dependencies.
|
|
102
|
+
#Pipfile.lock
|
|
103
|
+
|
|
104
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
105
|
+
__pypackages__/
|
|
106
|
+
|
|
107
|
+
# Celery stuff
|
|
108
|
+
celerybeat-schedule
|
|
109
|
+
celerybeat.pid
|
|
110
|
+
|
|
111
|
+
# SageMath parsed files
|
|
112
|
+
*.sage.py
|
|
113
|
+
|
|
114
|
+
# Environments
|
|
115
|
+
.env
|
|
116
|
+
.venv
|
|
117
|
+
env/
|
|
118
|
+
venv/
|
|
119
|
+
ENV/
|
|
120
|
+
env.bak/
|
|
121
|
+
venv.bak/
|
|
122
|
+
|
|
123
|
+
# Spyder project settings
|
|
124
|
+
.spyderproject
|
|
125
|
+
.spyproject
|
|
126
|
+
|
|
127
|
+
# Rope project settings
|
|
128
|
+
.ropeproject
|
|
129
|
+
|
|
130
|
+
# mkdocs documentation
|
|
131
|
+
/site
|
|
132
|
+
|
|
133
|
+
# mypy
|
|
134
|
+
.mypy_cache/
|
|
135
|
+
.dmypy.json
|
|
136
|
+
dmypy.json
|
|
137
|
+
|
|
138
|
+
# Build artifacts (hatch-vcs generated)
|
|
139
|
+
src/*/_version.py
|
|
140
|
+
|
|
141
|
+
# Pyre type checker
|
|
142
|
+
.pyre/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
type: software
|
|
4
|
+
title: "serial-scale-hx711: Python driver for Arduino+HX711 serial weighing scales"
|
|
5
|
+
version: "2.0.4"
|
|
6
|
+
repository-code: https://github.com/MurineShiftWork/serial-scale-hx711
|
|
7
|
+
license: GPL-3.0
|
|
8
|
+
authors:
|
|
9
|
+
- family-names: Rollik
|
|
10
|
+
given-names: Lars B.
|
|
11
|
+
orcid: https://orcid.org/0000-0003-0160-6971
|
|
12
|
+
|
|
13
|
+
# Zenodo integration (optional):
|
|
14
|
+
# 1. Enable the Zenodo webhook at zenodo.org/account/settings/github
|
|
15
|
+
# 2. After first tag release, copy the concept DOI from Zenodo and add:
|
|
16
|
+
#
|
|
17
|
+
# identifiers:
|
|
18
|
+
# - type: doi
|
|
19
|
+
# value: 10.5281/zenodo.XXXXXXX
|
|
20
|
+
# description: Zenodo concept DOI (resolves to latest version)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021, Lars B. Rollik
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: serial-scale-hx711
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Python driver for Arduino+HX711 serial weighing scales.
|
|
5
|
+
Project-URL: Homepage, https://github.com/MurineShiftWork/serial-scale-hx711
|
|
6
|
+
Project-URL: Documentation, https://larsrollik.github.io/serial-scale-hx711/
|
|
7
|
+
Project-URL: Issue Tracker, https://github.com/MurineShiftWork/serial-scale-hx711/issues
|
|
8
|
+
Author-email: "Lars B. Rollik" <L.B.Rollik@protonmail.com>
|
|
9
|
+
License: BSD 3-Clause License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2021, Lars B. Rollik
|
|
12
|
+
All rights reserved.
|
|
13
|
+
|
|
14
|
+
Redistribution and use in source and binary forms, with or without
|
|
15
|
+
modification, are permitted provided that the following conditions are met:
|
|
16
|
+
|
|
17
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
18
|
+
list of conditions and the following disclaimer.
|
|
19
|
+
|
|
20
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
21
|
+
this list of conditions and the following disclaimer in the documentation
|
|
22
|
+
and/or other materials provided with the distribution.
|
|
23
|
+
|
|
24
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
25
|
+
contributors may be used to endorse or promote products derived from
|
|
26
|
+
this software without specific prior written permission.
|
|
27
|
+
|
|
28
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
29
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
30
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
31
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
32
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
33
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
34
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
35
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
36
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
37
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Requires-Python: >=3.10
|
|
40
|
+
Requires-Dist: pyserial
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: commitizen; extra == 'dev'
|
|
43
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
44
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
46
|
+
Provides-Extra: docs
|
|
47
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# serial-scale-hx711
|
|
51
|
+
|
|
52
|
+
Python driver for Arduino+HX711 serial weighing scales.
|
|
53
|
+
|
|
54
|
+
> **Renamed from `serial-weighing-scale`** (PyPI: `serial-weighing-scale` ≤ 2.0.4).
|
|
55
|
+
> New releases publish under `serial-scale-hx711`. A deprecation stub remains on PyPI
|
|
56
|
+
> under the old name pointing here.
|
|
57
|
+
|
|
58
|
+
## Hardware
|
|
59
|
+
|
|
60
|
+
- Arduino Uno (USB-A to USB-B cable)
|
|
61
|
+
- HX711 load cell amplifier (e.g. SparkFun SEN-13879)
|
|
62
|
+
- Load cell 100 g or 500 g (e.g. SEN-14727 / SEN-14728)
|
|
63
|
+
- Firmware: see `scale_firmware/` (uses [olkal/HX711_ADC](https://github.com/olkal/HX711_ADC))
|
|
64
|
+
|
|
65
|
+
The scale identifies itself as `<SerialWeighingScale>` over 115200 baud.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install serial-scale-hx711
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or editable from this repo:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from serial_scale_hx711 import Scale
|
|
83
|
+
|
|
84
|
+
scale = Scale(serial_port="/dev/ttyACM1")
|
|
85
|
+
scale.start() # connect and wait for firmware init
|
|
86
|
+
scale.tare()
|
|
87
|
+
weight = scale.read_weight_blocking() # grams, blocks until stable
|
|
88
|
+
scale.disconnect()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Auto-detect port
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from serial_scale_hx711 import connect_serial_scale
|
|
95
|
+
|
|
96
|
+
scale = connect_serial_scale(["/dev/ttyACM0", "/dev/ttyACM1", "/dev/ttyACM2"])
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
| Method | Description |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `start(timeout=10)` | Connect and wait for firmware ready |
|
|
104
|
+
| `tare()` | Zero the scale |
|
|
105
|
+
| `read_weight()` | Single reading (float or None) |
|
|
106
|
+
| `read_weight_blocking(n_valid, timeout)` | Block until N valid readings, return median |
|
|
107
|
+
| `read_weight_reliable(n_readings, measure)` | Repeated reads with custom aggregation |
|
|
108
|
+
| `identify()` | True if firmware responds with identity string |
|
|
109
|
+
| `get_calibration_factor()` | Read calibration factor from firmware |
|
|
110
|
+
| `disconnect()` | Close serial connection |
|
|
111
|
+
|
|
112
|
+
## murineshiftwork integration
|
|
113
|
+
|
|
114
|
+
Used via `murineshiftwork.logic.scale.SerialWeighingScaleAdapter`. Install with:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install "murineshiftwork[calibration]"
|
|
118
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# serial-scale-hx711
|
|
2
|
+
|
|
3
|
+
Python driver for Arduino+HX711 serial weighing scales.
|
|
4
|
+
|
|
5
|
+
> **Renamed from `serial-weighing-scale`** (PyPI: `serial-weighing-scale` ≤ 2.0.4).
|
|
6
|
+
> New releases publish under `serial-scale-hx711`. A deprecation stub remains on PyPI
|
|
7
|
+
> under the old name pointing here.
|
|
8
|
+
|
|
9
|
+
## Hardware
|
|
10
|
+
|
|
11
|
+
- Arduino Uno (USB-A to USB-B cable)
|
|
12
|
+
- HX711 load cell amplifier (e.g. SparkFun SEN-13879)
|
|
13
|
+
- Load cell 100 g or 500 g (e.g. SEN-14727 / SEN-14728)
|
|
14
|
+
- Firmware: see `scale_firmware/` (uses [olkal/HX711_ADC](https://github.com/olkal/HX711_ADC))
|
|
15
|
+
|
|
16
|
+
The scale identifies itself as `<SerialWeighingScale>` over 115200 baud.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install serial-scale-hx711
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or editable from this repo:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from serial_scale_hx711 import Scale
|
|
34
|
+
|
|
35
|
+
scale = Scale(serial_port="/dev/ttyACM1")
|
|
36
|
+
scale.start() # connect and wait for firmware init
|
|
37
|
+
scale.tare()
|
|
38
|
+
weight = scale.read_weight_blocking() # grams, blocks until stable
|
|
39
|
+
scale.disconnect()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Auto-detect port
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from serial_scale_hx711 import connect_serial_scale
|
|
46
|
+
|
|
47
|
+
scale = connect_serial_scale(["/dev/ttyACM0", "/dev/ttyACM1", "/dev/ttyACM2"])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
| Method | Description |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `start(timeout=10)` | Connect and wait for firmware ready |
|
|
55
|
+
| `tare()` | Zero the scale |
|
|
56
|
+
| `read_weight()` | Single reading (float or None) |
|
|
57
|
+
| `read_weight_blocking(n_valid, timeout)` | Block until N valid readings, return median |
|
|
58
|
+
| `read_weight_reliable(n_readings, measure)` | Repeated reads with custom aggregation |
|
|
59
|
+
| `identify()` | True if firmware responds with identity string |
|
|
60
|
+
| `get_calibration_factor()` | Read calibration factor from firmware |
|
|
61
|
+
| `disconnect()` | Close serial connection |
|
|
62
|
+
|
|
63
|
+
## murineshiftwork integration
|
|
64
|
+
|
|
65
|
+
Used via `murineshiftwork.logic.scale.SerialWeighingScaleAdapter`. Install with:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install "murineshiftwork[calibration]"
|
|
69
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.1.0
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "serial-scale-hx711"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python driver for Arduino+HX711 serial weighing scales."
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Lars B. Rollik", email = "L.B.Rollik@protonmail.com" },
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pyserial",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/MurineShiftWork/serial-scale-hx711"
|
|
21
|
+
Documentation = "https://larsrollik.github.io/serial-scale-hx711/"
|
|
22
|
+
"Issue Tracker" = "https://github.com/MurineShiftWork/serial-scale-hx711/issues"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"commitizen",
|
|
27
|
+
"pytest",
|
|
28
|
+
"pytest-cov",
|
|
29
|
+
"pre-commit",
|
|
30
|
+
]
|
|
31
|
+
docs = [
|
|
32
|
+
"mkdocs-material",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Build
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
[tool.hatch.version]
|
|
40
|
+
source = "vcs"
|
|
41
|
+
fallback-version = "2.0.4"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.hooks.vcs]
|
|
44
|
+
version-file = "src/serial_scale_hx711/_version.py"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/serial_scale_hx711"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.sdist]
|
|
50
|
+
include = [
|
|
51
|
+
"/src/serial_scale_hx711",
|
|
52
|
+
"/tests",
|
|
53
|
+
"/README.md",
|
|
54
|
+
"/LICENSE",
|
|
55
|
+
"/pyproject.toml",
|
|
56
|
+
"/CITATION.cff",
|
|
57
|
+
"/VERSION",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Commitizen
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
[tool.commitizen]
|
|
65
|
+
name = "cz_conventional_commits"
|
|
66
|
+
version_provider = "commitizen"
|
|
67
|
+
version = "2.1.0"
|
|
68
|
+
tag_format = "v$version"
|
|
69
|
+
update_changelog_on_bump = false
|
|
70
|
+
version_files = ["VERSION"]
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Pytest
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
testpaths = ["tests"]
|
|
78
|
+
addopts = "--cov=serial_scale_hx711 --durations=0"
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Ruff
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
[tool.ruff]
|
|
85
|
+
line-length = 99
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint]
|
|
88
|
+
select = ["E", "F", "I", "UP", "W"]
|
|
89
|
+
ignore = ["E501"]
|
|
90
|
+
|
|
91
|
+
[tool.ruff.format]
|
|
92
|
+
quote-style = "double"
|
|
93
|
+
indent-style = "space"
|
|
94
|
+
line-ending = "auto"
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Mypy
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
[tool.mypy]
|
|
101
|
+
mypy_path = "src"
|
|
102
|
+
ignore_missing_imports = true
|
|
103
|
+
|
|
104
|
+
[[tool.mypy.overrides]]
|
|
105
|
+
module = "toml"
|
|
106
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
__author__ = "Lars B. Rollik"
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from serial_scale_hx711.scale import Scale
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("serial-scale-hx711")
|
|
9
|
+
except PackageNotFoundError:
|
|
10
|
+
__version__ = "unknown"
|
|
11
|
+
|
|
12
|
+
DEFAULT_TEST_PORTS = [f"/dev/ttyACM{x}" for x in range(5)]
|
|
13
|
+
|
|
14
|
+
# Backwards-compatibility alias for code that imported SerialWeighingScale
|
|
15
|
+
SerialWeighingScale = Scale
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def connect_serial_scale(
|
|
19
|
+
serial_port_list: list = DEFAULT_TEST_PORTS,
|
|
20
|
+
) -> Scale | None:
|
|
21
|
+
"""Connect to the first available serial scale from the provided list of ports."""
|
|
22
|
+
from serial import SerialException
|
|
23
|
+
|
|
24
|
+
for serial_port in serial_port_list:
|
|
25
|
+
try:
|
|
26
|
+
scale = Scale(serial_port=serial_port)
|
|
27
|
+
scale.start()
|
|
28
|
+
return scale
|
|
29
|
+
except SerialException:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ["Scale", "SerialWeighingScale", "connect_serial_scale", "__version__"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '2.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import struct
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from serial import Serial
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SerialConnection:
|
|
9
|
+
serial_port: str = ""
|
|
10
|
+
baudrate: int
|
|
11
|
+
timeout: float
|
|
12
|
+
connection: Serial
|
|
13
|
+
_connected = False
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
serial_port: str = "",
|
|
18
|
+
baudrate: int = 115200,
|
|
19
|
+
timeout: float = 1,
|
|
20
|
+
**kwargs: Any,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.serial_port = serial_port
|
|
23
|
+
self.baudrate = baudrate or 115200
|
|
24
|
+
self.timeout = timeout or 0.1
|
|
25
|
+
self.connection = None
|
|
26
|
+
|
|
27
|
+
def dict(self) -> dict:
|
|
28
|
+
class_data = {
|
|
29
|
+
"serial_port": self.serial_port,
|
|
30
|
+
"baudrate": self.baudrate,
|
|
31
|
+
"timeout": self.timeout,
|
|
32
|
+
}
|
|
33
|
+
return class_data
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return (
|
|
37
|
+
f"SerialConnection(serial_port={self.serial_port}, "
|
|
38
|
+
f"baudrate={self.baudrate}, "
|
|
39
|
+
f"timeout={self.timeout})"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return (
|
|
44
|
+
f"SerialConnection: {self.serial_port} @ {self.baudrate} baud, timeout={self.timeout}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __del__(self) -> None:
|
|
48
|
+
self.disconnect()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def connected(self) -> bool:
|
|
52
|
+
return self._connected
|
|
53
|
+
|
|
54
|
+
def connect(self) -> "SerialConnection":
|
|
55
|
+
if not self.connected:
|
|
56
|
+
self.connection = Serial(
|
|
57
|
+
port=self.serial_port,
|
|
58
|
+
baudrate=self.baudrate,
|
|
59
|
+
timeout=self.timeout,
|
|
60
|
+
dsrdtr=False, # prevent DTR toggle from resetting the Arduino on open
|
|
61
|
+
rtscts=False,
|
|
62
|
+
)
|
|
63
|
+
# is open?
|
|
64
|
+
if self.connection.is_open:
|
|
65
|
+
logging.info(f"Connected to {self.serial_port} at {self.baudrate} baud.")
|
|
66
|
+
else:
|
|
67
|
+
logging.error(f"Failed to open serial port {self.serial_port}.")
|
|
68
|
+
|
|
69
|
+
# finalize connection
|
|
70
|
+
self._connected = True
|
|
71
|
+
self._clear_buffer()
|
|
72
|
+
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def disconnect(self) -> None:
|
|
76
|
+
if self.connection is not None:
|
|
77
|
+
self.connection.close()
|
|
78
|
+
self.connection = None
|
|
79
|
+
self._connected = False
|
|
80
|
+
logging.info(f"Disconnected from {self.serial_port}.")
|
|
81
|
+
|
|
82
|
+
def _encode(self, data: Any, order: str) -> bytes:
|
|
83
|
+
"""Encode & pack as byte struct & flank by start/stop bytes."""
|
|
84
|
+
# check that data is list
|
|
85
|
+
if not isinstance(data, list):
|
|
86
|
+
data = [data]
|
|
87
|
+
|
|
88
|
+
# encode str to bytes
|
|
89
|
+
data_encoded = [item.encode() if isinstance(item, str) else item for item in data]
|
|
90
|
+
|
|
91
|
+
# pack the data
|
|
92
|
+
data_packed = struct.pack(order, *data_encoded)
|
|
93
|
+
|
|
94
|
+
# flank the packed data with start/stop bytes </>
|
|
95
|
+
message = b"<" + data_packed + b">"
|
|
96
|
+
|
|
97
|
+
logging.debug(f"Encoded message: '{str(message)}'")
|
|
98
|
+
return message
|
|
99
|
+
|
|
100
|
+
def _clear_buffer(self):
|
|
101
|
+
self.connection.read(self.connection.in_waiting)
|
|
102
|
+
return not self.connection.in_waiting
|
|
103
|
+
|
|
104
|
+
def send(
|
|
105
|
+
self,
|
|
106
|
+
command: str,
|
|
107
|
+
data: list | int | str | None = None,
|
|
108
|
+
order: str = "",
|
|
109
|
+
) -> None:
|
|
110
|
+
""""""
|
|
111
|
+
assert isinstance(command, str)
|
|
112
|
+
assert isinstance(data, list | int | str | type(None))
|
|
113
|
+
assert isinstance(order, str)
|
|
114
|
+
|
|
115
|
+
# fix data type
|
|
116
|
+
if data is not None and not isinstance(data, list):
|
|
117
|
+
data = [data]
|
|
118
|
+
|
|
119
|
+
raw_data = [command] + data if data is not None else command
|
|
120
|
+
|
|
121
|
+
# encode/pack
|
|
122
|
+
data_to_send = self._encode(raw_data, order=order)
|
|
123
|
+
|
|
124
|
+
# send data
|
|
125
|
+
if self.connected:
|
|
126
|
+
self._clear_buffer()
|
|
127
|
+
self.connection.write(data_to_send)
|
|
128
|
+
self.connection.flush()
|
|
129
|
+
logging.debug(f"Sent data: {str(data_to_send)}")
|
|
130
|
+
|
|
131
|
+
def read_bytes(self, n_bytes: int, unpack_order: str) -> tuple[Any, ...]:
|
|
132
|
+
"""
|
|
133
|
+
Read n_bytes from the serial port and unpack them according to the
|
|
134
|
+
specified unpack_order.
|
|
135
|
+
The unpack_order should be a format string compatible with the
|
|
136
|
+
struct module.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
n_bytes : int
|
|
141
|
+
unpack_order : str
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
tuple
|
|
146
|
+
Unpacked data as a tuple of values.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
raw_data = self.connection.read(n_bytes)
|
|
150
|
+
|
|
151
|
+
# Check if the correct amount of data was read
|
|
152
|
+
if len(raw_data) != n_bytes:
|
|
153
|
+
raise ValueError(f"Did not receive {n_bytes} bytes from serial port")
|
|
154
|
+
|
|
155
|
+
# Unpack the data as separate variables
|
|
156
|
+
unpacked_bytes = struct.unpack(unpack_order, raw_data)
|
|
157
|
+
|
|
158
|
+
logging.debug(f"Unpacked bytes: {unpacked_bytes}")
|
|
159
|
+
return unpacked_bytes
|
|
160
|
+
|
|
161
|
+
def read_line(self) -> str:
|
|
162
|
+
"""
|
|
163
|
+
Read a line from the serial port and decode it to a string.
|
|
164
|
+
"""
|
|
165
|
+
line = self.connection.readline().decode("utf-8").strip()
|
|
166
|
+
logging.debug(f"Received line: {line}")
|
|
167
|
+
return line
|
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import statistics
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
from serial_scale_hx711.connection import SerialConnection
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Scale(SerialConnection):
|
|
10
|
+
_identity_response = "<SerialWeighingScale>"
|
|
11
|
+
|
|
12
|
+
def __init__(self, serial_port: str, baudrate: int = 115200, timeout: float = 1) -> None:
|
|
13
|
+
# init serial connection
|
|
14
|
+
super().__init__(serial_port=serial_port, baudrate=baudrate, timeout=timeout)
|
|
15
|
+
|
|
16
|
+
def start(self, timeout=10) -> None:
|
|
17
|
+
"""Connect and wait for the scale firmware to finish initialising.
|
|
18
|
+
|
|
19
|
+
Firmware only responds to <i> (identify) once the HX711 tare is complete,
|
|
20
|
+
so polling identify() is sufficient — no need to hammer read_weight().
|
|
21
|
+
"""
|
|
22
|
+
self.connect()
|
|
23
|
+
|
|
24
|
+
start_time = time.time()
|
|
25
|
+
while time.time() - start_time < timeout:
|
|
26
|
+
try:
|
|
27
|
+
if self.identify():
|
|
28
|
+
elapsed = time.time() - start_time
|
|
29
|
+
logging.info(f"Scale ready: {self.serial_port} (after {elapsed:.2f}s)")
|
|
30
|
+
return
|
|
31
|
+
except (UnicodeDecodeError, Exception):
|
|
32
|
+
pass
|
|
33
|
+
time.sleep(0.1)
|
|
34
|
+
|
|
35
|
+
raise TimeoutError(
|
|
36
|
+
f"Scale did not respond within {timeout}s on {self.serial_port}. "
|
|
37
|
+
f"Check wiring and that firmware is loaded."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_ready(self) -> bool:
|
|
42
|
+
"""True if the firmware is initialised and responding to identity queries."""
|
|
43
|
+
return self.identify()
|
|
44
|
+
|
|
45
|
+
def read_weight(self) -> float | None:
|
|
46
|
+
"""
|
|
47
|
+
Get the weight from the scale.
|
|
48
|
+
"""
|
|
49
|
+
self.send(command="w", order="c")
|
|
50
|
+
weight_result = self.read_line()
|
|
51
|
+
|
|
52
|
+
# Convert to float
|
|
53
|
+
try:
|
|
54
|
+
weight = round(float(weight_result), 2)
|
|
55
|
+
return weight
|
|
56
|
+
except ValueError:
|
|
57
|
+
logging.error(f"Failed to convert weight result to float: {weight_result}")
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def tare(self) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Tare the scale.
|
|
63
|
+
"""
|
|
64
|
+
self.send(command="t", order="c")
|
|
65
|
+
# LoadCell.tare() on firmware is blocking (~100 ms at 10 Hz / 1 sample).
|
|
66
|
+
# The rolling-average buffer also needs time to flush old pre-tare values.
|
|
67
|
+
# Wait 0.5 s so the next read sees fully tared samples.
|
|
68
|
+
time.sleep(0.5)
|
|
69
|
+
|
|
70
|
+
def identify(self) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Identify the device connected to the serial port.
|
|
73
|
+
This method sends a command to the device and waits for a response.
|
|
74
|
+
"""
|
|
75
|
+
self.send(command="i", order="c")
|
|
76
|
+
response = self.read_line()
|
|
77
|
+
return response == self._identity_response
|
|
78
|
+
|
|
79
|
+
def get_calibration_factor(self) -> float | None:
|
|
80
|
+
"""
|
|
81
|
+
Get the calibration factor from the scale.
|
|
82
|
+
"""
|
|
83
|
+
self.send(command="f", order="c")
|
|
84
|
+
|
|
85
|
+
calibration_result = self.read_line()
|
|
86
|
+
|
|
87
|
+
# Convert to float
|
|
88
|
+
try:
|
|
89
|
+
calibration_factor = float(calibration_result)
|
|
90
|
+
return calibration_factor
|
|
91
|
+
except ValueError:
|
|
92
|
+
logging.error(f"Failed to convert calibration result to float: {calibration_result}")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def read_weight_repeated(self, n_readings: int = 5, inter_read_delay: float = 0.1) -> list:
|
|
96
|
+
"""Read weight n times, return list of valid (non-None) readings."""
|
|
97
|
+
readings = []
|
|
98
|
+
for _ in range(n_readings):
|
|
99
|
+
reading = self.read_weight()
|
|
100
|
+
if reading is not None:
|
|
101
|
+
readings.append(reading)
|
|
102
|
+
time.sleep(inter_read_delay)
|
|
103
|
+
return readings
|
|
104
|
+
|
|
105
|
+
def read_weight_reliable(
|
|
106
|
+
self,
|
|
107
|
+
n_readings: int = 5,
|
|
108
|
+
inter_read_delay: float = 0.1,
|
|
109
|
+
measure: Callable = statistics.median,
|
|
110
|
+
) -> float:
|
|
111
|
+
"""Repeated reads with statistical measure. Raises if no valid readings."""
|
|
112
|
+
readings = self.read_weight_repeated(
|
|
113
|
+
n_readings=n_readings, inter_read_delay=inter_read_delay
|
|
114
|
+
)
|
|
115
|
+
if not readings:
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
f"Scale on {self.serial_port} returned no valid readings "
|
|
118
|
+
f"after {n_readings} attempts."
|
|
119
|
+
)
|
|
120
|
+
return measure(readings)
|
|
121
|
+
|
|
122
|
+
def read_weight_blocking(
|
|
123
|
+
self,
|
|
124
|
+
n_valid: int = 3,
|
|
125
|
+
inter_read_delay: float = 0.2,
|
|
126
|
+
timeout: float = 30,
|
|
127
|
+
) -> float:
|
|
128
|
+
"""Block until n_valid successful weight readings are collected, return their median.
|
|
129
|
+
|
|
130
|
+
Use this at every point in the calibration where the task must not proceed
|
|
131
|
+
until the scale has returned a trustworthy value. Raises TimeoutError if
|
|
132
|
+
the scale does not produce enough valid readings within `timeout` seconds.
|
|
133
|
+
"""
|
|
134
|
+
readings = []
|
|
135
|
+
deadline = time.time() + timeout
|
|
136
|
+
while time.time() < deadline:
|
|
137
|
+
reading = self.read_weight()
|
|
138
|
+
if reading is not None:
|
|
139
|
+
readings.append(reading)
|
|
140
|
+
if len(readings) >= n_valid:
|
|
141
|
+
return statistics.median(readings)
|
|
142
|
+
time.sleep(inter_read_delay)
|
|
143
|
+
raise TimeoutError(
|
|
144
|
+
f"Scale on {self.serial_port} could not produce {n_valid} valid readings "
|
|
145
|
+
f"within {timeout}s. Check connection and sensor."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
print("TEST")
|
|
151
|
+
|
|
152
|
+
s = Scale(serial_port="/dev/ttyACM1", baudrate=115200, timeout=1)
|
|
153
|
+
s.connect()
|
|
154
|
+
s.read_weight()
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TypedDict
|
|
3
|
+
|
|
4
|
+
from serial_weighing_scale import SerialWeighingScale
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ScaleConfig(TypedDict):
|
|
8
|
+
serial_port: str
|
|
9
|
+
baudrate: int
|
|
10
|
+
timeout: float
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
logger = logging.getLogger()
|
|
15
|
+
logger.setLevel(logging.DEBUG)
|
|
16
|
+
|
|
17
|
+
serial_param: ScaleConfig = {
|
|
18
|
+
"serial_port": "/dev/ttyACM3",
|
|
19
|
+
"baudrate": 115200,
|
|
20
|
+
"timeout": 1,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# scale = connect_serial_scale()
|
|
24
|
+
scale = SerialWeighingScale(**serial_param)
|
|
25
|
+
scale.start()
|
|
26
|
+
|
|
27
|
+
# # measure time until scale is ready
|
|
28
|
+
# start_time = time.time()
|
|
29
|
+
# while not scale.is_ready:
|
|
30
|
+
# time.sleep(0.1)
|
|
31
|
+
#
|
|
32
|
+
# elapsed_time = time.time() - start_time
|
|
33
|
+
#
|
|
34
|
+
# print("Scale ready", elapsed_time)
|
|
35
|
+
# scale.read_weight()
|
|
36
|
+
# scale.tare()
|
|
37
|
+
#
|
|
38
|
+
# # query every .25 seconds and print the weight, continue until interrupted
|
|
39
|
+
# try:
|
|
40
|
+
# while True:
|
|
41
|
+
# weight = scale.read_weight()
|
|
42
|
+
# print("Weight:", weight)
|
|
43
|
+
# time.sleep(0.25)
|
|
44
|
+
# except KeyboardInterrupt:
|
|
45
|
+
# pass
|
|
46
|
+
print("")
|
|
47
|
+
print("NEXT")
|
|
48
|
+
print("")
|
|
49
|
+
|
|
50
|
+
scale = SerialWeighingScale(**serial_param)
|
|
51
|
+
scale.start()
|
|
52
|
+
scale.identify()
|
|
53
|
+
scale.read_weight()
|
|
54
|
+
|
|
55
|
+
print("Scale:", scale.is_ready)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
import toml
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_package_name() -> str:
|
|
7
|
+
"""Retrieve package name from pyproject.toml."""
|
|
8
|
+
project_data = toml.load("pyproject.toml")
|
|
9
|
+
return str(project_data["project"]["name"])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_package_functionality() -> None:
|
|
13
|
+
"""Test the package dynamically after installation."""
|
|
14
|
+
package_name = get_package_name()
|
|
15
|
+
# Replace hyphens with underscores for import compatibility
|
|
16
|
+
package_name = package_name.replace("-", "_")
|
|
17
|
+
|
|
18
|
+
# Install the package
|
|
19
|
+
subprocess.run(["pip", "install", "."], check=True)
|
|
20
|
+
|
|
21
|
+
# Dynamically import the package
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
["python", "-c", f"import {package_name}"],
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
)
|
|
27
|
+
assert result.returncode == 0, f"Dynamic package import failed: {result.stderr}"
|
|
28
|
+
|
|
29
|
+
# Cleanup: Uninstall the package
|
|
30
|
+
subprocess.run(["pip", "uninstall", "-y", package_name], check=True)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def ensure_toml_installed() -> None:
|
|
6
|
+
"""Ensure toml is installed in the current environment."""
|
|
7
|
+
try:
|
|
8
|
+
__import__("toml")
|
|
9
|
+
except ImportError:
|
|
10
|
+
subprocess.run(
|
|
11
|
+
[sys.executable, "-m", "pip", "install", "toml"],
|
|
12
|
+
check=True,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_python_version() -> None:
|
|
17
|
+
"""Test that Python version is compatible with the environment."""
|
|
18
|
+
assert sys.version_info >= (3, 8), "Python version must be 3.8 or higher"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_package_installation() -> None:
|
|
22
|
+
"""Test dynamic installation and uninstallation of the package."""
|
|
23
|
+
ensure_toml_installed()
|
|
24
|
+
import toml
|
|
25
|
+
|
|
26
|
+
# Dynamically read the package name
|
|
27
|
+
package_name = toml.load("pyproject.toml")["project"]["name"]
|
|
28
|
+
|
|
29
|
+
# Install the package
|
|
30
|
+
subprocess.run([sys.executable, "-m", "pip", "install", "."], check=True)
|
|
31
|
+
|
|
32
|
+
# Check that the package is installed
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
[sys.executable, "-m", "pip", "show", package_name],
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
)
|
|
38
|
+
assert result.returncode == 0, "Package installation failed"
|
|
39
|
+
|
|
40
|
+
# Uninstall the package
|
|
41
|
+
subprocess.run(
|
|
42
|
+
[sys.executable, "-m", "pip", "uninstall", "-y", package_name],
|
|
43
|
+
check=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Verify the package is uninstalled
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
[sys.executable, "-m", "pip", "show", package_name],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
)
|
|
52
|
+
assert result.returncode != 0, "Package uninstallation failed"
|