From b4bdb65b9a307fbd68e58d5b0049d2f318a1dd95 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Fri, 13 Jun 2025 18:17:04 -0500 Subject: [PATCH] Add serial api endpoint for rotary encoder --- src/pqnstack/app/api/main.py | 2 ++ src/pqnstack/app/api/routes/serial.py | 28 +++++++++++++++++++++ src/pqnstack/app/core/config.py | 5 ++++ src/pqnstack/pqn/drivers/rotaryencoder.py | 30 +++++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 src/pqnstack/app/api/routes/serial.py create mode 100644 src/pqnstack/pqn/drivers/rotaryencoder.py diff --git a/src/pqnstack/app/api/main.py b/src/pqnstack/app/api/main.py index 7720b003..d50d7c12 100644 --- a/src/pqnstack/app/api/main.py +++ b/src/pqnstack/app/api/main.py @@ -3,6 +3,7 @@ from pqnstack.app.api.routes import chsh from pqnstack.app.api.routes import qkd from pqnstack.app.api.routes import rng +from pqnstack.app.api.routes import serial from pqnstack.app.api.routes import timetagger api_router = APIRouter() @@ -10,3 +11,4 @@ api_router.include_router(qkd.router) api_router.include_router(timetagger.router) api_router.include_router(rng.router) +api_router.include_router(serial.router) diff --git a/src/pqnstack/app/api/routes/serial.py b/src/pqnstack/app/api/routes/serial.py new file mode 100644 index 00000000..02119358 --- /dev/null +++ b/src/pqnstack/app/api/routes/serial.py @@ -0,0 +1,28 @@ +import logging +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends +from pydantic import BaseModel + +from pqnstack.app.core.config import settings +from pqnstack.pqn.drivers.rotaryencoder import SerialRotaryEncoder + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/serial", tags=["measure"]) + + +def get_rotary_encoder() -> SerialRotaryEncoder: + return settings.rotary_encoder + + +SERDep = Annotated[SerialRotaryEncoder, Depends(get_rotary_encoder)] + + +class AngleResponse(BaseModel): + theta: float + + +@router.get("/") +async def read_angle(rotary_encoder: SERDep) -> AngleResponse: + return AngleResponse(theta=rotary_encoder.read()) diff --git a/src/pqnstack/app/core/config.py b/src/pqnstack/app/core/config.py index 59b25455..d4412087 100644 --- a/src/pqnstack/app/core/config.py +++ b/src/pqnstack/app/core/config.py @@ -10,6 +10,7 @@ from pqnstack.constants import BellState from pqnstack.constants import QKDEncodingBasis +from pqnstack.pqn.drivers.rotaryencoder import SerialRotaryEncoder from pqnstack.pqn.protocols.measurement import MeasurementConfig logger = logging.getLogger(__name__) @@ -39,6 +40,10 @@ class Settings(BaseSettings): bell_state: BellState = BellState.Phi_plus timetagger: tuple[str, str] | None = None # Name of the timetagger to use for the CHSH experiment. + rotary_encoder: SerialRotaryEncoder = SerialRotaryEncoder( + label="rotary_encoder", address="/dev/ttyACM0", offset_degrees=0.0 + ) + model_config = SettingsConfigDict(toml_file="./config.toml", env_file=".env", env_file_encoding="utf-8") @classmethod diff --git a/src/pqnstack/pqn/drivers/rotaryencoder.py b/src/pqnstack/pqn/drivers/rotaryencoder.py new file mode 100644 index 00000000..7ddb3741 --- /dev/null +++ b/src/pqnstack/pqn/drivers/rotaryencoder.py @@ -0,0 +1,30 @@ +import atexit +from dataclasses import dataclass +from dataclasses import field + +import serial + + +@dataclass(slots=True) +class SerialRotaryEncoder: + label: str + address: str + offset_degrees: float = 0.0 + _conn: serial.Serial = field(init=False, repr=False) + + def __post_init__(self) -> None: + self._conn = serial.Serial(self.address, baudrate=115200, timeout=1) + self._conn.write(b"open_channel") + self._conn.read(100) + self._conn.write(b"ready") + self._conn.read(100) + + atexit.register(self.close) + + def close(self) -> None: + self._conn.close() + + def read(self) -> float: + self._conn.write(b"ANGLE?\n") + angle = self._conn.readline().decode().strip() + return float(angle) + self.offset_degrees