今天主要介绍最近基于python学习完成的上位机开发,通过这个项目学习了python语言同时也为后续机器人所需的上位机开发搭建提供了良好的基础,而且确实相比c来说我选择python在使用过后,虽然有些编程习惯需要调整如全局变量调用、没有switch、格式进位、IDE简单,但确实挺不错除了跨平台、移植性强外库丰富而且支持矩阵运算对于机器人控制编程来说十分完美。

后续将免费开放使用的机器人调试上位机

上位机界面主要完成对底层嵌入式控制器和机器人参数的实时显示、配置和遥控,更重要的是实时显示内部参数并绘制相应曲线,这在后续参数调节中非常重要。通常情况下对于PC机来说主要通过USB读取外部设备,因此我们采用串口转USB模块将如单片机串口数据发送给上位机,当然也可以通过如无线WIFI、蓝牙、射频串口透传设备代替有线连接,传统PC上位机开发主要基于C++、VB、VC和MFC等语言,而我们使用Python进行界面开发使用开发的Serial库完成串口通信的功能,首先通过pip install serial的方式进行安装并通过import serial的方式进行调用,目前网络上有许多基于serial库开发的串口收发程序,但要实现主动下方并实时接收底层数据、解码校验最终提取如float和int型的浮点数据尚无完整例子,因此本节给出设计串口上位机的各关键程序介绍。

(1)串口初始化

在所建立的串口类中首先需要完成对串口所需要的关键参数,串口号“COM”,波特率“BAUD”和停止位等参数进行设置,同时也要定义用于发送和接受的数组,为实现对接受数据的持续接受我们采用多线程的方式建立一个独立于主程序的串口接收线程不断从缓存中读取接受到的数据,具体程序如下:

        global timer_print_uart,rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    global time_start
    print("uart param init")
    timer_print_uart = 0
    time_start =time.time() 
    BYTE0 = lambda x : (x>>0)&0xff
    BYTE1 = lambda x : (x>>8)&0xff
    BYTE2 = lambda x : (x>>16)&0xff
    BYTE3 = lambda x : (x>>24)&0xff    
    rx_data =  [0]*500
    tx_data =  [0]*500
    rx_cnt = 0
    rx_num_now= 4
    uartState = gl.get_value('UART_ON')#
    print_cnt = 0
    Baudrate = gl.get_value('BAUD')#
    COM =  gl.get_value('COM')#   # #创建变量,便于取值
    Stopbits = "1"
    ser = serial.Serial()
    ser.timeout=0.5#读超时设置
    ser.writeTimeout=0.5#写超时def    
    if(gl.get_value('UART_INIT')==False):#切换界面最多开启两个线程
        gl.set_value('UART_INIT', True)#
        print("new_uart_thread")
        ReadUARTThread = threading.Thread(target=ReadUART)
        cond_uart = threading.Condition() # 锁
        stop_uart = False
        ReadUARTThread.start() 上位机界面主要完成对底层嵌入式控制器和机器人参数的实时显示、配置和遥控,更重要的是实时显示内部参数并绘制相应曲线,这在后续参数调节中非常重要。通常情况下对于PC机来说主要通过USB读取外部设备,因此我们采用串口转USB模块将如单片机串口数据发送给上位机,当然也可以通过如无线WIFI、蓝牙、射频串口透传设备代替有线连接,传统PC上位机开发主要基于C++、VB、VC和MFC等语言,而我们使用Python进行界面开发使用开发的Serial库完成串口通信的功能,首先通过pip install serial的方式进行安装并通过import serial的方式进行调用,目前网络上有许多基于serial库开发的串口收发程序,但要实现主动下方并实时接收底层数据、解码校验最终提取如float和int型的浮点数据尚无完整例子,因此本节给出设计串口上位机的各关键程序介绍。

上述程序通过global的方式使得全局参数不会在界面关闭时被初始化,首先定义波特率和串口号等参数,通过ser=serial.Serial()定义串口函数,通过import threading使用多线程函数并开启串口线程。

