Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,17 @@ Before running the library on a Pi:
- connect `TX -> RX`, `RX -> TX`, and `GND -> GND`

```python
from xkc_kl200_python import XKC_KL200
"""Read one XKC-KL200 measurement."""

with XKC_KL200(port="/dev/serial0", baudrate=9600) as sensor:
distance_mm = sensor.read_distance()
print(f"Distance: {distance_mm} mm")
import logging

from xkc_kl200_python import XkcKl200

logging.basicConfig(level=logging.INFO, format="%(message)s")
_LOGGER = logging.getLogger(__name__)

with XkcKl200(port="/dev/serial0", baudrate=9600) as sensor:
_LOGGER.info("Distance: %s mm", sensor.read_distance())
```

The library requests exclusive access when opening the serial port, so another
Expand All @@ -58,16 +64,31 @@ mixed reads later.
If you want continuous data, loop in your own application:

```python
"""Continuously read XKC-KL200 measurements."""

import logging
import time

from xkc_kl200_python import XKC_KL200, XKC_KL200_ReadError
from xkc_kl200_python import XkcKl200, XkcKl200ReadError

_LOGGER = logging.getLogger(__name__)


def log_distance(sensor: XkcKl200) -> None:
"""Read and log one measurement or the resulting sensor error."""
try:
distance_mm = sensor.read_distance()
except XkcKl200ReadError:
_LOGGER.exception("Sensor read failed")
else:
_LOGGER.info("Distance: %s mm", distance_mm)


logging.basicConfig(level=logging.INFO, format="%(message)s")

with XKC_KL200(port="/dev/serial0", baudrate=9600) as sensor:
with XkcKl200(port="/dev/serial0", baudrate=9600) as sensor:
while True:
try:
print(sensor.read_distance())
except XKC_KL200_ReadError:
print("Read failed")
log_distance(sensor)
time.sleep(0.1)
```

Expand Down
12 changes: 10 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@
`SensorConfig` stores the serial connection parameters used by the library:

```python
from xkc_kl200_python import SensorConfig, XKC_KL200
"""Configure an XKC-KL200 sensor connection."""

import logging

from xkc_kl200_python import SensorConfig, XkcKl200

logging.basicConfig(level=logging.INFO, format="%(message)s")
_LOGGER = logging.getLogger(__name__)

config = SensorConfig(
port="/dev/ttyUSB0",
Expand All @@ -17,7 +24,8 @@ config = SensorConfig(
startup_delay_s=0.1,
)

sensor = XKC_KL200(config=config)
with XkcKl200(config=config) as sensor:
_LOGGER.info("Sensor configured at address %#06x", sensor.address)
```

---
Expand Down
52 changes: 41 additions & 11 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@
## Basic Initialization

```python
from xkc_kl200_python import XKC_KL200
"""Initialize an XKC-KL200 sensor connection."""

sensor = XKC_KL200(port="/dev/serial0", baudrate=9600)
import logging

from xkc_kl200_python import XkcKl200

logging.basicConfig(level=logging.INFO, format="%(message)s")
_LOGGER = logging.getLogger(__name__)

with XkcKl200(port="/dev/serial0", baudrate=9600) as sensor:
_LOGGER.info("Sensor ready at address %#06x", sensor.address)
```

The default connection settings are:
Expand All @@ -31,27 +39,49 @@ Before using the library on a Pi:
## Manual Distance Read

```python
from xkc_kl200_python import XKC_KL200
"""Read one XKC-KL200 measurement."""

import logging

from xkc_kl200_python import XkcKl200

logging.basicConfig(level=logging.INFO, format="%(message)s")
_LOGGER = logging.getLogger(__name__)

with XKC_KL200(port="/dev/serial0", baudrate=9600) as sensor:
print(sensor.read_distance())
with XkcKl200(port="/dev/serial0", baudrate=9600) as sensor:
_LOGGER.info("Distance: %s mm", sensor.read_distance())
```

---

## Looping In Your App

