vention-barcode-scanner 0.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.
Files changed (28) hide show
  1. vention_barcode_scanner-0.1.0/.gitignore +24 -0
  2. vention_barcode_scanner-0.1.0/PKG-INFO +162 -0
  3. vention_barcode_scanner-0.1.0/README.md +153 -0
  4. vention_barcode_scanner-0.1.0/config/config.local.yaml.example +17 -0
  5. vention_barcode_scanner-0.1.0/config/config.yaml +25 -0
  6. vention_barcode_scanner-0.1.0/docs/planning/current-state.md +28 -0
  7. vention_barcode_scanner-0.1.0/project.json +30 -0
  8. vention_barcode_scanner-0.1.0/pyproject.toml +50 -0
  9. vention_barcode_scanner-0.1.0/src/barcode_scanner/__init__.py +8 -0
  10. vention_barcode_scanner-0.1.0/src/barcode_scanner/__main__.py +201 -0
  11. vention_barcode_scanner-0.1.0/src/barcode_scanner/_compat.py +22 -0
  12. vention_barcode_scanner-0.1.0/src/barcode_scanner/config.py +124 -0
  13. vention_barcode_scanner-0.1.0/src/barcode_scanner/log_codes.py +124 -0
  14. vention_barcode_scanner-0.1.0/src/barcode_scanner/logger.py +85 -0
  15. vention_barcode_scanner-0.1.0/src/barcode_scanner/models.py +218 -0
  16. vention_barcode_scanner-0.1.0/src/barcode_scanner/scanners/__init__.py +22 -0
  17. vention_barcode_scanner-0.1.0/src/barcode_scanner/scanners/base.py +112 -0
  18. vention_barcode_scanner-0.1.0/src/barcode_scanner/scanners/evdev.py +247 -0
  19. vention_barcode_scanner-0.1.0/src/barcode_scanner/scanners/keyence.py +720 -0
  20. vention_barcode_scanner-0.1.0/src/barcode_scanner/scanners/mock.py +78 -0
  21. vention_barcode_scanner-0.1.0/src/barcode_scanner/service.py +392 -0
  22. vention_barcode_scanner-0.1.0/tests/__init__.py +0 -0
  23. vention_barcode_scanner-0.1.0/tests/test_config.py +110 -0
  24. vention_barcode_scanner-0.1.0/tests/test_keyence_scanner.py +662 -0
  25. vention_barcode_scanner-0.1.0/tests/test_logger.py +93 -0
  26. vention_barcode_scanner-0.1.0/tests/test_mock_scanner.py +81 -0
  27. vention_barcode_scanner-0.1.0/tests/test_models.py +171 -0
  28. vention_barcode_scanner-0.1.0/tests/test_service.py +297 -0