(2)打开串口

将串口参数赋值给ser结构之后当串口号存在时可以打开串口,为保证打开出错时程序不会奔溃,这里通过try的方式提供故障处理函数,则具体代码如下:

def uart_open():
    global rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    if (uartState==True):#关闭
        uartState = False
        gl.set_value('UART_ON', uartState)#
        ser.close()
    else:
        # restart serial port
        ser.port = COM
        ser.baudrate = Baudrate
        gl.set_value('COM', COM)#
        gl.set_value('BAUD', Baudrate)#
        ser.parity = serial.PARITY_NONE;
        ser.stopbits = serial.STOPBITS_ONE;

        try:
            ser.open()
            if (ser.isOpen()): # open success
                uartState = True
                if gl.get_value('UART_LOSS')==1:#
                    gl.set_value('UART_LOSS', 0)#
                gl.set_value('UART_ON', uartState)#
                print("Uart ON!\n",gl.get_value('UART_ON'))
        except:
            infromStr = "无法打开串口 "+ser.port
            InformWindow(infromStr)

为获取串口号可以通过人为指定的方式给定“COM”但在实际应用时PC机对于不同的设备往往会分配不同的串口号,如以正确匹配往往需要通过硬件管理器的方式来确认,这对于上位机来说不太方便,这里可以通过自动检测串口的方式给定所需参数:

