2024年1月12日发(作者:)
入门级Python应用程序---伺服电机控制
v信@宝德,百度@baode_w
前言
本文只适合像作者这样的入门级小白,大虾级别的请忽略。
由于作者水平有限,不当之处,欢迎批评指正。
本文所述应用程序只考虑功能实现,不考虑程序优化。
一、应用环境
系统环境:Window 10;
Python版本:3.7;
IDE:PyCharm;
界面设计:PYQT5;
电机:光毓机电RMD-S系列电机;
控制端口:RS-485,使用USB转485模块。
二、准备环境
软件安装及环境变量设置方法网上较多,请自行查阅,这里不再赘述。
双击打开PyCharm,新建一个New Project,注意添加项目的环境依赖venv。配置下Python3.7的依赖环境库venv library root,External Libraries—>Python
3.7(venv) —>venv library root—>,将值改成“true”,如下图。
为PyCharm添加外部工具,FileSettingsToolsExternal Tools “+”。
以PYQT5为例,单击“+”,弹出新建外部工具对话框,如下图所示。
对话框中Name是在IDE中的显示名字,可以自定义填写。Tool Settings需填写两项,Program项单击浏览Python3.7根目录“Python37Libsite-packagespyqt5_”(当然,你得先安装有PYQT5,>_<),Working directory项填入“$ProjectFileDir$”,其他选项默认即可,单击“OK”保存。
其他外部工具如PYUIC、PYRCC等,添加过程类似。
三、编写程序
在项目中新建两个py文件,其中一个py文件()关联QT界面的信号(Signal)和槽(Slot),另外一个py文件()用来编写主程序,如此便于分离QT界面和逻辑程序,避免修改界面时对主程序存在干扰,当修改QT界面时,QT只会更新,不会更新主程序。
主程序中需要包含PTContrl完成调用,以及主程序需要加载一些要用到的模块,如sys、serial、Qtimer等,如下图所示。
主程序功能包括,串口端口检测、波特率设置、打开端口,串口实时发送两个电机的角度值、速度值、转动方向,定时从串口接收两个电机编码器数据,并解码显示角度值。
主程序完整代码如下:
import sys
import serial
import binascii
import _ports
#from PyQt5 import QtCore, QtGui, QtWidgets
from import QTimer
from ets import QApplication,QMainWindow,QFileDialog
from ets import QMessageBox
from time import sleep
import PTContrl
#import PDial
#from ActiveMain import RulerProgress
#主程序
class MainControl(QMainWindow,_SFmotorcontrol):
def __init__(self):
QMainWindow.__init__(self)
_SFmotorcontrol.__init__(self)
i(self)
= ()
_num_sended = 0
1 = (_()-18000)/100
2 = (_() - 9000) / 100
1_H = 1#电机1速度高8位
1_L = 244 # 电机1速度,0x1F4,5dps
2_H = 1#电机2速度5dps高8位
2_L = 244 # 电机2速度5dps,低8位
1_zero = 0#水平电机零位0
2_zero = 0#垂直电机零位0
lect_m("115200")
lect_m("19200")
lect_m("57600")
lect_m("9600")
1_postvalue = 0
2_postvalue = 0
_dir = 0
_dir1 = 0
s1 = 1
s2 = 2
_left = 0
_right = 1
_up = 0
_down = 1
_flag = 0
_flag = 0
_stop = 0
_auto_act =0
1 = QTimer(self)
1_timeout = 250#timer1溢出时间250ms
#for addr in range(1,33,1):
# m(str(addr))
# m(str(addr))
_check()
t(_Connect)#按钮点击事件链接到自定义对象
#_t(1_change)#拨盘值改变事件链接到自定义对象
#_t(1_change)
#_t(1_change)
#_check_ible(False)
_check_t(_check)
#_t(_send)
#_t(_send)
#self.H_speed_t(_change)
#self.V_speed_t(_change)
_t(_send1)
_t(_send2)
_t(_send1)
_t(_send2)
_t(_stop)
_t(_stop)
_t(_stop)
_t(_stop)
_t(_send)
t(_send)
select_t(_check)
_ue(18000)
#_bled(True)
_bled(False)
_bled(False)
_bled(False)
_bled(False)
_bled(False)
_leSheet("background-color:rgb(0,200,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;")
_leSheet("border:2px groove gray;border-radius:10px;padding:2px
4px;font:12pt;")
_leSheet("border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
_leSheet("border:2px groove gray;border-radius:10px;padding:2px
4px;font:12pt;")
_leSheet("border:2px groove gray;border-radius:10px;padding:2px
4px;font:12pt;")
_leSheet("border-color:rgb(0,0,255);gridline-color:rgb(0,0,255);color:
rgb(0,170,0);border-left-color: rgb(255, 0, 0);")
leSheet(
"background-color:rgb(0,200,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
_check_leSheet("border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
#leSheet("border:2px groove gray;border-radius:10px;")
#_leSheet("handle{background:blue;}")
#border:0px solid transparent;
#border-radius:2px;} ")
#while True:
#在下面编写自己的子程序
#端口打开或关闭
def on_Connect(self):
if ():
()
()
t("连 接")
leSheet("background-color:rgb(0,200,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font: 12pt;")
_bled(False)
_bled(False)
_bled(False)
_bled(False)
_bled(False)
if _() == "停止":
_send()
else:
= select_tText()
te = int(lect_tText())
ze = 8
ts = 1
= "N"
le = True
t = 2
try:
()
if _open:
t("断 开")
leSheet(
"background-color:rgb(200,0,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font: 12pt;")
(1_timeout)#启动定时器
_bled(True)
_bled(True)
_bled(True)
_bled(True)
_bled(True)
#_()
except:
al(self, "Port Error", "此串口不能被打开!")
return None
#it_ = _()
#t(str(_()/100))# =
#_()
#print(it)
#水平转电机停止
def roll_stop(self):
_leSheet("border:2px groove gray;border-radius:10px;padding:2px
4px;font:12pt;")
_leSheet("border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
order_sum = 62 + 166 + 8 + s1
order_sum = int(hex(order_sum)[-2:], 16)
_s = [62, 166, s1, 8, order_sum, 0, 60, 140, 0, 0, 0, 0, 0, 200]
# 发送位置359度,速度0dps,顺时针,停止命令
_send()
#1_t(str(_()/100)+"度")
#2_t(str(_() / 100) + "度")
#俯仰电机停止
def pitch_stop(self):
_leSheet("border:2px groove gray;border-radius:10px;padding:2px
4px;font:12pt;")
_leSheet("border:2px groove gray;border-radius:10px;padding:2px
4px;font:12pt;")
order_sum = 62 + 166 + 8 + s2
order_sum = int(hex(order_sum)[-2:], 16)
_s = [62, 166, s2, 8, order_sum, 0, 40, 35, 0, 0, 0, 0, 0, 75] #
发送位置90度,速度0dps,顺时针
_send()
#it_t(str(self.H_speed_()/100)+"dps")
#it_t(str(self.V_speed_() / 100)+"dps")
#串口端口检测
def port_check(self):
# 检测所有存在的串口,将信息存储在字典中
_Dict = {}
port_list = list(_ts())
select_()
for port in port_list:
_Dict["%s" % port[0]] = "%s" % port[1]
select_m(port[0])
if len(_Dict) == 0:
_t(" 无可用串口")
#t(" 检测无可用串口")
# 发送数据
def data_send(self):
if ():
if _s != "":
# 非空字符串
_t("发送:" + str(_s))#print(send_list)
input = bytes(_s)
#else:
# ascii发送
#input_s = (input_s + 'rn').encode('utf-8')
num = (input)#发送数据
_num_sended += num
#_t("发送:" + hex(input_s))
else:
al(self, "Port Error", "串口未打开!")
pass
def auto_send(self):
if _flag == 0:
_flag = 1
_t("停止")
_leSheet("background-color:rgb(200,0,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
else:
_flag = 0
_auto_act = 0
_t("自动")
_leSheet("background-color:rgb(0,200,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
_stop = 1
pass
def ask_send(self):
if ():
ask_list1 = [0x3e,0x90,0x01,0x00,0xcf]
ask_list2 = [0x3e,0x90,0x02,0x00,0xd0]
if _flag:
(ask_list1)
_flag = 0
else:
(ask_list2)
_flag = 1
#else:
sleep(0.02)#线程挂起0.05秒
rec_data = ""
#rec_hex = ""
n = ing()
if n :
rec_data = str(binascii.b2a_hex((n)))[2:-1]
#for rec_list in rec_hex:
# rec_data = rec_data + str(rec_list) #= rec_data + str(ne())
#print(rec_data)
if len(rec_data) > 15:
#print(rec_data)
#print(len(rec_data))
_t("接收:" + rec_data + ",长度:" + str(len(rec_data)))
# print(send_list)
#rec_data = rec_data[0:8]
#head = rec_data[:2]
#order = rec_data[3:4]
addr = int((rec_data[5:6]),16)
engle_data1 = rec_data[10:12]
engle_data2 = rec_data[12:14]
engle_data = int((engle_data2 + engle_data1),16)
#engle_data = engle_data[::-1]#倒置
#engle_sum = engle_data[1] * 0x100 + engle_data[2]
#engle_sum = int(str(engle_sum),16)
if addr == s1:#电机1返回数据
engle_data = (engle_data - 1_zero) * 36000 // 4096#精度360/4096,*100
1_postvalue = engle_data
1_t(str('%.1f'%(engle_data/100)))#保留1位小数点
engle_data = engle_data + 18000
if engle_data > 36000:
engle_data = engle_data - 36000
_ue(engle_data)
if addr == s2:#电机2返回数据
engle_data = (engle_data - 2_zero) * 36000 // 4096 # 精度360/4096,*100
2_postvalue = engle_data
if engle_data > 10000:
2_t("-" + str('%.1f' % ((36000-engle_data )/
100))) # 保留1位小数点
else:
2_t(str('%.1f' % (engle_data / 100))) # 保留1位小数点
engle_data = engle_data + 9000
#2_t(str('%.1f'%(engle_data/100)))#保留1位小数点
_ue(engle_data)
if _flag:
if _auto_act ==1:#自动翻转
if 1_postvalue > 35500:
_send1()#send 0deg
_auto_act = 1
if 1_postvalue < 400:
_send2()#send 359 deg
_auto_act = 1
else:#初次自动运行
if 1_postvalue > 18000 or 1_postvalue == 18000 :
_send2()#send 359 deg
_auto_act = 1
if 1_postvalue < 18000:
_send1()#send 0deg
_send()
_auto_act = 1
#自动停止
if _stop == 1:
_stop = 0
_stop()#send auto stop
#print(rec_data)
#al(self, "Port Error", "串口未打开!")
# pass
#手动与自动之间的切换?手动点击时停止自动
#水平电机顺时针转动
def roll_send1(self):
if _() and _flag ==1:
_send()
if _flag ==0:
_leSheet("background-color:rgb(0,200,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
order_sum = 62 + 166 + 8 + s1
order_sum = int(hex(order_sum)[-2:], 16)
data_sum = int(hex(1 + 1_H + 1_L)[-2:] ,16)
_s =
[62,166,s1,8,order_sum,1,0,0,0,1_L,1_H,0,0,data_sum]#发送位置0度,速度2dps,顺时针
#input_s = bytes(input_s)
#(input_s)
_send()
#水平电机逆时针转动
def roll_send2(self):
if _() and _flag ==1:
_send()
if _flag == 0:
_leSheet("background-color:rgb(0,200,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
order_sum = 62 + 166 + 8 + s1
order_sum = int(hex(order_sum)[-2:], 16)
data_sum = int(hex(60+140 + 1_H + 1_L)[-2:], 16)
_s =
[62,166,s1,8,order_sum,0,60,140,0,1_L,1_H,0,0,data_sum]#发送位置359度,速度2dps,逆时针
#input_s = bytes(input_s)
#(input_s)
_send()
#俯仰电机顺时针转动
def pitch_send1(self):
_leSheet("background-color:rgb(0,200,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
order_sum = 62 + 166 + 8 + s2
order_sum = int(hex(order_sum)[-2:], 16)
data_sum = int(hex(40 + 35 + 2_H + 2_L)[-2:], 16)
_s =
[62,166,s2,8,order_sum,0,40,35,0,2_L,2_H,0,0,data_sum]#发送位置90度,速度2dps,顺时针
#input_s = bytes(input_s)
#(input_s)
_send()
#俯仰电机逆时针转动
def pitch_send2(self):
_leSheet("background-color:rgb(0,200,0);border:2px groove
gray;border-radius:10px;padding:2px 4px;font:12pt;")
order_sum = 62 + 166 + 8 + s2
order_sum = int(hex(order_sum)[-2:], 16)
data_sum = int(hex(1+120 + 105 + 2_H + 2_L)[-2:], 16)
_s =
[62,166,s2,8,order_sum,1,120,105,0,2_L,2_H,0,0,data_sum]#发送位置270度,速度2dps,逆时针
#input_s = bytes(input_s)
#(input_s)
_send()
#程序启动关联
if __name__ == "__main__":
app=QApplication() # 创建一个QApplication,也就是你要开发的软件app
#MainWindow = indow() # 创建一个QMainWindow,用来装载你需要的各种组件、控件
ui = MainControl() # ui是Ui_SFmotorcontrol类的实例化对象
#i(MainWindow) # 执行类中的setupUi方法,方法的参数是第二步中创建的QMainWindow
() # 执行QMainWindow的show()方法,显示这个QMainWindow
(_()) # 使用exit()或者点击关闭按钮退出QApplication
四、设计QT界面
在PyCharm软件中启动外部工具PYQT5,Tools—>External Tools—>QT5,如下图所示。
启动PYQT5的设计界面,进入创建对话框,如下图,创建一个新的Main
Window。
修改Main Window的对象名称ObjectName为“SFmotorcontrol”(也可以自己定义),但要记住,在关联py文件里面要用到。QT设计界面左侧有许多控件,将需要的控件直接往建好的空白Main Window里拖,简单排布一下,统一修改好记的对象名称,具体设计方法自行参阅QT使用方法,设计好的界面如下图所示。
编辑好后保存成ui文件(),并添加到PyCharm项目中,使用PyCharm的外部工具PYUIC将做好的ui文件转换成py文件(就是前文所说的界面关联py文件),得到,完整的代码如下。
QT界面关联py文件完整代码:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file ''
#
# Created by: PyQt5 UI code generator 5.11.3
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_SFmotorcontrol(object):
def setupUi(self, SFmotorcontrol):
ectName("SFmotorcontrol")
(640, 580)
imumSize((640, 580))
imumSize((640, 580))
font = ()
ily("宋体")
ntSize(12)
t(font)
icon = ()
map(p(""), , )
dowIcon(icon)
lwidget = t(SFmotorcontrol)
ectName("centralwidget")
onnect = Box(lwidget)
metry((10, 410, 621, 131))
t(False)
ckable(False)
ectName("SetupConnect")
t = utton(onnect)
metry((500, 50, 111, 41))
ectName("Connect")
select_box = Box(onnect)
select_metry((70, 60, 69, 22))
select_table(True)
select_rentText("")
select_ectName("Serialselect_box")
lect_box = Box(onnect)
lect_metry((350, 60, 71, 22))
lect_table(True)
lect_ectName("BaudSelect_box")
= (onnect)
metry((20, 60, 54, 21))
ectName("label")
_3 = (onnect)
_metry((280, 55, 71, 31))
_ectName("label_3")
_check_button = utton(onnect)
_check_metry((150, 50, 81, 41))
_check_ectName("port_check_button")
_send = (onnect)
_metry((20, 100, 571, 21))
_t("")
_ectName("label_send")
_engle1 = (lwidget)
_bled(False)
_metry((50, 36, 181, 181))
_sor(r(ngHandCursor))
_leSheet("border-color: rgb(0, 0, 255);n"
"gridline-color: rgb(0, 0, 255);n"
"color: rgb(0, 170, 0);")
_imum(36000)
_eStep(100)
_perty("value", 18000)
_derPosition(18000)
_cking(True)
_pping(True)
_chTarget(10.0)
_chesVisible(True)
_ectName("dial_engle1")
_engle2 = (lwidget)
_bled(False)
_metry((380, 36, 181, 181))
_sor(r(ngHandCursor))
_imum(0)
_imum(36000)
_gleStep(1)
_eStep(100)
_perty("value", 9000)
_derPosition(9000)
_cking(True)
_entation(al)
_ertedAppearance(False)
_ertedControls(True)
_pping(True)
_chTarget(10.0)
_chesVisible(True)
_ectName("dial_engle2")
_4 = (lwidget)
_metry((40, 320, 91, 31))
font = ()
ntSize(16)
d(True)
ght(75)
_t(font)
_ectName("label_4")
_5 = (lwidget)
_metry((480, 320, 91, 31))
font = ()
ntSize(16)
d(True)
ght(75)
_t(font)
_ectName("label_5")
_9 = (lwidget)
_metry((129, 225, 54, 12))
_ectName("label_9")
_10 = (lwidget)
_metry((463, 15, 54, 12))
_ectName("label_10")
_11 = (lwidget)
_metry((235, 120, 54, 12))
_ectName("label_11")
_12 = (lwidget)
_metry((360, 120, 54, 12))
_tFormat(xt)
_ectName("label_12")
_13 = (lwidget)
_metry((136, 14, 54, 12))
_ectName("label_13")
_14 = (lwidget)
_metry((454, 222, 54, 12))
_ectName("label_14")
_15 = (lwidget)
_metry((20, 120, 54, 12))
_ectName("label_15")
_label = (lwidget)
_metry((20, 511, 131, 21))
_t("")
_ectName("statu_label")
1_label = (lwidget)
1_metry((105, 110, 71, 31))
font = ()
ily("Times New Roman")
ntSize(16)
d(True)
ght(75)
1_t(font)
1_gnment(enter)
1_ectName("engle1_label")
_Button1 = utton(lwidget)
_metry((280, 270, 50, 50))
font = ()
ntSize(12)
_t(font)
_ectName("auto_Button1")
2_label = (lwidget)
2_metry((436, 113, 71, 31))
font = ()
ily("Times New Roman")
ntSize(16)
d(True)
ght(75)
2_t(font)
2_gnment(enter)
2_ectName("engle2_label")
= t(lwidget)
metry((474, 30, 120, 191))
lTip("")
tusTip("")
oFillBackground(True)
ectName("widget")
_Button = utton(lwidget)
_metry((280, 210, 50, 50))
_ectName("up_Button")
_Button = utton(lwidget)
_metry((340, 270, 50, 50))
_ectName("right_Button")
_Button = utton(lwidget)
_metry((220, 270, 50, 50))
_ectName("left_Button")
_Button = utton(lwidget)
_metry((280, 330, 50, 50))
_ectName("down_Button")
_()
__()
__()
__()
__()
__()
__()
__()
__()
__()
__()
__()
1__()
__()
_()
__()
2__()
__()
__()
__()
__()
tralWidget(lwidget)
r = ar(SFmotorcontrol)
metry((0, 0, 640, 23))
ectName("menubar")
uBar(r)
bar = sBar(SFmotorcontrol)
ectName("statusbar")
tusBar(bar)
slateUi(SFmotorcontrol)
tSlotsByName(SFmotorcontrol)
def retranslateUi(self, SFmotorcontrol):
_translate = ate
dowTitle(_translate("SFmotorcontrol", "云台控制软件"))
le(_translate("SFmotorcontrol", "连接设置"))
t(_translate("SFmotorcontrol", "连 接"))
lect_rentText(_translate("SFmotorcontrol", "115200"))
t(_translate("SFmotorcontrol", "串口:"))
_t(_translate("SFmotorcontrol", "波特率:"))
_check_t(_translate("SFmotorcontrol", "串口检测"))
_lTip(_translate("SFmotorcontrol",
"
使用鼠标拖动
"))
_t(_translate("SFmotorcontrol", "水平旋转"))
_t(_translate("SFmotorcontrol", "垂直俯仰"))
_t(_translate("SFmotorcontrol", "180"))
_t(_translate("SFmotorcontrol", "90"))
_t(_translate("SFmotorcontrol", "90"))
_t(_translate("SFmotorcontrol", "0"))
_t(_translate("SFmotorcontrol", "0"))
_t(_translate("SFmotorcontrol", "-90"))
_t(_translate("SFmotorcontrol", "270"))
1_t(_translate("SFmotorcontrol", "0.0"))
_t(_translate("SFmotorcontrol", "自动"))
2_t(_translate("SFmotorcontrol", "0.0"))
_t(_translate("SFmotorcontrol", "上"))
_t(_translate("SFmotorcontrol", "右"))
_t(_translate("SFmotorcontrol", "左"))
_t(_translate("SFmotorcontrol", "下"))
在PyCharm中运行主程序,点击“RUN”—>RUN‘PTMainPRO’,无错则显示设计好的界面,如下图所示,按钮在程序中做了些美化,形状和颜色和前文所述的有所不同。
五、应用程序打包
程序编好后,最后一步当然是打包成可执行的exe文件啦,如此才能摆脱程序对IDE环境的依赖,能够在其他电脑上运行。在这里,我们使用pyinstaller来进行程序打包,当然还有其他方法来完成这件事情,感兴趣的可以自行尝试一哈。
首先,需要安装pyinstaller,懒人可在cmd中pip安装,输入“pip install
pyinstaller”回车,然后等待即可。
在执行pyinstaller之前,需要确认所有关于应用程序所需的资源(包含图片以及依赖的.py文件都放进dist下的项目目录中),否则会打包不成功。
为确保打包成功,项目路径中最好不要出现中文(大虾指导的,具体为什么,额,不清楚,包括前文所述的环境搭建时也有这个坑,爬过的才知道)。
管理员运行cmd,cd到项目路径下,执行“pyinstaller ”回车,执行结果如下图。
如此表明打包成功,来到我们的项目目录下,会发现在dist文件夹下多了一个文件夹PTMainPRO,这就是我们的应用程序所有需要的文件,当然包括可执行文件,如下图所示。
当然,也可以打包成只有一个exe文件,有兴趣的朋友可以深入研究下
pyinstaller的执行参数。
至此,一个简单的基于Python+QT的PC应用程序就完成了,感谢大家的耐心阅读,若有不当之处,请批评指正。
评论列表(0条)