@@ -0,0 +1,24 @@
1
+ # Node / Nx
2
+ node_modules/
3
+ dist/
4
+ .nx/
5
+ tmp/
6
+ coverage/
7
+
8
+ # Python / uv
9
+ .venv/
10
+ **/.venv/
11
+ __pycache__/
12
+ *.pyc
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ coverage.xml
17
+ htmlcov/
18
+
19
+ # Editor / OS
20
+ .DS_Store
21
+ *.log
22
+
23
+ # Claude Code (local, per-developer)
24
+ .claude/
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: vention-barcode-scanner
3
+ Version: 0.1.0
4
+ Summary: Async barcode scanner fleet manager for Vention industrial automation. Supports Keyence TCP, USB evdev, and mock scanners.
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: pydantic>=2.0
7
+ Requires-Dist: pyyaml>=6.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # vention-barcode-scanner
11
+
12
+ Async barcode scanner fleet manager for Vention industrial automation. Unified `Scanner` ABC over Keyence TCP, USB HID (evdev), and mock backends.
13
+
14
+ ## Scanner Types
15
+
16
+ | Type | Protocol | Platform | Use Case |
17
+ |------|----------|----------|----------|
18
+ | `KeyenceScanner` | Async TCP (port 9004) | Any | Keyence SR-series (SR-1000, SR-2000, SR-5000) |
19
+ | `EvdevScanner` | Linux USB (evdev) | Linux | USB keyboard-emulating barcode readers |
20
+ | `MockScanner` | In-memory | Any | Simulation and testing |
21
+
22
+ ## Usage
23
+
24
+ ```python
25
+ from barcode_scanner.models import ScannerConfig, ScannerFleetConfig
26
+ from barcode_scanner.service import ScannerService
27
+
28
+ config = ScannerFleetConfig(
29
+ scanners=[
30
+ ScannerConfig(id="s1", host="192.168.7.100", location_id="station-a"),
31
+ ScannerConfig(id="s2", host="192.168.7.101", location_id="station-b"),
32
+ ],
33
+ )
34
+ service = ScannerService.from_config(config, mode="real", scanner_type="keyence")
35
+
36
+ await service.connect_all()
37
+ result = await service.scan("station-a")
38
+ await service.disconnect_all()
39
+
40
+ result.status # ScanStatus.OK | NO_READ | TIMEOUT | ERROR
41
+ result.label_detected # True / False
42
+ result.label_string # "GT-205" or None
43
+ ```
44
+
45
+ ### Label Validation
46
+
47
+ Optional callback to reject invalid labels at the scanner layer:
48
+
49
+ ```python
50
+ def validate_label(label: str) -> bool:
51
+ return label.startswith("GT-") and len(label) <= 20
52
+
53
+ service = ScannerService.from_config(config, label_validator=validate_label)
54
+ # Labels that fail validation are downgraded to NO_READ
55
+ ```
56
+
57
+ ### Metrics
58
+
59
+ ```python
60
+ metrics = service.get_metrics()["s1"]
61
+ metrics.total_scans # 1234
62
+ metrics.success_rate # 0.89
63
+ metrics.avg_scan_ms # 45.2
64
+ ```
65
+
66
+ ### Tuning and Diagnostics
67
+
68
+ ```python
69
+ await service.auto_tune("station-a") # One-shot FTUNE + SAVE
70
+ await service.tune("station-a", bank=0) # Interactive TUNE per bank + TQUIT
71
+ status = await service.get_scanner_status("station-a")
72
+ # {"busy": 0, "last_cmd": 0, "error": 0} # BUSYSTAT / CMDSTAT / ERRSTAT
73
+ ```
74
+
75
+ ### Structured Logging
76
+
77
+ ```python
78
+ from barcode_scanner.logger import set_log_callback
79
+
80
+ def on_scanner_log(code, source, level, message):
81
+ mqtt_publish("scanner/logs", {"code": code, "source": source, "message": message})
82
+
83
+ set_log_callback(on_scanner_log)
84
+ ```
85
+
86
+ ## Keyence Protocol
87
+
88
+ TCP port 9004. ASCII commands terminated with `\r`.
89
+
90
+ **Scan sequence:** `LON\r` (open scan window) -> dwell -> `LOFF\r` (close window) -> read response. The scanner only sends data after LOFF.
91
+
92
+ | Command | Response | Purpose |
93
+ |---------|----------|---------|
94
+ | `LON\r` | barcode or `ERROR\r` | Open scan window (default bank) |
95
+ | `LON,01\r` | barcode or `ERROR\r` | Open scan window (bank 1) |
96
+ | `LOFF\r` | -- | Close scan window |
97
+ | `BCLR\r` | `OK\r` | Clear read buffer |
98
+ | `RESET\r` | `OK\r` | Reset scanner (reboots, drops TCP) |
99
+ | `FTUNE\r` | `OK,FTUNE\r` then result | One-shot auto-focus calibration |
100
+ | `TUNE,00\r` | `OK\r` | Start interactive tuning (bank 0) |
101
+ | `TQUIT\r` | `OK\r` | Stop tuning and save |
102
+ | `SAVE\r` | `OK\r` | Persist settings across power cycles |
103
+ | `BUSYSTAT\r` | `0`/`1`/`2` | Query busy state (idle/reading/tuning) |
104
+ | `CMDSTAT\r` | `0`/`1`/`2` | Query last command result |
105
+ | `ERRSTAT\r` | `0`/`1`/`2` | Query hardware error state |
106
+
107
+ ## API
108
+
109
+ ### ScannerService
110
+
111
+ ```python
112
+ class ScannerService:
113
+ @classmethod
114
+ def from_config(cls, config, mode="real", scanner_type="keyence",
115
+ label_validator=None) -> ScannerService
116
+
117
+ async def scan(self, location_id, timeout=None, bank=None) -> ScanResult
118
+ async def connect_all(self) -> dict[str, bool]
119
+ async def disconnect_all(self) -> None
120
+ async def reset(self, location_id) -> bool
121
+ async def reset_all(self) -> dict[str, bool]
122
+ async def auto_tune(self, location_id) -> bool
123
+ async def tune(self, location_id, bank=0) -> bool
124
+ async def get_scanner_status(self, location_id) -> dict
125
+ def get_metrics(self) -> dict[str, ScanMetrics]
126
+ def get_metrics_summary(self) -> dict
127
+ ```
128
+
129
+ ### ScanResult
130
+
131
+ ```python
132
+ class ScanResult:
133
+ status: ScanStatus # OK | NO_READ | TIMEOUT | ERROR
134
+ label_detected: bool
135
+ label_string: str | None
136
+ scanner_id: str
137
+ error: str | None
138
+ ```
139
+
140
+ ### Scanner ABC
141
+
142
+ ```python
143
+ class Scanner(ABC):
144
+ async def scan(self, timeout=None, bank=None) -> ScanResult
145
+ async def connect(self) -> bool
146
+ async def disconnect(self) -> None
147
+ async def reset(self) -> bool
148
+ async def auto_tune(self) -> bool
149
+ async def tune(self, bank=0) -> bool
150
+ async def get_status(self) -> dict
151
+ connected: bool # property
152
+ ```
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ cd barcode-scanner
158
+ uv sync
159
+ make test # 115 tests
160
+ make lint # ruff check + format
161
+ make type-check # pyright
162
+ ```
@@ -0,0 +1,153 @@
1
+ # vention-barcode-scanner
2
+
3
+ Async barcode scanner fleet manager for Vention industrial automation. Unified `Scanner` ABC over Keyence TCP, USB HID (evdev), and mock backends.
4
+
5
+ ## Scanner Types
6
+
7
+ | Type | Protocol | Platform | Use Case |
8
+ |------|----------|----------|----------|
9
+ | `KeyenceScanner` | Async TCP (port 9004) | Any | Keyence SR-series (SR-1000, SR-2000, SR-5000) |
10
+ | `EvdevScanner` | Linux USB (evdev) | Linux | USB keyboard-emulating barcode readers |
11
+ | `MockScanner` | In-memory | Any | Simulation and testing |
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from barcode_scanner.models import ScannerConfig, ScannerFleetConfig
17
+ from barcode_scanner.service import ScannerService
18
+
19
+ config = ScannerFleetConfig(
20
+ scanners=[
21
+ ScannerConfig(id="s1", host="192.168.7.100", location_id="station-a"),
22
+ ScannerConfig(id="s2", host="192.168.7.101", location_id="station-b"),
23
+ ],
24
+ )
25
+ service = ScannerService.from_config(config, mode="real", scanner_type="keyence")
26
+
27
+ await service.connect_all()
28
+ result = await service.scan("station-a")
29
+ await service.disconnect_all()
30
+
31
+ result.status # ScanStatus.OK | NO_READ | TIMEOUT | ERROR
32
+ result.label_detected # True / False
33
+ result.label_string # "GT-205" or None
34
+ ```
35
+
36
+ ### Label Validation
37
+
38
+ Optional callback to reject invalid labels at the scanner layer:
39
+
40
+ ```python
41
+ def validate_label(label: str) -> bool:
42
+ return label.startswith("GT-") and len(label) <= 20
43
+
44
+ service = ScannerService.from_config(config, label_validator=validate_label)
45
+ # Labels that fail validation are downgraded to NO_READ
46
+ ```
47
+
48
+ ### Metrics
49
+
50
+ ```python
51
+ metrics = service.get_metrics()["s1"]
52
+ metrics.total_scans # 1234
53
+ metrics.success_rate # 0.89
54
+ metrics.avg_scan_ms # 45.2
55
+ ```
56
+
57
+ ### Tuning and Diagnostics
58
+
59
+ ```python
60
+ await service.auto_tune("station-a") # One-shot FTUNE + SAVE
61
+ await service.tune("station-a", bank=0) # Interactive TUNE per bank + TQUIT
62
+ status = await service.get_scanner_status("station-a")
63
+ # {"busy": 0, "last_cmd": 0, "error": 0} # BUSYSTAT / CMDSTAT / ERRSTAT
64
+ ```
65
+
66
+ ### Structured Logging
67
+
68
+ ```python
69
+ from barcode_scanner.logger import set_log_callback
70
+
71
+ def on_scanner_log(code, source, level, message):
72
+ mqtt_publish("scanner/logs", {"code": code, "source": source, "message": message})
73
+
74
+ set_log_callback(on_scanner_log)
75
+ ```
76
+
77
+ ## Keyence Protocol
78
+
79
+ TCP port 9004. ASCII commands terminated with `\r`.
80
+
81
+ **Scan sequence:** `LON\r` (open scan window) -> dwell -> `LOFF\r` (close window) -> read response. The scanner only sends data after LOFF.
82
+
83
+ | Command | Response | Purpose |
84
+ |---------|----------|---------|
85
+ | `LON\r` | barcode or `ERROR\r` | Open scan window (default bank) |
86
+ | `LON,01\r` | barcode or `ERROR\r` | Open scan window (bank 1) |
87
+ | `LOFF\r` | -- | Close scan window |
88
+ | `BCLR\r` | `OK\r` | Clear read buffer |
89
+ | `RESET\r` | `OK\r` | Reset scanner (reboots, drops TCP) |
90
+ | `FTUNE\r` | `OK,FTUNE\r` then result | One-shot auto-focus calibration |
91
+ | `TUNE,00\r` | `OK\r` | Start interactive tuning (bank 0) |
92
+ | `TQUIT\r` | `OK\r` | Stop tuning and save |
93
+ | `SAVE\r` | `OK\r` | Persist settings across power cycles |
94
+ | `BUSYSTAT\r` | `0`/`1`/`2` | Query busy state (idle/reading/tuning) |
95
+ | `CMDSTAT\r` | `0`/`1`/`2` | Query last command result |
96
+ | `ERRSTAT\r` | `0`/`1`/`2` | Query hardware error state |
97
+
98
+ ## API
99
+
100
+ ### ScannerService
101
+
102
+ ```python
103
+ class ScannerService:
104
+ @classmethod
105
+ def from_config(cls, config, mode="real", scanner_type="keyence",
106
+ label_validator=None) -> ScannerService
107
+
108
+ async def scan(self, location_id, timeout=None, bank=None) -> ScanResult
109
+ async def connect_all(self) -> dict[str, bool]
110
+ async def disconnect_all(self) -> None
111
+ async def reset(self, location_id) -> bool
112
+ async def reset_all(self) -> dict[str, bool]
113
+ async def auto_tune(self, location_id) -> bool
114
+ async def tune(self, location_id, bank=0) -> bool
115
+ async def get_scanner_status(self, location_id) -> dict
116
+ def get_metrics(self) -> dict[str, ScanMetrics]
117
+ def get_metrics_summary(self) -> dict
118
+ ```
119
+
120
+ ### ScanResult
121
+
122
+ ```python
123
+ class ScanResult:
124
+ status: ScanStatus # OK | NO_READ | TIMEOUT | ERROR
125
+ label_detected: bool
126
+ label_string: str | None
127
+ scanner_id: str
128
+ error: str | None
129
+ ```
130
+
131
+ ### Scanner ABC
132
+
133
+ ```python
134
+ class Scanner(ABC):
135
+ async def scan(self, timeout=None, bank=None) -> ScanResult
136
+ async def connect(self) -> bool
137
+ async def disconnect(self) -> None
138
+ async def reset(self) -> bool
139
+ async def auto_tune(self) -> bool
140
+ async def tune(self, bank=0) -> bool
141
+ async def get_status(self) -> dict
142
+ connected: bool # property
143
+ ```
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ cd barcode-scanner
149
+ uv sync
150
+ make test # 115 tests
151
+ make lint # ruff check + format
152
+ make type-check # pyright
153
+ ```
@@ -0,0 +1,17 @@
1
+ # Local development overrides for barcode scanner service
2
+ # Copy to config.local.yaml and customize for your environment.
3
+ # This file is gitignored and will not be committed.
4
+ #
5
+ # Only include values you want to override — everything else
6
+ # falls through to config.yaml defaults.
7
+
8
+ # Force mock mode for local development (no hardware needed)
9
+ scanner_type: "mock"
10
+
11
+ # Increase mock label probability for easier testing
12
+ mock_label_probability: 0.5
13
+ mock_labels: ["LABEL-001", "LABEL-002", "LABEL-003", "LABEL-EMPTY"]
14
+
15
+ # Override fleet with localhost scanners (for Keyence simulator testing)
16
+ # fleet:
17
+ # - { id: "scanner-1", host: "127.0.0.1", port: 9004, location_id: "station-1" }
@@ -0,0 +1,25 @@
1
+ # vention-barcode-scanner — Example Fleet Configuration
2
+ #
3
+ # Each scanner needs a unique ID, its IP address,
4
+ # and a location_id that the host application uses to trigger scans.
5
+ #
6
+ # Local overrides: config/config.local.yaml (gitignored)
7
+
8
+ scanner_type: "keyence"
9
+
10
+ default_timeout: 2.0
11
+ reconnect_delay: 5.0
12
+ max_reconnect_attempts: 0 # 0 = unlimited
13
+ max_scan_retries: 2
14
+ scan_retry_delay: 0.3
15
+ keepalive_interval: 10.0 # BCLR ping every 10s to detect dead TCP connections
16
+
17
+ mock_label_probability: 0.3
18
+ mock_labels: ["LABEL-001", "LABEL-002", "LABEL-003"]
19
+
20
+ fleet:
21
+ - { id: "scanner-1", host: "192.168.1.100", port: 9004, location_id: "zone-a" }
22
+ - { id: "scanner-2", host: "192.168.1.101", port: 9004, location_id: "zone-b" }
23
+
24
+ # Disabled scanner (skipped during fleet init)
25
+ # - { id: "scanner-3", host: "192.168.1.102", port: 9004, location_id: "zone-c", enabled: false }
@@ -0,0 +1,28 @@
1
+ # Current state — barcode-scanner
2
+
3
+ What's actually built in `barcode-scanner` **now** — the fastest orientation for
4
+ a new dev or agent. Read this first. Distinct from siblings:
5
+
6
+ - **Repo-wide state** — [`../../../../docs/planning/current-state.md`](../../../../docs/planning/current-state.md).
7
+ - `backlog.md` / `open-questions.md` / `tech-debt.md` — seeded here if/when this
8
+ lib has its own; it has none today (no empty stubs).
9
+
10
+ It **grows by pruning, not accretion** — superseded state is rewritten, not
11
+ appended. Last updated: 2026-06-03.
12
+
13
+ ## What's working
14
+
15
+ - Async **scanner fleet manager** — a `Scanner` ABC + `KeyenceScanner` (TCP),
16
+ `EvdevScanner` (USB, Linux), `MockScanner`, behind `ScannerService`
17
+ (location-id → scanner). Config loader, log codes, structured logger.
18
+ - **118 tests, 86% coverage**, ruff clean. Lifted from The Feed; unchanged in
19
+ behavior.
20
+ - `evdev` is Linux/USB-only (omitted from coverage; exercised on CI).
21
+
22
+ ## Recently landed
23
+
24
+ - 2026-06-02 — lifted into delivery-products as `scope:device`; `pytest-cov` wired.
25
+
26
+ <!-- No backlog.md: no open lib-specific work. Repo-wide CI coverage gate
27
+ (≥80%) is tracked in ../../../../docs/planning/backlog.md — barcode-scanner
28
+ is at 86%. -->
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "barcode-scanner",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "projectType": "library",
5
+ "sourceRoot": "libs/barcode-scanner/src",
6
+ "tags": ["scope:device", "type:backend"],
7
+ "release": {
8
+ "version": { "generator": "@nxlv/python:release-version" }
9
+ },
10
+ "targets": {
11
+ "nx-release-publish": {
12
+ "executor": "nx:run-commands",
13
+ "options": {
14
+ "command": "bash tools/publish-codeartifact.sh --projectRoot={projectRoot} --dryRun={args.dryRun}"
15
+ }
16
+ },
17
+ "lint": {
18
+ "executor": "nx:run-commands",
19
+ "options": { "command": "uv run ruff check .", "cwd": "libs/barcode-scanner" }
20
+ },
21
+ "test": {
22
+ "executor": "nx:run-commands",
23
+ "options": { "command": "uv run pytest -q --cov=barcode_scanner --cov-report=term-missing", "cwd": "libs/barcode-scanner" }
24
+ },
25
+ "check": {
26
+ "executor": "nx:run-commands",
27
+ "options": { "command": "uv run python -c \"import barcode_scanner\"", "cwd": "libs/barcode-scanner" }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "vention-barcode-scanner"
3
+ version = "0.1.0"
4
+ description = "Async barcode scanner fleet manager for Vention industrial automation. Supports Keyence TCP, USB evdev, and mock scanners."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "pydantic>=2.0",
9
+ "pyyaml>=6.0",
10
+ ]
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["src/barcode_scanner"]
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=8.3.4",
22
+ "pytest-asyncio>=0.24.0",
23
+ "pytest-cov>=5.0",
24
+ "ruff>=0.8.0",
25
+ ]
26
+
27
+ [tool.ruff]
28
+ line-length = 100
29
+ target-version = "py310"
30
+
31
+ [tool.ruff.lint]
32
+ select = ["E", "F", "I", "W"]
33
+
34
+ [tool.pytest.ini_options]
35
+ asyncio_mode = "auto"
36
+ testpaths = ["tests"]
37
+
38
+ [tool.coverage.run]
39
+ source = ["barcode_scanner"]
40
+ omit = [
41
+ "src/barcode_scanner/__main__.py", # CLI dev tool, not library code
42
+ "src/barcode_scanner/scanners/evdev.py", # Linux-only, tested on CI
43
+ ]
44
+
45
+ [tool.coverage.report]
46
+ show_missing = true
47
+ # CI coverage gate (backlog item, now wired): a device lib asserts full coverage,
48
+ # so a drop below 80% fails the build. Currently ≈86%. The `test` target collects
49
+ # coverage and CI runs it (affected + the Python matrix), so fail_under is enforced.
50
+ fail_under = 80
@@ -0,0 +1,8 @@
1
+ """Vention barcode scanner library.
2
+
3
+ Unified async abstraction over different barcode scanner hardware.
4
+ This is a library, not a service — no HTTP server, no separate process.
5
+ Host applications import it and call service.scan(location_id) in-process.
6
+
7
+ Supports Keyence (TCP), Evdev (USB), and Mock scanner types.
8
+ """