带有 pyserial RS232 的微型开关在 tkinter 线程中启动/停止计时器,但即使停止也继续运行

Posted

技术标签:

【中文标题】带有 pyserial RS232 的微型开关在 tkinter 线程中启动/停止计时器,但即使停止也继续运行【英文标题】:Micro Switch with pyserial RS232 starts/stops a timer in a tkinter thread but continues to run even when stopped 【发布时间】:2019-06-26 20:58:00 【问题描述】:

我一直在使用连接到我的 Windows PC 上的 RS232/USB 串行转换器电缆的微动开关来启动停止和重置计时器。

程序大部分时间运行平稳,但每隔一段时间更新计时器小部件就会卡在运行中并且计时器不会停止。

使用串行协议,我想接收 1 个字节 b'\x00' 表示关闭,任何不是 b'\x00' 的都应该表示开启。

我已经用按钮小部件替换了微动开关来模拟开关并且没有得到相同的错误,或者我只是没有保持足够长的时间。

这可能是 RS232 的问题,导致我看不到错误,但我对此的了解很粗略,并且已经用尽了所有途径在网上寻找有关这方面的任何信息。

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()

我希望按下微动开关时计时器开始运行。按下时它应该停止并重置为零并等待下一次按下

【问题讨论】:

【参考方案1】:

更好地了解您要做什么,然后想到更好的解决方案。

事实证明,您没有使用串行端口发送或接收串行数据。你实际上在做的是将一个开关连接到它的 RX 线上,并用一个机械开关手动切换它,根据开关的位置提供高电平或低电平。

因此,您要做的是用串行端口的 RX 线模拟数字输入线。如果你看一下how a serial port works,你会发现当你发送一个字节时,TX 线以波特率从低到高切换,但在数据之上,你必须考虑开始位和停止位。那么,为什么您的解决方案有效(至少有时如此):当您查看范围图片时很容易看出这一点:

这是发送 \x00 字节的 TX 线的屏幕截图,在引脚 3 (TX) 和 5 (GND) 之间测量,没有奇偶校验位。如您所见,该步骤仅持续 7.5 毫秒(波特率为 1200)。你用你的开关做的事情是类似的,但理想情况下是无限长的(或者直到你把你的开关拨回来,不管你做得多快,这将在 7.5 毫秒之后)。我没有可以尝试的开关,但如果我打开端口上的终端并使用电缆将 RX 线短路到针脚 4(在 SUB-D9 连接器上),有时我会得到一个0x00 字节,但主要是这是另一回事。你可以用 PuTTy 或 RealTerm 和你的 switch 自己尝试这个实验,我想你会得到更好的结果,但由于联系人弹跳,仍然不是你期望的字节。

另一种方法:我相信可能有一些方法可以改进你所拥有的,可能会将波特率降低到 300 或 150 bps,检查break in the line 或其他创意。

但是您要做的更像是读取 GPIO 线,实际上,串行端口有几条数字线(在过去)用于flow control。

要使用这些线路,您应该将开关上的公共极连接到 DSR 线路(SUB-D9 上的引脚 6),并将 NO 和 NC 极连接到线路 DTR(引脚 4)和 RTS(引脚 7)。

软件方面实际上比读取字节更简单:您只需激活硬件流控制:

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()

为您的交换机定义逻辑级别:

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

并使用ser.getDSR() 检查循环中 DSR 行的逻辑级别:

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")

我将您的 self._cycRunning 变量定义为布尔值(在您的初始化代码中,您已将其定义为浮点数,但这可能是一个错字)。

即使使用剥线作为开关,此代码也完全没有故障。

【讨论】:

是的,这是一个很好的答案,它提高了我对我想要实现的目标的理解。如果我以前有这方面的知识,我本可以提出一个更好的问题,但我感谢你坚持解决这个问题。 没问题,尼克,我们在这里都是为了从自己的错误中吸取教训,也为了其他人的错误。重要的是吸取你所学的知识并在此基础上继续发展。【参考方案2】:

你没有很好地解释你的协议是如何工作的(我的意思是你的开关应该发送什么,或者它是否只发送一次或几次或连续的状态更改)。

但是你的代码还是有一些危险信号:

-使用data = self.ser.read(size=1).strip(),您读取了 1 个字节,但您立即检查是否收到了 2 个字节。有这样做的理由吗?

-您的计时器停止条件与 NULL 字符相比有效。这应该不是问题,但根据您的特定配置,它可能会(在某些配置中,NULL 字符被读取为其他内容,因此确保您确实正确接收它是明智的)。

-您的计时器启动条件似乎太松了。无论你在端口上收到什么,如果它是一个字节,你就启动你的定时器。同样,我不知道这是否是您的协议的工作方式,但它似乎很容易出现问题。

-当您用软件仿真替换硬件开关时,它会按预期工作,但这并不奇怪,因为您可能会强加条件。当您从串行端口读取数据时,您必须处理现实世界的问题,例如噪音、通信错误或开关bouncing 从 ON 到 OFF 来回切换。也许对于一个非常简单的协议,您不需要使用任何错误检查方法,但至少检查奇偶校验错误似乎是明智的。我不完全确定用 pyserial 做到这一点会很直接;快速浏览了一下,我发现this issue 已经开放了一段时间。

-再次,您的协议缺乏信息:您是否应该使用 XON-XOFF 流控制和两个停止位?我想你有理由这样做,但你应该非常清楚你为什么以及如何使用这些。

编辑:使用下面的 cmets,我可以尝试改进一下我的答案。这只是您开发的一个想法:您可以计算设置为 1 的位数并在小于或等于 2 时停止计数器,而不是使停止条件与 0x00 完全比较。这样您就可以解释未正确接收的位。

你可以对开始条件做同样的事情,但我不知道你发送的是什么十六进制值。

位计数功能的积分转到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")

【讨论】:

谢谢,我已经对问题的描述进行了修改。概述我只需要一点 b'\x00' 来让我确定开关是打开还是关闭。我还按照您的权利清理了代码,我不需要检查 2 个字节并已删除(奇偶校验、停止位、字节大小、xonxoff),因为没有它们,问题是一样的。 不客气,尼克。您的交换机是否只发送一次 NULL 字符?我认为问题是有时您没有抓住它,也许您可​​以在更改开关状态时多次发送字符...对于一种状态和所有其他 255 个特定字节似乎不太可靠另一个状态的字节。串行链路两端的奇偶校验、停止位和字节大小应相同。 它确实只返回一次 b'\00' 但不是 NULL/None 是否有影响? 那是我的意思,如果你只得到一次 b'\00' 命令,如果你错过了会发生什么?如果由于噪声而改变了字节中的一位,那么您将永远不会停止计数器。我认为您可能需要实施一些措施来解决这个问题。大多数串行协议都有一种错误控制机制;他们将CRC 字符附加到消息中。这个字符可以从消息本身计算出来,这样你就可以确定你收到的是正确的,否则你要求另一端重新发送消息 我缺乏系列知识可能会在这方面做得更好。我已经解码了我认为如果字节无论如何都改变了会给我一个不同的结果的字节。不确定这是否是一种有效的检查方法,但结果不会改变并且计数器继续运行。

以上是关于带有 pyserial RS232 的微型开关在 tkinter 线程中启动/停止计时器,但即使停止也继续运行的主要内容,如果未能解决你的问题,请参考以下文章

无法使用 pyserial 发送整数

RS232转换为RS485的接线方法最好有图

RS232转换为RS485的接线方法最好有图

rs232转rs422电路原理图

485和232的电平信号有啥区别?

Pyserial 检测中断条件