Skip to content

Commit

Permalink
WIP to abstract out TCP connection handling.
Browse files Browse the repository at this point in the history
  • Loading branch information
tjsr committed Feb 5, 2025
1 parent 0b7af88 commit dceff77
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 42 deletions.
108 changes: 106 additions & 2 deletions TimingDevices/TimingDevice.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import datetime
import socket
from abc import abstractmethod
from logging import Logger, getLogger
from types import TracebackType
from typing import List, Type, Callable
from typing import List, Type, Callable, Any, Generic, TypeVar

from LogQueue import LogQueue

CrossingListenerCallableType = Callable[[(str, datetime.datetime)], None]

class SettingChangeCommand:
pass


class DecoderMessage:
def __init__(self):
pass

class UnknownTimingDeviceSetting(Exception):
def __init__(self, setting: str):
super().__init__(f'Unknown setting: {setting}')

class UnrecognisedDecoderMessage(DecoderMessage):
_message: str
def __init__(self, message: str):
super().__init__()
self._message = message

class TimingDeviceCommand:
CommandResponse = TypeVar('CommandResponse')

class TimingDeviceCommand(Generic[CommandResponse]):
_command_str: str
_sync: bool = False
_response: Any

def __init__( self, _command_str: str, sync: bool = False ):
self._command_str = _command_str
Expand All @@ -31,12 +44,74 @@ def is_sync_command(self) -> bool:
def get_command_string(self) -> str:
return self._command_str

@property
def response(self) -> CommandResponse:
return self._response


class TCPTimingDevice:
DEFAULT_PORT: int = 23
DEFAULT_HOST: str = '127.0.0.1'

_host: str = DEFAULT_HOST
_port: int = DEFAULT_PORT
_s: socket.socket | None = None
_timeoutSecs: int = 5

def __init__(self, host: str, port: int ):
self._host = host
self._port = port

@abstractmethod
def getLog(self) -> Logger:
pass

@abstractmethod
def getDeviceType(self) -> str:
pass

def connect(self) -> bool:
log = self.getLog()
device = self.getDeviceType()
# TODO: wrap with _ for internationalisation
description = f'{device} decoder at {self._host}:{self._port}'

# -----------------------------------------------------------------------------------------------------
log.info(_('Attempting to connect to {}').format(description))
try:
self._s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._s.settimeout(self._timeoutSecs)
self._s.connect((self._host, self._port))
except Exception as e:
log.exception('{}: {}'.format(_('Connection failed to {}'), description, e))
self._s = None
return False

log.info(_('Successfully connected to {}').format(description))
return True

def disconnect(self) -> bool:
if self._s is not None:
try:
self._s.shutdown(socket.SHUT_RDWR)
self._s.close()
return True
except Exception:
pass
return False


class TimingDevice:
_readonly = False
_logger: LogQueue | None = None
_log: Logger | None = None
_messageBuffer: List[DecoderMessage] = []

def getLog(self) -> Logger:
if self._log is None:
self._log = getLogger(self.__class__.__name__)
return self._log

def is_readonly_device(self) -> bool:
return self._readonly

Expand Down Expand Up @@ -81,6 +156,35 @@ def begin_reading( self ):
self.send_command('start')
pass

def get_status( self ):
self.send_command('status')
pass

def get_time( self ):
self.send_command('get_time')
pass

def send_records_from_last(self):
self.send_command('send_records')
pass

def send_records_from_time(self, time: datetime.datetime):
self.send_command('send_records')
pass

def get_setting(self, setting: str):
if not self.is_valid_setting(setting):
raise UnknownTimingDeviceSetting(setting)
self.send_command('get_setting')
pass

def change_setting(self, settingChangeCommand: SettingChangeCommand):
raise NotImplementedError()

def is_valid_setting(self, setting: str) -> bool:
# TODO: Implement this
return True

def sync_send_command(self, command: str, comment: str = None):
pass

Expand Down
88 changes: 48 additions & 40 deletions TimingDevices/UltraTimingDevice.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import datetime
import socket
import time
from logging import Logger
from typing import List, Optional

from openpyxl.pivot.fields import Boolean

from LogQueue import LogQueue
from SocketUtils import socketReadDelimited, socketSendMessage
from TimingDevices.TimingDevice import TimingDeviceCommand, UnrecognisedCommandException, TimingDevice, DecoderMessage, \
UnrecognisedDecoderMessage, CrossingListenerCallableType
UnrecognisedDecoderMessage, CrossingListenerCallableType, TCPTimingDevice
import re