def evt_combox_dropdown(self, event):#自动检测串口
        import serial.tools.list_ports
        serial_list = list(serial.tools.list_ports.comports())
        if serial_list: #判断是否为空
            portName_list = []
            for i in range(0, len(serial_list)):
                portname = list(serial_list[i])
                portName_list.append(str(portname[0]))
            print (portName_list)
            
            self.p_com.set(portName_list

上面这段程序绑定了窗口中的下拉菜单控件,可以通过bind绑定按键选中回调或者在线程中周期检测的方式来自动获取。

(3)发送数组

当串口开启成功后可以通过ser.write(bytesToSend)发送数组,因此只需要在之前完成数据的封装,如果需要周期发送则可以放在相应的线程中周期调用,则代码如下:

def uart_test():#发送  fasong
    global rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    en_tx = 0
    if (uartState==True):
        if gl.get_value('RC_OCU_MODE')== 1:
            en_tx = 1
            lens = uart_formate_ocu_spd()
        gl.set_value('UART_WINDOW_SEL', 0)
    #默认发送遥控命令
        bytesToSend = tx_data[0:lens]   
        if (ser.isOpen()):
            try:
                ser.write(bytesToSend)
            except:         
                ser.close()
                uartState = False
                gl.set_value('UART_ON', uartState)#
                gl.set_value('UART_LOSS', 1)#
    else:
        infromStr = "未连接串口!"
        InformWindow(infromStr)

对于数据格式的封装可以采用目前飞控中常用的帧头+数据标志位+数据+校验的方式发送,通过字节组合能发送浮点、整型和标志位,下面给出一个常用的组码方式:

def uart_formate_ocu_spd():#速度遥控命令
    global rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    sum = 0
    _cnt = 0
    data_to_send  =  []
    data_to_send.append(0xAA)
    data_to_send.append(0xAF)
    data_to_send.append(0x31)#机器人参数
    data_to_send.append(0)

    send_float(data_to_send,gl.get_value('SPD_B')[0])#
    send_float(data_to_send,gl.get_value('SPD_B')[1])#
    send_float(data_to_send,gl.get_value('SPD_B')[2])#
    send_float(data_to_send,gl.get_value('DPIT'))#
    send_float(data_to_send,gl.get_value('DROL'))#
    send_float(data_to_send,gl.get_value('DYAW'))#

    _cnt = len(data_to_send)
    data_to_send[3] = _cnt - 4
    for i in range(0,_cnt):
        sum = sum+ data_to_send[i]

    data_to_send.append(BYTE0(sum)) 

    tx_data[0:len(data_to_send)]= data_to_send

    return len(data_to_send)

其中通过将浮点数拆分为4个字节可以进行快速的收发,下面给出我尝试多种方法后成功的浮点转字符函数:

def send_float(tx_Buf,data):
    temp_B= struct.pack('f',float(data))
    tx_Buf.append(temp_B[0])
    tx_Buf.append(temp_B[1])
    tx_Buf.append(temp_B[2])
    tx_Buf.append(temp_B[3])

def send_int(tx_Buf,data):
    temp_B= struct.pack('i',int(data))
    tx_Buf.append(temp_B[0])
    tx_Buf.append(temp_B[1])
    tx_Buf.append(temp_B[2])
    tx_Buf.append(temp_B[3])

def send_char(tx_Buf,data):
    tx_Buf.append(data)

上述函数调用全局发送数组,同时自动在数组后增加新字节,通过顺序调用既可以完成发送数组的构成十分方便。

(4)数据接收

当串口配置正确且连接后既可以接收底层上传的数据,这里采用一个线程一直读取串口寄存器,代码如下:

def ReadUART():#读取线程
    global timer_print_uart,rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    global time_start
    while True:
            timer_cost = time.time() - time_start   #结束计时
            if timer_cost > 1 and uartState:
                time_start= time.time()
                timer_print_uart=0
                rx_cnt =0
            print_cnt =print_cnt +1
            if (print_cnt>0 and 1):
                print_cnt=0
                if (uartState):
                    uart_test()

            if (uartState):
                    try:
                        _len = ser.inWaiting()#获取接收到的数据长度2            
                        if _len: 
                            data = ser.read(_len)
                            try:#解码
                                for i in range(0 ,_len):
                                    uart_anal(data[i])#重要 字节串转 Int
                            except:
                                _len = 0
                    except:         
                        ser.close()
                        uartState = False
                        gl.set_value('UART_ON', uartState)#
                        gl.set_value('UART_LOSS', 1)#

程序中同时集成了发送部分,通过自定义计数器确定发送频率,为保证串口中断异常这里给出了串口中断后的异常处理机制。由于PC非嵌入式中断机制读取因此无法像单片机一样一个字节一个字节读取并解码,因此这里是读取一段数据并缓存到接受数组中,通过循环依次判断是否满足帧格式,解码状态机如下:

def uart_anal(com_data):
    global rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    state=   gl.get_value('UART_RX_STATE')
    cnt_ =   gl.get_value('UART_RX_CNT')
    len_ =   gl.get_value('UART_RX_LEN')
    #print("rx:",com_data,state)
    if state ==5:
        rx_data[4+cnt_]=com_data
        Anal_Data(rx_data,cnt_+5)
        state = 0
        cnt_ = 0
        for i in range(0,500):
            rx_data[i]=0
        gl.set_value('UART_RX_STATE',state)
        gl.set_value('UART_RX_CNT',cnt_)
        gl.set_value('UART_RX_LEN',len_)

    if state==4: #4
        len_=len_-1
        rx_data[4+cnt_]=com_data
        cnt_=cnt_+1
        if(len_<=0):
            state= 5
        gl.set_value('UART_RX_STATE',state)
        gl.set_value('UART_RX_CNT',cnt_)
        gl.set_value('UART_RX_LEN',len_)

    if state==3:    #3
        if com_data<254:
            state= 4
            rx_data[3]=com_data
            len_ = com_data
            cnt_ = 0
        else:
            state = 0
        gl.set_value('UART_RX_STATE',state)
        gl.set_value('UART_RX_CNT',cnt_)
        gl.set_value('UART_RX_LEN',len_)

    if state ==2: #2
        if  com_data>0 and com_data<int('0xF1', 16):
            state=3
            rx_data[2]=com_data
        else:
            state=0
        gl.set_value('UART_RX_STATE',state)
        gl.set_value('UART_RX_CNT',cnt_)
        gl.set_value('UART_RX_LEN',len_)

    if state ==1:   #1
        if(com_data ==int('0xAF', 16)):
            rx_data[1]=com_data
            state=2
        else :
            state=0
        gl.set_value('UART_RX_STATE',state)
        gl.set_value('UART_RX_CNT',cnt_)
        gl.set_value('UART_RX_LEN',len_)

    if state ==0: #0
        if(com_data ==int('0xAA', 16)):
            rx_data[0]=com_data
            state=1
        gl.set_value('UART_RX_STATE',state)
        gl.set_value('UART_RX_CNT',cnt_)
        gl.set_value('UART_RX_LEN',len_)

这里都采用gl的全局变量,也可以定义为类内部变量。对于接受解码程序主要是判断帧头和校验位,最终完成对浮点4个字节的恢复提取,由于python没有很多16位字节类型,强制转换有时候会直接报错,这里给出我尝试成功的解码方式:

def Anal_Data(data_buf,lens):#接受解码 jiema
    global rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    sum=0
    for i in range(0,lens-1): 
        sum = sum + data_buf[i]

    if(BYTE0(sum)!=data_buf[lens-1]):       
        return
    if(data_buf[0]!=int('0xAA', 16) or data_buf[1]!=int('0xAF', 16)):       
        return
    
    if data_buf[2]==int('0x51', 16):#参数 配置
        rx_num_now = 4 
        #当前PWM
        gl.set_value_sel('P_PID_PIT_B', decode_float(data_buf,rx_num_now),0)
        gl.set_value_sel('P_PID_PIT_B', decode_float(data_buf,rx_num_now),1)
        gl.set_value_sel('P_PID_PIT_B', decode_float(data_buf,rx_num_now),2)
        gl.set_value_sel('P_PID_ROL_B', decode_float(data_buf,rx_num_now),0)
        gl.set_value_sel('P_PID_ROL_B', decode_float(data_buf,rx_num_now),1)
        gl.set_value_sel('P_PID_ROL_B', decode_float(data_buf,rx_num_now),2)

相应组合浮点与整型的代码如下:

def decode_float(rx_Buf,start_Byte_num):
    global rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    temp=bytes([rx_Buf[start_Byte_num],rx_Buf[start_Byte_num+1],rx_Buf[start_Byte_num+2],rx_Buf[start_Byte_num+3]])
    rx_num_now= rx_num_now+4
    return struct.unpack('f',temp)[0]

def decode_char(rx_Buf,start_Byte_num):
    global rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    temp = rx_Buf[start_Byte_num]
    rx_num_now = rx_num_now+1
    return temp

def decode_int(rx_Buf,start_Byte_num):
    global rx_data,tx_data,rx_cnt,rx_num_now,uartState,print_cnt,BYTE0,BYTE1,BYTE2,BYTE3
    global Baudrate,COM,Stopbits,ser,ReadUARTThread,cond_d,stop_d
    #temp=bytes([rx_Buf[start_Byte_num],rx_Buf[start_Byte_num+1]])
    temp=bytes([rx_Buf[start_Byte_num],rx_Buf[start_Byte_num+1],rx_Buf[start_Byte_num+2],rx_Buf[start_Byte_num+3]])
    rx_num_now= rx_num_now+4
    #return struct.unpack('h',temp)[0]
    return struct.unpack('i',temp)[0]

上述代码依序完成从接受数组中提取所需数据并能自动增加帧开始位置,能较方便地进行编程与发送顺序的修改。以上就是整个串口接受发送的代码除了可以用于串口上位机外也可以集成与Linux嵌入式主控与底层的通信交互,ROS节点以及内部存储单元与FLASH、EPROOM的简单存储读取。

完成代码下载地址如下,具体运行还需要各位自行集成与修改,代码无法直接运行!!

链接:pan.baidu.com/s/1jnvmii

提取码:ob1r

Tkinter界面学习资料我整理了几个常用的如下:

链接:pan.baidu.com/s/1olXo0N

提取码:7buk