Skip to content

Commit

Permalink
#3 client get/set/command to accept timeout argument.
Browse files Browse the repository at this point in the history
  • Loading branch information
yozik04 committed Mar 31, 2020
1 parent 7257efe commit d7aa1b1
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 109 deletions.
4 changes: 2 additions & 2 deletions nextion/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .client import Nextion, EventType
from .client import EventType, Nextion
from .exceptions import CommandFailed, CommandTimeout

__all__ = ['Nextion', 'CommandFailed', 'CommandTimeout']
__all__ = ["Nextion", "CommandFailed", "CommandTimeout"]
135 changes: 83 additions & 52 deletions nextion/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,69 @@

import serial_asyncio

from .exceptions import CommandFailed, CommandTimeout, ConnectionFailed
from .protocol import EventType, NextionProtocol, ResponseType

TIME_TO_RECOVER_FROM_SLEEP = 0.15
READ_TIMEOUT = 0.15
IO_TIMEOUT = 0.15
BAUDRATES = [2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400]

from .exceptions import CommandFailed, CommandTimeout, ConnectionFailed
from .protocol import NextionProtocol, EventType, ResponseType

logger = logging.getLogger('nextion').getChild(__name__)
logger = logging.getLogger("nextion").getChild(__name__)

TouchDataPayload = namedtuple('Touch', 'page_id component_id touch_event')
TouchCoordinateDataPayload = namedtuple('TouchCoordinate', 'x y touch_event')
TouchDataPayload = namedtuple("Touch", "page_id component_id touch_event")
TouchCoordinateDataPayload = namedtuple("TouchCoordinate", "x y touch_event")


class Nextion:
def __init__(self, url: str, baudrate: int = None, event_handler: typing.Callable[[EventType, any], None] = None,
loop=asyncio.get_event_loop()):
def __init__(
self,
url: str,
baudrate: int = None,
event_handler: typing.Callable[[EventType, any], None] = None,
loop=asyncio.get_event_loop(),
):
self._loop = loop

self._url = url
self._baudrate = baudrate
self._connection = None
self._command_lock = asyncio.Lock()
self.event_handler = event_handler or (lambda t, d: logger.info('Event %s data: %s' % (t, str(d))))
self.event_handler = event_handler or (
lambda t, d: logger.info("Event %s data: %s" % (t, str(d)))
)

self._sleeping = True
self.sets_todo = {}

async def on_wakeup(self):
await asyncio.sleep(TIME_TO_RECOVER_FROM_SLEEP) # do not execute next messages until full wakeup
await asyncio.sleep(
TIME_TO_RECOVER_FROM_SLEEP
) # do not execute next messages until full wakeup
for k, v in self.sets_todo.items():
self._loop.create_task(self.set(k, v))
self.sets_todo = {}
self._sleeping = False

def event_message_handler(self, message):
logger.debug('Handle event: %s', message)
logger.debug("Handle event: %s", message)

typ = message[0]
if typ == EventType.TOUCH: # Touch event
self.event_handler(EventType(typ), TouchDataPayload._make(struct.unpack('BBB', message[1:])))
self.event_handler(
EventType(typ),
TouchDataPayload._make(struct.unpack("BBB", message[1:])),
)
elif typ == EventType.TOUCH_COORDINATE: # Touch coordinate
self.event_handler(EventType(typ), TouchCoordinateDataPayload._make(struct.unpack('HHB', message[1:])))
self.event_handler(
EventType(typ),
TouchCoordinateDataPayload._make(struct.unpack("HHB", message[1:])),
)
elif typ == EventType.TOUCH_IN_SLEEP: # Touch event in sleep mode
self.event_handler(EventType(typ), TouchCoordinateDataPayload._make(struct.unpack('HHB', message[1:])))
self.event_handler(
EventType(typ),
TouchCoordinateDataPayload._make(struct.unpack("HHB", message[1:])),
)
elif typ == EventType.AUTO_SLEEP: # Device automatically enters into sleep mode
self._sleeping = True
self.event_handler(EventType(typ), None)
Expand All @@ -62,7 +81,7 @@ def event_message_handler(self, message):
elif typ == EventType.SD_CARD_UPGRADE: # Start SD card upgrade
self.event_handler(EventType(typ), None)
else:
logger.warning('Other event: 0x%02x', typ)
logger.warning("Other event: 0x%02x", typ)