from TimingDevices.UltraAutodetect import AutoDetect
Expand All @@ -17,30 +20,26 @@
# if we get the same time, make sure we give it a small offset to make it unique, but preserve the order.
tSmall = datetime.timedelta( seconds = 0.000001 )

class UltraDecoder(TimingDevice):
class UltraDecoder(TimingDevice, TCPTimingDevice):
commands = {
'start': TimingDeviceCommand('R', False),
'stop': TimingDeviceCommand('S', False)
'stop': TimingDeviceCommand('S', False),
'status': TimingDeviceCommand('?', True)
}

DEFAULT_PORT: int = 23
# DEFAULT_PORT = 8642
DEFAULT_HOST: str = '127.0.0.1' # Port to connect to the Ultra receiver.

_host: str = DEFAULT_HOST
_port: int = DEFAULT_PORT
_s: socket.socket | None = None
_delaySecs: int = 3
_timeoutSecs: int = 5
_lastVoltage: datetime.datetime | None = None
_computerTimeDiff: datetime.timedelta | None = None
_crossing_listener: CrossingListenerCallableType | None = None

def __init__( self, log: LogQueue, host: str, port: int ):
super().__init__()
TCPTimingDevice.__init__(self, host, port)
self.logger = log
self._host = host
self._port = port

@property
def crossingListener(self) -> CrossingListenerCallableType:
Expand Down Expand Up @@ -70,32 +69,6 @@ def stop_reading(self) -> None:
except ValueError:
pass

def disconnect(self) -> bool:
if self._s is not None:
try:
self._s.shutdown(socket.SHUT_RDWR)
self._s.close()
return True
except Exception:
pass
return False

def connect(self) -> bool:
# -----------------------------------------------------------------------------------------------------
self.log('connection', '{} {}:{}'.format(_('Attempting to connect to Ultra reader at'), self._host, self._port))
try:
self._s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._s.settimeout(self._timeoutSecs)
self._s.connect((self._host, self._port))
except Exception as e:
self.log('connection', '{}: {}'.format(_('Connection to Ultra reader failed'), e))
self._s = None
return False

self.log('connection', '{} {}:{}'.format(_('connect to Ultra reader SUCCEEDS on'), self._host, self._port))
return True


def autoconnect(self, autoDetectCallback) -> bool:
self.log('autoconnect', '{}'.format(_('Attempting AutoDetect...')))
HOST_AUTO = AutoDetect(callback=autoDetectCallback)
Expand Down Expand Up @@ -276,11 +249,19 @@ def makeCall(self, message: str, comment: str = '') -> None:
CONNECT_INFO_FORMAT = r'^\d{1,2}:\d{1,2}:\d{1,2} \d{1,2}-\d{1,2}-\d{4} \(-?\d+\)$'

class UltraDecoderMessage(DecoderMessage):
UltraId: int # Integer value. See section 3.1
_UltraId: int | None # Integer value. See section 3.1

def __init__(self, ultraId: int):
def __init__(self, ultraId: int | None):
super().__init__()
self.UltraId = ultraId
self._UltraId = ultraId

@property
def UltraId(self) -> int | None:
return self._UltraId

@UltraId.setter
def UltraId(self, value: int):
self._UltraId = value

class UltraConnectConfirmationMessage(UltraDecoderMessage):
def __init__(self, ultraId: int):
Expand Down Expand Up @@ -325,9 +306,36 @@ def __init__(self, ultraId: int, voltage: float):
def Voltage(self) -> float:
return self._Voltage

class DecoderStatusMessage(UltraDecoderMessage):
_readStatus: bool
_sendStatus: bool

@property
def Voltage(self) -> float:
return self.Voltage
def readStatus(self) -> bool:
return self._readStatus

@property
def sendStatus(self) -> bool:
return self._sendStatus

@staticmethod
def parse(message: str) -> Optional['DecoderStatusMessage']:
if message is not None and message.startswith('S'):
try:
_, Payload = message.split('=', 1)
if len(Payload) == 2:
statusInt = int(Payload)
readStatus = statusInt // 10
sendStatus = statusInt % 10
return DecoderStatusMessage(readStatus == 1, sendStatus == 1)

except ValueError:
return None
return
def __init__(self, readStatus: bool, sendStatus: bool):
super().__init__(None)
self._readStatus = readStatus
self._sendStatus = sendStatus

# Definitions from https://rfidtiming.com/Software/UltraManual.pdf Pg41
class UltraChipReadMessage(UltraDecoderMessage):
Expand Down

0 comments on commit dceff77

Please sign in to comment.