Skip to content

Commit 084b589

Browse files
committed
enforces request/response communication with Orbiter
1 parent 89375b5 commit 084b589

4 files changed

Lines changed: 60 additions & 43 deletions

File tree

src/artisanlib/async_comm.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ async def create_serial_connection(
251251

252252
class AsyncComm:
253253

254-
__slots__ = [ '_asyncLoopThread', '_write_queue', '_running', '_host', '_port', '_serial', '_connected_handler', '_disconnected_handler',
254+
__slots__ = [ '_asyncLoopThread', '_write_queue', '_running', '_serialize_write_lock', '_host', '_port', '_serial', '_connected_handler', '_disconnected_handler',
255255
'_verify_crc', '_logging', '_send_timeout' ]
256256

257257
def __init__(self, host:str = '127.0.0.1', port:int = 8080, serial:'SerialSettings|None' = None,
@@ -262,6 +262,9 @@ def __init__(self, host:str = '127.0.0.1', port:int = 8080, serial:'SerialSettin
262262
self._write_queue: asyncio.Queue[bytes]|None = None # noqa: UP037 # quotes for Python3.8 # the write_queue
263263
self._running:bool = False # while true we keep running the thread
264264

265+
# lock to serialize write_await calls to realize request/response patterns
266+
self._serialize_write_lock:asyncio.Lock = asyncio.Lock()
267+
265268
# connection
266269
self._host:str = host
267270
self._port:int = port
@@ -355,12 +358,12 @@ async def handle_writes(self, writer: asyncio.StreamWriter, queue: 'asyncio.Queu
355358
try:
356359
with suppress(asyncio.CancelledError):
357360
# assignments in while are only only available from Python 3.8
358-
# while (message := await queue.get()) != b'':
359-
# await self.write(writer, message)
360-
message = await queue.get()
361-
while message != b'':
361+
while (message := await queue.get()) != b'':
362362
await self.write(writer, message)
363-
message = await queue.get()
363+
# message = await queue.get()
364+
# while message != b'':
365+
# await self.write(writer, message)
366+
# message = await queue.get()
364367
# on empty messages we close the connection
365368
writer.close()
366369
except Exception as e: # pylint: disable=broad-except
@@ -434,36 +437,60 @@ def send(self, message:bytes) -> None:
434437
if self.async_loop_thread is not None and self._write_queue is not None:
435438
asyncio.run_coroutine_threadsafe(self._write_queue.put(message), self.async_loop_thread.loop)
436439

437-
438-
# adds message to write queue and awaits new data
439-
async def write_await(self, message:bytes, event:asyncio.Event, send_timeout:float) -> None:
440+
# adds message to write queue and awaits new data which is assumed to event.set() once received()
441+
# if serialize is set, writes are serialized such that at any momemnt only one response is awaited using the given events
442+
# ensuring minimum delay of 'delay' between writes (in seconds)
443+
# returns True on success and False on timeout
444+
# on return the event is always cleared
445+
async def write_await(self, message:bytes, event:asyncio.Event, send_timeout:float, serialize:bool, delay:float) -> bool:
440446
if self._write_queue is None:
441-
return
442-
await self._write_queue.put(message)
443-
# await a response containing a new value for var with timeout
447+
return False
444448
try:
445-
await asyncio.wait_for(event.wait(), send_timeout)
446-
except TimeoutError:
447-
if self._logging:
448-
_log.info('write_await (msg=%s, send_timeout:%s)', message.strip(), send_timeout)
449+
if serialize:
450+
was_writing = self._serialize_write_lock.locked() # remember if writing was in progress
451+
try:
452+
await asyncio.wait_for(self._serialize_write_lock.acquire(), send_timeout)
453+
except TimeoutError:
454+
pass
455+
if was_writing and delay > 0: # only if writing was in progress we add a delay before writing
456+
await asyncio.sleep(delay)
457+
# write out the message
458+
await self._write_queue.put(message)
459+
# await a response with timeout indicated by the event being set
460+
try:
461+
await asyncio.wait_for(event.wait(), send_timeout)
462+
return True
463+
except TimeoutError:
464+
if self._logging:
465+
_log.info('write_await (msg=%s, send_timeout:%s)', message.strip(), send_timeout)
466+
return False
467+
finally:
468+
# in any case, clear the event belonging to this message
469+
event.clear()
470+
if serialize:
471+
# release the serializing write lock
472+
self._serialize_write_lock.release()
473+
449474

450-
def send_await(self, message:bytes, event:asyncio.Event, timeout:float|None = None) -> None:
475+
# returns True if message was sent successfully
476+
def send_await(self, message:bytes, event:asyncio.Event, timeout:float|None = None, serialize:bool = False, delay:float = 0) -> bool:
451477
if self.async_loop_thread is not None and self._write_queue is not None:
452478
send_timeout:float = self._send_timeout
453479
if timeout is not None:
454480
send_timeout = timeout
455-
task = self.write_await(message, event, send_timeout)
481+
task = self.write_await(message, event, send_timeout, serialize, delay)
456482
if self._asyncLoopThread is not None:
457483
future = asyncio.run_coroutine_threadsafe(task, self._asyncLoopThread.loop)
458484
try:
459-
future.result()
485+
return future.result()
460486
except TimeoutError:
461487
# the coroutine took too long, cancelling the task...
462488
if self._logging:
463489
_log.info('send_request timeout (msg=%s, timeout:%s, send_timeout:%s)',message,timeout,send_timeout)
464490
future.cancel()
465491
except Exception as ex: # pylint: disable=broad-except
466492
_log.error(ex)
493+
return False
467494

468495

469496

src/artisanlib/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17967,7 +17967,7 @@ def kaleidoSendMessage(self, target:str, value:str) -> None:
1796717967
@pyqtSlot(bytes,bytes,bytes,int)
1796817968
def orbiterSendMessage(self, cmd:bytes, data:bytes, param:bytes, time:int) -> None:
1796917969
if self.orbiter is not None:
17970-
self.orbiter.send_msg(cmd, data, param, time)
17970+
self.orbiter.send_msg_await(cmd, data, param, time)
1797117971

1797217972
# if record is True, an event is added during recording, otherwise only the slider is moved
1797317973
# if fire_slider_action is True, the slider action is fired

src/artisanlib/orbiter.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,9 @@ class Orbiter(AsyncComm):
6363

6464
HEADER:Final[bytes] = b'\xFF\xFF'
6565
EVENT:Final[bytes] = b'\x00'
66-
CMD_INIT:Final[bytes] = b'\x01'
6766
CMD_SYNC:Final[bytes] = b'\x00'
6867

69-
__slots__ = [ 'send_timeout', '_connected', '_new_readings_available', '_BT', '_ET', '_IT', '_DT', '_air', '_drum', '_damper',
68+
__slots__ = [ 'send_timeout', '_connected', '_ACK_received', '_BT', '_ET', '_IT', '_DT', '_air', '_drum', '_damper',
7069
'_heater', '_sound', '_RoR', '_master_control', '_SERIAL', '_FW_VERSION', '_PCB_VERSION', '_DASHBOARD_STATUS', '_MODEL', '_MODEL_NUM',
7170
'isRoaster_Roasting' ]
7271

@@ -82,7 +81,7 @@ def __init__(self, serial:'SerialSettings',
8281

8382
# current readings
8483
self._connected:int = 0 # connection status (0:disconnected, 1:connected)
85-
self._new_readings_available:asyncio.Event = asyncio.Event()
84+
self._ACK_received:asyncio.Event = asyncio.Event()
8685
#-
8786
self._BT:float = -1 # bean temperature
8887
self._ET:float = -1 # environmental temperature
@@ -116,9 +115,8 @@ def setLogging(self, b:bool) -> None:
116115
# getBT triggers fetching a complete set of new readings
117116
# time is the preheat/roasting/cooling time in seconds send along the sync command to the machine
118117
def getBT(self, time:int = 0) -> float:
119-
if not self._new_readings_available.is_set():
118+
if not self._ACK_received.is_set(): # only send if no sync command is currently send
120119
self.send_sync_await(time)
121-
self._new_readings_available.clear()
122120
return self._BT
123121
def getET(self) -> float:
124122
return self._ET
@@ -178,7 +176,7 @@ async def read_msg(self, stream: asyncio.StreamReader|IteratorReader) -> None:
178176
# check for the second header byte
179177
if await stream.readexactly(1) == self.HEADER[1:2]:
180178
cmd = await stream.readexactly(1)
181-
if cmd[0] == 0: # sync data (total 28 bytes)
179+
if cmd[0] == 0: # sync data ACK (total 28 bytes)
182180
if self._logging:
183181
_log.debug('Orbiter CMD sync data')
184182
data = await stream.readexactly(25)
@@ -207,7 +205,7 @@ async def read_msg(self, stream: asyncio.StreamReader|IteratorReader) -> None:
207205
self._damper = data[21]
208206
self._sound = data[22]
209207
self._master_control = data[23]
210-
self._new_readings_available.set()
208+
self._ACK_received.set()
211209
else:
212210
_log.debug('Orbiter CRC failed: %s != %s', compute_crc(cmd[0:1] + data[:24]), data[24])
213211
except Exception as e: # pylint: disable=broad-except
@@ -237,6 +235,7 @@ async def read_msg(self, stream: asyncio.StreamReader|IteratorReader) -> None:
237235
_log.debug('Orbiter MODEL_NUM: %s', self._MODEL_NUM)
238236
_log.debug('Orbiter _sound: %s', self._sound)
239237
_log.debug('Orbiter _master_control: %s', self._master_control)
238+
self._ACK_received.set()
240239
else:
241240
_log.debug('Orbiter CRC failed: %s != %s', compute_crc(cmd[0:1] + data[:24]), data[24])
242241
except Exception as e: # pylint: disable=broad-except
@@ -257,24 +256,15 @@ def create_msg(self, cmd:bytes, data:bytes, param:bytes, time:int) -> bytes:
257256
return self.HEADER + payload + crc.to_bytes(1, 'little')
258257

259258
# data byte order: LSB last (little-endian); eg. data=b'\x07\x00' equals 7
260-
def send_msg(self, cmd:bytes, data:bytes = b'\x00\x00', param:bytes = b'\x00', time:int = 0) -> None:
261-
# send via socket
262-
self.send(self.create_msg(cmd, data, param, time))
263-
264-
def send_msg_await(self, event:asyncio.Event, timeout:float, cmd:bytes, data:bytes = b'\x00\x00', param:bytes = b'\x00', time:int = 0) -> None:
265-
# send via socket
266-
self.send_await(self.create_msg(cmd, data, param, time), event, timeout)
259+
def send_msg_await(self, cmd:bytes, data:bytes = b'\x00\x00', param:bytes = b'\x00', time:int = 0) -> None:
260+
# send via socket using a request/response pattern (serialize=True) awaiting a response that sets the _ACK_received event
261+
# ensuring a 100ms delay between those request/response pairs
262+
self.send_await(self.create_msg(cmd, data, param, time), self._ACK_received, self.send_timeout, serialize=True, delay=0.1)
267263

268264
#
269265

270-
def send_init(self) -> None:
271-
self.send_msg(self.CMD_INIT)
272-
273-
def send_sync(self) -> None:
274-
self.send_msg(self.CMD_SYNC)
275-
276266
def send_sync_await(self, time:int) -> None:
277-
self.send_msg_await(self._new_readings_available, self.send_timeout, self.CMD_SYNC, time=time)
267+
self.send_msg_await(self.CMD_SYNC, time=time)
278268

279269
#
280270

@@ -672,7 +662,7 @@ def main() -> None:
672662
for _ in range(4):
673663
print('>>> hallo')
674664
val:int = 7
675-
orbiter.send_msg(b'\x0D', val.to_bytes(2, 'little')) # set power to 7
665+
orbiter.send_msg_await(b'\x0D', val.to_bytes(2, 'little')) # set power to 7
676666
time.sleep(1)
677667
print('BT', orbiter.getBT())
678668
time.sleep(1)

src/includes/Machines/Orbiter/OB-1.aset

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ bytesize=8
101101
comport=COM3
102102
parity=N
103103
stopbits=1
104-
timeout=1
104+
timeout=0.4
105105

106106
[Sliders]
107107
ModeTempSliders=C

0 commit comments

Comments
 (0)