pysd10 0.2.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.
- pysd10-0.2.0/PKG-INFO +124 -0
- pysd10-0.2.0/README.md +114 -0
- pysd10-0.2.0/pyproject.toml +23 -0
- pysd10-0.2.0/pysd10.egg-info/PKG-INFO +124 -0
- pysd10-0.2.0/pysd10.egg-info/SOURCES.txt +19 -0
- pysd10-0.2.0/pysd10.egg-info/dependency_links.txt +1 -0
- pysd10-0.2.0/pysd10.egg-info/entry_points.txt +2 -0
- pysd10-0.2.0/pysd10.egg-info/requires.txt +4 -0
- pysd10-0.2.0/pysd10.egg-info/top_level.txt +1 -0
- pysd10-0.2.0/sd10/__init__.py +44 -0
- pysd10-0.2.0/sd10/__main__.py +3 -0
- pysd10-0.2.0/sd10/cli.py +320 -0
- pysd10-0.2.0/sd10/client.py +736 -0
- pysd10-0.2.0/sd10/color.py +287 -0
- pysd10-0.2.0/sd10/data/lab_weights.json +1 -0
- pysd10-0.2.0/sd10/export.py +67 -0
- pysd10-0.2.0/sd10/protocol.py +448 -0
- pysd10-0.2.0/sd10/py.typed +0 -0
- pysd10-0.2.0/sd10/sync.py +140 -0
- pysd10-0.2.0/setup.cfg +4 -0
- pysd10-0.2.0/tests/test_sd10.py +545 -0
pysd10-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pysd10
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python client for the Epson SD-10 spectrophotometer over Bluetooth LE (spectrum and L*a*b*)
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: bleak>=0.21
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest; extra == "test"
|
|
10
|
+
|
|
11
|
+
# sd10
|
|
12
|
+
|
|
13
|
+
Python client for the **Epson SD-10** spectrophotometer over Bluetooth LE: take measurements and get
|
|
14
|
+
the 31-band spectrum (400–700 nm) and CIE L\*a\*b\* exactly as the Epson Spectrometer app computes it.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
pip install -e path\to\sd10_client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Python 3.9+ and [bleak](https://github.com/hbldh/bleak) (installed automatically).
|
|
23
|
+
|
|
24
|
+
## Quick start (blocking — scripts, Jupyter)
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import sd10
|
|
28
|
+
|
|
29
|
+
result = sd10.measure_once() # connect, measure, disconnect
|
|
30
|
+
print(result.lab) # Lab(L=..., a=..., b=...)
|
|
31
|
+
|
|
32
|
+
with sd10.connect_sync() as dev: # keep the connection open
|
|
33
|
+
r = dev.measure(illuminant="D65", observer=10, m_factor=0)
|
|
34
|
+
print(r.lab, r.lch, r.spectrum)
|
|
35
|
+
print(r.lab_under("A", 2)) # same reading, other illuminant
|
|
36
|
+
|
|
37
|
+
for r in dev.button_measurements(): # react to the device's own button
|
|
38
|
+
print(r.lab)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Async
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio, sd10
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with sd10.connect() as dev: # strongest SD-10, or connect("40:84:32:52:FD:0F")
|
|
48
|
+
print(await dev.get_info())
|
|
49
|
+
r = await dev.measure()
|
|
50
|
+
print(r.lab)
|
|
51
|
+
async for r in dev.button_measurements():
|
|
52
|
+
print(r.lab)
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API overview
|
|
58
|
+
|
|
59
|
+
| | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `scan()` / `scan_sync()` | nearby SD-10s (`address`, `name`, `rssi`) |
|
|
62
|
+
| `connect(address=None)` / `connect_sync(...)` | connect and open a session; on exit the device is handed back to its own UI |
|
|
63
|
+
| `dev.measure(illuminant, observer, m_factor, save)` | one spot measurement → `MeasurementResult` |
|
|
64
|
+
| `dev.button_measurements()` | iterator of results taken with the device button |
|
|
65
|
+
| `dev.get_history(after_id=0, limit=None, progress=None)` | download readings stored on the device (it keeps its last 100), oldest first; pass the last `identifier` you have as `after_id` to fetch only new ones |
|
|
66
|
+
| `dev.history_count(after_id=0)` | how many stored readings are newer than `after_id` |
|
|
67
|
+
| `dev.get_groups()` | colour groups (palettes) stored on the device: 4 slots × up to 80 L\*a\*b\* colours → `[ColorGroup]` |
|
|
68
|
+
| `dev.set_group(ColorGroup(slot, name, colors, illuminant, observer, m_factor, diff_formula, tolerance))` | store/replace a palette in slot 1–4 |
|
|
69
|
+
| `dev.delete_group(slot)` | clear a slot |
|
|
70
|
+
| `dev.calibrate()` | white calibration with the cover **closed** (raises `CoverOpen` otherwise) → `Status` |
|
|
71
|
+
| `dev.scan_row(expected=None, patches=None, tolerance=None, ...)` | scan one row of ≤18 patches by sliding the device; with `expected` + `tolerance` each patch is judged → `RowResult` |
|
|
72
|
+
| `dev.scan_palette_row(slot, row, patches_per_row=10)` | scan one row of a printed chart of a stored palette and compare with it → `RowResult` |
|
|
73
|
+
| `dev.scan_palette(slot, patches_per_row=10)` | whole chart, row by row (async iterator of `RowResult`), then `finish_chart()` |
|
|
74
|
+
| `dev.measure_palette_patch(slot, patch)` | spot-measure one patch against palette colour `patch` → `PatchResult` |
|
|
75
|
+
| `dev.get_info()`, `get_status()`, `get_name()`, `get_serial()`, `get_firmware_version()` | device info |
|
|
76
|
+
| `dev.get_setting(Setting.X)` / `set_setting(...)` | buzzer, panel direction, auto power-off, language |
|
|
77
|
+
| `dev.set_clock()`, `dev.set_name(...)` | |
|
|
78
|
+
| `dev.transact(frame)` | raw command (see `sd10.protocol`) |
|
|
79
|
+
| `MeasurementResult` | `.lab`, `.lch`, `.lab_under(ill, obs)`, `.spectrum`, `.wavelengths`, `.timestamp`, `.to_dict()` |
|
|
80
|
+
| `spectrum_to_lab(spectrum, illuminant, observer)` | offline conversion |
|
|
81
|
+
| `delta_e(ref, sample, "DE2000" / "DE94GA" / "DE94T" / "DE76")`, `delta_e_2000`, `delta_e_94`, `delta_e_76` | colour difference (matches Epson's library) |
|
|
82
|
+
| `median_lab(labs)` | per-component median of several readings (the app's "Median") |
|
|
83
|
+
| `color_scheme(lab, "complementary" / "split_complementary" / "analogous" / "triadic" / "tetradic", illuminant, observer)` | the app's "Color Scheme" |
|
|
84
|
+
| `lab_to_hex(lab, illuminant, observer)`, `lab_to_srgb`, `srgb_to_lab` | sRGB / HEX as the app shows it |
|
|
85
|
+
| `export_aco(path, colors, names)`, `read_aco(path)` | Adobe Color Swatch (.aco) files for Photoshop / Illustrator |
|
|
86
|
+
|
|
87
|
+
Illuminants: D50, D65, A, F1, F2, F3, F7–F12. Observers: 2, 10.
|
|
88
|
+
|
|
89
|
+
Errors derive from `sd10.SD10Error` (`DeviceNotFound`, `DeviceRefused`, `SD10Timeout`, `MeasurementFailed`,
|
|
90
|
+
`CoverClosed` — raised instead of measuring when the cover is shut, because the device would just repeat its
|
|
91
|
+
previous reading).
|
|
92
|
+
Enable `logging.getLogger("sd10").setLevel(logging.DEBUG)` to see all traffic.
|
|
93
|
+
|
|
94
|
+
## Command line
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
sd10 scan
|
|
98
|
+
sd10 measure --illuminant D65 --observer 10 [--spectrum] [--json]
|
|
99
|
+
sd10 listen # L*a*b* for every button press
|
|
100
|
+
sd10 history [--after ID] [--last N] [--json | --csv FILE] # readings stored on the device
|
|
101
|
+
sd10 history --aco FILE # stored readings as an Adobe Color Swatch
|
|
102
|
+
sd10 scan-row --patches 6 | --expected row.json [--tolerance 2]
|
|
103
|
+
sd10 chart SLOT [--per-row 10] # scan a printed chart of a stored palette
|
|
104
|
+
sd10 groups list | delete SLOT | export SLOT FILE(.json|.aco) | import FILE [--slot N]
|
|
105
|
+
sd10 groups from-history SLOT NAME --ids 951,952,953 [--illuminant D65 --tolerance 2]
|
|
106
|
+
sd10 info | calibrate | set-time | set-name NAME | raw ID TYPE [HEX]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
(`python -m sd10 ...` works without installing the script; global options such as `-a ADDRESS`
|
|
110
|
+
and `-v` go before the command.)
|
|
111
|
+
|
|
112
|
+
## Notes
|
|
113
|
+
|
|
114
|
+
- The SD-10 accepts one host at a time — close the Epson app on your phone first.
|
|
115
|
+
- A random 16-byte host id is kept in `~/.sd10_app_id`, like the app does.
|
|
116
|
+
- L\*a\*b\* reproduces Epson's `librmcc` conversion to ~1e-6 ΔE; bands outside 400–700 nm count as
|
|
117
|
+
zero, so a perfect white reads L\* ≈ 99.99, same as the app.
|
|
118
|
+
|
|
119
|
+
## Tests
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
pip install -e path\to\sd10_client[test]
|
|
123
|
+
pytest path\to\sd10_client\tests
|
|
124
|
+
```
|
pysd10-0.2.0/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# sd10
|
|
2
|
+
|
|
3
|
+
Python client for the **Epson SD-10** spectrophotometer over Bluetooth LE: take measurements and get
|
|
4
|
+
the 31-band spectrum (400–700 nm) and CIE L\*a\*b\* exactly as the Epson Spectrometer app computes it.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
pip install -e path\to\sd10_client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python 3.9+ and [bleak](https://github.com/hbldh/bleak) (installed automatically).
|
|
13
|
+
|
|
14
|
+
## Quick start (blocking — scripts, Jupyter)
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import sd10
|
|
18
|
+
|
|
19
|
+
result = sd10.measure_once() # connect, measure, disconnect
|
|
20
|
+
print(result.lab) # Lab(L=..., a=..., b=...)
|
|
21
|
+
|
|
22
|
+
with sd10.connect_sync() as dev: # keep the connection open
|
|
23
|
+
r = dev.measure(illuminant="D65", observer=10, m_factor=0)
|
|
24
|
+
print(r.lab, r.lch, r.spectrum)
|
|
25
|
+
print(r.lab_under("A", 2)) # same reading, other illuminant
|
|
26
|
+
|
|
27
|
+
for r in dev.button_measurements(): # react to the device's own button
|
|
28
|
+
print(r.lab)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Async
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import asyncio, sd10
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
async with sd10.connect() as dev: # strongest SD-10, or connect("40:84:32:52:FD:0F")
|
|
38
|
+
print(await dev.get_info())
|
|
39
|
+
r = await dev.measure()
|
|
40
|
+
print(r.lab)
|
|
41
|
+
async for r in dev.button_measurements():
|
|
42
|
+
print(r.lab)
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API overview
|
|
48
|
+
|
|
49
|
+
| | |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `scan()` / `scan_sync()` | nearby SD-10s (`address`, `name`, `rssi`) |
|
|
52
|
+
| `connect(address=None)` / `connect_sync(...)` | connect and open a session; on exit the device is handed back to its own UI |
|
|
53
|
+
| `dev.measure(illuminant, observer, m_factor, save)` | one spot measurement → `MeasurementResult` |
|
|
54
|
+
| `dev.button_measurements()` | iterator of results taken with the device button |
|
|
55
|
+
| `dev.get_history(after_id=0, limit=None, progress=None)` | download readings stored on the device (it keeps its last 100), oldest first; pass the last `identifier` you have as `after_id` to fetch only new ones |
|
|
56
|
+
| `dev.history_count(after_id=0)` | how many stored readings are newer than `after_id` |
|
|
57
|
+
| `dev.get_groups()` | colour groups (palettes) stored on the device: 4 slots × up to 80 L\*a\*b\* colours → `[ColorGroup]` |
|
|
58
|
+
| `dev.set_group(ColorGroup(slot, name, colors, illuminant, observer, m_factor, diff_formula, tolerance))` | store/replace a palette in slot 1–4 |
|
|
59
|
+
| `dev.delete_group(slot)` | clear a slot |
|
|
60
|
+
| `dev.calibrate()` | white calibration with the cover **closed** (raises `CoverOpen` otherwise) → `Status` |
|
|
61
|
+
| `dev.scan_row(expected=None, patches=None, tolerance=None, ...)` | scan one row of ≤18 patches by sliding the device; with `expected` + `tolerance` each patch is judged → `RowResult` |
|
|
62
|
+
| `dev.scan_palette_row(slot, row, patches_per_row=10)` | scan one row of a printed chart of a stored palette and compare with it → `RowResult` |
|
|
63
|
+
| `dev.scan_palette(slot, patches_per_row=10)` | whole chart, row by row (async iterator of `RowResult`), then `finish_chart()` |
|
|
64
|
+
| `dev.measure_palette_patch(slot, patch)` | spot-measure one patch against palette colour `patch` → `PatchResult` |
|
|
65
|
+
| `dev.get_info()`, `get_status()`, `get_name()`, `get_serial()`, `get_firmware_version()` | device info |
|
|
66
|
+
| `dev.get_setting(Setting.X)` / `set_setting(...)` | buzzer, panel direction, auto power-off, language |
|
|
67
|
+
| `dev.set_clock()`, `dev.set_name(...)` | |
|
|
68
|
+
| `dev.transact(frame)` | raw command (see `sd10.protocol`) |
|
|
69
|
+
| `MeasurementResult` | `.lab`, `.lch`, `.lab_under(ill, obs)`, `.spectrum`, `.wavelengths`, `.timestamp`, `.to_dict()` |
|
|
70
|
+
| `spectrum_to_lab(spectrum, illuminant, observer)` | offline conversion |
|
|
71
|
+
| `delta_e(ref, sample, "DE2000" / "DE94GA" / "DE94T" / "DE76")`, `delta_e_2000`, `delta_e_94`, `delta_e_76` | colour difference (matches Epson's library) |
|
|
72
|
+
| `median_lab(labs)` | per-component median of several readings (the app's "Median") |
|
|
73
|
+
| `color_scheme(lab, "complementary" / "split_complementary" / "analogous" / "triadic" / "tetradic", illuminant, observer)` | the app's "Color Scheme" |
|
|
74
|
+
| `lab_to_hex(lab, illuminant, observer)`, `lab_to_srgb`, `srgb_to_lab` | sRGB / HEX as the app shows it |
|
|
75
|
+
| `export_aco(path, colors, names)`, `read_aco(path)` | Adobe Color Swatch (.aco) files for Photoshop / Illustrator |
|
|
76
|
+
|
|
77
|
+
Illuminants: D50, D65, A, F1, F2, F3, F7–F12. Observers: 2, 10.
|
|
78
|
+
|
|
79
|
+
Errors derive from `sd10.SD10Error` (`DeviceNotFound`, `DeviceRefused`, `SD10Timeout`, `MeasurementFailed`,
|
|
80
|
+
`CoverClosed` — raised instead of measuring when the cover is shut, because the device would just repeat its
|
|
81
|
+
previous reading).
|
|
82
|
+
Enable `logging.getLogger("sd10").setLevel(logging.DEBUG)` to see all traffic.
|
|
83
|
+
|
|
84
|
+
## Command line
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
sd10 scan
|
|
88
|
+
sd10 measure --illuminant D65 --observer 10 [--spectrum] [--json]
|
|
89
|
+
sd10 listen # L*a*b* for every button press
|
|
90
|
+
sd10 history [--after ID] [--last N] [--json | --csv FILE] # readings stored on the device
|
|
91
|
+
sd10 history --aco FILE # stored readings as an Adobe Color Swatch
|
|
92
|
+
sd10 scan-row --patches 6 | --expected row.json [--tolerance 2]
|
|
93
|
+
sd10 chart SLOT [--per-row 10] # scan a printed chart of a stored palette
|
|
94
|
+
sd10 groups list | delete SLOT | export SLOT FILE(.json|.aco) | import FILE [--slot N]
|
|
95
|
+
sd10 groups from-history SLOT NAME --ids 951,952,953 [--illuminant D65 --tolerance 2]
|
|
96
|
+
sd10 info | calibrate | set-time | set-name NAME | raw ID TYPE [HEX]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
(`python -m sd10 ...` works without installing the script; global options such as `-a ADDRESS`
|
|
100
|
+
and `-v` go before the command.)
|
|
101
|
+
|
|
102
|
+
## Notes
|
|
103
|
+
|
|
104
|
+
- The SD-10 accepts one host at a time — close the Epson app on your phone first.
|
|
105
|
+
- A random 16-byte host id is kept in `~/.sd10_app_id`, like the app does.
|
|
106
|
+
- L\*a\*b\* reproduces Epson's `librmcc` conversion to ~1e-6 ΔE; bands outside 400–700 nm count as
|
|
107
|
+
zero, so a perfect white reads L\* ≈ 99.99, same as the app.
|
|
108
|
+
|
|
109
|
+
## Tests
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
pip install -e path\to\sd10_client[test]
|
|
113
|
+
pytest path\to\sd10_client\tests
|
|
114
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pysd10"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python client for the Epson SD-10 spectrophotometer over Bluetooth LE (spectrum and L*a*b*)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = ["bleak>=0.21"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
test = ["pytest"]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
sd10 = "sd10.cli:main"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools]
|
|
20
|
+
packages = ["sd10"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
sd10 = ["data/*.json", "py.typed"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pysd10
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python client for the Epson SD-10 spectrophotometer over Bluetooth LE (spectrum and L*a*b*)
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: bleak>=0.21
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest; extra == "test"
|
|
10
|
+
|
|
11
|
+
# sd10
|
|
12
|
+
|
|
13
|
+
Python client for the **Epson SD-10** spectrophotometer over Bluetooth LE: take measurements and get
|
|
14
|
+
the 31-band spectrum (400–700 nm) and CIE L\*a\*b\* exactly as the Epson Spectrometer app computes it.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
pip install -e path\to\sd10_client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Python 3.9+ and [bleak](https://github.com/hbldh/bleak) (installed automatically).
|
|
23
|
+
|
|
24
|
+
## Quick start (blocking — scripts, Jupyter)
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import sd10
|
|
28
|
+
|
|
29
|
+
result = sd10.measure_once() # connect, measure, disconnect
|
|
30
|
+
print(result.lab) # Lab(L=..., a=..., b=...)
|
|
31
|
+
|
|
32
|
+
with sd10.connect_sync() as dev: # keep the connection open
|
|
33
|
+
r = dev.measure(illuminant="D65", observer=10, m_factor=0)
|
|
34
|
+
print(r.lab, r.lch, r.spectrum)
|
|
35
|
+
print(r.lab_under("A", 2)) # same reading, other illuminant
|
|
36
|
+
|
|
37
|
+
for r in dev.button_measurements(): # react to the device's own button
|
|
38
|
+
print(r.lab)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Async
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio, sd10
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with sd10.connect() as dev: # strongest SD-10, or connect("40:84:32:52:FD:0F")
|
|
48
|
+
print(await dev.get_info())
|
|
49
|
+
r = await dev.measure()
|
|
50
|
+
print(r.lab)
|
|
51
|
+
async for r in dev.button_measurements():
|
|
52
|
+
print(r.lab)
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API overview
|
|
58
|
+
|
|
59
|
+
| | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `scan()` / `scan_sync()` | nearby SD-10s (`address`, `name`, `rssi`) |
|
|
62
|
+
| `connect(address=None)` / `connect_sync(...)` | connect and open a session; on exit the device is handed back to its own UI |
|
|
63
|
+
| `dev.measure(illuminant, observer, m_factor, save)` | one spot measurement → `MeasurementResult` |
|
|
64
|
+
| `dev.button_measurements()` | iterator of results taken with the device button |
|
|
65
|
+
| `dev.get_history(after_id=0, limit=None, progress=None)` | download readings stored on the device (it keeps its last 100), oldest first; pass the last `identifier` you have as `after_id` to fetch only new ones |
|
|
66
|
+
| `dev.history_count(after_id=0)` | how many stored readings are newer than `after_id` |
|
|
67
|
+
| `dev.get_groups()` | colour groups (palettes) stored on the device: 4 slots × up to 80 L\*a\*b\* colours → `[ColorGroup]` |
|
|
68
|
+
| `dev.set_group(ColorGroup(slot, name, colors, illuminant, observer, m_factor, diff_formula, tolerance))` | store/replace a palette in slot 1–4 |
|
|
69
|
+
| `dev.delete_group(slot)` | clear a slot |
|
|
70
|
+
| `dev.calibrate()` | white calibration with the cover **closed** (raises `CoverOpen` otherwise) → `Status` |
|
|
71
|
+
| `dev.scan_row(expected=None, patches=None, tolerance=None, ...)` | scan one row of ≤18 patches by sliding the device; with `expected` + `tolerance` each patch is judged → `RowResult` |
|
|
72
|
+
| `dev.scan_palette_row(slot, row, patches_per_row=10)` | scan one row of a printed chart of a stored palette and compare with it → `RowResult` |
|
|
73
|
+
| `dev.scan_palette(slot, patches_per_row=10)` | whole chart, row by row (async iterator of `RowResult`), then `finish_chart()` |
|
|
74
|
+
| `dev.measure_palette_patch(slot, patch)` | spot-measure one patch against palette colour `patch` → `PatchResult` |
|
|
75
|
+
| `dev.get_info()`, `get_status()`, `get_name()`, `get_serial()`, `get_firmware_version()` | device info |
|
|
76
|
+
| `dev.get_setting(Setting.X)` / `set_setting(...)` | buzzer, panel direction, auto power-off, language |
|
|
77
|
+
| `dev.set_clock()`, `dev.set_name(...)` | |
|
|
78
|
+
| `dev.transact(frame)` | raw command (see `sd10.protocol`) |
|
|
79
|
+
| `MeasurementResult` | `.lab`, `.lch`, `.lab_under(ill, obs)`, `.spectrum`, `.wavelengths`, `.timestamp`, `.to_dict()` |
|
|
80
|
+
| `spectrum_to_lab(spectrum, illuminant, observer)` | offline conversion |
|
|
81
|
+
| `delta_e(ref, sample, "DE2000" / "DE94GA" / "DE94T" / "DE76")`, `delta_e_2000`, `delta_e_94`, `delta_e_76` | colour difference (matches Epson's library) |
|
|
82
|
+
| `median_lab(labs)` | per-component median of several readings (the app's "Median") |
|
|
83
|
+
| `color_scheme(lab, "complementary" / "split_complementary" / "analogous" / "triadic" / "tetradic", illuminant, observer)` | the app's "Color Scheme" |
|
|
84
|
+
| `lab_to_hex(lab, illuminant, observer)`, `lab_to_srgb`, `srgb_to_lab` | sRGB / HEX as the app shows it |
|
|
85
|
+
| `export_aco(path, colors, names)`, `read_aco(path)` | Adobe Color Swatch (.aco) files for Photoshop / Illustrator |
|
|
86
|
+
|
|
87
|
+
Illuminants: D50, D65, A, F1, F2, F3, F7–F12. Observers: 2, 10.
|
|
88
|
+
|
|
89
|
+
Errors derive from `sd10.SD10Error` (`DeviceNotFound`, `DeviceRefused`, `SD10Timeout`, `MeasurementFailed`,
|
|
90
|
+
`CoverClosed` — raised instead of measuring when the cover is shut, because the device would just repeat its
|
|
91
|
+
previous reading).
|
|
92
|
+
Enable `logging.getLogger("sd10").setLevel(logging.DEBUG)` to see all traffic.
|
|
93
|
+
|
|
94
|
+
## Command line
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
sd10 scan
|
|
98
|
+
sd10 measure --illuminant D65 --observer 10 [--spectrum] [--json]
|
|
99
|
+
sd10 listen # L*a*b* for every button press
|
|
100
|
+
sd10 history [--after ID] [--last N] [--json | --csv FILE] # readings stored on the device
|
|
101
|
+
sd10 history --aco FILE # stored readings as an Adobe Color Swatch
|
|
102
|
+
sd10 scan-row --patches 6 | --expected row.json [--tolerance 2]
|
|
103
|
+
sd10 chart SLOT [--per-row 10] # scan a printed chart of a stored palette
|
|
104
|
+
sd10 groups list | delete SLOT | export SLOT FILE(.json|.aco) | import FILE [--slot N]
|
|
105
|
+
sd10 groups from-history SLOT NAME --ids 951,952,953 [--illuminant D65 --tolerance 2]
|
|
106
|
+
sd10 info | calibrate | set-time | set-name NAME | raw ID TYPE [HEX]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
(`python -m sd10 ...` works without installing the script; global options such as `-a ADDRESS`
|
|
110
|
+
and `-v` go before the command.)
|
|
111
|
+
|
|
112
|
+
## Notes
|
|
113
|
+
|
|
114
|
+
- The SD-10 accepts one host at a time — close the Epson app on your phone first.
|
|
115
|
+
- A random 16-byte host id is kept in `~/.sd10_app_id`, like the app does.
|
|
116
|
+
- L\*a\*b\* reproduces Epson's `librmcc` conversion to ~1e-6 ΔE; bands outside 400–700 nm count as
|
|
117
|
+
zero, so a perfect white reads L\* ≈ 99.99, same as the app.
|
|
118
|
+
|
|
119
|
+
## Tests
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
pip install -e path\to\sd10_client[test]
|
|
123
|
+
pytest path\to\sd10_client\tests
|
|
124
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pysd10.egg-info/PKG-INFO
|
|
4
|
+
pysd10.egg-info/SOURCES.txt
|
|
5
|
+
pysd10.egg-info/dependency_links.txt
|
|
6
|
+
pysd10.egg-info/entry_points.txt
|
|
7
|
+
pysd10.egg-info/requires.txt
|
|
8
|
+
pysd10.egg-info/top_level.txt
|
|
9
|
+
sd10/__init__.py
|
|
10
|
+
sd10/__main__.py
|
|
11
|
+
sd10/cli.py
|
|
12
|
+
sd10/client.py
|
|
13
|
+
sd10/color.py
|
|
14
|
+
sd10/export.py
|
|
15
|
+
sd10/protocol.py
|
|
16
|
+
sd10/py.typed
|
|
17
|
+
sd10/sync.py
|
|
18
|
+
sd10/data/lab_weights.json
|
|
19
|
+
tests/test_sd10.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sd10
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Python client for the Epson SD-10 spectrophotometer over Bluetooth LE.
|
|
2
|
+
|
|
3
|
+
Async::
|
|
4
|
+
|
|
5
|
+
import asyncio, sd10
|
|
6
|
+
|
|
7
|
+
async def main():
|
|
8
|
+
async with sd10.connect() as dev:
|
|
9
|
+
result = await dev.measure(illuminant="D65", observer=10)
|
|
10
|
+
print(result.lab, result.lch)
|
|
11
|
+
|
|
12
|
+
asyncio.run(main())
|
|
13
|
+
|
|
14
|
+
Blocking::
|
|
15
|
+
|
|
16
|
+
import sd10
|
|
17
|
+
print(sd10.measure_once().lab)
|
|
18
|
+
|
|
19
|
+
with sd10.connect_sync() as dev:
|
|
20
|
+
for result in dev.button_measurements():
|
|
21
|
+
print(result.lab)
|
|
22
|
+
"""
|
|
23
|
+
from .client import (SD10, CalibrationNeeded, CoverClosed, CoverOpen, DeviceInfo, DeviceNotFound, DeviceRefused,
|
|
24
|
+
FoundDevice, MeasurementFailed, PatchResult, RowResult, SD10Error, SD10Timeout, connect,
|
|
25
|
+
find_device, load_app_id, scan)
|
|
26
|
+
from .color import (COLOR_SCHEMES, DELTA_E_FORMULAS, ILLUMINANTS, OBSERVERS, WAVELENGTHS, Lab, LCh, color_scheme,
|
|
27
|
+
delta_e, delta_e_76, delta_e_94, delta_e_2000, lab_to_hex, lab_to_srgb, median_lab,
|
|
28
|
+
spectrum_to_lab, srgb_to_lab)
|
|
29
|
+
from .export import aco_bytes, export_aco, read_aco
|
|
30
|
+
from .protocol import ColorGroup, Event, MeasurementResult, Method, Setting, Status
|
|
31
|
+
from .sync import SyncSD10, connect_sync, measure_once, scan_sync
|
|
32
|
+
|
|
33
|
+
__version__ = "0.2.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"SD10", "connect", "scan", "find_device", "load_app_id", "DeviceInfo", "FoundDevice",
|
|
37
|
+
"SyncSD10", "connect_sync", "scan_sync", "measure_once",
|
|
38
|
+
"MeasurementResult", "Status", "Setting", "Event", "Method", "ColorGroup", "PatchResult", "RowResult",
|
|
39
|
+
"Lab", "LCh", "spectrum_to_lab", "delta_e", "delta_e_76", "delta_e_94", "delta_e_2000", "median_lab",
|
|
40
|
+
"color_scheme", "lab_to_srgb", "srgb_to_lab", "lab_to_hex", "COLOR_SCHEMES", "DELTA_E_FORMULAS",
|
|
41
|
+
"ILLUMINANTS", "OBSERVERS", "WAVELENGTHS", "export_aco", "aco_bytes", "read_aco",
|
|
42
|
+
"SD10Error", "SD10Timeout", "DeviceNotFound", "DeviceRefused", "MeasurementFailed", "CoverClosed",
|
|
43
|
+
"CalibrationNeeded", "CoverOpen",
|
|
44
|
+
]
|