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
33 changes: 33 additions & 0 deletions .github/workflows/compile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Test Compilation

on:
push:
paths:
- "src/**/*.py"
- "pyproject.toml"
- "requirements.txt"
- ".github/workflows/compile.yml"
pull_request:
paths:
- "src/**/*.py"
- "pyproject.toml"
- "requirements.txt"
- ".github/workflows/compile.yml"

jobs:
compile:
name: Library Compilation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install build tools
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install build
- name: Attempt building wheel and sdist
run: python3 -m build --sdist --wheel
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
name: Build and Publish to PyPi

on: push

Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/type-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Type Check

on:
push:
paths:
- "src/**/*.py"
- "pyproject.toml"
- "requirements.txt"
- ".github/workflows/type-check.yml"
pull_request:
paths:
- "src/**/*.py"
- "pyproject.toml"
- "requirements.txt"
- ".github/workflows/type-check.yml"

jobs:
type-check:
name: Type Checking with mypy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
run: |
pip install --upgrade pip
pip install mypy
pip install -r requirements.txt
pip install .
- name: Run mypy
run: mypy src/
118 changes: 59 additions & 59 deletions MEMORY_TABLE.md

Large diffs are not rendered by default.

27 changes: 24 additions & 3 deletions examples/14_change_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import os
from python_st3215 import ST3215

OLD_ID = 1
NEW_ID = 5

controller = ST3215(os.environ.get("ST3215_PORT", "/dev/ttyUSB0"))

Expand All @@ -15,7 +13,21 @@
print("CHANGE SERVO ID")
print("=" * 60)
print("\nWARNING: Only connect ONE servo to avoid ID conflicts!")
print(f"This will change servo ID from {OLD_ID} to {NEW_ID}")

try:
old_id_input = input("\nEnter CURRENT Servo ID (default 1): ").strip()
OLD_ID = int(old_id_input) if old_id_input else 1

new_id_input = input("Enter NEW Servo ID: ").strip()
if not new_id_input:
print("New ID is required. Exiting.")
exit(1)
NEW_ID = int(new_id_input)
except ValueError:
print("Invalid input. Please enter numeric IDs.")
exit(1)

print(f"\nThis will change servo ID from {OLD_ID} to {NEW_ID}")

response = input("\nType 'yes' to continue: ")
if response.lower() != "yes":
Expand All @@ -26,8 +38,17 @@
print(f"\nCurrent ID: {servo.eeprom.read_id()}")
print(f"Changing to ID {NEW_ID}...")

# Unlock EEPROM to allow saving changes
print("Unlocking EEPROM...")
servo.sram.unlock()

print("Writing new ID...")
servo.eeprom.write_id(NEW_ID)

# Lock EEPROM again (optional but recommended)
print("Locking EEPROM...")
servo.sram.lock()

print("\nVerifying change...")
new_servo = controller.wrap_servo(NEW_ID)
confirmed_id = new_servo.eeprom.read_id()
Expand Down
13 changes: 12 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "python-st3215"
version = "1.0.0"
dynamic = ["version"]
authors = [
{ name="Alessio D'Ambrosio", email="alessio@alessiodam.dev" },
]
Expand All @@ -26,6 +26,8 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)"
]
Expand All @@ -39,3 +41,12 @@ Issues = "https://github.com/alessiodam/python-st3215/issues"
[build-system]
requires = ["hatchling >= 1.27"]
build-backend = "hatchling.build"

[tool.hatch.version]
path = "src/python_st3215/__init__.py"

[tool.mypy]
python_version = "3.13"
packages = ["python_st3215"]
exclude = ["examples"]
strict = true
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pyserial>=3.5
types-pyserial>=3.5
2 changes: 1 addition & 1 deletion src/python_st3215/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .errors import ST3215Error, ServoNotRespondingError, InvalidInstructionError
from .servo import Servo

__version__ = "1.0.0"
__version__ = "1.1.0"

