mirror of
https://github.com/lowobservable/oec.git
synced 2026-02-25 08:21:24 +00:00
Refactor controller and add device
This commit is contained in:
@@ -6,39 +6,37 @@ oec.controller
|
||||
import time
|
||||
import logging
|
||||
import selectors
|
||||
from coax import Poll, PollAck, KeystrokePollResponse, ReceiveTimeout, \
|
||||
ReceiveError, ProtocolError
|
||||
from coax import Poll, PollAck, KeystrokePollResponse, ReceiveTimeout
|
||||
|
||||
from .interface import address_commands
|
||||
from .terminal import create_terminal, UnsupportedTerminalError
|
||||
from .device import address_commands, format_address, UnsupportedDeviceError
|
||||
from .keyboard import Key
|
||||
from .session import SessionDisconnectedError
|
||||
|
||||
class Controller:
|
||||
"""The controller."""
|
||||
|
||||
def __init__(self, interface, get_keymap, create_session):
|
||||
def __init__(self, interface, create_device, create_session):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
self.interface = interface
|
||||
self.running = False
|
||||
|
||||
self.interface = interface
|
||||
self.get_keymap = get_keymap
|
||||
self.create_device = create_device
|
||||
self.create_session = create_session
|
||||
|
||||
self.terminal = None
|
||||
self.session = None
|
||||
self.device = None
|
||||
|
||||
self.session = None
|
||||
self.session_selector = None
|
||||
|
||||
# Target time between POLL commands in seconds when a terminal is connected or
|
||||
# no terminal is connected.
|
||||
# Target time between POLL commands in seconds when a device is attached or
|
||||
# no device is attached.
|
||||
#
|
||||
# The connected poll period only applies in cases where the terminal responded
|
||||
# with TR/TA to the last poll - this is an effort to improve the keystroke
|
||||
# The attached poll period only applies in cases where the device responded
|
||||
# with TT/AR to the last poll - this is an effort to improve the keystroke
|
||||
# responsiveness.
|
||||
self.connected_poll_period = 1 / 10
|
||||
self.disconnected_poll_period = 5
|
||||
self.attached_poll_period = 1 / 10
|
||||
self.detatached_poll_period = 5
|
||||
|
||||
self.last_poll_time = None
|
||||
self.last_poll_response = None
|
||||
@@ -58,82 +56,54 @@ class Controller:
|
||||
|
||||
self.session_selector = None
|
||||
|
||||
if self.terminal:
|
||||
self.terminal = None
|
||||
if self.device:
|
||||
self.device = None
|
||||
|
||||
def stop(self):
|
||||
"""Stop the controller."""
|
||||
self.running = False
|
||||
|
||||
def _run_loop(self):
|
||||
device_address = None
|
||||
|
||||
poll_delay = self._calculate_poll_delay(time.perf_counter())
|
||||
|
||||
# If POLLing is delayed, handle the host output, otherwise just sleep.
|
||||
if poll_delay > 0:
|
||||
if self.session:
|
||||
try:
|
||||
self._update_session(poll_delay)
|
||||
except SessionDisconnectedError:
|
||||
self._handle_session_disconnected()
|
||||
self._update_session(poll_delay)
|
||||
else:
|
||||
time.sleep(poll_delay)
|
||||
|
||||
# POLL devices.
|
||||
self._poll_attached_device()
|
||||
self._poll_detatched_device()
|
||||
|
||||
def _update_session(self, duration):
|
||||
try:
|
||||
poll_response = self._poll(device_address)
|
||||
except ReceiveTimeout:
|
||||
if self.terminal:
|
||||
self._handle_terminal_detached()
|
||||
update_count = 0
|
||||
|
||||
return
|
||||
except ReceiveError as error:
|
||||
self.logger.warning(f'POLL receive error: {error}', exc_info=error)
|
||||
return
|
||||
except ProtocolError as error:
|
||||
self.logger.warning(f'POLL protocol error: {error}', exc_info=error)
|
||||
return
|
||||
while duration > 0:
|
||||
start_time = time.perf_counter()
|
||||
|
||||
if not self.terminal:
|
||||
try:
|
||||
self._handle_terminal_attached(device_address, poll_response)
|
||||
except UnsupportedTerminalError as error:
|
||||
self.logger.error(f'Unsupported terminal: {error}')
|
||||
return
|
||||
selected = self.session_selector.select(duration)
|
||||
|
||||
if poll_response:
|
||||
self._handle_poll_response(poll_response)
|
||||
if not selected:
|
||||
break
|
||||
|
||||
def _handle_terminal_attached(self, device_address, poll_response):
|
||||
self.logger.info('Terminal attached')
|
||||
for (key, _) in selected:
|
||||
session = key.fileobj
|
||||
|
||||
self.terminal = create_terminal(self.interface, device_address, poll_response,
|
||||
self.get_keymap)
|
||||
if session.handle_host():
|
||||
update_count += 1
|
||||
|
||||
self.terminal.setup()
|
||||
duration -= (time.perf_counter() - start_time)
|
||||
|
||||
# Show the attached indicator on the status line.
|
||||
self.terminal.display.status_line.write_string(0, 'S')
|
||||
|
||||
# Start the session.
|
||||
self._start_session()
|
||||
|
||||
def _handle_terminal_detached(self):
|
||||
self.logger.info('Terminal detached')
|
||||
|
||||
self._terminate_session()
|
||||
|
||||
self.terminal = None
|
||||
|
||||
def _handle_session_disconnected(self):
|
||||
self.logger.info('Session disconnected')
|
||||
|
||||
self._terminate_session()
|
||||
|
||||
# Restart the session.
|
||||
self._start_session()
|
||||
if update_count > 0:
|
||||
self.session.render()
|
||||
except SessionDisconnectedError:
|
||||
self._handle_session_disconnected()
|
||||
|
||||
def _start_session(self):
|
||||
self.session = self.create_session(self.terminal)
|
||||
self.session = self.create_session(self.device)
|
||||
|
||||
self.session.start()
|
||||
|
||||
@@ -149,36 +119,90 @@ class Controller:
|
||||
|
||||
self.session = None
|
||||
|
||||
def _update_session(self, duration):
|
||||
update_count = 0
|
||||
def _handle_session_disconnected(self):
|
||||
self.logger.info('Session disconnected')
|
||||
|
||||
while duration > 0:
|
||||
start_time = time.perf_counter()
|
||||
self._terminate_session()
|
||||
|
||||
selected = self.session_selector.select(duration)
|
||||
# Restart the session.
|
||||
self._start_session()
|
||||
|
||||
if not selected:
|
||||
break
|
||||
def _poll_attached_device(self):
|
||||
if not self.device:
|
||||
return
|
||||
|
||||
for (key, events) in selected:
|
||||
session = key.fileobj
|
||||
self.last_poll_time = time.perf_counter()
|
||||
|
||||
if session.handle_host():
|
||||
update_count += 1
|
||||
try:
|
||||
poll_response = self.device.poll()
|
||||
except ReceiveTimeout:
|
||||
self._handle_device_lost()
|
||||
return
|
||||
|
||||
duration -= (time.perf_counter() - start_time)
|
||||
if poll_response:
|
||||
self._poll_ack(self.device.device_address)
|
||||
|
||||
if update_count > 0:
|
||||
self.session.render()
|
||||
self._handle_poll_response(poll_response)
|
||||
|
||||
self.last_poll_response = poll_response
|
||||
|
||||
def _poll_detatched_device(self):
|
||||
if self.device:
|
||||
return
|
||||
|
||||
self.last_poll_time = time.perf_counter()
|
||||
|
||||
device_address = None
|
||||
|
||||
try:
|
||||
poll_response = self._poll(device_address)
|
||||
except ReceiveTimeout:
|
||||
return
|
||||
|
||||
if poll_response:
|
||||
self._poll_ack(device_address)
|
||||
|
||||
self._handle_device_found(device_address, poll_response)
|
||||
|
||||
self.last_poll_response = poll_response
|
||||
|
||||
def _handle_device_found(self, device_address, poll_response):
|
||||
self.logger.info(f'Found device @ {format_address(self.interface, device_address)}')
|
||||
|
||||
try:
|
||||
device = self.create_device(self.interface, device_address, poll_response)
|
||||
except UnsupportedDeviceError as error:
|
||||
self.logger.error(f'Unsupported device @ {format_address(self.interface, device_address)}: {error}')
|
||||
return
|
||||
|
||||
device.setup()
|
||||
|
||||
self.device = device
|
||||
|
||||
self.logger.info(f'Attached device @ {format_address(self.interface, device_address)}')
|
||||
|
||||
self._start_session()
|
||||
|
||||
def _handle_device_lost(self):
|
||||
device_address = self.device.device_address
|
||||
|
||||
self.logger.info(f'Lost device @ {format_address(self.interface, device_address)}')
|
||||
|
||||
self._terminate_session()
|
||||
|
||||
self.device = None
|
||||
|
||||
self.logger.info(f'Detached device @ {format_address(self.interface, device_address)}')
|
||||
|
||||
def _handle_poll_response(self, poll_response):
|
||||
if isinstance(poll_response, KeystrokePollResponse):
|
||||
self._handle_keystroke_poll_response(poll_response)
|
||||
|
||||
def _handle_keystroke_poll_response(self, poll_response):
|
||||
terminal = self.device
|
||||
scan_code = poll_response.scan_code
|
||||
|
||||
(key, modifiers, modifiers_changed) = self.terminal.keyboard.get_key(scan_code)
|
||||
(key, modifiers, modifiers_changed) = terminal.keyboard.get_key(scan_code)
|
||||
|
||||
if self.logger.isEnabledFor(logging.DEBUG):
|
||||
self.logger.debug((f'Keystroke detected: Scan Code = {scan_code}, '
|
||||
@@ -186,41 +210,27 @@ class Controller:
|
||||
|
||||
# Update the status line if modifiers have changed.
|
||||
if modifiers_changed:
|
||||
self.terminal.display.status_line.write_keyboard_modifiers(modifiers)
|
||||
terminal.display.status_line.write_keyboard_modifiers(modifiers)
|
||||
|
||||
if not key:
|
||||
return
|
||||
|
||||
if key == Key.CURSOR_BLINK:
|
||||
self.terminal.display.toggle_cursor_blink()
|
||||
terminal.display.toggle_cursor_blink()
|
||||
elif key == Key.ALT_CURSOR:
|
||||
self.terminal.display.toggle_cursor_reverse()
|
||||
terminal.display.toggle_cursor_reverse()
|
||||
elif key == Key.CLICKER:
|
||||
self.terminal.keyboard.toggle_clicker()
|
||||
terminal.keyboard.toggle_clicker()
|
||||
elif self.session:
|
||||
self.session.handle_key(key, modifiers, scan_code)
|
||||
|
||||
self.session.render()
|
||||
|
||||
def _poll(self, device_address):
|
||||
self.last_poll_time = time.perf_counter()
|
||||
return self.interface.execute(address_commands(device_address, Poll()))
|
||||
|
||||
# If a terminal is connected, use the terminal method to ensure that
|
||||
# any queued POLL action is applied.
|
||||
if self.terminal:
|
||||
poll_response = self.terminal.poll()
|
||||
else:
|
||||
poll_response = self.interface.execute(address_commands(device_address, Poll()))
|
||||
|
||||
if poll_response:
|
||||
try:
|
||||
self.interface.execute(address_commands(device_address, PollAck()))
|
||||
except ProtocolError as error:
|
||||
self.logger.warning(f'POLL_ACK protocol error: {error}', exc_info=error)
|
||||
|
||||
self.last_poll_response = poll_response
|
||||
|
||||
return poll_response
|
||||
def _poll_ack(self, device_address):
|
||||
self.interface.execute(address_commands(device_address, PollAck()))
|
||||
|
||||
def _calculate_poll_delay(self, current_time):
|
||||
if self.last_poll_response is not None:
|
||||
@@ -229,9 +239,9 @@ class Controller:
|
||||
if self.last_poll_time is None:
|
||||
return 0
|
||||
|
||||
if self.terminal:
|
||||
period = self.connected_poll_period
|
||||
if self.device:
|
||||
period = self.attached_poll_period
|
||||
else:
|
||||
period = self.disconnected_poll_period
|
||||
period = self.detatached_poll_period
|
||||
|
||||
return max((self.last_poll_time + period) - current_time, 0)
|
||||
|
||||
Reference in New Issue
Block a user