```python
"""Continuously read XKC-KL200 measurements."""

import logging
import time

from xkc_kl200_python import XKC_KL200, XKC_KL200_ReadError
from xkc_kl200_python import XkcKl200, XkcKl200ReadError

_LOGGER = logging.getLogger(__name__)


def log_distance(sensor: XkcKl200) -> None:
"""Read and log one measurement or the resulting sensor error."""
try:
distance_mm = sensor.read_distance()
except XkcKl200ReadError:
_LOGGER.exception("Sensor read failed")
else:
_LOGGER.info("Distance: %s mm", distance_mm)


logging.basicConfig(level=logging.INFO, format="%(message)s")

with XKC_KL200(port="/dev/serial0", baudrate=9600) as sensor:
with XkcKl200(port="/dev/serial0", baudrate=9600) as sensor:
while True:
try:
print(sensor.read_distance())
except XKC_KL200_ReadError:
print("Read failed")
log_distance(sensor)
time.sleep(0.1)
```

Expand Down
4 changes: 2 additions & 2 deletions examples/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import logging

from xkc_kl200_python import XKC_KL200
from xkc_kl200_python import XkcKl200

_LOGGER = logging.getLogger(__name__)


def main() -> None:
"""Read and display one distance measurement."""
logging.basicConfig(level=logging.INFO, format="%(message)s")
with XKC_KL200(port="/dev/ttyUSB0") as sensor:
with XkcKl200(port="/dev/ttyUSB0") as sensor:
_LOGGER.info("Distance: %s mm", sensor.read_distance())


Expand Down
125 changes: 82 additions & 43 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
[build-system]
requires = ["uv_build>=0.11.19,<0.12.0"]
build-backend = "uv_build"

[project]
name = "xkc-kl200-python"
version = "6.0.0"
description = "A typed Python library for controlling the XKC-KL200 UART laser distance sensor."
readme = "README.md"
authors = [
{ name = "sam0rr", email = "samor140@hotmail.com" }
{ name = "sam0rr", email = "samor140@hotmail.com" },
]
requires-python = ">=3.13"
dependencies = [
"pyserial>=3.5",
]

[build-system]
requires = ["uv_build>=0.11.19,<0.12.0"]
build-backend = "uv_build"

