# SPDX-License-Identifier: MIT
"""
Abstract interfaces for openbricks components.
MicroPython doesn't ship full ``typing.Protocol`` support, so these are plain
base classes. Drivers should subclass the appropriate interface and fill in
every method. The higher-level modules (``robotics``, ``config``) only depend
on these interfaces, never on concrete drivers — that's what makes the system
plug-and-play.
If you add a new category of component (e.g. a distance sensor), add its
interface here.
"""
[docs]
class Motor:
"""A bidirectional motor.
Implementations range from an open-loop H-bridge driver (L298N) to a
closed-loop geared motor with quadrature encoder (JGB37-520).
Units
-----
* ``power`` is -100..100 (percent duty cycle, sign = direction).
* ``speed`` is degrees per second at the output shaft (closed-loop only).
* ``angle`` is degrees at the output shaft (closed-loop only).
"""
[docs]
def run(self, power):
"""Run at the given power (-100..100). Non-blocking."""
raise NotImplementedError
[docs]
def brake(self):
"""Stop with active braking (both terminals shorted)."""
raise NotImplementedError
[docs]
def coast(self):
"""Stop by cutting drive power (motor free-wheels)."""
raise NotImplementedError
[docs]
def hold(self):
"""Stop and actively hold the current shaft angle via closed-loop
control. Only motors with position-mode hardware (e.g. ST-3215) or
a software position loop implement this; open-loop drivers raise
``NotImplementedError`` — pick ``brake`` or ``coast`` instead."""
raise NotImplementedError
# --- Optional closed-loop methods ---
# Open-loop drivers may raise NotImplementedError or simply not override.
[docs]
def angle(self):
"""Return the current shaft angle in degrees."""
raise NotImplementedError
[docs]
def reset_angle(self, angle=0):
"""Set the current angle to ``angle`` degrees."""
raise NotImplementedError
[docs]
def run_speed(self, deg_per_s):
"""Hold a target speed (closed loop)."""
raise NotImplementedError
[docs]
def run_angle(self, deg_per_s, target_angle, wait=True):
"""Rotate by ``target_angle`` degrees at ``deg_per_s``. Blocks
if ``wait``; otherwise returns immediately and the caller polls
``done()`` to advance the move and detect completion."""
raise NotImplementedError
[docs]
def done(self):
"""Return ``True`` if no non-blocking move is in flight or
the active ``run_angle(wait=False)`` move has reached its
target. Drivers that don't support non-blocking moves always
return ``True`` (a wait=True call is finished before
returning to the caller, by definition)."""
return True
[docs]
class Servo:
"""A position-controlled servo (angle-addressable)."""
[docs]
def move_to(self, angle_deg, speed=None, wait=True):
"""Move to absolute angle in degrees."""
raise NotImplementedError
[docs]
def angle(self):
"""Read back the current angle."""
raise NotImplementedError
[docs]
class IMU:
"""A 3-axis inertial measurement unit.
The expected unit convention is:
* heading/yaw/pitch/roll in degrees
* angular_velocity in degrees / second
* acceleration in m / s^2
"""
[docs]
def heading(self):
"""Return heading (yaw) in degrees, wrapped to [-180, 180)."""
raise NotImplementedError
[docs]
def angular_velocity(self):
"""Return (wx, wy, wz) in deg/s."""
raise NotImplementedError
[docs]
def acceleration(self):
"""Return (ax, ay, az) in m/s^2."""
raise NotImplementedError
[docs]
class ColorSensor:
"""An RGB-ish color sensor."""
[docs]
def rgb(self):
"""Return ``(r, g, b)`` each in 0..255."""
raise NotImplementedError
[docs]
def ambient(self):
"""Return ambient / clear-channel intensity in 0..100."""
raise NotImplementedError
# NOTE: ``DistanceSensor`` lives in ``openbricks.distance`` rather
# than here, even though it's an interface. ``openbricks/__init__.py``
# eagerly loads this module to re-export Motor/Servo/IMU/ColorSensor,
# so every byte of bytecode here is paid by *every* import of
# anything in the openbricks package — including the observer test
# which runs against a tight MicroPython heap budget. Distance-sensor
# users explicitly ``from openbricks.distance import DistanceSensor``
# (or just import their concrete driver, which does).
# Hub-layer interfaces (StatusLED, Button, Display, Hub) live in
# ``openbricks.hub`` alongside their concrete implementations so that
# tests which don't touch the hub don't pay the class-loading cost on
# MicroPython's tight unix heap.