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
10 changes: 7 additions & 3 deletions .github/workflows/compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ on:

jobs:
compile:
name: Library Compilation
name: Library Compilation (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: "3.x"
python-version: ${{ matrix.python-version }}
- name: Install build tools
run: |
pip install --upgrade pip
Expand Down
50 changes: 40 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,30 @@ name: Build and Publish to PyPi

on:
push:
branches: [main]
tags: ["*.*.*"]

jobs:
test:
name: Run tests before release
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 -r requirements.txt
pip install pytest
pip install -e .
- name: Run tests
run: pytest tests/ -v

build:
name: Build distribution
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -17,11 +36,7 @@ jobs:
with:
python-version: "3.x"
- name: Install pypa/build
run: >-
python3 -m
pip install
build
--user
run: python3 -m pip install build --user
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
Expand All @@ -30,11 +45,26 @@ jobs:
name: python-package-distributions
path: dist/

github-release:
name: Upload wheels to GitHub Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: dist/*

publish-to-pypi:
name: Publish Python distribution to PyPI
if: startsWith(github.ref, 'refs/tags/')
needs:
- build
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
Expand All @@ -47,5 +77,5 @@ jobs:
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
40 changes: 40 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Tests

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

jobs:
test:
name: Unit Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
pip install -e .
- name: Run tests
run: pytest tests/ -v
10 changes: 7 additions & 3 deletions .github/workflows/type-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ on:

jobs:
type-check:
name: Type Checking with mypy
name: Type Checking with mypy (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: "3.x"
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install --upgrade pip
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Python-ST3215

[![wakatime](https://wakatime.com/badge/user/b67f4ae3-1ee8-40ea-b8d8-196354064008/project/2d953882-4dfb-4c53-8a18-c698dc275f82.svg)](https://wakatime.com/badge/user/b67f4ae3-1ee8-40ea-b8d8-196354064008/project/2d953882-4dfb-4c53-8a18-c698dc275f82)
[![PyPI Version](https://img.shields.io/pypi/v/python-st3215)](https://pypi.org/project/python-st3215/)
![Python Versions](https://img.shields.io/pypi/pyversions/python-st3215)
![License](https://img.shields.io/github/license/alessiodam/python-st3215)
Expand Down Expand Up @@ -61,7 +60,7 @@ Hover over classes and functions in your editor to see type hints, parameter des
| Brand | SKU | Product |
|-----------|-------|------------------------------------------------------------------------------|
| Waveshare | 22414 | [ST3215 Series Serial Bus Servo](https://www.waveshare.com/st3215-servo.htm) |
| | | USB to RS485 Serial Converter |
| Waveshare | 25514 | [Bus Servo Adapter (A)](https://www.waveshare.com/bus-servo-adapter-a.htm) |

## Memory Table

Expand Down
18 changes: 10 additions & 8 deletions examples/01_scan_servos.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

from python_st3215 import ST3215

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

try:
with ST3215(PORT) as controller:
print("Scanning for servos...\n")
servos = controller.list_servos()

Expand All @@ -18,24 +18,26 @@
print(f"Found {len(servos)} servo(s)\n")
print("=" * 80)

mode_names = {0: "Position", 1: "Constant Speed", 2: "PWM", 3: "Stepper"}

for servo_id in servos:
servo = controller.wrap_servo(servo_id)

voltage_raw = servo.sram.read_current_voltage()
voltage_str = f"{voltage_raw / 10:.1f}V" if voltage_raw is not None else "N/A"

mode = servo.eeprom.read_operating_mode()

print(f"\nServo ID: {servo_id}")
print(
f" Firmware: v{servo.eeprom.read_firmware_major_version()}.{servo.eeprom.read_firmware_minor_version()}"
)
print(f" Position: {servo.sram.read_current_location()}")
print(f" Temperature: {servo.sram.read_current_temperature()}°C")
print(f" Voltage: {servo.sram.read_current_voltage() / 10:.1f}V")
print(f" Voltage: {voltage_str}")
print(
f" Min/Max Angle: {servo.eeprom.read_min_angle_limit()} / {servo.eeprom.read_max_angle_limit()}"
)

mode = servo.eeprom.read_operating_mode()
mode_names = {0: "Position", 1: "Constant Speed", 2: "PWM", 3: "Stepper"}
print(f" Operating Mode: {mode_names.get(mode, 'Unknown')}")

print("\n" + "=" * 80)
finally:
controller.close()
7 changes: 3 additions & 4 deletions examples/02_movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import os
import time

from python_st3215 import ST3215

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

try:
with ST3215(PORT) as controller:
servo = controller.wrap_servo(1)
servo.sram.torque_enable()

Expand All @@ -25,5 +26,3 @@
time.sleep(1)

servo.sram.torque_disable()
finally:
controller.close()
30 changes: 16 additions & 14 deletions examples/03_read_position.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@

from python_st3215 import ST3215

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

try:
with ST3215(PORT) as controller:
servo = controller.wrap_servo(1)
servo.sram.torque_disable()
print("Manually move the servo. Press Ctrl+C to exit.")
while True:
position = servo.sram.read_current_location()
speed = servo.sram.read_current_speed()
load = servo.sram.read_current_load()

print(
f"Position: {position:5d} | Speed: {speed:5d} | Load: {load:4d}", end="\r"
)
time.sleep(0.1)
except KeyboardInterrupt:
print("\nStopped.")
finally:
controller.close()
try:
while True:
position = servo.sram.read_current_location()
speed = servo.sram.read_current_speed()
load = servo.sram.read_current_load()

pos_str = f"{position:5d}" if position is not None else " N/A"
spd_str = f"{speed:5d}" if speed is not None else " N/A"
load_str = f"{load:4d}" if load is not None else " N/A"

print(f"Position: {pos_str} | Speed: {spd_str} | Load: {load_str}", end="\r")
time.sleep(0.1)
except KeyboardInterrupt:
print("\nStopped.")
24 changes: 13 additions & 11 deletions examples/04_speed_control.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
"""
Control servo movement speed using acceleration parameter.
Control servo movement speed using the acceleration parameter.

Acceleration unit: 100 steps/s² per unit (e.g. 10 = 1000 steps/s²).
Lower values = slower ramp-up, smoother motion.
"""

import os
import time

from python_st3215 import ST3215

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

try:
with ST3215(PORT) as controller:
servo = controller.wrap_servo(1)
servo.sram.torque_enable()

print("Slow movement (acceleration = 10)...")
print("Slow movement (acceleration = 10 → 1000 steps/s²)...")
servo.sram.write_acceleration(10)
servo.sram.write_target_location(3000)
time.sleep(2)
time.sleep(3)

print("Fast movement (acceleration = 100)...")
servo.sram.write_acceleration(100)
print("Medium movement (acceleration = 50 → 5000 steps/s²)...")
servo.sram.write_acceleration(50)
servo.sram.write_target_location(1000)
time.sleep(2)

print("Very fast movement (acceleration = 254)...")
print("Fast movement (acceleration = 254 → 25400 steps/s²)...")
servo.sram.write_acceleration(254)
servo.sram.write_target_location(2048)
time.sleep(2)
time.sleep(1)

servo.sram.torque_disable()
finally:
controller.close()
11 changes: 7 additions & 4 deletions examples/05_torque_limit.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""
Demonstrate torque limiting to reduce servo force.

Torque limit unit: 0.1% per unit (0-1000, where 1000 = 100%).
This affects SRAM only; it resets to max_torque (EEPROM) on power-up.
"""

import os
import time

from python_st3215 import ST3215

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

try:
with ST3215(PORT) as controller:
servo = controller.wrap_servo(1)
servo.sram.torque_enable()

Expand All @@ -27,7 +31,6 @@
servo.sram.write_target_location(3000)
time.sleep(1.5)

# Restore full torque before disabling so the next power-on is predictable
servo.sram.write_torque_limit(1000)
servo.sram.torque_disable()
finally:
controller.close()
12 changes: 7 additions & 5 deletions examples/06_angle_limit.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""
Set minimum and maximum angle limits to restrict servo range.

Limits are stored in EEPROM and persist across power cycles.
The servo will clamp movement to stay within the configured limits.
"""

import os
import time

from python_st3215 import ST3215

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

try:
with ST3215(PORT) as controller:
servo = controller.wrap_servo(1)

print("Setting angle limits: 1500 to 2500")
Expand All @@ -33,10 +37,8 @@
time.sleep(1)
print(f"Actual position: {servo.sram.read_current_location()}")

print("\nRestoring full range...")
print("\nRestoring full range (0-4095)...")
servo.eeprom.write_min_angle_limit(0)
servo.eeprom.write_max_angle_limit(4095)

servo.sram.torque_disable()
finally:
controller.close()
Loading
Loading