def _make_protocol(self) -> NextionProtocol:
return NextionProtocol(event_message_handler=self.event_message_handler)
Expand All @@ -78,85 +97,93 @@ async def connect(self) -> bool:
baudrates.insert(0, self._baudrate)

for baud in baudrates:
logger.info('Connecting: %s, baud: %s', self._url, baud)
logger.info("Connecting: %s, baud: %s", self._url, baud)
try:
_, self._connection = await serial_asyncio.create_serial_connection(self._loop, self._make_protocol,
url=self._url,
baudrate=baud)
_, self._connection = await serial_asyncio.create_serial_connection(
self._loop, self._make_protocol, url=self._url, baudrate=baud
)
except OSError as e:
if e.errno == 2:
raise ConnectionFailed('Connect failed: %s' % e)
raise ConnectionFailed("Connect failed: %s" % e)
else:
logger.warning('Baud %s not supported: %s', baud, e)
logger.warning("Baud %s not supported: %s", baud, e)
continue

await self._connection.wait_connection()

self._connection.write('')
self._connection.write("")

async with self._command_lock:
self._connection.write('connect')
self._connection.write("connect")
try:
result = await self._read()
if result[:6] == b'comok ':
if result[:6] == b"comok ":
self._baudrate = baud
connected = True
break
else:
logger.warning('Wrong reply to connect attempt. Closing connection')
logger.warning(
"Wrong reply to connect attempt. Closing connection"
)
self._connection.close()
except asyncio.TimeoutError as e:
logger.warning('Time outed connection attempt. Closing connection')
logger.warning("Time outed connection attempt. Closing connection")
self._connection.close()

await asyncio.sleep(READ_TIMEOUT)
await asyncio.sleep(IO_TIMEOUT)

if not connected:
raise ConnectionFailed('Connect failed') from e
raise ConnectionFailed("Connect failed") from e

data = result[7:].decode().split(",")
logger.info('Detected model: %s', data[2])
logger.info('Firmware version: %s', data[3])
logger.info('Serial number: %s', data[5])
logger.debug('Flash size: %s', data[6])
logger.info("Detected model: %s", data[2])
logger.info("Firmware version: %s", data[3])
logger.info("Serial number: %s", data[5])
logger.debug("Flash size: %s", data[6])

try:
await self.command('bkcmd=3')
await self.command("bkcmd=3")
except CommandTimeout as e:
logging.debug('Command "bkcmd=3" timeout')
self._sleeping = await self.get('sleep')
self._sleeping = await self.get("sleep")

logger.info("Successfully connected to the device")
return True

async def _read(self, timeout=READ_TIMEOUT):
async def _read(self, timeout=IO_TIMEOUT):
return await asyncio.wait_for(self._connection.read(), timeout=timeout)

async def get(self, key):
return await self.command('get %s' % key)
async def get(self, key, timeout=IO_TIMEOUT):
return await self.command("get %s" % key, timeout=timeout)

async def set(self, key, value):
async def set(self, key, value, timeout=IO_TIMEOUT):
if isinstance(value, str):
out_value = '"%s"' % value
elif isinstance(value, float):
logger.warn('Float is not supported. Converting to string')
logger.warn("Float is not supported. Converting to string")
out_value = '"%s"' % str(value)
elif isinstance(value, int):
out_value = str(value)
else:
raise AssertionError('value type "%s" is not supported for set' % type(value).__name__)

if self._sleeping and key not in ['sleep']:
logging.debug('Device sleeps. Scheduling "%s" set for execution after wakeup', key)
raise AssertionError(
'value type "%s" is not supported for set' % type(value).__name__
)

if self._sleeping and key not in ["sleep"]:
logging.debug(
'Device sleeps. Scheduling "%s" set for execution after wakeup', key
)
self.sets_todo[key] = value
else:
return await self.command('%s=%s' % (key, out_value))
return await self.command("%s=%s" % (key, out_value), timeout=timeout)

async def command(self, command, timeout=READ_TIMEOUT):
async def command(self, command, timeout=IO_TIMEOUT):
async with self._command_lock:
try:
while True:
logger.debug("Dropping dangling: %s", self._connection.read_no_wait())
logger.debug(
"Dropping dangling: %s", self._connection.read_no_wait()
)
except asyncio.QueueEmpty:
pass

