0

I have an Arduino which is communicating properly with my Mac over serial using the pySerialTransfer library, running for hours. Then there is an interruption of the serial of some sort - though I've been unable to identify the cause when this happens overnight, I can reproduce the behavior pretty easily by just unplugging the Arduino USB cable from the laptop. The python code on my laptop continues to run, but gets into this infinite error loop:

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/pySerialTransfer/pySerialTransfer.py", line 257, in send
    self.connection.write(stack)
  File "/usr/local/lib/python3.7/site-packages/serial/serialposix.py", line 571, in write
    raise SerialException('write failed: {}'.format(e))
serial.serialutil.SerialException: write failed: [Errno 6] Device not configured
SENT (12 byte struct): (0, -55.836434114277004, 31.732435543849192)
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/serial/serialposix.py", line 537, in write
    n = os.write(self.fd, d)
OSError: [Errno 6] Device not configured

During handling of the above exception, another exception occurred:

I seem unable to catch the exceptions using any try / except blocks in my python code, so that I can detect this and restart or try to reconnect. Below is my current python code - appreciate any pointers! Thank you.

#!/usr/bin/python3

import sys
import time

import arduino
import messageboard

VERBOSE = arduino.VERBOSE
SN = '75835343130351802272'  # arduino uno serial

SIMULATE_ARDUINO = arduino.SIMULATE_ARDUINO
if '-s' in sys.argv:
  SIMULATE_ARDUINO = True

COMMAND_DELAY_TIME = 1  # send a command to servos every n seconds


def main():
  servos = arduino.OpenArduino(sn=SN)
  format_string = '<lff'
  format_byte_size = arduino.SizeOf(format_string)
  azimuth=90

  while True:

    if not servos and not SIMULATE_ARDUINO:
      servos = arduino.OpenArduino(sn=SN)
      if VERBOSE:
        print('Reopening connection')

    if servos or SIMULATE_ARDUINO:
      if azimuth == 90:
        azimuth = 85
      else:
        azimuth = 90
      values = (0, azimuth, 0)

      arduino.StuffObject(servos, values, format_string, format_byte_size)
      if not SIMULATE_ARDUINO:
        servos.send(format_byte_size)


    time.sleep(COMMAND_DELAY_TIME)


if __name__ == "__main__":
  main()
David W
  • 23
  • 4
  • What type of computer is it plugged into? Are you sure it isn't going to sleep or putting the USB to sleep? Check your power settings on the computer. – Delta_G Apr 24 '20 at 00:00
  • Mac OS X. The manual unplugging of the USB cable gets me into the same problem where there is the infinite loop of exceptions that I can't detect or get out of except via manually killing the program. – David W Apr 24 '20 at 01:48
  • Yes, the fact that a manual unplug gets the same result is what makes me think you've got a power option somewhere that is putting the USB controller to sleep. – Delta_G Apr 24 '20 at 05:37
  • Thanks. Certainly possible, though other python processes (albeit that don't connect to the Arduino) on the same laptop run all night without problem. But I'm looking for any tips to catch the exception and recover from it. – David W Apr 24 '20 at 07:26
  • Are the other python processes using a USB port? Have you looked through the power options yet? I don't know about mac but I know for certain that there is a setting on Windows that will put the USB ports to sleep. I work with analytical instruments that connect to those computers via USB and we have trouble all the time with that setting causing us to lose communication in the middle of the night even though everything else on the computer still works. Sounds exactly like your problem. Don't know what it would hurt to run through your settings and check. – Delta_G Apr 24 '20 at 15:25
  • Thanks - I'll confess I'm also worried about movement of the setup (and thus temporarily unplugging / replugging in of the USB), which is why I'm trying to solve this via exception detection and handling, rather than just the power settings. That being said, I did check the power settings on the Mac, and nothing will sleep (not even the screen or drive, though its an ssd drive) when plugged in; the "power nap" is also disabled. – David W Apr 24 '20 at 17:54
  • I gotcha. But to me it seems that if you want to handle the exception it usually helps to know what the exception is and why you are getting it. – Delta_G Apr 24 '20 at 18:22
  • Have you tried just putting it in a try catch and see if you can catch that SerialException before it throws the second recursive exception? – Delta_G Apr 24 '20 at 18:23
  • Thank you. Yes, unfortunately. I first tried try catch with specific error messages matching those being raised, and then tried a "bare" catch that would catch any errors; neither caught anything. And in some sense, that is logical - since even before I had added the try-catch, the SerialException didn't raise an exception to the level of my program, so there's no error to catch. And thus, the root of my problem. – David W Apr 24 '20 at 20:12

1 Answers1

0

I started down the tack of approaches to kill function calls that run too long, with potential solutions using StopIt and multiprocessing. But those failed because it seems that what was actually happening was that pySerialTransfer was spawning a separate thread to handle the serial communication and then returning control back to the calling function, but keeping that separate thread alive, and that separate thread was the one stuck in infinite error loops. So I couldn't detect the condition by the solutions to terminate long-running function calls. And as mentioned in the comment thread, no exceptions were passed up to the caller either, so try-except clauses also failed.

However, the one thing I could detect was that an error to stderr was logged, and so if I could redirect that and detect a new error there, then that would get me down the right path. Enter this guidance, which steered me towards this solution:

#!/usr/bin/python3

import sys
import time
import io

import arduino
import messageboard
import contextlib

VERBOSE = arduino.VERBOSE
SN = '75835343130351802272'  # arduino uno serial

SIMULATE_ARDUINO = arduino.SIMULATE_ARDUINO
if '-s' in sys.argv:
  SIMULATE_ARDUINO = True

COMMAND_DELAY_TIME = 1  # send a command to servos every n seconds


def main():
  servos = arduino.OpenArduino(sn=SN)
  format_string = '<lff'
  format_byte_size = arduino.SizeOf(format_string)
  azimuth=90

  while True:

    if not servos and not SIMULATE_ARDUINO:
      servos = arduino.OpenArduino(sn=SN)
      if VERBOSE:
        print('Reopening connection')

    if servos or SIMULATE_ARDUINO:
      if azimuth == 90:
        azimuth = 85
      else:
        azimuth = 90
      values = (0, azimuth, 0)

      # Detects any exceptions that print to stderr but are not raised to caller
      with io.StringIO() as buf, contextlib.redirect_stderr(buf):
        arduino.StuffObject(servos, values, format_string, format_byte_size)
        if not SIMULATE_ARDUINO:
          servos.send(format_byte_size)

        # if there's an exception, probably the connection failed; clear the connection
        # so that it can be reopened at top of loop
        if buf.getvalue():
          servos = None

    time.sleep(COMMAND_DELAY_TIME)


if __name__ == "__main__":
  main()

This leads to the following stdio output when I manually unplug the USB cable to arduino and then plug it back in, 2x during the testing.

$ python3 arduino_servo_test.py 
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
Reopening connection
Reopening connection
Reopening connection
Reopening connection
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
Reopening connection
Reopening connection
Reopening connection
Reopening connection
Reopening connection
Reopening connection
Reopening connection
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)
SENT (12 byte struct): (0, 85, 0)
SENT (12 byte struct): (0, 90, 0)

Problem solved - perhaps not the most elegant solution, but it seems to work.

David W
  • 23
  • 4