__all__ = [
"ST3215",
Expand Down
168 changes: 12 additions & 156 deletions src/python_st3215/decorators.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from functools import wraps
from typing import Callable
from typing import Any, Callable, TypeVar

F = TypeVar("F", bound=Callable[..., Any])

def validate_servo_id(func: Callable) -> Callable:

def validate_servo_id(func: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator to validate servo ID is not broadcast ID (254).
"""

@wraps(func)
def wrapper(self, servo_id: int, *args, **kwargs):
def wrapper(self: Any, servo_id: int, *args: Any, **kwargs: Any) -> Any:
if servo_id == 254:
from .errors import ST3215Error

Expand All @@ -18,13 +20,13 @@ def wrapper(self, servo_id: int, *args, **kwargs):
return wrapper


def validate_broadcast_only(func: Callable) -> Callable:
def validate_broadcast_only(func: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator to ensure operation is only used with broadcast servo (ID 254).
"""

@wraps(func)
def wrapper(self, *args, **kwargs):
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
if self.servo.id != 254:
from .errors import ST3215Error

Expand All @@ -36,14 +38,16 @@ def wrapper(self, *args, **kwargs):
return wrapper


def validate_value_range(min_val: int, max_val: int) -> Callable:
def validate_value_range(
min_val: int, max_val: int
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""
Decorator to validate that a value parameter is within the specified range.
"""

def decorator(func: Callable) -> Callable:
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(self, value: int, *args, **kwargs):
def wrapper(self: Any, value: int, *args: Any, **kwargs: Any) -> Any:
if not (min_val <= value <= max_val):
raise ValueError(
f"{func.__name__}: value {value} out of range [{min_val}, {max_val}]"
Expand Down Expand Up @@ -102,151 +106,3 @@ def encode_unsigned_word(value: int) -> tuple[int, int]:
low = value & 0xFF
high = (value >> 8) & 0xFF
return low, high


def write_method(address: int, size: int = 1, signed: bool = False):
"""
Decorator factory to create write methods for memory addresses.

Args:
address: Memory address to write to
size: Size in bytes (1 or 2)
signed: Whether the value is signed (for 2-byte values)

Returns:
Decorator function
"""

def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(self, value: int, reg: bool = False):
write_fn = self.servo._reg_write_memory if reg else self.servo._write_memory
if size == 1:
return write_fn(address, [value & 0xFF])
elif size == 2:
if signed:
low, high = encode_signed_word(value)
else:
low, high = encode_unsigned_word(value)
return write_fn(address, [low, high])
raise ValueError(f"Unsupported size {size} for write_method.")

return wrapper

return decorator


def read_method(address: int, size: int = 1, signed: bool = False):
"""
Decorator factory to create read methods for memory addresses.

Args:
address: Memory address to read from
size: Size in bytes (1 or 2)
signed: Whether the value is signed (for 2-byte values)

Returns:
Decorator function
"""

def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(self):
raw = self.servo._read_memory(address, size)
if raw is None:
return None
if size == 2 and signed:
return decode_signed_word(raw)
return raw

return wrapper

return decorator


def sync_write_method(address: int, size: int = 1, signed: bool = False):
"""
Decorator factory to create sync write methods for broadcast operations.

Args:
address: Memory address to write to
size: Size in bytes (1 or 2)
signed: Whether the value is signed (for 2-byte values)

Returns:
Decorator function
"""

def decorator(func: Callable) -> Callable:
@wraps(func)
@validate_broadcast_only
def wrapper(self, servo_data: dict[int, int]):
"""
Perform SYNC WRITE to multiple servos.

Args:
servo_data: Dictionary mapping servo_id to value

Returns:
None (broadcast operation, no response)
"""
formatted_data = {}
for servo_id, value in servo_data.items():
if size == 1:
formatted_data[servo_id] = [value & 0xFF]
elif size == 2:
if signed:
low, high = encode_signed_word(value)
else:
low, high = encode_unsigned_word(value)
formatted_data[servo_id] = [low, high]
return self.servo._sync_write(address, size, formatted_data)

return wrapper

return decorator


def sync_read_method(address: int, size: int = 1, signed: bool = False):
"""
Decorator factory to create sync read methods for broadcast operations.

Args:
address: Memory address to read from
size: Size in bytes (1 or 2)
signed: Whether the value is signed (for 2-byte values)

Returns:
Decorator function
"""

def decorator(func: Callable) -> Callable:
@wraps(func)
@validate_broadcast_only
def wrapper(self, servo_ids: list[int]):
"""
Perform SYNC READ from multiple servos.

Args:
servo_ids: List of servo IDs to query

Returns:
Dictionary mapping servo_id to value
"""
responses = self.servo._sync_read(address, size, servo_ids)
results = {}
for servo_id, response in responses.items():
if response and response.get("parameters"):
data = response["parameters"]
if size == 1:
results[servo_id] = data[0]
elif size == 2:
raw = data[0] | (data[1] << 8)
results[servo_id] = decode_signed_word(raw) if signed else raw
else:
results[servo_id] = None
return results

return wrapper

return decorator
Loading
Loading