From fb0f8d930d706c9b8e61b69ea41c1b6568fa9390 Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Thu, 13 Feb 2025 14:49:48 -0500 Subject: [PATCH 01/12] add polling/timeout for read_statusbyte --- plotink/ebb3_serial.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index a7b782b..55f305d 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -48,6 +48,7 @@ class EBB3: ''' EBB3: Class for managing EiBotBoard connectivity ''' MIN_VERSION_STRING = "3.0.2" # Minimum supported EBB firmware version. + readline_poll_max = 25 def __init__(self): self.port_name = None # Port name (enumeration), if any @@ -309,11 +310,11 @@ def command(self, cmd): self.port.write((cmd + '\r').encode('ascii')) response = self.port.readline().decode('ascii').strip() - n_retry_count = 0 - while len(response) == 0 and n_retry_count < 25: + n_poll_count = 0 + while len(response) == 0 and n_poll_count < self.readline_poll_max: # get new response to replace null response if necessary response = self.port.readline().decode('ascii').strip() - n_retry_count += 1 + n_poll_count += 1 if not response.startswith(cmd_name): if response: @@ -364,11 +365,11 @@ def query(self, qry): self.port.write((qry + '\r').encode('ascii')) response = self.port.readline().decode('ascii').strip() - n_retry_count = 0 - while len(response) == 0 and n_retry_count < 25: + n_poll_count = 0 + while len(response) == 0 and n_poll_count < self.readline_poll_max: # get new response to replace null response if necessary response = self.port.readline().decode('ascii').strip() - n_retry_count += 1 + n_poll_count += 1 except (serial.SerialException, IOError, RuntimeError, OSError): if qry_name.lower() not in ["rb", "r", "bl"]: # Ignore err on these commands @@ -405,7 +406,12 @@ def query_statusbyte(self): response = '' try: self.port.write('QG\r'.encode('ascii')) - response = self.port.readline().decode('ascii').strip() + + n_poll_count = 0 + while len(response) == 0 and n_poll_count < self.readline_poll_max: + # get new response to replace null response if necessary + response = self.port.readline().decode('ascii').strip() + n_poll_count += 1 if not response.startswith('QG'): if response: From dbe417433b8e1597fcaaf51c1058e0aff89680b2 Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Thu, 13 Feb 2025 15:57:57 -0500 Subject: [PATCH 02/12] factor out polling/waiting for a response after a query or command --- plotink/ebb3_serial.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index 55f305d..5264e8a 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -308,14 +308,7 @@ def command(self, cmd): response = '' try: self.port.write((cmd + '\r').encode('ascii')) - response = self.port.readline().decode('ascii').strip() - - n_poll_count = 0 - while len(response) == 0 and n_poll_count < self.readline_poll_max: - # get new response to replace null response if necessary - response = self.port.readline().decode('ascii').strip() - n_poll_count += 1 - + response = self._readline_with_polls() if not response.startswith(cmd_name): if response: error_msg = '\nUnexpected response from EBB.' +\ @@ -335,7 +328,6 @@ def command(self, cmd): return bool(self.err is None) # Return True if no error, False if error. - def query(self, qry): ''' General function to send a query to the EiBotBoard. Like command, but returns a reponse. @@ -363,14 +355,7 @@ def query(self, qry): response = '' try: self.port.write((qry + '\r').encode('ascii')) - response = self.port.readline().decode('ascii').strip() - - n_poll_count = 0 - while len(response) == 0 and n_poll_count < self.readline_poll_max: - # get new response to replace null response if necessary - response = self.port.readline().decode('ascii').strip() - n_poll_count += 1 - + response = self._readline_with_polls() except (serial.SerialException, IOError, RuntimeError, OSError): if qry_name.lower() not in ["rb", "r", "bl"]: # Ignore err on these commands error_msg = f'USB communication error after query: {qry}' @@ -406,13 +391,7 @@ def query_statusbyte(self): response = '' try: self.port.write('QG\r'.encode('ascii')) - - n_poll_count = 0 - while len(response) == 0 and n_poll_count < self.readline_poll_max: - # get new response to replace null response if necessary - response = self.port.readline().decode('ascii').strip() - n_poll_count += 1 - + response = self._readline_with_polls() if not response.startswith('QG'): if response: error_msg = '\nUnexpected response from EBB.' +\ @@ -436,6 +415,15 @@ def query_statusbyte(self): except (TypeError, ValueError): return None + def _readline_with_polls(self): + response = "" + n_poll_count = 0 + while len(response) == 0 and n_poll_count < self.readline_poll_max: + # get new response to replace null response if necessary + response = self.port.readline().decode('ascii').strip() + n_poll_count += 1 + return response + def var_write(self, value, index): """ Store a variable in (volatile) EBB RAM using SL command. From 986b9cd68e76f616df12019d6837ced89429941a Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Thu, 13 Feb 2025 16:52:09 -0500 Subject: [PATCH 03/12] make EBB3.query behave more like EBB3.command and EBB3.query_statusbyte --- plotink/ebb3_serial.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index 5264e8a..741f94a 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -356,18 +356,24 @@ def query(self, qry): try: self.port.write((qry + '\r').encode('ascii')) response = self._readline_with_polls() + if not response.startswith(qry_name): + if response: + error_msg = '\nUnexpected response from EBB.' +\ + f' Query: {qry}\n Response: {response}' + else: + error_msg = f'EBB Serial Timeout after query: {qry}' + self.record_error(error_msg) + return None except (serial.SerialException, IOError, RuntimeError, OSError): if qry_name.lower() not in ["rb", "r", "bl"]: # Ignore err on these commands error_msg = f'USB communication error after query: {qry}' self.record_error(error_msg) return None - if ('Err:' in response) or (not response.startswith(qry_name)): - if response: - error_msg = '\nUnexpected response from EBB.' +\ - f' Query: {qry}\n Response: {response}' - else: - error_msg = f'EBB Serial Timeout after query: {qry}' + + if 'Err:' in response: + error_msg = 'Error reported by EBB.\n' +\ + f' Query: {qry}\n Response: {response}' self.record_error(error_msg) return None From 57f85bdd4990e4ec3d94043749bb2c15d18da274 Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Thu, 13 Feb 2025 16:55:36 -0500 Subject: [PATCH 04/12] factor out what happens if EBB reports an error --- plotink/ebb3_serial.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index 741f94a..063daad 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -321,10 +321,8 @@ def command(self, cmd): if cmd_name.lower() not in ["rb", "r", "bl"]: # Ignore err on these commands error_msg = f'USB communication error after command: {cmd}' self.record_error(error_msg) - if 'Err:' in response: - error_msg = 'Error reported by EBB.\n' +\ - f' Command: {cmd}\n Response: {response}' - self.record_error(error_msg) + + self._check_and_record_ebb_error(response, 'Command', cmd) return bool(self.err is None) # Return True if no error, False if error. @@ -370,11 +368,7 @@ def query(self, qry): self.record_error(error_msg) return None - - if 'Err:' in response: - error_msg = 'Error reported by EBB.\n' +\ - f' Query: {qry}\n Response: {response}' - self.record_error(error_msg) + if self._check_and_record_ebb_error(response, 'Query', qry): return None header_len = len(qry_name) @@ -411,11 +405,9 @@ def query_statusbyte(self): self.record_error(error_msg) return None - if 'Err:' in response: - error_msg = 'Error reported by EBB.\n' +\ - f' Query: QG\n Response: {response}' - self.record_error(error_msg) + if self._check_and_record_ebb_error(response, 'Query', 'QG'): return None + try: return int(response[3:], 16) # Strip off query name ("QG,") and convert to int. except (TypeError, ValueError): @@ -430,6 +422,21 @@ def _readline_with_polls(self): n_poll_count += 1 return response + def _check_and_record_ebb_error(self, response, type, request): + ''' + `response` is the response from the EBB, encoded etc. + `type` is "command" or "query" + `request` is the previous command or query sent to the EBB + returns True if the ebb reported an error, else False + ''' + if 'Err:' in response: + formatted_type = type.capitalize() + error_msg = 'Error reported by EBB.\n' +\ + f' {formatted_type}: {request}\n Response: {response}' + self.record_error(error_msg) + return True + return False + def var_write(self, value, index): """ Store a variable in (volatile) EBB RAM using SL command. From 802b0f2d0621e96e38eec789f2dc9414d742972d Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Thu, 13 Feb 2025 18:05:51 -0500 Subject: [PATCH 05/12] factor out unexpected response and timeout --- plotink/ebb3_serial.py | 50 +++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index 063daad..db62070 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -308,15 +308,9 @@ def command(self, cmd): response = '' try: self.port.write((cmd + '\r').encode('ascii')) - response = self._readline_with_polls() - if not response.startswith(cmd_name): - if response: - error_msg = '\nUnexpected response from EBB.' +\ - f' Command: {cmd}\n Response: {response}' - else: - error_msg = f'EBB Serial Timeout after command: {cmd}' - self.record_error(error_msg) - + response = self._get_and_eval_response('command', cmd, cmd_name) + if response is None: + return False except (serial.SerialException, IOError, RuntimeError, OSError): if cmd_name.lower() not in ["rb", "r", "bl"]: # Ignore err on these commands error_msg = f'USB communication error after command: {cmd}' @@ -353,14 +347,8 @@ def query(self, qry): response = '' try: self.port.write((qry + '\r').encode('ascii')) - response = self._readline_with_polls() - if not response.startswith(qry_name): - if response: - error_msg = '\nUnexpected response from EBB.' +\ - f' Query: {qry}\n Response: {response}' - else: - error_msg = f'EBB Serial Timeout after query: {qry}' - self.record_error(error_msg) + response = self._get_and_eval_response('query', qry, qry_name) + if response is None: return None except (serial.SerialException, IOError, RuntimeError, OSError): if qry_name.lower() not in ["rb", "r", "bl"]: # Ignore err on these commands @@ -391,15 +379,9 @@ def query_statusbyte(self): response = '' try: self.port.write('QG\r'.encode('ascii')) - response = self._readline_with_polls() - if not response.startswith('QG'): - if response: - error_msg = '\nUnexpected response from EBB.' +\ - f' Response to QG query: {response}' - else: - error_msg = 'EBB Serial Timeout while reading status byte.' - self.record_error(error_msg) - + response = self._get_and_eval_response('query', 'QG', 'QG') + if response is None: + return None except (serial.SerialException, IOError, RuntimeError, OSError): error_msg = 'USB communication error after status byte query' self.record_error(error_msg) @@ -413,13 +395,27 @@ def query_statusbyte(self): except (TypeError, ValueError): return None - def _readline_with_polls(self): + def _get_and_eval_response(self, type, request, request_name): + ''' + `request` is the previous command or query ent to the Ebb + `type` is 'command' or 'query' + return None if there's an error + ''' response = "" n_poll_count = 0 while len(response) == 0 and n_poll_count < self.readline_poll_max: # get new response to replace null response if necessary response = self.port.readline().decode('ascii').strip() n_poll_count += 1 + + if not response.startswith(request_name): + if response: + error_msg = '\nUnexpected response from EBB.' +\ + f' Command: {request}\n Response: {response}' + else: + error_msg = f'EBB Serial Timeout after {type}: {request}' + self.record_error(error_msg) + return None return response def _check_and_record_ebb_error(self, response, type, request): From a1a0485035ce8d43e3a3e2178c05d638a27e8d0f Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Thu, 13 Feb 2025 18:34:34 -0500 Subject: [PATCH 06/12] factor out sending the command/query to the EBB; and implement two retries for sending a request; record how often retries happen --- plotink/ebb3_serial.py | 48 ++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index db62070..5045db0 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -58,7 +58,9 @@ def __init__(self): self.name = None # EBB "nickname," if known self.err = None # None, or a string giving first fatal error message. self.caller = None # None, or a string indicating which program opened the port - + self.retry_count = 0 # A counter keeping track of how many times a command or + # query had to be retried due to timing out or an unexpected + # response from the EBB def find_first(self): ''' @@ -305,10 +307,8 @@ def command(self, cmd): else: cmd_name = cmd[0:2] # All other cases: Command names are two letters long. - response = '' try: - self.port.write((cmd + '\r').encode('ascii')) - response = self._get_and_eval_response('command', cmd, cmd_name) + response = self._send_request('command', cmd, cmd_name) if response is None: return False except (serial.SerialException, IOError, RuntimeError, OSError): @@ -344,10 +344,8 @@ def query(self, qry): else: qry_name = qry[0:2] # Cases except QU: Query responses are two letters long. - response = '' try: - self.port.write((qry + '\r').encode('ascii')) - response = self._get_and_eval_response('query', qry, qry_name) + response = self._send_request('query', qry, qry_name) if response is None: return None except (serial.SerialException, IOError, RuntimeError, OSError): @@ -376,10 +374,8 @@ def query_statusbyte(self): if (self.port is None) or (self.err is not None): return None - response = '' try: - self.port.write('QG\r'.encode('ascii')) - response = self._get_and_eval_response('query', 'QG', 'QG') + response = self._send_request('query', 'QG', 'QG') if response is None: return None except (serial.SerialException, IOError, RuntimeError, OSError): @@ -395,12 +391,18 @@ def query_statusbyte(self): except (TypeError, ValueError): return None - def _get_and_eval_response(self, type, request, request_name): - ''' - `request` is the previous command or query ent to the Ebb + def _send_request(self, type, request, request_name, num_tries = 3): + ''' `type` is 'command' or 'query' + `request` is the command or query to send to the EBB + `request_name` is the short name of `request` + `num_tries` is the number of times to try if something went wrong. "1" means no retries. return None if there's an error ''' + # send the request + self.port.write((request + '\r').encode('ascii')) + + # and wait for a response response = "" n_poll_count = 0 while len(response) == 0 and n_poll_count < self.readline_poll_max: @@ -408,14 +410,20 @@ def _get_and_eval_response(self, type, request, request_name): response = self.port.readline().decode('ascii').strip() n_poll_count += 1 + # evaluate that response + # if the response is unexpected or empty, recursively try again according to `num_tries` if not response.startswith(request_name): - if response: - error_msg = '\nUnexpected response from EBB.' +\ - f' Command: {request}\n Response: {response}' - else: - error_msg = f'EBB Serial Timeout after {type}: {request}' - self.record_error(error_msg) - return None + if num_tries > 1: + self.retry_count += 1 + self._send_request(type, request, request_name, num_tries - 1) + else: # base case; num_tries == 1 (or less but that would be silly) + if response: + error_msg = '\nUnexpected response from EBB.' +\ + f' Command: {request}\n Response: {response}' + else: + error_msg = f'EBB Serial Timeout after {type}: {request}' + self.record_error(error_msg) + return None return response def _check_and_record_ebb_error(self, response, type, request): From 073031bfa5661798bb4e481c280059a5eac26420 Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Wed, 19 Feb 2025 02:15:54 -0500 Subject: [PATCH 07/12] reimplement with the understanding of how important EOL character is --- plotink/ebb3_serial.py | 62 ++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index 5045db0..cd097ee 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -399,32 +399,64 @@ def _send_request(self, type, request, request_name, num_tries = 3): `num_tries` is the number of times to try if something went wrong. "1" means no retries. return None if there's an error ''' + def response_incomplete(the_response): + return len(the_response) == 0 or the_response[-1] != "\n" + # send the request self.port.write((request + '\r').encode('ascii')) # and wait for a response response = "" n_poll_count = 0 + + # poll port until we get any kind of response or timeout while len(response) == 0 and n_poll_count < self.readline_poll_max: # get new response to replace null response if necessary response = self.port.readline().decode('ascii').strip() n_poll_count += 1 - # evaluate that response - # if the response is unexpected or empty, recursively try again according to `num_tries` - if not response.startswith(request_name): - if num_tries > 1: - self.retry_count += 1 - self._send_request(type, request, request_name, num_tries - 1) - else: # base case; num_tries == 1 (or less but that would be silly) - if response: - error_msg = '\nUnexpected response from EBB.' +\ - f' Command: {request}\n Response: {response}' - else: - error_msg = f'EBB Serial Timeout after {type}: {request}' - self.record_error(error_msg) - return None - return response + if len(response) != 0 and response_incomplete(response): # received a partial response; poll a little longer waiting for the last character to be '\n' + n_poll_count = 0 + while response_incomplete(response) and n_poll_count < self.readline_poll_max: + response = response + self.port.readline.decode('ascii') + n_poll_count += 1 + + if self.port.in_waiting > 0: + logging.error('IN_WAITING > 0') + + # four possibilities now + # len(response) == 0, aka a classic timeout + # len(response) != 0 and response[-1] != "\n", aka a timeout but received some information + # len(response) != 0 and response[-1] == "\n", aka no timeout + # response.startswith(request_name) # yay + # not response.startswith(request_name) # boo + + # evaluate the response + if not response_incomplete(response) and response.startswith(request_name): + # the response is complete, and it is as expected + return response.strip() + + # otherwise, recursively try again according to `num_tries` + error_type = "" + if len(response) != 0: + error_type = "Timeout with no response" + elif response_incomplete(response): + error_type = "Timeout with partial response" + else: # aka not response.startswith(request_name) + error_type = "Unexpected response" + + response = response.strip() + if num_tries > 1: # recursive case + self.retry_count += 1 + logging.error(f'USB ERROR {error_type}: {self.retry_count} retrying {type}: {request} (response was "{response}")') + self.port.reset_input_buffer() # Flush input buffer, discarding all its contents. Especially important if port timed out with a partial response + response = self._send_request(type, request, request_name, num_tries - 1) + logging.error(f'response to retry {self.retry_count} was "{response}"') + return response + else: # base case + self.record_error('\nEBB Serial Error.' +\ + f' Command: {request}\n {error_type}: {response}') + return None def _check_and_record_ebb_error(self, response, type, request): ''' From 6079954a4137bb837f2dd7941fb7712211b5a97c Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Wed, 19 Feb 2025 13:21:24 -0500 Subject: [PATCH 08/12] make retrieval of response robust to extra/leftover response bytes; ignore empty response lines --- plotink/ebb3_serial.py | 87 +++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index cd097ee..eebec20 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -48,7 +48,6 @@ class EBB3: ''' EBB3: Class for managing EiBotBoard connectivity ''' MIN_VERSION_STRING = "3.0.2" # Minimum supported EBB firmware version. - readline_poll_max = 25 def __init__(self): self.port_name = None # Port name (enumeration), if any @@ -397,67 +396,61 @@ def _send_request(self, type, request, request_name, num_tries = 3): `request` is the command or query to send to the EBB `request_name` is the short name of `request` `num_tries` is the number of times to try if something went wrong. "1" means no retries. - return None if there's an error - ''' - def response_incomplete(the_response): - return len(the_response) == 0 or the_response[-1] != "\n" + return None if there's an error, otherwise return the response bytestring + ''' + try: + readline_poll_max = 25 # send the request self.port.write((request + '\r').encode('ascii')) # and wait for a response - response = "" + responses = [] n_poll_count = 0 - - # poll port until we get any kind of response or timeout - while len(response) == 0 and n_poll_count < self.readline_poll_max: - # get new response to replace null response if necessary - response = self.port.readline().decode('ascii').strip() + # poll for response until we get any response and self.port indicates there is no more input, a maximum of readline_poll_max times + while (len(responses) == 0 or self.port.in_waiting > 0) and n_poll_count < readline_poll_max: + in_bytes = self.port.readline() n_poll_count += 1 - - if len(response) != 0 and response_incomplete(response): # received a partial response; poll a little longer waiting for the last character to be '\n' - n_poll_count = 0 - while response_incomplete(response) and n_poll_count < self.readline_poll_max: - response = response + self.port.readline.decode('ascii') - n_poll_count += 1 - - if self.port.in_waiting > 0: - logging.error('IN_WAITING > 0') - - # four possibilities now - # len(response) == 0, aka a classic timeout - # len(response) != 0 and response[-1] != "\n", aka a timeout but received some information - # len(response) != 0 and response[-1] == "\n", aka no timeout - # response.startswith(request_name) # yay - # not response.startswith(request_name) # boo - - # evaluate the response - if not response_incomplete(response) and response.startswith(request_name): - # the response is complete, and it is as expected - return response.strip() - - # otherwise, recursively try again according to `num_tries` - error_type = "" - if len(response) != 0: - error_type = "Timeout with no response" - elif response_incomplete(response): - error_type = "Timeout with partial response" - else: # aka not response.startswith(request_name) - error_type = "Unexpected response" - - response = response.strip() + if len(in_bytes.decode('ascii').strip()) == 0: # received nothing, keep polling + continue + + # store in_bytes either as a new line (if no previous line or previous line is complete) or as an addition to the previous line + if len(responses) == 0: + responses.append(in_bytes) + elif responses[-1][-1] == "\n": # previous line (responses[-1]) is complete, indicated by its last character (response[-1][-1]) being a newline + responses.append(in_bytes) + else: # previous line is incomplete; don't create a new entry in responses + responses[-1] += in_bytes + + # evaluate the responses + response = '' + while len(response) == 0 and len(responses) != 0: + response = responses.pop().decode('ascii').strip() # we only care about the last response; previous responses are probably related to prior writes and irrelevant here + + if len(response) == 0: + raise RuntimeError(f'Timed out with no response (or empty responses) after {n_poll_count} polls.') + + if not response.startswith(request_name): + raise RuntimeError(f'Received unexpected response after {n_poll_count} polls.') + return response + except RuntimeError as re: if num_tries > 1: # recursive case self.retry_count += 1 - logging.error(f'USB ERROR {error_type}: {self.retry_count} retrying {type}: {request} (response was "{response}")') - self.port.reset_input_buffer() # Flush input buffer, discarding all its contents. Especially important if port timed out with a partial response + self.port.reset_input_buffer() # clear out any inputs from EBB prior to the new request response = self._send_request(type, request, request_name, num_tries - 1) - logging.error(f'response to retry {self.retry_count} was "{response}"') return response else: # base case self.record_error('\nEBB Serial Error.' +\ - f' Command: {request}\n {error_type}: {response}') + f' Command: {request}\n Response: {response}') return None + # four possibilities now + # len(response) == 0, aka a classic timeout + # len(response) != 0 and response[-1] != "\n", aka a timeout but received some information + # len(response) != 0 and response[-1] == "\n", aka no timeout + # response.startswith(request_name) # yay + # not response.startswith(request_name) # boo + def _check_and_record_ebb_error(self, response, type, request): ''' `response` is the response from the EBB, encoded etc. From 545f76974643deb4d7d391f0b29887d71c68afe6 Mon Sep 17 00:00:00 2001 From: Anna Berleant Date: Fri, 21 Feb 2025 15:15:37 -0800 Subject: [PATCH 09/12] handle errors reported by EBB the same way as timeouts and unexpected responses; retry once --- plotink/ebb3_serial.py | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index eebec20..fb62766 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -315,8 +315,6 @@ def command(self, cmd): error_msg = f'USB communication error after command: {cmd}' self.record_error(error_msg) - self._check_and_record_ebb_error(response, 'Command', cmd) - return bool(self.err is None) # Return True if no error, False if error. def query(self, qry): @@ -353,9 +351,6 @@ def query(self, qry): self.record_error(error_msg) return None - if self._check_and_record_ebb_error(response, 'Query', qry): - return None - header_len = len(qry_name) if len(response) > header_len: # Response is longer than the query length. if response[header_len] == ',': # Check if character after query is a comma. @@ -363,7 +358,6 @@ def query(self, qry): return response[header_len:] # Strip off leading repetition of command name. - def query_statusbyte(self): ''' Special function to manage the `QG` query and return an integer @@ -382,9 +376,6 @@ def query_statusbyte(self): self.record_error(error_msg) return None - if self._check_and_record_ebb_error(response, 'Query', 'QG'): - return None - try: return int(response[3:], 16) # Strip off query name ("QG,") and convert to int. except (TypeError, ValueError): @@ -432,6 +423,10 @@ def _send_request(self, type, request, request_name, num_tries = 3): if not response.startswith(request_name): raise RuntimeError(f'Received unexpected response after {n_poll_count} polls.') + + if 'Err:' in response: + raise RuntimeError(f'Error reported by EBB after {n_poll_count} polls.') + return response except RuntimeError as re: if num_tries > 1: # recursive case @@ -444,28 +439,6 @@ def _send_request(self, type, request, request_name, num_tries = 3): f' Command: {request}\n Response: {response}') return None - # four possibilities now - # len(response) == 0, aka a classic timeout - # len(response) != 0 and response[-1] != "\n", aka a timeout but received some information - # len(response) != 0 and response[-1] == "\n", aka no timeout - # response.startswith(request_name) # yay - # not response.startswith(request_name) # boo - - def _check_and_record_ebb_error(self, response, type, request): - ''' - `response` is the response from the EBB, encoded etc. - `type` is "command" or "query" - `request` is the previous command or query sent to the EBB - returns True if the ebb reported an error, else False - ''' - if 'Err:' in response: - formatted_type = type.capitalize() - error_msg = 'Error reported by EBB.\n' +\ - f' {formatted_type}: {request}\n Response: {response}' - self.record_error(error_msg) - return True - return False - def var_write(self, value, index): """ Store a variable in (volatile) EBB RAM using SL command. From e0839a3476b795671ee92f902f5791c5f2b77ddf Mon Sep 17 00:00:00 2001 From: Anna Berleant Date: Fri, 28 Feb 2025 07:42:58 -0800 Subject: [PATCH 10/12] correctly check for a previous line being complete? --- plotink/ebb3_serial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index fb62766..a154f7e 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -408,7 +408,7 @@ def _send_request(self, type, request, request_name, num_tries = 3): # store in_bytes either as a new line (if no previous line or previous line is complete) or as an addition to the previous line if len(responses) == 0: responses.append(in_bytes) - elif responses[-1][-1] == "\n": # previous line (responses[-1]) is complete, indicated by its last character (response[-1][-1]) being a newline + elif responses[-1].decode('ascii')[-1] == "\n": # previous line (responses[-1]) is complete, indicated by its last character (responses[-1], decoded, [-1]) being a newline responses.append(in_bytes) else: # previous line is incomplete; don't create a new entry in responses responses[-1] += in_bytes From 20b7197755b49ce8ca4511ec2da047f5491fe08f Mon Sep 17 00:00:00 2001 From: Anna Berleant Date: Fri, 28 Feb 2025 11:15:19 -0800 Subject: [PATCH 11/12] do not retry on timeouts for non-idempotent commands --- plotink/ebb3_serial.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index a154f7e..da2c9db 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -428,7 +428,16 @@ def _send_request(self, type, request, request_name, num_tries = 3): raise RuntimeError(f'Error reported by EBB after {n_poll_count} polls.') return response - except RuntimeError as re: + except RuntimeError as err: + if 'Timed out' in err.args[0]: + # it may not be appropriate to retry without knowing whether or not EBB received and executed the command + # if the command was idempotent, we can safely retry: + # if the command starts with "Q", it's a query and can be safely retried + # also "SP" (set pen position) and "CU" (configure settings) + if request_name[0] != 'Q' and request_name not in ["SP", "CU"]: + raise + + # retries! if num_tries > 1: # recursive case self.retry_count += 1 self.port.reset_input_buffer() # clear out any inputs from EBB prior to the new request From d33bcfe2397384d449719a888eeb64afcf72ca68 Mon Sep 17 00:00:00 2001 From: Anna S Berleant Date: Tue, 11 Mar 2025 12:59:19 -0400 Subject: [PATCH 12/12] remove type param; not necessary w less extensive logging --- plotink/ebb3_serial.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plotink/ebb3_serial.py b/plotink/ebb3_serial.py index da2c9db..b421cb6 100644 --- a/plotink/ebb3_serial.py +++ b/plotink/ebb3_serial.py @@ -307,7 +307,7 @@ def command(self, cmd): cmd_name = cmd[0:2] # All other cases: Command names are two letters long. try: - response = self._send_request('command', cmd, cmd_name) + response = self._send_request(cmd, cmd_name) if response is None: return False except (serial.SerialException, IOError, RuntimeError, OSError): @@ -342,7 +342,7 @@ def query(self, qry): qry_name = qry[0:2] # Cases except QU: Query responses are two letters long. try: - response = self._send_request('query', qry, qry_name) + response = self._send_request(qry, qry_name) if response is None: return None except (serial.SerialException, IOError, RuntimeError, OSError): @@ -368,7 +368,7 @@ def query_statusbyte(self): return None try: - response = self._send_request('query', 'QG', 'QG') + response = self._send_request('QG', 'QG') if response is None: return None except (serial.SerialException, IOError, RuntimeError, OSError): @@ -381,9 +381,8 @@ def query_statusbyte(self): except (TypeError, ValueError): return None - def _send_request(self, type, request, request_name, num_tries = 3): + def _send_request(self, request, request_name, num_tries = 3): ''' - `type` is 'command' or 'query' `request` is the command or query to send to the EBB `request_name` is the short name of `request` `num_tries` is the number of times to try if something went wrong. "1" means no retries. @@ -441,7 +440,7 @@ def _send_request(self, type, request, request_name, num_tries = 3): if num_tries > 1: # recursive case self.retry_count += 1 self.port.reset_input_buffer() # clear out any inputs from EBB prior to the new request - response = self._send_request(type, request, request_name, num_tries - 1) + response = self._send_request(request, request_name, num_tries - 1) return response else: # base case self.record_error('\nEBB Serial Error.' +\