diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-04-24 11:51:26 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-04-24 11:51:26 -0600 |
| commit | 67cfa0a514c7de4605ed7360e15a81aa781e510e (patch) | |
| tree | 297dfeca4adff1e65df4c9f2e5d7fb49a8b44ba0 /api_layers/protocols/mark10.py | |
| parent | 4244e5d97da0273fed70f734a4cfe822bd15c9cb (diff) | |
Added difference serial protocols
Diffstat (limited to 'api_layers/protocols/mark10.py')
| -rw-r--r-- | api_layers/protocols/mark10.py | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/api_layers/protocols/mark10.py b/api_layers/protocols/mark10.py new file mode 100644 index 0000000..1b5bf1c --- /dev/null +++ b/api_layers/protocols/mark10.py @@ -0,0 +1,95 @@ +""" +api_layers/protocols/mark10.py + +Mark-10 ASCII protocol layer (Series 5 primary, compatible with Series 4/3). + +Commands (sent with CR terminator): + ? — request current reading + Z — zero the gauge + U — cycle units (lb → kgF → N → ozF → …) + +Response format: "+0.1234 kgF" (sign, value, space, unit suffix) + +Channels exposed: + force — current force reading (in instrument's selected unit) + unit_code — numeric index into UNITS list (lb=0, kgF=1, N=2, ozF=3) +""" + +import math +import re +from typing import Dict, Optional + +from api_layers.protocols.base_protocol import BaseProtocol + + +UNITS = ["lb", "kgF", "N", "ozF"] + +_RESP_RE = re.compile(r"([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)\s*([a-zA-Z]*)") + + +class Mark10Layer(BaseProtocol): + + def __init__( + self, + port: str, + baud: int = 115200, + poll_interval: float = 0.05, + simulate: bool = True, + ): + super().__init__(port, baud, poll_interval, simulate) + self._unit = "N" + + # ── Protocol ────────────────────────────────────────────────────────── + + def _poll(self) -> Dict[str, float]: + try: + self._ser.write(b"?\r") + resp = self._ser.readline().decode(errors="replace").strip() + return self._parse(resp) + except Exception: + return {} + + def _parse(self, resp: str) -> Dict[str, float]: + m = _RESP_RE.search(resp) + if not m: + return {} + val = float(m.group(1)) + unit = m.group(2).strip() or self._unit + if unit in UNITS: + self._unit = unit + return { + "force": val, + "unit_code": float(UNITS.index(self._unit) if self._unit in UNITS else 2), + } + + def _simulate(self, t: float) -> Dict[str, float]: + cycle = t % 30.0 + ramp = cycle * 6.67 if cycle < 15.0 else (30.0 - cycle) * 6.67 + return { + "force": ramp + math.sin(t * 10.0) * 0.3, + "unit_code": float(UNITS.index("N")), + } + + def write(self, channel_id: str, value) -> bool: + cmd_map = { + "zero": b"Z\r", + "cycle_units": b"U\r", + } + cmd = cmd_map.get(channel_id) + if cmd is None or not self._ser: + return False + try: + self._ser.write(cmd) + return True + except Exception: + return False + + # ── Convenience ─────────────────────────────────────────────────────── + + def zero(self) -> None: + if self._ser: + self._ser.write(b"Z\r") + + def cycle_units(self) -> None: + if self._ser: + self._ser.write(b"U\r") |