Expand All @@ -170,7 +197,9 @@ async def command(self, command, timeout=READ_TIMEOUT):
try:
response = await self._read(timeout=timeout)
except asyncio.TimeoutError as e:
raise CommandTimeout('Command "%s" response was not received' % command) from e
raise CommandTimeout(
'Command "%s" response was not received' % command
) from e

res_len = len(response)
if res_len == 0:
Expand All @@ -189,24 +218,26 @@ async def command(self, command, timeout=READ_TIMEOUT):
elif type_ == ResponseType.STRING: # string
data = raw.decode()
elif type_ == ResponseType.NUMBER: # number
data = struct.unpack('i', raw)[0]
data = struct.unpack("i", raw)[0]
else:
logger.error("Unknown data received: %s" % binascii.hexlify(response))
logger.error(
"Unknown data received: %s" % binascii.hexlify(response)
)

return data if data is not None else result

async def sleep(self):
if self._sleeping:
return
await self.set('sleep', 1)
await self.set("sleep", 1)
self._sleeping = True

async def wakeup(self):
if not self._sleeping:
return
await self.set('sleep', 0)
await self.set("sleep", 0)
await self.on_wakeup()

async def dim(self, val: int):
assert 0 <= val <= 100
await self.set('dim', val)
await self.set("dim", val)
21 changes: 15 additions & 6 deletions nextion/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
command_failed_codes = {
0x00: "Invalid instruction", 0x02: "Component ID invalid",
0x03: "Page ID invalid", 0x04: "Picture ID invalid", 0x05: "Font ID invalid",
0x11: "Baud rate setting invalid", 0x12: "Curve control ID number or channel number is invalid",
0x1A: "Variable name invalid", 0x1B: "Variable operation invalid",
0x1C: "Failed to assign", 0x1D: "Operate EEPROM failed", 0x1E: "Parameter quantity invalid",
0x1F: "IO operation failed", 0x20: "Undefined escape characters", 0x23: "Too long variable name"
0x00: "Invalid instruction",
0x02: "Component ID invalid",
0x03: "Page ID invalid",
0x04: "Picture ID invalid",
0x05: "Font ID invalid",
0x11: "Baud rate setting invalid",
0x12: "Curve control ID number or channel number is invalid",
0x1A: "Variable name invalid",
0x1B: "Variable operation invalid",
0x1C: "Failed to assign",
0x1D: "Operate EEPROM failed",
0x1E: "Parameter quantity invalid",
0x1F: "IO operation failed",
0x20: "Undefined escape characters",
0x23: "Too long variable name",
}


Expand Down
12 changes: 6 additions & 6 deletions nextion/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import typing
from enum import IntEnum

logger = logging.getLogger('nextion').getChild(__name__)
logger = logging.getLogger("nextion").getChild(__name__)


class EventType(IntEnum):
Expand All @@ -24,11 +24,11 @@ class ResponseType(IntEnum):


class NextionProtocol(asyncio.Protocol):
EOL = b'\xff\xff\xff'
EOL = b"\xff\xff\xff"

def __init__(self, event_message_handler: typing.Callable):
self.transport = None
self.buffer = b''
self.buffer = b""
self.queue = asyncio.Queue()
self.connect_future = asyncio.get_event_loop().create_future()
self.event_message_handler = event_message_handler
Expand Down Expand Up @@ -56,7 +56,7 @@ def data_received(self, data):
if self.EOL in self.buffer:
messages = self.buffer.split(self.EOL)
for message in messages:
logger.debug('received: %s', binascii.hexlify(message))
logger.debug("received: %s", binascii.hexlify(message))
if self.is_event(message):
self.event_message_handler(message)
else:
Expand All @@ -73,10 +73,10 @@ def write(self, data):
if isinstance(data, str):
data = data.encode()
self.transport.write(data + self.EOL)
logger.debug('sent: %s', data)
logger.debug("sent: %s", data)

def connection_lost(self, exc):
logger.error('Connection lost')
logger.error("Connection lost")
if not self.connect_future.done():
self.connect_future.set_result(False)
# self.connect_future = asyncio.get_event_loop().create_future()
Loading

0 comments on commit d7aa1b1

Please sign in to comment.