Skip to content
This repository has been archived by the owner on Oct 28, 2023. It is now read-only.

PTCS Update 2 #9

Merged
merged 13 commits into from
Oct 8, 2023
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: 5 additions & 5 deletions .github/workflows/ptcs-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ on:
pull_request:
paths:
- '.github/workflows/ptcs-ci.yml'
- 'ptcs/ptcs_bridge/**'
- 'ptcs/ptcs_control/**'
- 'ptcs/ptcs_server/**'
- 'ptcs/usb_bt_bridge/**'
- 'ptcs/.flake8'
- 'ptcs/poetry.lock'
- 'ptcs/poetry.toml'
Expand Down Expand Up @@ -45,13 +45,13 @@ jobs:

- name: Sort imports
run: |
poetry run isort ptcs_control ptcs_server usb_bt_bridge 2>&1 | reviewdog -reporter=github-pr-review -f=isort
poetry run isort ptcs_bridge ptcs_control ptcs_server 2>&1 | reviewdog -reporter=github-pr-review -f=isort
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Format
run: |
poetry run black ptcs_control ptcs_server usb_bt_bridge 2>&1 | reviewdog -reporter=github-pr-review -f=black
poetry run black ptcs_bridge ptcs_control ptcs_server 2>&1 | reviewdog -reporter=github-pr-review -f=black
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Expand All @@ -62,12 +62,12 @@ jobs:

- name: Lint
run: |
poetry run flake8 ptcs_control ptcs_server usb_bt_bridge 2>&1 | reviewdog -reporter=github-pr-review -f=flake8
poetry run flake8 ptcs_bridge ptcs_control ptcs_server 2>&1 | reviewdog -reporter=github-pr-review -f=flake8
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Typecheck
run: |
poetry run mypy ptcs_control ptcs_server usb_bt_bridge 2>&1 | reviewdog -reporter=github-pr-review -f=mypy
poetry run mypy ptcs_bridge ptcs_control ptcs_server 2>&1 | reviewdog -reporter=github-pr-review -f=mypy
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
121 changes: 120 additions & 1 deletion ptcs/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
7 changes: 7 additions & 0 deletions ptcs/ptcs_bridge/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("[%(levelname)-8s] %(message)s"))
logger.addHandler(handler)
File renamed without changes.
20 changes: 20 additions & 0 deletions ptcs/ptcs_bridge/bridge2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import asyncio

from .train_base import TrainBase


class Bridge2:
trains: dict[str, TrainBase]

def __init__(self) -> None:
self.trains = {}

def add_train(self, train: TrainBase) -> None:
assert train.id not in self.trains
self.trains[train.id] = train

async def connect_all(self) -> None:
await asyncio.gather(*(train.connect() for train in self.trains.values()))

async def disconnect_all(self) -> None:
await asyncio.gather(*(train.disconnect() for train in self.trains.values()))
23 changes: 23 additions & 0 deletions ptcs/ptcs_bridge/train_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Callable

NotifyPositionIdCallback = Callable[["TrainBase", int], None]
NotifyRotationCallback = Callable[["TrainBase", int], None]


class TrainBase:
id: str

async def connect(self) -> None:
raise NotImplementedError()

async def disconnect(self) -> None:
raise NotImplementedError()

async def send_speed(self, speed: int) -> None:
raise NotImplementedError()

async def start_notify_position_id(self, callback: NotifyPositionIdCallback) -> None:
raise NotImplementedError()

async def start_notify_rotation(self, callback: NotifyRotationCallback) -> None:
raise NotImplementedError()
82 changes: 82 additions & 0 deletions ptcs/ptcs_bridge/train_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import logging
from uuid import UUID

from bleak import BleakClient
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.service import BleakGATTService

from .train_base import NotifyPositionIdCallback, NotifyRotationCallback, TrainBase

SERVICE_UUID = UUID("63cb613b-6562-4aa5-b602-030f103834a4")
CHARACTERISTIC_MOTOR_INPUT_UUID = UUID("88c9d9ae-bd53-4ab3-9f42-b3547575a743")
CHARACTERISTIC_POSITION_ID_UUID = UUID("8bcd68d5-78ca-c1c3-d1ba-96d527ce8968")
CHARACTERISTIC_ROTATION_UUID = UUID("aab17457-2755-8b50-caa1-432ff553d533")


logger = logging.getLogger(__name__)


class TrainClient(TrainBase):
id: str
_client: BleakClient

def __init__(self, id: str, address: str) -> None:
self.id = id
self._client = BleakClient(address)

def __str__(self) -> str:
return f"TrainClient({self.id}, {self._client.address})"

async def connect(self) -> None:
await self._client.connect()
logger.info("%s connected", self)

async def disconnect(self) -> None:
await self._client.disconnect()
logger.info("%s disconnected", self)

def _get_service(self) -> BleakGATTService:
service = self._client.services.get_service(SERVICE_UUID)
assert service is not None
return service

def _get_characteristic(self, uuid: UUID) -> BleakGATTCharacteristic:
service = self._get_service()
characteristic = service.get_characteristic(uuid)
assert characteristic is not None
return characteristic

def _get_characteristic_motor_input(self) -> BleakGATTCharacteristic:
return self._get_characteristic(CHARACTERISTIC_MOTOR_INPUT_UUID)

def _get_characteristic_position_id(self) -> BleakGATTCharacteristic:
return self._get_characteristic(CHARACTERISTIC_POSITION_ID_UUID)

def _get_characteristic_rotation(self) -> BleakGATTCharacteristic:
return self._get_characteristic(CHARACTERISTIC_ROTATION_UUID)

async def send_speed(self, speed: int) -> None:
assert 0 <= speed <= 255
characteristic_speed = self._get_characteristic_motor_input()
await self._client.write_gatt_char(characteristic_speed, bytes([speed]))
logger.info("%s send speed %s", self, speed)

async def start_notify_position_id(self, callback: NotifyPositionIdCallback) -> None:
def wrapped_callback(_characteristic: BleakGATTCharacteristic, data: bytearray):
assert len(data) == 1
position_id = data[0]
logger.info("%s notify position id %s", self, position_id)
callback(self, position_id)

characteristic_position_id = self._get_characteristic_position_id()
await self._client.start_notify(characteristic_position_id, wrapped_callback)

async def start_notify_rotation(self, callback: NotifyRotationCallback) -> None:
def wrapped_callback(_characteristic: BleakGATTCharacteristic, data: bytearray):
assert len(data) == 1
assert data[0] == 1
logger.info("%s notify rotation %s", self, 1)
callback(self, 1)

characteristic_rotation = self._get_characteristic_rotation()
await self._client.start_notify(characteristic_rotation, wrapped_callback)
Loading