From 0b66adab5ed2c09e683e3a51ef61ba2eb99d6982 Mon Sep 17 00:00:00 2001 From: scayac Date: Mon, 23 Mar 2026 20:56:37 +0100 Subject: [PATCH] =?UTF-8?q?Mise=20=C3=A0=20jour=20d=C3=A9pendances=20pytho?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- .../serial_listener.cpython-312.pyc | Bin 5189 -> 7947 bytes backend/app/serial_listener.py | 105 ++++++++++++++---- backend/requirements.txt | 5 +- 4 files changed, 91 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index b694934..46e5b6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.venv \ No newline at end of file +.venv +__pycache__ +*.pyc \ No newline at end of file diff --git a/backend/app/__pycache__/serial_listener.cpython-312.pyc b/backend/app/__pycache__/serial_listener.cpython-312.pyc index a1366b32f3f560b8ef063ea6d2fc9492dbcd7455..4ee0d079669ecf110e67caf33ac8a6b2dea40a36 100644 GIT binary patch literal 7947 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 WydwOL0^~n)dT845Erqpm8vg_0l1rNa delta 1864 zcmY*ZO>7fK6rS<^_$U5%oW$`j`KhzSF{L4)6;@PAfrL^BAwXiF#@HL!;Mg$UkOVo+ zh4wcg>-5||g1Zhq*r1_ir1-7y|Zqil^WW~6kTeH!vw!#+G<9K>bVVpDC2 zL^7Mr$w^ttW%KwIJPJ+tvC+*t$sT09^V)a~x*DkLA&_SXghCixk*V4^~`_x=}BlbGU4# zbj?k3^`DIT$&%N|6}UW{{j>n^c9+}1Ev!W`%@=sIyTIeG9d3)IYDL`@OO5r7r4|2Z zuESrs-L19)SK!kY`nSkzrE|3=Ajn=M{@$zXh8EoJ?(*CJ=T}1~y#BX%R?5>mG@cco za&Kdt1r9%~+kkI5z2>~`GMtT&awK2X*sB@8m;zIv8Egi5IH7}J7(nvPh9{X}Faeu^ zTuakL$7o2K)Iv(_mWvxqf!4GEcw>P&q_! z?^D^cIV7v32u~)*(ci?r^MbXMyy)mCBcoJ@&lX-}$~^%F^ym9nQ)b|NXsf}zqK>tj}HDu*Oe zxoj4n^VIrOMx0FLX$6yh)kdX>I*0}Dw>Ow*$AyWK{|GO6uFuq2gg@NgE63-K&$G8D z7wcEt2Uda8aS#>{ohS#IN`a^nh?W9fN}%h0U~4(Ju@vl7f}P93u2L|j1Y>uPEDjZe zvE|_5Qt+4(JhmMC>?->p*j)Ctl*8>eX1<@fEiZ>-SIrN+;rYF#NKA>u?jBl+>{v9H zdWV#m-l5`fVx{*)*%$h5@3p-pUx(uBD2IB>p_XzeGJi-3b<+8;658^@$oT_*n?YdX ze5MrbQKCI}$5)~|*X=Au4-^j`DGrOpku%D0=Du(2fnRt-Kfe;)wVpl`?Jo`-E)J!O z;u&R#@*lq)NHO}!;^2yJV6_A%K9*AQQBq2gQ10ZYn39hp4}Rp^ z9=d!g7)NVFKr!FDcEq_?+tI@gvuf{31-!lNQ{ij<48;- z5Pj}YfPxqQT3V emqGUvV0q5#p!M?bb@2rt!b{T!kmQYk7R-NVBjcq2 diff --git a/backend/app/serial_listener.py b/backend/app/serial_listener.py index b3cf235..36d1c99 100644 --- a/backend/app/serial_listener.py +++ b/backend/app/serial_listener.py @@ -1,10 +1,62 @@ from __future__ import annotations import logging +import os +import select import threading +import termios from typing import Callable, Dict, Optional, TypedDict -import serial + +class SerialConnectionError(Exception): + pass + + +_BAUDRATE_TO_TERM = { + 9600: termios.B9600, + 19200: termios.B19200, + 38400: termios.B38400, + 57600: termios.B57600, + 115200: termios.B115200, + 230400: termios.B230400, +} + + +def _configure_port(fd: int, baudrate: int) -> None: + speed = _BAUDRATE_TO_TERM.get(baudrate) + if speed is None: + raise SerialConnectionError(f"Unsupported baudrate: {baudrate}") + + attrs = termios.tcgetattr(fd) + + attrs[0] = 0 + attrs[1] = 0 + attrs[2] = termios.CS8 | termios.CREAD | termios.CLOCAL + attrs[3] = 0 + + attrs[4] = speed + attrs[5] = speed + + attrs[6][termios.VMIN] = 0 + attrs[6][termios.VTIME] = 0 + + termios.tcflush(fd, termios.TCIFLUSH) + termios.tcsetattr(fd, termios.TCSANOW, attrs) + + +def _open_serial_fd(port: str, baudrate: int) -> int: + try: + fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + except OSError as exc: + raise SerialConnectionError(f"Cannot open serial port {port}: {exc}") from exc + + try: + _configure_port(fd, baudrate) + except Exception: + os.close(fd) + raise + + return fd class TriggerLogInfo(TypedDict, total=False): @@ -48,25 +100,40 @@ class SerialTriggerListener: self._logger.info("Starting serial listener on %s @ %s", port, baudrate) while not self._stop_event.is_set(): try: - with serial.Serial(port=port, baudrate=baudrate, timeout=timeout) as serial_conn: + fd = _open_serial_fd(port=port, baudrate=baudrate) + line_buffer = bytearray() + try: while not self._stop_event.is_set(): - line = serial_conn.readline().decode("utf-8", errors="ignore").strip() - if not line: + readable, _, _ = select.select([fd], [], [], timeout) + if not readable: continue - trigger_info = self.on_trigger(line) - if trigger_info and trigger_info.get("key") and trigger_info.get("name"): - self._logger.info( - "Serial trigger received: %s -> %s (%s)", - line, - trigger_info["key"], - trigger_info["name"], - ) - elif trigger_info and trigger_info.get("key"): - self._logger.info("Serial trigger received: %s -> %s", line, trigger_info["key"]) - elif trigger_info and trigger_info.get("name"): - self._logger.info("Serial trigger received: %s (%s)", line, trigger_info["name"]) - else: - self._logger.info("Serial trigger received: %s", line) - except serial.SerialException as exc: + chunk = os.read(fd, 256) + if not chunk: + continue + line_buffer.extend(chunk) + + while b"\n" in line_buffer: + raw_line, _, remainder = line_buffer.partition(b"\n") + line_buffer = bytearray(remainder) + line = raw_line.decode("utf-8", errors="ignore").strip() + if not line: + continue + trigger_info = self.on_trigger(line) + if trigger_info and trigger_info.get("key") and trigger_info.get("name"): + self._logger.info( + "Serial trigger received: %s -> %s (%s)", + line, + trigger_info["key"], + trigger_info["name"], + ) + elif trigger_info and trigger_info.get("key"): + self._logger.info("Serial trigger received: %s -> %s", line, trigger_info["key"]) + elif trigger_info and trigger_info.get("name"): + self._logger.info("Serial trigger received: %s (%s)", line, trigger_info["name"]) + else: + self._logger.info("Serial trigger received: %s", line) + finally: + os.close(fd) + except (OSError, SerialConnectionError) as exc: self._logger.warning("Serial connection error: %s", exc) self._stop_event.wait(2) diff --git a/backend/requirements.txt b/backend/requirements.txt index 67e9238..c7593bc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,2 @@ -fastapi==0.116.1 -uvicorn[standard]==0.35.0 -pyserial==3.5 +fastapi==0.135.2 +uvicorn[standard]==0.42.0