Add files via upload
This commit is contained in:
1 parent
875dc44682
commit
e3bb87d4e7
15 files changed
+3623
-1
No files matched your search
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Smart Factory controller APIs
|
||||
"""
|
||||
|
||||
from .factory_controller import FactoryController
|
||||
from .pins import Inputs, Outputs
|
||||
from .pyduino import PyDuino
|
||||
from .pyft232 import PyFt232
|
||||
|
||||
__all__ = ('FactoryController', 'Inputs', 'Outputs', 'PyDuino', 'PyFt232')
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Robust debounce decorator for callbacks like GPIO interrupts.
|
||||
|
||||
Features:
|
||||
- Leading / trailing edge control
|
||||
- Optional max_wait (force-fire) to avoid indefinite deferral
|
||||
- Per-key debounce (e.g., per-pin) for shared handlers
|
||||
- Thread-safe (uses Lock), time.monotonic() for steady-time
|
||||
- Drop-in compatible with previous debounce(wait_sec) usage
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import time
|
||||
import threading
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
|
||||
def debounce(
|
||||
wait_sec: float,
|
||||
*,
|
||||
leading: bool = False,
|
||||
trailing: bool = True,
|
||||
max_wait: Optional[float] = None,
|
||||
key: Optional[Callable[..., Any]] = None,
|
||||
clock: Callable[[], float] = time.monotonic,
|
||||
):
|
||||
"""
|
||||
Debounce a function so that it’s not called too frequently.
|
||||
|
||||
Args:
|
||||
wait_sec: Quiet period length. Calls within this window are coalesced.
|
||||
leading: If True, call immediately on the first call in a burst.
|
||||
trailing: If True, call after the burst “settles” for wait_sec.
|
||||
max_wait: Force a call if this many seconds have elapsed since the
|
||||
last *invocation* (helps for steady streams).
|
||||
key: Per-key debounce. A function that maps (*args, **kwargs) to a
|
||||
hashable key (e.g., lambda pin, *_: pin). If None, a single
|
||||
global bucket is used.
|
||||
clock: Time source; default is time.monotonic().
|
||||
|
||||
Notes:
|
||||
- If both leading and trailing are True:
|
||||
* first call in a burst fires immediately,
|
||||
* further calls delay a trailing fire until wait_sec after the last call,
|
||||
* but a trailing fire is skipped if there were no calls after the leading fire.
|
||||
- If only leading is True => at most one call per burst, immediate.
|
||||
- If only trailing is True => only the last call args are used.
|
||||
- If max_wait is set, a call is forced at most every max_wait seconds.
|
||||
"""
|
||||
if not leading and not trailing:
|
||||
raise ValueError("At least one of leading or trailing must be True.")
|
||||
|
||||
class _Bucket:
|
||||
__slots__ = (
|
||||
"lock", "timer", "last_invoke", "last_call",
|
||||
"pending", "pending_args", "pending_kwargs",
|
||||
)
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.timer: Optional[threading.Timer] = None
|
||||
self.last_invoke: Optional[float] = None # last actual function call time
|
||||
self.last_call: Optional[float] = None # last wrapper call time
|
||||
self.pending: bool = False
|
||||
self.pending_args = ()
|
||||
self.pending_kwargs = {}
|
||||
|
||||
buckets = {}
|
||||
buckets_lock = threading.Lock()
|
||||
|
||||
def _get_bucket(k: Any) -> _Bucket:
|
||||
with buckets_lock:
|
||||
b = buckets.get(k)
|
||||
if b is None:
|
||||
b = _Bucket()
|
||||
buckets[k] = b
|
||||
return b
|
||||
|
||||
def decorator(fn: Callable):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
# derive bucket key
|
||||
k = key(*args, **kwargs) if key else None
|
||||
b = _get_bucket(k)
|
||||
now = clock()
|
||||
|
||||
def _invoke_locked():
|
||||
"""Call the function with the latest pending args/kwargs.
|
||||
b.lock must be held by caller."""
|
||||
b.last_invoke = clock()
|
||||
b.pending = False
|
||||
call_args = b.pending_args
|
||||
call_kwargs = b.pending_kwargs
|
||||
# release the lock during the user call to avoid deadlocks
|
||||
b.lock.release()
|
||||
try:
|
||||
return fn(*call_args, **call_kwargs)
|
||||
finally:
|
||||
b.lock.acquire()
|
||||
|
||||
def _schedule_trailing():
|
||||
"""(Re)schedule trailing call timer from 'now'."""
|
||||
if b.timer:
|
||||
b.timer.cancel()
|
||||
b.timer = threading.Timer(wait_sec, _trailing_fire)
|
||||
b.timer.daemon = True
|
||||
b.timer.start()
|
||||
|
||||
def _trailing_fire():
|
||||
with b.lock:
|
||||
# fire only if something is pending
|
||||
if not b.pending:
|
||||
return
|
||||
# if max_wait is set and we haven't exceeded it yet AND there
|
||||
# were no new calls, still trailing should run because timer fired
|
||||
_invoke_locked()
|
||||
# after trailing fire, clear timer
|
||||
t = b.timer
|
||||
b.timer = None
|
||||
if t:
|
||||
try:
|
||||
t.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with b.lock:
|
||||
b.last_call = now
|
||||
b.pending = True
|
||||
b.pending_args = args
|
||||
b.pending_kwargs = kwargs
|
||||
|
||||
call_now = False
|
||||
|
||||
# Decide leading fire
|
||||
if leading:
|
||||
if b.last_invoke is None:
|
||||
# first ever
|
||||
call_now = True
|
||||
else:
|
||||
# if enough time has passed since last_invoke -> new burst
|
||||
if (now - b.last_invoke) >= wait_sec and (b.timer is None):
|
||||
call_now = True
|
||||
|
||||
# Decide max_wait forced fire (never conflict with call_now)
|
||||
if (not call_now) and max_wait is not None and b.last_invoke is not None:
|
||||
if (now - b.last_invoke) >= max_wait:
|
||||
call_now = True
|
||||
|
||||
result = None
|
||||
if call_now:
|
||||
result = _invoke_locked()
|
||||
# after a leading/max_wait fire, we may still want trailing if more
|
||||
# calls keep arriving; re-arm the trailing timer if trailing is True
|
||||
if trailing:
|
||||
_schedule_trailing()
|
||||
else:
|
||||
# no immediate call; manage trailing
|
||||
if trailing:
|
||||
_schedule_trailing()
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
Smart Factory HW module controller
|
||||
"""
|
||||
|
||||
from logging import getLogger
|
||||
from os import listdir
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Optional, Union
|
||||
|
||||
from .debounce import debounce
|
||||
from .pins import Inputs, Outputs
|
||||
from .pyduino import PyDuino
|
||||
from .pyft232 import PyFt232
|
||||
|
||||
__all__ = ('FactoryController', )
|
||||
|
||||
|
||||
class FactoryController:
|
||||
"""
|
||||
Factory HW controller class
|
||||
"""
|
||||
|
||||
DEV_ON = False
|
||||
DEV_OFF = True
|
||||
|
||||
def __init__(self,
|
||||
port: Optional[Union[str, int]] = None,
|
||||
*,
|
||||
debug: bool = True):
|
||||
self.debug = debug
|
||||
if self.debug:
|
||||
self.logger = getLogger('CONTROLLER')
|
||||
|
||||
self.__force_stop = False
|
||||
|
||||
self.__device = None
|
||||
self.__device_name = None
|
||||
|
||||
# open device
|
||||
self.port = self.__detect_serial(port if port is not None else -1)
|
||||
if self.port is not None and 'ttyUSB' in self.port:
|
||||
try:
|
||||
self.__device = PyFt232(self.port, debug=debug)
|
||||
self.__device_name = 'ft232'
|
||||
except Exception:
|
||||
self.__device = None
|
||||
else:
|
||||
try:
|
||||
self.__device = PyDuino(self.port, debug=debug)
|
||||
self.__device_name = 'arduino'
|
||||
except Exception:
|
||||
self.__device = None
|
||||
|
||||
# Arduino beacon indicator
|
||||
self.red = False
|
||||
self.orange = False
|
||||
self.green = False
|
||||
|
||||
self.led = False
|
||||
|
||||
self.defect_sensor_status = False
|
||||
self.color_sensor_status = False
|
||||
|
||||
# interrupt handler
|
||||
if not self.is_dummy:
|
||||
self.__device.watch(Inputs.START_BUTTON,
|
||||
self.__button_interrupt)
|
||||
self.__device.watch(Inputs.STOP_BUTTON,
|
||||
self.__button_interrupt)
|
||||
|
||||
self.__device.watch(Inputs.PHOTOELECTRIC_SENSOR_1,
|
||||
self.__sensor_interrupt)
|
||||
self.__device.watch(Inputs.PHOTOELECTRIC_SENSOR_2,
|
||||
self.__sensor_interrupt)
|
||||
|
||||
if self.debug:
|
||||
self.logger.info(
|
||||
f'use {"Dummy" if self.is_dummy else "Arduino"} Controller')
|
||||
|
||||
def __detect_serial(self, port):
|
||||
if isinstance(port, str):
|
||||
return port
|
||||
|
||||
if not isinstance(port, int):
|
||||
raise RuntimeError(
|
||||
f'Invalid port argument type: {port} - {type(port)}')
|
||||
|
||||
if platform in {'win32', 'cygwin'}:
|
||||
return f'COM{port}'
|
||||
|
||||
if not platform.startswith('linux'):
|
||||
raise RuntimeError('Not supported OS')
|
||||
|
||||
if port == -1:
|
||||
for path in listdir("/dev"):
|
||||
if path.startswith("ttyACM"):
|
||||
return f"/dev/{path}"
|
||||
else:
|
||||
return f'/dev/ttyACM{port}'
|
||||
|
||||
return None
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
if not self.is_dummy:
|
||||
self.system_start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if not self.is_dummy:
|
||||
self.system_stop()
|
||||
self.close()
|
||||
|
||||
def __set(self, pin, value):
|
||||
if self.is_dummy or self.__force_stop:
|
||||
return
|
||||
|
||||
self.__device.set(pin, value)
|
||||
|
||||
def __get(self, pin):
|
||||
if self.is_dummy:
|
||||
return None
|
||||
|
||||
return self.__device.get(pin)
|
||||
|
||||
def __led(self, pin, on):
|
||||
if self.__device_name == 'arduino':
|
||||
self.__set(
|
||||
pin,
|
||||
FactoryController.DEV_ON if on else FactoryController.DEV_OFF)
|
||||
|
||||
def __actuator(self, actuator_id):
|
||||
if self.__device_name == 'arduino':
|
||||
self.__set(actuator_id, FactoryController.DEV_ON)
|
||||
sleep(0.1)
|
||||
self.__set(actuator_id, FactoryController.DEV_OFF)
|
||||
elif self.__device_name == 'ft232':
|
||||
self.__set(PyFt232.PKT_CMD_DETECTION, actuator_id)
|
||||
sleep(0.5)
|
||||
self.__set(PyFt232.PKT_CMD_DETECTION, PyFt232.PKT_CMD_DETECTION_0)
|
||||
|
||||
@debounce(0.1)
|
||||
def __button_interrupt(self, pin: int, value: int, *_):
|
||||
if value != 0 or self.__force_stop:
|
||||
return
|
||||
|
||||
if pin == Inputs.START_BUTTON:
|
||||
self.system_start()
|
||||
|
||||
elif pin == Inputs.STOP_BUTTON:
|
||||
self.system_stop()
|
||||
|
||||
@debounce(0.3)
|
||||
def __sensor_interrupt(self, pin: int, value: int, *_):
|
||||
status = value == 0
|
||||
|
||||
if pin == Inputs.PHOTOELECTRIC_SENSOR_1:
|
||||
self.defect_sensor_status = status
|
||||
|
||||
elif pin == Inputs.PHOTOELECTRIC_SENSOR_2:
|
||||
self.color_sensor_status = status
|
||||
|
||||
###########################################################################
|
||||
# Public properties
|
||||
|
||||
@property
|
||||
def is_dummy(self):
|
||||
return self.__device is None
|
||||
|
||||
@property
|
||||
def red(self):
|
||||
return bool(self.__get(Outputs.BEACON_RED))
|
||||
|
||||
@red.setter
|
||||
def red(self, on):
|
||||
self.__led(Outputs.BEACON_RED, on)
|
||||
|
||||
@property
|
||||
def orange(self):
|
||||
return bool(self.__get(Outputs.BEACON_ORANGE))
|
||||
|
||||
@orange.setter
|
||||
def orange(self, on):
|
||||
self.__led(Outputs.BEACON_ORANGE, on)
|
||||
|
||||
@property
|
||||
def green(self):
|
||||
return bool(self.__get(Outputs.BEACON_GREEN))
|
||||
|
||||
@green.setter
|
||||
def green(self, on):
|
||||
self.__led(Outputs.BEACON_GREEN, on)
|
||||
|
||||
@property
|
||||
def led(self):
|
||||
return bool(self.__get(Outputs.LED))
|
||||
|
||||
@led.setter
|
||||
def led(self, on):
|
||||
self.__led(Outputs.LED, on)
|
||||
|
||||
@property
|
||||
def conveyor(self):
|
||||
return bool(self.__get(Outputs.CONVEYOR_EN))
|
||||
|
||||
@conveyor.setter
|
||||
def conveyor(self, on):
|
||||
if on:
|
||||
self.__set(Outputs.CONVEYOR_EN, FactoryController.DEV_ON)
|
||||
self.__set(Outputs.CONVEYOR_PWM, 255)
|
||||
else:
|
||||
self.__set(Outputs.CONVEYOR_PWM, 0)
|
||||
self.__set(Outputs.CONVEYOR_EN, FactoryController.DEV_OFF)
|
||||
|
||||
def push_actuator(self, num):
|
||||
if self.__device_name == 'arduino':
|
||||
if num == 1:
|
||||
self.__actuator(Outputs.ACTUATOR_1)
|
||||
else:
|
||||
self.__actuator(Outputs.ACTUATOR_2)
|
||||
elif self.__device_name == 'ft232':
|
||||
self.__actuator(num)
|
||||
|
||||
###########################################################################
|
||||
# Public methods
|
||||
|
||||
def system_start(self) -> None:
|
||||
if self.debug:
|
||||
self.logger.info("Start System")
|
||||
|
||||
self.conveyor = False
|
||||
|
||||
def system_stop(self) -> None:
|
||||
if self.debug:
|
||||
self.logger.info("Stop System")
|
||||
|
||||
self.conveyor = True
|
||||
|
||||
def close(self):
|
||||
if self.debug:
|
||||
self.logger.info("Shutdown System")
|
||||
|
||||
self.__force_stop = True
|
||||
if self.is_dummy:
|
||||
return
|
||||
|
||||
self.__device.close()
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Input / Output Pin map for the smart factory demo kit
|
||||
"""
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class Outputs(IntEnum):
|
||||
BEACON_RED = 2
|
||||
BEACON_ORANGE = 3
|
||||
BEACON_GREEN = 4
|
||||
|
||||
BEACON_BUZZER = 5
|
||||
|
||||
ACTUATOR_1 = 6
|
||||
ACTUATOR_2 = 7
|
||||
|
||||
CONVEYOR_EN = 8
|
||||
CONVEYOR_PWM = 9
|
||||
|
||||
LED = 14
|
||||
|
||||
|
||||
class Inputs(IntEnum):
|
||||
START_BUTTON = 10
|
||||
STOP_BUTTON = 11
|
||||
PHOTOELECTRIC_SENSOR_1 = 12
|
||||
PHOTOELECTRIC_SENSOR_2 = 13
|
||||
@@ -0,0 +1,294 @@
|
||||
"""
|
||||
Raw level arduino GPIO controll module
|
||||
"""
|
||||
|
||||
from logging import getLogger
|
||||
from queue import Queue
|
||||
from threading import Condition, Lock, Thread
|
||||
from typing import Callable, Optional
|
||||
|
||||
from serial import Serial
|
||||
|
||||
__all__ = ('PyDuino', )
|
||||
|
||||
|
||||
def _dummy(*_):
|
||||
pass
|
||||
|
||||
|
||||
class PyDuino:
|
||||
"""
|
||||
Raw level arduino GPIO controll class
|
||||
"""
|
||||
|
||||
PACKET_START = 0x55
|
||||
PACKET_END = 0xaa
|
||||
|
||||
PACKET_ID_INTERRUPT = 0
|
||||
PACKET_ID_MAX = 199
|
||||
PACKET_ID_ERROR = 0xff
|
||||
|
||||
def __init__(self,
|
||||
port: str,
|
||||
baudrate: int = 115200,
|
||||
*,
|
||||
debug: bool = False):
|
||||
self.debug = debug
|
||||
if self.debug:
|
||||
self.logger = getLogger('PYDUINO')
|
||||
self.logger.info("serial port %s (%d)", port, baudrate)
|
||||
|
||||
self.__arduino = None
|
||||
self.__stop_requested = False # to support graceful exit
|
||||
self.__force_stop = False
|
||||
self.__watcher = {}
|
||||
self.__values = [1] * 70 # Pin status
|
||||
for idx in (0, 1):
|
||||
self.__values[idx] = 0xff # NC
|
||||
|
||||
# init arduio
|
||||
arduino = Serial(port=port, baudrate=baudrate, timeout=1)
|
||||
arduino.reset_input_buffer()
|
||||
arduino.reset_output_buffer()
|
||||
self.__arduino = arduino
|
||||
|
||||
self.__init_cv = Condition()
|
||||
self.__init_input_pins = (10, 11, 12, 13)
|
||||
self.__init_count = 0
|
||||
|
||||
self.__rx = Thread(target=self.__receiver)
|
||||
self.__rx.name = 'Arduino Rx'
|
||||
self.__rx.start()
|
||||
|
||||
self.__init()
|
||||
|
||||
self.__tx_lock = Lock()
|
||||
self.__tx_cv = Condition()
|
||||
self.__tx_queue = Queue()
|
||||
self.__tx_packet_id = 1
|
||||
self.__tx = Thread(target=self.__transmitter)
|
||||
self.__tx.name = 'Arduino Tx'
|
||||
self.__tx.start()
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __wait_for_input_status(self, pin: int, value: int, _):
|
||||
self.__values[pin] = value
|
||||
|
||||
with self.__init_cv:
|
||||
self.__init_count += 1
|
||||
if self.__init_count == len(self.__init_input_pins):
|
||||
self.__init_cv.notify()
|
||||
|
||||
def __init(self):
|
||||
with self.__init_cv:
|
||||
self.__init_cv.wait()
|
||||
|
||||
for idx in self.__init_input_pins:
|
||||
self.watch(idx)
|
||||
|
||||
# pyduino.py - inside class PyDuino
|
||||
|
||||
def __transmitter(self):
|
||||
arduino = self.__arduino
|
||||
|
||||
while True:
|
||||
packet = self.__tx_queue.get()
|
||||
if self.debug:
|
||||
self.logger.info(packet)
|
||||
|
||||
pin, value, packet_id = packet
|
||||
self.__tx_queue.task_done()
|
||||
if pin is None:
|
||||
break # End of Tx
|
||||
|
||||
pin = int(pin) & 0xff
|
||||
value = int(value) & 0xff
|
||||
|
||||
try:
|
||||
arduino.write(bytes([
|
||||
PyDuino.PACKET_START, packet_id, pin, value,
|
||||
PyDuino.PACKET_END
|
||||
]))
|
||||
except Exception as e:
|
||||
# 치명 오류 시: 강제 정지하고 Tx 종료
|
||||
try:
|
||||
from serial import SerialException
|
||||
except Exception:
|
||||
SerialException = Exception
|
||||
if isinstance(e, (SerialException, OSError)):
|
||||
if self.debug:
|
||||
try: self.logger.warning("Tx write failed / serial down: %s", e)
|
||||
except Exception: pass
|
||||
self.__force_stop = True
|
||||
# set()에서 wait중일 수 있으니 깨워준다
|
||||
try:
|
||||
with self.__tx_cv:
|
||||
self.__tx_cv.notify_all()
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
# 기타 오류는 약간 쉬고 재시도(선택)
|
||||
import time; time.sleep(0.02)
|
||||
|
||||
# Rx Thread
|
||||
def __receiver(self):
|
||||
import time
|
||||
# 초기 입력핀 감시 등록(기존 로직 유지)
|
||||
for idx in self.__init_input_pins:
|
||||
self.__values[idx] = 0xff
|
||||
self.watch(idx, self.__wait_for_input_status)
|
||||
|
||||
while self.__force_stop is False:
|
||||
try:
|
||||
packet = self.__receive_packet()
|
||||
except Exception as e:
|
||||
# 시리얼 분리/중복접근 등 치명 오류 → 1회 로깅 후 조용히 종료
|
||||
try:
|
||||
from serial import SerialException
|
||||
except Exception:
|
||||
SerialException = Exception
|
||||
if isinstance(e, (SerialException, OSError)):
|
||||
if self.debug:
|
||||
try: self.logger.warning("Serial disconnected/busy: %s", e)
|
||||
except Exception: pass
|
||||
# 상태 플래그(옵션)
|
||||
try: self._connected = False
|
||||
except Exception: pass
|
||||
break
|
||||
# 기타 오류는 소프트 리트라이
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
|
||||
if packet is None:
|
||||
# 타임아웃/빈 읽기 → 살짝 쉬고 계속
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
|
||||
# 정상 패킷 처리
|
||||
pin, value, packet_id = packet
|
||||
self.__values[pin] = value
|
||||
|
||||
if packet_id == PyDuino.PACKET_ID_INTERRUPT:
|
||||
self.__watcher.get(pin, _dummy)(*packet)
|
||||
else:
|
||||
with self.__tx_cv:
|
||||
self.__tx_cv.notify()
|
||||
|
||||
def __receive_packet(self):
|
||||
arduino = self.__arduino
|
||||
|
||||
# 안전 타임아웃 보정(0/None이면 짧게 설정)
|
||||
try:
|
||||
if getattr(arduino, "timeout", None) in (None, 0):
|
||||
arduino.timeout = 0.2
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 첫 바이트 읽기
|
||||
try:
|
||||
data = arduino.read()
|
||||
except Exception:
|
||||
# 상위에서 예외 처리
|
||||
raise
|
||||
if not data or data[0] != PyDuino.PACKET_START:
|
||||
return None
|
||||
|
||||
# 나머지 4바이트 조립(타임아웃이면 None 반환)
|
||||
packet = []
|
||||
while len(packet) != 4:
|
||||
try:
|
||||
data = arduino.read()
|
||||
except Exception:
|
||||
raise
|
||||
if not data:
|
||||
return None # Timeout → 상위 루프가 부드럽게 스킵
|
||||
packet.append(data[0])
|
||||
|
||||
packet_id, pin, value, packet_end = packet
|
||||
if packet_id < 0 or packet_id > PyDuino.PACKET_ID_ERROR:
|
||||
return None
|
||||
|
||||
if pin < 0 or pin > len(self.__values):
|
||||
return None
|
||||
|
||||
if packet_end != PyDuino.PACKET_END:
|
||||
return None
|
||||
|
||||
return (pin, value, packet_id)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close existing arduino connection
|
||||
"""
|
||||
if self.__arduino is None:
|
||||
return
|
||||
|
||||
self.__stop_requested = True
|
||||
if self.__force_stop:
|
||||
return
|
||||
|
||||
# post end of tx marker
|
||||
self.__tx_queue.put((None, None, None))
|
||||
self.__tx.join()
|
||||
|
||||
# Stop Rx
|
||||
self.__force_stop = True
|
||||
self.__rx.join()
|
||||
|
||||
# close serial
|
||||
self.__arduino.close()
|
||||
|
||||
if self.debug:
|
||||
self.logger.info("stopped")
|
||||
|
||||
# pyduino.py - inside class PyDuino
|
||||
|
||||
def set(self, pin: int, value: int) -> bool:
|
||||
"""
|
||||
Set arduino pin with given value
|
||||
"""
|
||||
if self.__stop_requested or self.__force_stop:
|
||||
return False
|
||||
|
||||
with self.__tx_lock:
|
||||
packet_id = self.__tx_packet_id
|
||||
|
||||
ok = False
|
||||
with self.__tx_cv:
|
||||
self.__tx_queue.put((pin, value, packet_id))
|
||||
# wait()에 타임아웃 추가 (예: 0.5s)
|
||||
self.__tx_cv.wait(timeout=0.5)
|
||||
ok = not self.__force_stop # Rx/Tx가 죽었으면 False
|
||||
|
||||
self.__tx_packet_id += 1
|
||||
if self.__tx_packet_id == PyDuino.PACKET_ID_MAX:
|
||||
self.__tx_packet_id = 1
|
||||
|
||||
return ok
|
||||
|
||||
def get(self, pin: int) -> int:
|
||||
"""
|
||||
Get current arduino pin value from proxy
|
||||
"""
|
||||
return self.__values[pin]
|
||||
|
||||
def watch(
|
||||
self,
|
||||
pin: int,
|
||||
callback: Optional[Callable[[int, int, int],
|
||||
None]] = None) -> None:
|
||||
"""
|
||||
Set callback function for given pin
|
||||
"""
|
||||
if callback is None:
|
||||
callback = _dummy
|
||||
|
||||
self.__watcher[pin] = callback
|
||||
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
Raw level FT232 controll module
|
||||
"""
|
||||
|
||||
from logging import getLogger
|
||||
from queue import Queue
|
||||
from threading import Condition, Lock, Thread
|
||||
from typing import Callable, Optional
|
||||
|
||||
from serial import Serial
|
||||
|
||||
__all__ = ('PyFt232', )
|
||||
|
||||
|
||||
def _dummy(*_):
|
||||
pass
|
||||
|
||||
|
||||
class PyFt232:
|
||||
"""
|
||||
Raw level FT232 controll class
|
||||
"""
|
||||
|
||||
PKT_START = 0xaa
|
||||
PKT_END = 0xaa
|
||||
|
||||
PKT_CMD_ACK = 0x00
|
||||
|
||||
PKT_CMD_START = 0x01
|
||||
PKT_CMD_START_STOP = 0x00
|
||||
PKT_CMD_START_START = 0x01
|
||||
|
||||
PKT_CMD_LOADING = 0x02
|
||||
PKT_CMD_LOADING_STOP = 0x00
|
||||
PKT_CMD_LOADING_LOAD = 0x01
|
||||
|
||||
PKT_CMD_DETECTION = 0x03
|
||||
PKT_CMD_DETECTION_0 = 0x00
|
||||
PKT_CMD_DETECTION_1 = 0x01
|
||||
PKT_CMD_DETECTION_2 = 0x02
|
||||
PKT_CMD_DETECTION_3 = 0x03
|
||||
|
||||
PKT_CMD_SPEED = 0x04
|
||||
PKT_CMD_SPEED_STOP = 0x00
|
||||
PKT_CMD_SPEED_UP = 0x01
|
||||
PKT_CMD_SPEED_DOWN = 0x02
|
||||
|
||||
#PKT_CMD_AUTO_LOADING = 0x05
|
||||
#PKT_CMD_VERSION = 0x64
|
||||
|
||||
PKT_ID_MAX = 199
|
||||
PKT_ID_ERROR = 0xff
|
||||
|
||||
def __init__(self,
|
||||
port: str = '/dev/ttyUSB0',
|
||||
baudrate: int = 19200,
|
||||
*,
|
||||
debug: bool = False):
|
||||
self.debug = debug
|
||||
if self.debug:
|
||||
self.logger = getLogger('PYFT232')
|
||||
self.logger.info("serial port %s (%d)", port, baudrate)
|
||||
|
||||
self.__ft232 = None
|
||||
self.__stop_requested = False # to support graceful exit
|
||||
self.__force_stop = False
|
||||
self.__values = [1] * 14 # Pin status
|
||||
for idx in (0, 1):
|
||||
self.__values[idx] = 0xff # NC
|
||||
|
||||
# init arduio
|
||||
ft232 = Serial(port=port, baudrate=baudrate, timeout=1)
|
||||
ft232.reset_input_buffer()
|
||||
ft232.reset_output_buffer()
|
||||
self.__ft232 = ft232
|
||||
|
||||
self.__rx = Thread(target=self.__receiver)
|
||||
self.__rx.name = 'Arduino Rx'
|
||||
self.__rx.start()
|
||||
|
||||
self.__init()
|
||||
|
||||
self.__tx_lock = Lock()
|
||||
self.__tx_cv = Condition()
|
||||
self.__tx_queue = Queue()
|
||||
self.__tx = Thread(target=self.__transmitter)
|
||||
self.__tx.name = 'Arduino Tx'
|
||||
self.__tx.start()
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __checksum(self, length: int, cmd: int, data: list):
|
||||
return ~((length + cmd + sum(data)) & 0xff) & 0xff
|
||||
|
||||
def __init(self):
|
||||
pass
|
||||
|
||||
# Tx Thread
|
||||
def __transmitter(self):
|
||||
ft232 = self.__ft232
|
||||
|
||||
while True:
|
||||
packet = self.__tx_queue.get()
|
||||
if self.debug:
|
||||
self.logger.info(packet)
|
||||
|
||||
if len(packet) != 2:
|
||||
break
|
||||
|
||||
cmd, data = packet
|
||||
self.__tx_queue.task_done()
|
||||
if cmd is None:
|
||||
break # End of Tx
|
||||
|
||||
ft232.write(
|
||||
bytes([
|
||||
PyFt232.PKT_START, 0x05, cmd, data, self.__checksum(0x05, cmd, [data])
|
||||
]))
|
||||
|
||||
# Rx Thread
|
||||
def __receiver(self):
|
||||
while self.__force_stop is False:
|
||||
packet = self.__receive_packet()
|
||||
if self.debug:
|
||||
self.logger.info(packet)
|
||||
if packet is None:
|
||||
continue
|
||||
|
||||
# TODO : What need to be done?
|
||||
cmd, data = packet
|
||||
|
||||
# run callback
|
||||
with self.__tx_cv:
|
||||
self.__tx_cv.notify()
|
||||
|
||||
def __receive_packet(self):
|
||||
ft232 = self.__ft232
|
||||
|
||||
data = ft232.read()
|
||||
if len(data) == 0 or data[0] != PyFt232.PKT_START:
|
||||
return None
|
||||
|
||||
packet = []
|
||||
while len(packet) != 5:
|
||||
data = ft232.read()
|
||||
if len(data) == 0:
|
||||
return None # Timeout
|
||||
|
||||
packet.append(data[0])
|
||||
|
||||
length, cmd, data, dataerr, checksum = packet
|
||||
if length != 6:
|
||||
return None
|
||||
|
||||
if cmd != 0x00 or dataerr != 0x00:
|
||||
return None
|
||||
|
||||
if self.__checksum(length, cmd, [data, dataerr]) != checksum:
|
||||
return None
|
||||
|
||||
return (cmd, data)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close existing ft232 connection
|
||||
"""
|
||||
if self.__ft232 is None:
|
||||
return
|
||||
|
||||
self.__stop_requested = True
|
||||
if self.__force_stop:
|
||||
return
|
||||
|
||||
# post end of tx marker
|
||||
self.__tx_queue.put((None, None, None))
|
||||
self.__tx.join()
|
||||
|
||||
# Stop Rx
|
||||
self.__force_stop = True
|
||||
self.__rx.join()
|
||||
|
||||
# close serial
|
||||
self.__ft232.close()
|
||||
|
||||
if self.debug:
|
||||
self.logger.info("stopped")
|
||||
|
||||
def set(self, cmd: int, value: int) -> bool:
|
||||
"""
|
||||
Set ft232 pin with given value
|
||||
"""
|
||||
if self.__stop_requested:
|
||||
return False
|
||||
|
||||
with self.__tx_lock:
|
||||
with self.__tx_cv:
|
||||
self.__tx_queue.put((cmd, value))
|
||||
self.__tx_cv.wait()
|
||||
|
||||
return True
|
||||
|
||||
def get(self, pin: int) -> int:
|
||||
"""
|
||||
Get current ft232 pin value from proxy
|
||||
"""
|
||||
return self.__values[pin]
|
||||
Reference in new issue
Block a user