[dependency-groups]
dev = [
"mypy>=2.1.0",
Expand All @@ -25,83 +25,122 @@ dev = [
"vulture>=2.16",
]

[tool.pytest.ini_options]
addopts = "-v --maxfail=1 --cov=xkc_kl200_python --cov-fail-under=100"

[tool.mypy]
strict = true
disallow_any_explicit = true
files = ["src", "tests"]

[tool.ruff]
target-version = "py313"

[tool.ruff.lint]
# Based on the official Ruff configurations used by FastAPI, Pydantic,
# Pydantic AI, and HTTPX:
# Informed by the official Ruff configurations used by FastAPI, Pydantic,
# Pydantic AI, and HTTPX, then tailored for strict typed projects:
# https://github.com/fastapi/fastapi/blob/master/pyproject.toml
# https://github.com/pydantic/pydantic/blob/main/pyproject.toml
# https://github.com/pydantic/pydantic-ai/blob/main/pyproject.toml
# https://github.com/encode/httpx/blob/master/pyproject.toml
select = [
# Core correctness.
# Core correctness and safety.
"E", # Pycodestyle errors
"W", # Pycodestyle warnings
"F", # Pyflakes correctness
"B", # Flake8 Bugbear
"B", # Flake8 Bugbear correctness checks
"A004", # Prevent imports from shadowing built-ins
"A005", # Prevent modules from shadowing the standard library
"ARG", # Reject unused callable arguments
"PLE", # Pylint error-level correctness checks

# FastAPI and asynchronous correctness.
"FAST", # FastAPI correctness and modern parameter declarations
"ASYNC", # Blocking and unsafe async patterns
"RUF006", # Dangling asyncio tasks
"PLW", # Pylint warning-level correctness checks
"S", # Bandit-derived security checks
"TRY004", # Use TypeError for invalid argument types

# Typing and dataclass correctness.
"PYI034", # Use Self for self-returning special methods
"RUF008", # Reject mutable dataclass defaults
"RUF009", # Reject function calls in dataclass defaults
"RUF012", # Require ClassVar for mutable class attributes
"RUF033", # Reject defaults on __post_init__ parameters
"RUF049", # Reject dataclass-decorated Enum classes

# Targeted runtime and data correctness.
"PTH124", # Reject legacy py.path usage
"PTH210", # Reject invalid Path.with_suffix() arguments
"RUF024", # Reject mutable values passed to dict.fromkeys()
"RUF026", # Pass defaultdict factories positionally
"RUF032", # Avoid constructing Decimal from float literals
"RUF064", # Require octal literals for file-permission modes

# Asynchronous and FastAPI correctness.
"ASYNC", # Detect blocking and unsafe async patterns
"FAST", # FastAPI-specific correctness checks
"RUF006", # Retain references to created asyncio tasks

# Imports, modernization, and unnecessary code.
"I", # Import ordering
"UP", # Python 3.13 syntax modernization
"C4", # Comprehension improvements
"PIE", # Unnecessary code
"RUF022", # Sort __all__ exports
"UP", # Modernize syntax for Python 3.13
"YTT", # Validate Python-version checks
"FURB", # Stable Refurb modernization checks
"C4", # Improve comprehensions
"RUF005", # Prefer unpacking over collection concatenation
"PIE", # Remove unnecessary code
"RUF059", # Mark intentionally unused unpacked variables explicitly

# Control-flow and resource safety.
"RUF021", # Parenthesize mixed and/or expressions
"RUF060", # Reject membership tests against empty collections
"SIM105", # Use contextlib.suppress for intentional suppression
"SIM107", # Prevent finally returns from masking exceptions
"SIM114", # Combine branches with identical bodies
"SIM115", # Require context managers for locally opened files
"SIM220", # Reject expressions equivalent to x and not x
"SIM221", # Reject expressions equivalent to x or not x
"TRY300", # Keep success-only statements outside try blocks

# Documentation, maintainability, and performance.
"D", # Docstring conventions
"N", # PEP 8 naming conventions
"C90", # McCabe complexity
"PLR0913", # Excessive function arguments
"PERF", # Performance anti-patterns
"RUF017", # Prevent quadratic list flattening with sum()

# Logging and development hygiene.
"LOG", # Logging correctness
"G", # Logging format correctness
"LOG", # Logging API correctness
"G", # Logging format-string correctness
"T10", # Debugger calls
"T20", # Print statements

# Test and assertion correctness.
"PT", # Pytest correctness and style
"RUF018", # Assignments inside assertions
"RUF043", # Ambiguous pytest.raises patterns

# Suppression and project metadata hygiene.
"PGH", # Blanket suppressions and invalid mock access
"RUF10", # Noqa and suppression-comment checks
"RUF200", # Invalid pyproject.toml metadata
"PT", # Pytest correctness and style checks
"RUF018", # Reject assignments inside assertions
"RUF030", # Reject print() calls used as assert messages
"RUF040", # Reject non-string literal assert messages
"RUF043", # Require unambiguous pytest.raises() patterns

# Suppression hygiene.
"PGH", # Detect blanket suppressions and invalid mock access
"RUF028", # Validate Ruff formatter-suppression comments
"RUF10", # Validate noqa and suppression directives

# Project configuration correctness.
"RUF200", # Validate pyproject.toml
]
ignore = [
"E501", # Line length is handled by the Ruff formatter
]

[tool.ruff.lint.mccabe]
# Pydantic AI uses 15: strict enough to expose genuinely complex functions
# without forcing ordinary orchestration code into unnecessary abstractions.
max-complexity = 15

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.per-file-ignores]
# Pytest assertions are test checks, not runtime security guards.
"tests/**/*.py" = ["S101"]
# Vulture whitelist files intentionally use undefined bare-name expressions
# to mark dynamically consumed symbols as used.
"vulture_whitelist.py" = ["B018", "F821"]

[tool.mypy]
strict = true
disallow_any_explicit = true
files = ["src", "tests"]

[tool.vulture]
min_confidence = 60
paths = ["src", "tests", "vulture_whitelist.py"]
sort_by_size = true

[tool.pytest.ini_options]
addopts = "-v --maxfail=1 --cov=xkc_kl200_python --cov-fail-under=100"
Loading