0

I have been using a micro switch connected to an RS232/USB serial converter cable on my windows PC to start stop and reset a timer.

The program runs smoothly most of the time but every so often updating the timer widget gets stuck running and the timer will not stop.

With the serial protocol i want to receive 1 byte b'\x00' for off and anything that's not b'\x00' should signify on.

I have replaced the micro switch with button widgets to simulate the switch and don't get the same error or i just have not kept at it for long enough.

It could be an issue with the RS232 causing an error i cannot see but my knowledge on this is sketchy and have exhausted all avenues looking online for any information on this.

import time
import sys
import serial
import threading
from tkinter import *
from tkinter import ttk

class Process(Frame):
    def __init__(self, root, parent=None, **kw):
        Frame.__init__(self, parent, kw)
        self.root = root
        self._cycStart = 0.0
        self._cycTimeElapsed = 0.0
        self._cycRunning = 0.0
        self.cycTimeStr = StringVar()
        self.cycTime_label_widget()

        self.ser = serial.Serial(
            port='COM4',
            baudrate=1200,
            timeout=0
            )

        self.t1 = threading.Thread(target=self.start_stop, name='t1')
        self.t1.start()

    def initUI(self):
        root.focus_force()
        root.title("")
        root.bind('<Escape>', lambda e: root.destroy())


    def cycTime_label_widget(self):
    # Make the time label
        cycTimeLabel = Label(root, textvariable=self.cycTimeStr, font= 
        ("Ariel 12"))
        self._cycleSetTime(self._cycTimeElapsed)
        cycTimeLabel.place(x=1250, y=200)

        cycTimeLabel_2 = Label(root, text="Cycle Timer:", font=("Ariel 
        12"))
        cycTimeLabel_2.place(x=1150, y=200)

    def _cycleUpdate(self): 
        """ Update the label with elapsed time. """
        self._cycTimeElapsed = time.time() - self._cycStart
        self._cycleSetTime(self._cycTimeElapsed)
        self._cycTimer = self.after(50, self._cycleUpdate)

    def _cycleSetTime(self, elap):
        """ Set the time string to Minutes:Seconds:Hundreths """
        minutes = int(elap/60)
        seconds = int(elap - minutes*60.0)
        hseconds = int((elap - minutes*60.0 - seconds)*100)                
        self.cycTimeStr.set('%02d:%02d:%02d' % (minutes, seconds, 
        hseconds))
        return

    def cycleStart(self):                                                     
        """ Start the stopwatch, ignore if running. """
        if not self._cycRunning:           
            self._cycStart = time.time() - self._cycTimeElapsed
            self._cycleUpdate()
            self._cycRunning = 1
        else:
            self.cycleReset()


     def cycleStop(self):                                    
        """ Stop the stopwatch, ignore if stopped. """
        if self._cycRunning:
            self.after_cancel(self._cycTimer)            
            self._cycTimeElapsed = time.time() - self._cycStart    
            self._cycleSetTime(self._cycTimeElapsed)
            self._cycRunning = 0
            self._cycTimeElapsed = round(self._cycTimeElapsed, 1)
            self.cycleTimeLabel = Label(root, text=(self._cycTimeElapsed, 
            "seconds"), font=("Ariel 35"))
            self.cycleTimeLabel.place(x=900, y=285)
            self.cycleReset()

     def cycleReset(self):                                  
         """ Reset the stopwatch. """
         self._cycStart = time.time()         
         self._cycTimeElapsed = 0   
         self._cycleSetTime(self._cycTimeElapsed)

     def start_stop(self):
         while True :
             try:
                 data_to_read = self.ser.inWaiting()
                 if data_to_read != 0: # read if there is new data
                     data = self.ser.read(size=1).strip()
                     if data == bytes(b'\x00'):
                         self.cycleStop()
                         print("Off")

                     elif data is not bytes(b'\x00'):
                         self.cycleStart()
                         print("On")

             except serial.SerialException as e:
                 print("Error")
if __name__ == '__main__':
    root = Tk()
    application = Process(root)
    root.mainloop()

I expect the timer to start running when the micro switch is pressed. when depressed it should stop and reset back to zero and wait for the next press

2 Answers2

1

With a better understanding of what you're trying to do better solutions come to mind.

As it turns out, you're not using your serial port to send or receive serial data. What you're actually doing is wiring a switch to its RX line and toggling it manually with a mechanical switch, feeding a high or low level depending on the position of the switch.

So what you're trying to do is emulating a digital input line with the RX line of your serial port. If you take a look a how a serial port works you'll see that when you send a byte the TX line toggles from low to high at the baud rate, but on top of the data you have to consider the start and stop bits. So, why your solution works (at least sometimes): that's easy to see when you look at a scope picture:

scope capture TX line sending "\x00"

This is a screenshot of the TX line sending the \x00 byte, measured between pins 3 (TX) and 5 (GND) with no parity bit. As you can see the step only lasts for 7.5 ms (with a 1200 baud rate). What you are doing with your switch is something similar but ideally infinitely long (or until you toggle your switch back, which will be way after 7.5 ms no matter how fast you do it). I don't have a switch to try but if I open a terminal on my port and use a cable to shortcircuit the RX line to pin 4 (on a SUB-D9 connector) sometimes I do get a 0x00 byte, but mostly it's something else. You can try this experiment yourself with PuTTy or RealTerm and your switch, I guess you'll get better results but still not always the byte you expect because of the contacts bouncing.

Another approach: I'm sure there might be ways to improve on what you have, maybe reducing the baud rate to 300 or 150 bps, checking for a break in the line or other creative ideas.

But what you're trying to do is more akin to reading a GPIO line, and actually, the serial port has several digital lines intended (in the old days) for flow control.

To use these lines you should connect the common pole on your switch to the DSR line (pin 6 on a SUB-D9) and the NO and NC poles to lines DTR (pin 4) and RTS (pin 7).

The software side would be actually simpler than reading bytes: you just have to activate hardware flow control :

self.ser = serial.Serial()
self.ser.port='COM4'
self.ser.baudrate=1200  #Baud rate does not matter now
self.ser.timeout=0
self.ser.rtscts=True
self.ser.dsrdtr=True
self.ser.open()

Define the logical levels for your switch:

self.ser.setDTR(False)   # We use DTR for low level state
self.ser.setRTS(True)  # We use RTS for high level state
self.ser.open()         # Open port after setting everything up, to avoid unkwnown states

And use ser.getDSR() to check the logical level of the DSR line in your loop:

def start_stop(self):
    while True :
        try:
            switch_state = self.ser.getDSR()
            if switch_state == False and self._cycRunning == True:
                self.cycleStop()
                print("Off")

            elif switch_state == True and self._cycRunning == False:
                 self.cycleStart()
                 print("On")

        except serial.SerialException as e:
            print("Error")

I defined your self._cycRunning variable as boolean (in your initialization code you had defined it as float, but that was probably a typo).

This code works with no glitches at all even using a stripped wire as a switch.

Marcos G.
  • 3,371
  • 2
  • 8
  • 16
  • Yes a great answer that has improved my understanding of what i am trying to achieve. If I had the knowledge previously I would have been able to articulate a better question nonetheless I thank you for your persistence on solving this issue. –  Jul 03 '19 at 13:58
  • No problem Nick, we are all here to learn from our own mistakes and for everyone else's. The important thing is to take what you've learnt and keep building on it. – Marcos G. Jul 03 '19 at 14:49
0

You don't explain very well how your protocol works (I mean what is your switch supposed to be sending, or if it's sending a state change only once or several times or continuously).

But there are some red flags on your code anyway:

-With data = self.ser.read(size=1).strip() you read 1 byte but immediately you check if you have received 2 bytes. Is there a reason to do that?

-Your timer stop condition works comparing with the NULL character. That should not be a problem, but depending on your particular configuration it might (in some configurations the NULL character is read as something else, so it's wise to make sure you're really receiving it correctly).

-Your timer start condition seems too loose. Whatever you receive on the port, if it's one byte, you start your timer. Again, I don't know if that's the way your protocol works but it seems prone to trouble.

-When you replace your hardware switch with a software emulation it works as intended, but that is not surprising since you're probably imposing the condition. When you read from the serial port you have to deal with real world issues like noise, communication errors or the switch bouncing back and forth from ON to OFF. Maybe for a very simple protocol you don't need to use any error checking method, but it seems wise to at least check for parity errors. I'm not completely sure it would be straight-forward to do that with pyserial; on a quick glance I found this issue that's been open for a while.

-Again, the lack of info on your protocol: should you be using XON-XOFF flow control and two stop bits? I guess you have a reason to do it, but you should be very aware of why and how you're using those.

EDIT: With the comments below I can try to improve a bit my answer. This is just an idea for you to develop: instead of making the stop condition comparing exactly with 0x00 you can count the number of bits set to 1 and stop the counter if it's less or equal to 2. That way you can account for bits that are not received correctly.

You can do the same with the start condition but I don't know what hex value you send.

Credits for the bit counting function go to this question.

...
def numberOfSetBits(i):
    i = i - ((i >> 1) & 0x55555555)
    i = (i & 0x33333333) + ((i >> 2) & 0x33333333)
    return (((i + (i >> 4) & 0xF0F0F0F) * 0x1010101) & 0xffffffff) >> 24


def start_stop(self):
     while True :
         try:
             data_to_read = self.ser.inWaiting()
             if data_to_read != 0: # read if there is new data
                 data = self.ser.read(size=1).strip()
                 if numberOfSetBits(int.from_bytes(data, "big")) <= 2:
                     self.cycleStop()
                     print("Off")

                 elif numberOfSetBits(int.from_bytes(data, "big")) >= 3:  #change the condition here according to your protocol
                     self.cycleStart()
                     print("On")

         except serial.SerialException as e:
             print("Error")
Marcos G.
  • 3,371
  • 2
  • 8
  • 16
  • Thank you, I have made changes to the description of the issue. outlining that i just need the one bit of b'\x00' to allow me to determine if the switch is on or off. I have also cleaned up the code as your right i do not need to check for 2 bytes and have removed (parity, stopbits, bytesize, xonxoff) as the issue is the same without them. –  Jun 27 '19 at 09:04
  • you're welcome Nick. Is your switch sending the NULL character only once? I think the problem is sometimes you're not catching it, maybe you can send the character several times when you change the state of the switch... It does not seem very reliable to have one particular byte for one state and all other 255 bytes for the other state. Parity, stop bits and byte size should be the same on both ends of the serial link. – Marcos G. Jun 27 '19 at 10:14
  • It does return b'\00' only once but is not NULL/None if that makes a difference? –  Jun 27 '19 at 10:40
  • That was my point, if you get the b'\00' command only once, what happens if you miss it? If you one of the bits within the byte is changed due to noise then you'll never stop your counter. I think you might need to implement something to account for that. Most serial protocols have a kind of error control mechanism; they attach a [CRC](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) character to the message. This character can be calculated from the message itself so you can be sure that what you have received is correct, or otherwise you ask the other end to send the message again – Marcos G. Jun 27 '19 at 11:04
  • My lack of serial knowledge may be getting the better of me on this one. I have decoded the byte which i assume would give me a different result if the byte had changed in anyway. Not sure if this is an effective way of checking however the result does not change and the counter continues to run. –  Jun 27 '19 at 14:58
  • I have updated my answer. Maybe you can get something working based on that. – Marcos G. Jun 27 '19 at 15:53
  • I appreciate the update. Just tried it out and printed what i was getting for On and Off which is (2 bytes for Off and 4 for ON). So it works as it should however after a few presses the counter remains running. I will continue to play around with the code you have given and ill get back to you asap –  Jun 27 '19 at 16:35
  • Well, it was just a starting point. Maybe it's not robust enough. You might also have hardware issues... If you get stuck post a comment and we'll look at it. By the way, are you reading the switch with a microcontroller? – Marcos G. Jun 27 '19 at 16:45
  • Im not using a micro control at the moment. I will end up using one. This is just playing around to see what i can do with the rs232 –  Jun 29 '19 at 12:10
  • OK, but how are you making the switch send the message? – Marcos G. Jun 29 '19 at 14:30
  • I have pin 1 wired to common of the switch with pins 2 and 4 wired to the normally open of the switch. –  Jul 01 '19 at 10:02
  • Hello Nick, Sorry but I don't really see how your switch is interfaced with the serial port... these pin numbers you refer to are on a SUB-D9 connector? – Marcos G. Jul 01 '19 at 11:09
  • OK, clear. That means you are not sending anything on the serial port but just switching voltage levels on it... I finally understood your setup; it took a while!. With this in mind I think there might be other tricks to improve the software side. I'll think about it and maybe write a new answer if I come up with something. – Marcos G. Jul 01 '19 at 12:14
  • Sorry for the poor explanation, i should have detailed the circuit at the start. I am not sure how this would make much of a difference to the issue. –  Jul 01 '19 at 13:54
  • No problem, it was very obvious for you, and the same for me but in a completely wrong way. It does make a difference though, what you're doing is kind of a hack. The serial port is not intended to work like that. And if you insist on working like that (I imagine in a quest to keep hardware to a bare minimum) there are better ways to detect the change on the switch state than `serial.read()`. I will post a new answer tomorow with a couple of ideas. Just to be sure, what model and brand is your RS232-to-USB adaptor? – Marcos G. Jul 01 '19 at 15:16
  • Manufacturer is FTDI not sure on the model –  Jul 01 '19 at 17:57
  • OK, thanks, I'm assuming it's similar to [this one](https://www.ftdichip.com/Support/Documents/DataSheets/Cables/DS_US232R-10_R-100-500.pdf). How did you choose your wiring? Did you measure voltages and decided to use those pins (4 and 1) because they had the voltage levels you wanted for the switch to put pin 2 high and low? – Marcos G. Jul 01 '19 at 18:14
  • Yes very similar. I basically played around using a ribbon cable testing which ones gave the best results. between pin 1 and 4 i get about 9 V, when pressed it drops to zero. –  Jul 01 '19 at 19:00
  • I have added a new answer using DIO signals, I have tested it with a very similar device (mine uses the FTDI chip as well but it has two ports). Give it a try to see if this works better. – Marcos G. Jul 02 '19 at 09:13
  • Thanks. I have not had a chance to test it out yet but i will reply as soon as i have –  Jul 03 '19 at 11:14