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

Micro Switch with pyserial RS232 starts/stops a timer in a tkinter thread but continues to run even when stopped

我一直在使用连接到 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()

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

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

但是您的代码中仍然存在一些危险信号:

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

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

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

-当您用软件仿真替换硬件开关时,它会按预期工作,但这并不奇怪,因为您可能强加了条件。当您从串行端口读取时,您必须处理现实世界中的问题,例如噪声、通信错误或开关 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 已经打开了一段时间。

-同样,您的协议缺少信息:您应该使用 XON-XOFF 流量控制和两个停止位吗?我猜你有这样做的理由,但你应该非常清楚为什么以及如何使用它们。

编辑: 通过下面的评论,我可以尝试改进我的答案。这只是您开发的一个想法:您可以计算设置为 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")

通过更好地了解您正在尝试做的事情,可以想到更好的解决方案。

事实证明,您没有使用串行端口发送或接收串行数据。你实际上在做的是将一个开关连接到它的 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 和您的开关自己尝试这个实验,我想您会得到更好的结果,但由于接触弹跳,仍然不能总是达到您期望的字节数。

另一种方法:我相信可能有一些方法可以改进你所拥有的,也许将波特率降低到 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 变量定义为布尔值(在您的初始化代码中,您已将其定义为浮点数,但这可能是一个错字)。

即使使用剥线作为开关,此代码也能正常工作。