• 机器人制作开源方案 | 智能快递付件机器人


    一、作品简介

    作者:贺沅、聂开发、王兴文、石宇航、盛余庆
    单位:黑龙江科技大学
    指导老师:邵文冕、苑鹏涛

    1. 项目背景

    受新冠疫情的影响,大学校园内都采取封闭式管理来降低传染的风险,导致学生不能外出,学校工作人员不能入校。通过封闭式的管理以此来尽最大可能保证学生在当前新冠传染和大规模人群被感染的情况下的安全,在此种情况下出现了取件困难、取件效率低、快递堆积在驿站等诸多快递服务问题,严重时也导致了快递无法进校。同时也严重提升了在校学生们的感染风险,严重影响了同学们的日常生活需要。

    疫情下快递堆积

           为了解决在校快递取件的问题,我们设计了一种由行走机构、抓取机构、控制系统和视觉系统组成的校园智能无人快递小车,以实现”无接触“式、消毒式快递配送,解决快递取件效率低的问题,减小了人力和物力,使得快递处理简单高效快捷,在快递的最后一站充分降低学生拿快递时新冠病毒感染风险,同时避免了校外人员入校传播病毒的风险。

    2. 项目进展

    2.1技术路线

    技术流程图

           如上图所示,我们所设计的快递附件机器人由机器人本体与被检测物(货物)组成。其总体流程如下:机器人通过一部分检测模块识别货物所在位置;将该信息反馈于快递附件机器人的控制板模块,控制板模块则命令驱动模块驱动,行走模块按照指定路线进行运动,等待抓取模块完成操作,抓取操作完成后控制模块驱动小车的行走模块进行下一步运动。

    2.2设计路线

    2.3项目创新点

    2.3.1结构部分
           采用了连杆机构,其运动副一般均为低副。低副两运动副元素为面接触,压强较小,可承受较大的载荷;可以方便地用来达到增力、扩大行程和实现远距离传动的优势,可方便机械臂抓取高层快递,我们采用中型球型件代替普通连杆使传动更稳定,且具有各部件之间不易松动的特点;采用齿轮传动结构,通过6个齿轮进行传动能保证稳定传动的同时具有准确的传动比,可以实现平行轴、相交轴、交错轴等空间任意两轴间传动的优点。

    中型球型件

    齿轮传动

    2.3.2运动部分
           运动部分通过四个直流马达支架将四个直流马达固定并配合四个轮胎组成运动机构,采用差速法控制小车转向,采用循迹进行路线规划,使用pwm进行调速,具有速度快的特点,且采用提取取件码第一位数字的方式,判断快递架位置和小车取完快递从后门出发,具有高效、快捷的优点,减少了空间的占用和取件的时间。

    2.3.3视觉部分
           采用了图像畸变矫正处理、轮廓提取算法和神经网络模型训练,解决了图像显示不清晰,识别率不够高问题的同时,实现了在不同光照条件下快递编号的识别,且有较高的准确率。

    3. 项目总结

           为了解决受新冠疫情影响、学校封闭式管理、学生不能外出取件、快递取件难、快递在快递站堆积的问题,我们设计了一种由行走机构、抓取机构、控制系统和视觉系统组成的校园智能无人快递小车,以实现”无接触“式、消毒式快递配送,这样避免了校外人员入校传播病毒的潜在风险,由智能快递付件机器人帮忙取校外快递,仅需对小车和快递进行消毒处理,简化了消毒流程,减少了人力、物力的开销,方便快捷了学生生活,减少了快递长时间不取退回的现象。

    二、功能介绍

    1. 产品结构图

           智能快递付件机器人由行走机构、控制模块、抓取机构和视觉模块组成,整个系统由两个12v锂电池分别对控制模块和视觉模块进行供电,以延长机器人的使用时间间隔。控制模块以Basra为主控,实现对机器人的行走、控制、抓取、视觉等过程的控制。机器人搭载了无线蓝牙和语音识别模块,在实现了蓝牙远程操控的同时也能够完成操作参数的动态调整等操作;行走机构采用探索者套件中的轮胎与联轴器相互配合,由直流减速电机驱动,在电机转动下控制小车行走。通过循迹进行路线规划;抓取机构采用连杆机构控制机械爪,对快递进行抓取;视觉模块采用Edge impulse对数字模型进行神经网络训练来实现快递编号的识别,并与下位机实现通信。

    整体结构图

    2. 主要功能

           ① 可自主抵达相应的快递架
           ② 可对所需要取的快递进行识别
           ③ 可实现远程操作与抓取参数调整
           ④ 可实现识别与抓取时的状况监控

    3. 结构介绍

           本作品总体结构由探索者套件拼装,分为主板平面、运动机构、机械臂、抓取结构、载物台、电源仓。

    3.1主板平面

           使用四个7*11孔平板和两块5*7孔平板构成的搭载主体平台,作为承载机械臂和载物台、连接运动机构主体,同时放置开发板和传感器等其他元器件。

    3.2运动机构

           通过四个直流马达支架将四个直流马达固定并配合四个轮胎组成运动机构,采用差速法控制小车转向。

    3.3机械臂

           使用4个输出支架和两个双足连杆搭建机械臂在主板平面上的支座,使用四个大舵机支架连接大舵机,机械臂的底盘舵机装上大舵机输出头后与大臂的舵机支架连接,再将两个大舵机U型支架反向连接作为机械臂大臂,一端连接大臂舵机一端连接小臂舵机。

    机械臂小臂

    机械臂大臂

           ① 机械臂小臂:由与抓取机构连接的舵机和舵机架构成,另一端连接大舵机U型支架,可实现正转70°,反转55°,可小范围调整抓取机构抓取角度。
           ② 机械臂大臂:由两个大舵机U型支架反向连接形成,正转110°反转70°,调整抓取结构置前置后,置前时抓取,置后时放置。
           ③ 机械臂底盘:由支座和舵机支架构成,可使机械手左右转动,调整抓取机构在水平方向上的位置。

    3.4抓取结构

           抓取结构的运动采用了齿轮传动结构和连杆结构,使用六个30齿齿轮两两叠加构成三个双层齿轮、使用5×7 孔平板作为机械爪零件主板,四个机械手指和四个机械手40mm驱动、两个3×5 折弯、中型球型件构成,滑动零件连接处使用轴套连接,以便抓取机构活动顺畅,且不易松动。抓机构自由度在0-55,如下图所示:

    机械爪

           ① 连杆结构:由中型球型件和大舵机输出头组成,将舵机产生的扭力,通过连杆传到齿轮上使齿轮转动,并且由于使用的连杆是弧形,中间不会因为触碰到零件主板而导致活动不顺畅。
           ② 传动结构:传动结构通过三组齿轮啮合将扭力均匀施加到两侧与齿轮连接的机械手40mm驱动上,带动机械手指,使机械手实现张合功能。

    3.5载物台

           使用一块7×11 孔平板、四块3×5 折弯、和两个大轮组成的载物平台,每个圆板为一个载物平台,每次可装载两件物品,如下图所示,载物台下方留有一定的空腔,作为电池仓,用于放置电源,在一定程度上节约了空间且载物台抬高可减少机械臂运行路程,使机械臂方便、快速放置快递,提高了运行效率。

    载物台与电池仓

    4. 电控部分

    4.1控制板的选择

           在开发板上我们选择Basra,Basra是一款基于Arduino开源方案设计的一款开发板,Basra的处理器核心是ATmega328,同时具有14路数字输入/输出口(其中6路可作为PWM输出),6路模拟输入,一个16MHz晶体振荡器,一个USB口,一个电源插座,一个ICSPheader和一个复位按钮。主CPU采用AVRATMEGA328型控制芯片,支持C语言编程方式。该系统的硬件电路包括:电源电路、串口通信电路、MCU基本电路、烧写接口、显示模块、AD/DA转换模块、输入模块、IIC存储模块等其他电路模块电路。控制板尺寸不超过60*60mm,便于安装。CPU硬件软件全部开放,除能完成对小车控制外,还能使用本实验板完成单片机所有基础实验。供电范围宽泛,支持5v~9v的电压,干电池或锂电池都适用。控制板含3A6V的稳压芯片,可为舵机提供6v额定电压。

    开发板

    4.2传感器的选择

           灰度传感器又称黑标传感器,可以帮助进行黑线的跟踪,可以识别白色背景中的黑色区域,或悬崖边缘。寻线信号可以提供稳定的输出信号,使寻线更准确更稳定。有效距离在0.7cm~3cm之间。
           工作电压:4.7~5.5V,
           工作电流:1.2mA。
           ① 固定孔,便于用螺丝将模块固定于机器人上;
           ② 四芯输入线接口,连接四芯输入线;
           ③ 黑标/白标传感器元件,用于检测黑线/白线信号。

    灰度传感器

    4.3语音模块

           语音处理技术是下一代多模式交互的人机界面设计中的核心技术之一。随着消费类电子产品中对于高性能、高稳健性的语音接口需求的快速增加,嵌入式语音处理技术快速发展。
           根据市场对嵌入式语音识别系统的需求,探索者推出了新的语音识别模块。该模块采用了基于helios-adsp新一代中大词汇语音识别协处理方案的语音识别专用芯片HBR740,非特定人语音识别技术可对用户的语音进行识别,支持中文音素识别,可任意指定中文识别词条(小于8个汉字),单次识别可支持1000条以上的语音命令,安静环境下,标准普通话,识别率大于95%,可自动检测环境噪声,噪声环境下也能保证较好的识别率。

    4.4电动机的选择

           我们经过讨论确定选用轮式驱动,但是考虑到只是为了完成自主行走功能,实验也无需越障爬坡,所以我们选择双轴直流电机作为与轮子配合的驱动电机。

    电机实物图

           除了驱动机器人需要引用电机,检测功能也会需要电机。由于舵机的可控性强,可以在工作范围内精确控制电机的转动角度,而快递机器人的主要工作就是“识别快递、精确定位、作出处理”,所以舵机能够为智能快递付件机器人的工作提供极大的便利。四个舵机使得机器人有了四个自由度,工作范围由线性转变为面性,大大提高了机器人的工作效率。

    5. 视觉部分

    5.1 训练神经网络模型

    通过对大量的图像收集,在Edge Impulse进行图像分类、统一贴标签和训练对应的数据集,以此完成在识别过程中的识别不稳定以及减少错误信息的传递,多次调整图片训练数据集来提高匹配准确率。

    数据采集图样

    上传数据集

    训练模型效果显示

    训练准确度显示

    5.2 灰度转化(轮廓提取)以及畸变图像处理   

    ① 灰度转化
           通过灰度变换来使图像对比度扩展,图像清晰,特征明显,有选择的突出图像感兴趣的特征或者去抑制图像中不需要的特征,进而更加有效的改变图像的直方图分布,使得像素的分布更加均匀,从而提高图像识别精度。

    处理图像部分程序

    灰度数字处理图

           以12数字为例,1代表通道第一层,2代表第二个(从左到右)。先进行整体分开显示,再进行判断快递所在的位置,来传回下位机具体的信息返回值。为了提升识别的准确值,在与训练模型匹配时,再去使用轮廓提取的方法,提取出数字的形状。
    ② 轮廓提取算法
           使用闭运算的方法,即梯度=膨胀-腐蚀,得到图像的轮廓外形,通过使用findcontour ()函数,对灰度图处理过后的图像,找取边界点的坐标,存储到contours参数中,运用drawcontours绘制轮廓线。
    下面是findcontour函数的六个参数值:

    轮廓点信息特征

    ③ 畸变矫正处理
           在测试识别时出现了识别精度低,图像信息获取不全,识别效率低等问题,为此我们采用图像畸变矫正处理,以提高识别精度和效率。
           畸变矫正处理是像差的一种,在人们的感官上看原本直线变成了曲线,但是图像的信息不会丢失,调用openmv官方库中的库函数进行图像的处理。对镜头进行了畸变矫正,以去除镜头滤波造成的图像鱼眼效果。

    矫正效果演示前后

    5.3 取件抓取视觉流程图

    三、程序代码

    1. 示例程序

    1.1上位机程序
    ① data_collection.py

    1. import sensor, lcd
    2. from Maix import GPIO
    3. from fpioa_manager import fm
    4. from board import board_info
    5. import os, sys
    6. import time
    7. import image
    8. import KPU as kpu
    9. sensor.reset()
    10. sensor.set_pixformat(sensor.RGB565)
    11. sensor.set_framesize(sensor.QVGA)
    12. set_windowing = (224,224)
    13. sensor.set_windowing(set_windowing)
    14. sensor.set_hmirror(0)
    15. sensor.run(1)
    16. #####Other####
    17. deinit_flag = False #用于在拍照的时候将yolo模型卸载,等到循环重新开始时再加载,减少内存消耗
    18. ##############
    19. #### lcd config ####
    20. lcd.init(type=1, freq=15000000)
    21. lcd.rotation(2)
    22. #### boot key ####
    23. boot_pin = 16
    24. fm.register(boot_pin, fm.fpioa.GPIOHS0)
    25. key = GPIO(GPIO.GPIOHS0, GPIO.PULL_UP)
    26. ##############################
    27. ######KPU#######
    28. task = kpu.load("/sd/number.kmodel")
    29. f=open("num_anchors.txt","r") #修改锚点处
    30. anchor_txt=f.read()
    31. L=[]
    32. for i in anchor_txt.split(","): #直接读出来的i是str类型
    33. L.append(float(i))
    34. anchor=tuple(L)
    35. f.close()
    36. a = kpu.init_yolo2(task, 0.6, 0.3, 5, anchor)
    37. f=open("num_labels.txt","r") #修改锚点处
    38. labels_txt=f.read()
    39. labels = labels_txt.split(",")
    40. f.close()
    41. ##################
    42. #### main ####
    43. def capture_main(key):
    44. global deinit_flag,anchor,task
    45. def draw_string(img, x, y, text, color, scale, bg=None , full_w = False):
    46. if bg:
    47. if full_w:
    48. full_w = img.width()
    49. else:
    50. full_w = len(text)*8*scale+4
    51. img.draw_rectangle(x-2,y-2, full_w, 16*scale, fill=True, color=bg)
    52. img = img.draw_string(x, y, text, color=color,scale=scale)
    53. return img
    54. def del_all_images():
    55. os.chdir("/sd")
    56. images_dir = "cap_images"
    57. if images_dir in os.listdir():
    58. os.chdir(images_dir)
    59. types = os.listdir()
    60. for t in types:
    61. os.chdir(t)
    62. files = os.listdir()
    63. for f in files:
    64. os.remove(f)
    65. os.chdir("..")
    66. os.rmdir(t)
    67. os.chdir("..")
    68. os.rmdir(images_dir)
    69. # del_all_images()
    70. os.chdir("/sd")
    71. dirs = os.listdir()
    72. images_dir = "cap_images" #cap_images_1
    73. last_dir = 0
    74. for d in dirs: #把每个已经存在的以cap_images开头的目录遍历一遍得到本次拍照的总目录序号
    75. if d.startswith(images_dir):
    76. if len(d) > 11:
    77. n = int(d[11:])
    78. if n > last_dir:
    79. last_dir = n
    80. '''
    81. 这一段的作用是每次上电都重新创建一个新的最大文件夹
    82. '''
    83. #images_dir = "{}_{}".format(images_dir, last_dir+1)
    84. #print("save to ", images_dir)
    85. #if images_dir in os.listdir():
    86. ##print("please del cap_images dir")
    87. #img = image.Image()
    88. #img = draw_string(img, 2, 200, "please del cap_images dir", color=lcd.WHITE,scale=1, bg=lcd.RED)
    89. #lcd.display(img)
    90. #sys.exit(1)
    91. #os.mkdir(images_dir)
    92. '''
    93. 这一段的作用是每次上电只有手动才重新创建一个新的最大文件夹,默认是从已经创建的编号最大的文件夹开始
    94. '''
    95. images_dir = "{}_{}".format(images_dir, last_dir)
    96. if not images_dir in os.listdir():
    97. os.mkdir(images_dir)
    98. '''
    99. 开机检测第二级目录的起始位置
    100. '''
    101. os.chdir("/sd/{}".format(images_dir))
    102. dirs = os.listdir()
    103. last_type = 0
    104. for d in dirs: #把每个已经存在的以cap_images开头的目录遍历一遍得到本次拍照的总目录序号
    105. n = int(d[0:])
    106. if n > last_type:
    107. last_type = n
    108. if not str(last_type) in os.listdir(): #不存在要重新创建
    109. os.chdir("/sd")
    110. os.mkdir("{}/{}".format(images_dir, str(last_type)))
    111. '''
    112. 开机检测第三级目录的起始位置
    113. '''
    114. os.chdir("/sd/{}/{}".format(images_dir,last_type))
    115. dirs = os.listdir()
    116. last_image = -1
    117. for d in dirs: #把每个已经存在的以cap_images开头的目录遍历一遍得到本次拍照的总目录序号
    118. n = int(d[len(str(last_type))+1:][:-4]) #去除.jpg
    119. if n > last_image:
    120. last_image = n
    121. os.chdir("/sd")
    122. last_cap_time = 0 #用于记录按键松手前按下时候的系统时间
    123. last_btn_status = 1 #用于松手检测
    124. save_dir = last_type #change type 第二级目录,默认跟上次开机目录一样
    125. save_count = last_image + 1 #保存的第几张图片
    126. while(True):
    127. if deinit_flag:
    128. task = kpu.load("/sd/number.kmodel")
    129. a = kpu.init_yolo2(task, 0.6, 0.3, 5, anchor)
    130. deinit_flag = False
    131. img0 = sensor.snapshot()
    132. code = kpu.run_yolo2(task, img0)
    133. if code:
    134. for i in code:
    135. a=img0.draw_rectangle(i.rect(),(0,255,0),2)
    136. lcd.draw_string(i.x()+45, i.y()-5, labels[i.classid()]+" "+'%.2f'%i.value(), lcd.WHITE,lcd.GREEN)
    137. b=str(labels[i.classid()])
    138. b.replace(" ","")
    139. if set_windowing:
    140. img = image.Image()
    141. img = img.draw_image(img0, (img.width() - set_windowing[0])//2, img.height() - set_windowing[1]) #//2取整
    142. else:
    143. img = img0.copy()
    144. if key.value() == 0:
    145. time.sleep_ms(30)
    146. if key.value() == 0 and (last_btn_status == 1) and (time.ticks_ms() - last_cap_time > 500):
    147. last_btn_status = 0
    148. last_cap_time = time.ticks_ms()
    149. else:
    150. #2秒内直接拍照,四秒内提示切换数字种类,6秒内提示切换总目录,8秒后切换总目录
    151. if time.ticks_ms() - last_cap_time > 4000 and time.ticks_ms() - last_cap_time <6000:
    152. img = draw_string(img, 2, 200, "release to change type", color=lcd.WHITE,scale=1, bg=lcd.RED)
    153. elif time.ticks_ms() - last_cap_time > 8000:
    154. img = draw_string(img, 2, 200, "release to change images directory", color=lcd.WHITE,scale=1, bg=lcd.RED)
    155. elif time.ticks_ms() - last_cap_time <= 8000 and time.ticks_ms() - last_cap_time >6000:
    156. img = draw_string(img, 2, 200, "release to change type", color=lcd.WHITE,scale=1, bg=lcd.RED)
    157. img = draw_string(img, 2, 160, "keep push to images directory", color=lcd.WHITE,scale=1, bg=lcd.RED)
    158. elif time.ticks_ms() - last_cap_time <= 4000:
    159. img = draw_string(img, 2, 200, "release to change type", color=lcd.WHITE,scale=1, bg=lcd.RED)
    160. if time.ticks_ms() - last_cap_time > 2000:
    161. img = draw_string(img, 2, 160, "keep push to change type", color=lcd.WHITE,scale=1, bg=lcd.RED)
    162. else:
    163. time.sleep_ms(30)
    164. if key.value() == 1 and (last_btn_status == 0):
    165. a = kpu.deinit(task)
    166. del task
    167. deinit_flag = True
    168. if time.ticks_ms() - last_cap_time >= 4000 and time.ticks_ms() - last_cap_time < 8000:
    169. img = draw_string(img, 2, 200, "change object type", color=lcd.WHITE,scale=1, bg=lcd.RED)
    170. lcd.display(img)
    171. time.sleep_ms(1000)
    172. save_dir += 1
    173. save_count = 0
    174. dir_name = "{}/{}".format(images_dir, save_dir)
    175. os.mkdir(dir_name)
    176. elif time.ticks_ms() - last_cap_time >= 8000:
    177. img = draw_string(img, 2, 200, "change images directory", color=lcd.WHITE,scale=1, bg=lcd.RED)
    178. lcd.display(img)
    179. time.sleep_ms(1000)
    180. last_dir += 1
    181. save_dir = 0
    182. save_count = 0
    183. images_dir = "{}_{}".format('cap_images', last_dir)
    184. os.mkdir(images_dir)
    185. print("save to ", images_dir)
    186. dir_name = "{}/{}".format(images_dir, save_dir)
    187. os.mkdir(dir_name)
    188. else:
    189. draw_string(img, 2, 200, "capture image {}".format(save_count), color=lcd.WHITE,scale=1, bg=lcd.RED)
    190. lcd.display(img)
    191. f_name = "{}/{}/{}.jpg".format(images_dir, save_dir, str(save_dir)+'_'+str(save_count))
    192. img0.save(f_name, quality=95) #报错ENOENT表示路径不存在
    193. save_count += 1
    194. last_btn_status = 1
    195. img = draw_string(img, 2, 0, "will save to {}/{}/{}.jpg".format(images_dir, save_dir, str(save_dir)+'_'+str(save_count)), color=lcd.WHITE,scale=1, bg=lcd.RED, full_w=True)
    196. lcd.display(img)
    197. del img
    198. del img0
    199. def main():
    200. try:
    201. capture_main(key)
    202. except Exception as e:
    203. print("error:", e)
    204. import uio
    205. s = uio.StringIO()
    206. sys.print_exception(e, s)
    207. s = s.getvalue()
    208. img = image.Image()
    209. img.draw_string(0, 0, s)
    210. lcd.display(img)
    211. main()

    ② shijue.py

    1. import sensor
    2. import image
    3. import lcd
    4. import KPU as kpu
    5. lcd.init()
    6. sensor.reset()
    7. sensor.set_pixformat(sensor.RGB565)
    8. sensor.set_framesize(sensor.QVGA)
    9. sensor.set_windowing((224, 224))
    10. sensor.set_hmirror(0)
    11. sensor.run(1)
    12. task = kpu.load(0x300000)
    13. anchor=[] #放你的标签
    14. labels = [] #放anchor
    15. a = kpu.init_yolo2(task, 0.6, 0.3, 5, anchor)
    16. while(True):
    17. img = sensor.snapshot()
    18. code = kpu.run_yolo2(task, img)
    19. if code:
    20. for i in code:
    21. a=img.draw_rectangle(i.rect(),(0,255,0),2)
    22. a = lcd.display(img)
    23. for i in code:
    24. lcd.draw_string(i.x()+45, i.y()-5, labels[i.classid()]+" "+'%.2f'%i.value(), lcd.WHITE,lcd.GREEN)
    25. else:
    26. a = lcd.display(img)
    27. a = kpu.deinit(task)

    1.2下位机程序
    ① jixiebi.ino

    1. #include<Servo.h>//使用servo库
    2. Servo base,fArm,rArm,claw;//创建4个servo对象
    3. //base(底座舵机11)fArm(第三关节3)rArm(第二关节12)claw(爪4)
    4. #include <SoftwareSerial.h>
    5. //实例化软串口,设置虚拟输入输出串口。
    6. SoftwareSerial BT(2, 3); // 设置数字引脚2是arduino的RX端,3是TX端
    7. VoiceRecognition Voice;//声明一个语音识别对象
    8. #define Led 8 //定义LED控制引脚
    9. #define pi 3.1415926
    10. void dateProcessing();
    11. void armDataCmd(char serialCmd);//实现机械臂在openmv指示下工作
    12. void armLanYaCmd(char serialCmd);
    13. void servoCmd(char serialCmd,int toPos);//电机旋转功能函数
    14. void vel_segmentation(int fromPos,int toPos,Servo arm_servo);
    15. void reportStatus();//电机状态信息控制函数
    16. void Arminit();
    17. void GrabSth();
    18. //建立4个int型变量存储当前电机角度值,设定初始值
    19. int basePos=70;
    20. int rArmPos=90;
    21. int fArmPos=30;
    22. int clawPos=45;
    23. int data2dArray[4][5] = { //建立二维数组用以控制四台舵机
    24. {0, 45, 90, 135, 180},
    25. {45, 90, 135, 90, 45},
    26. {135, 90, 45, 90, 135},
    27. {180, 135, 90, 45, 0}
    28. };
    29. //存储电机极限值
    30. const int PROGMEM baseMin=0;
    31. const int PROGMEM baseMax=180;
    32. const int PROGMEM rArmMin=0;//留一定裕度空间
    33. const int PROGMEM rArmMax=180;//留一定裕度空间
    34. const int PROGMEM fArmMin=0;
    35. const int PROGMEM fArmMax=270;
    36. const int PROGMEM clawMin=0;
    37. const int PROGMEM clawMax=54;
    38. const int PROGMEM Dtime = 15;//机械臂运动延迟时间,通过改变该值来控制机械臂运动速度
    39. //机械臂运动角速度为:π*1000/(180*Dtime) rad/s
    40. bool mode = 0;//mode = 1:指令模式;mode = 0:蓝牙模式
    41. const int PROGMEM moveStep = 5;//按下按键,舵机的位移量
    42. void serialEvent()
    43. {
    44. while (Serial.available ()) {
    45. [char inChar = (char)Serial.read();
    46. shuju += inChar;
    47. if (inChar == (' n')
    48. {
    49. [stringComplete = true;
    50. }
    51. }
    52. }
    53. void yuyin();
    54. {
    55. switch(Voice.read()) //判断识别
    56. {
    57. case 0: //若是指令“kai deng”
    58. digitalWrite(Led,HIGH); //点亮LED
    59. break;
    60. case 1: //若是指令“guan deng”
    61. digitalWrite(Led,LOW);//熄灭LED
    62. break;
    63. default:
    64. break;
    65. }
    66. }
    67. void setup() {
    68. // put your setup code here, to run once:
    69. Serial.begin(9600); //设置arduino的串口波特率与蓝牙模块的默认值相同为9600
    70. BT.begin(9600); //设置虚拟输入输出串口波特率与蓝牙模块的默认值相同为9600
    71. Serial.println("HELLO"); //如果连接成功,在电脑串口显示HELLO,在蓝牙串口显示hello
    72. BT.println("hello");
    73. pinMode(Led,OUTPUT); //初始化LED引脚为输出模式
    74. digitalWrite(Led,LOW); //LED引脚低电平
    75. Voice.init(); //初始化VoiceRecognition模块
    76. Voice.addCommand("kai deng",0); //添加指令,参数(指令内容,指令标签(可重复))
    77. Voice.addCommand("qidongixiebi",0);
    78. Voice.addCommand("guan deng",1); //添加指令,参数(指令内容,指令标签(可重复))
    79. Voice.addCommand("tingzhi",1);
    80. Voice.start();//开始识别
    81. base.attach(12);
    82. delay(200);
    83. rArm.attach(9);
    84. delay(200);
    85. fArm.attach(5);
    86. delay(200);
    87. claw.attach(6);
    88. delay(200);
    89. // Serial.begin(9600);
    90. dateProcessing();
    91. base.write(90);
    92. delay(10);
    93. fArm.write(140);
    94. delay(10);
    95. rArm.write(90);
    96. delay(10);
    97. claw.write(30);
    98. delay(10);
    99. }
    100. void loop() {
    101. // put your main code here, to run repeatedly:
    102. if(Serial.available()>0){ //判断串口缓冲区是否有数值
    103. char serialCmd = Serial.read(); //将Arduino串口输入的字符赋给serialCmd
    104. Serial.println(serialCmd); //在串口监视器打印出输入的字符serialCmd
    105. BT.println(serialCmd); //蓝牙模块的串口(在手机屏幕上显示)打印出电脑输入的字符serialCmd
    106. }
    107. //蓝牙模块有数据输入,就显示在电脑上
    108. if(BT.available()>0){
    109. int ch = BT.read(); //读取蓝牙模块获得的数据
    110. Serial.println(ch);
    111. }
    112. if(Serial.available()>0){
    113. char serialCmd=Serial.read();//read函数为按字节读取,要注意!
    114. delay(10);
    115. if(mode == 1){
    116. armDataCmd(serialCmd);//openmv控制
    117. }
    118. else{
    119. armLanYaCmd(serialCmd);//蓝牙控制机械臂
    120. }
    121. }
    122. }
    123. void dateProcessing(){//数据处理
    124. }
    125. void armDataCmd(char serialCmd){//实现机械臂在openmv指令下工作
    126. if(serialCmd == 'b' || serialCmd == 'c' || serialCmd == 'f' || serialCmd == 'r'){
    127. Serial.print("serialCmd = ");
    128. Serial.print(serialCmd);
    129. int servoData = Serial.parseInt();//读取指令中的电机转角
    130. servoCmd(serialCmd,servoData);
    131. }
    132. else{
    133. {//x的位置
    134. int X location;
    135. int Y location;//Y的位置
    136. //Y的位置
    137. int B location;string X str;
    138. String Y str;
    139. x location = foundstr('X');
    140. Y location = foundstr('Y');
    141. x str=shujuduan(X location+1,Y location); //x到y的位置X
    142. location = foundstr('Y');
    143. B location = foundstr('B');
    144. Y str=shujuduan(Y location+1,B location); //Y到B的位置
    145. Centerx-x str.toInt();//转成可以用的整型
    146. CenterY=Y str.toInt();
    147. Serial.print("Centerx:");
    148. Serial.print(Centerx);
    149. Serial.print("Centery: ");
    150. Serial.printIn(Centery);
    151. for (j1 = 0; j1 < 180; j1++)
    152. j1 *= RAD2ANG;
    153. j3 = acos(pow(a, 2) + pow(b, 2) + pow(Ll, 2) - pow(L2, 2) - pow(L3, 2) - 2 * a*L1*sin(1) - 2 * b*L1*cos(j1)) / (2 * L2*L3);
    154. //if (abs(ANG2RAD(j3)) >= 135) [ j1 = ANG2RAD(j1); continue; }
    155. m = L2 * sin(j1) + L3 * sin(j1)*cos(j3) + L3 * cos(j1)*sin(j3);
    156. n = L2 * cos(j1) + L3 * cos (j1)*cos(j3) - L3 * sin(j1)*sin(j3);
    157. t = a - Ll *sin(jl);
    158. p = pow(pow(n,2) + pow(m,2),0.5);
    159. q = asin(m / p);
    160. j2 = asin(t / p) - q;
    161. x1 = (Ll * sin(j1) + L2 * sin(jl + j2) + L3 * sin(jl + j2 + j3))*cos(j0);
    162. y1 =(Ll *sin(jl) + L2 *sin(jl + j2) + L3 * sin(jl + j2 + j3))*sin(jo);
    163. zl = Ll * cos(j1) + L2 * cos(jl + j2) + L3 * cos(jl + j2 + j3);
    164. j1 = ANG2RAD(j1) ;
    165. j2 = ANG2RAD(j2);
    166. j3 = ANG2RAD(j3) ;
    167. if (xl<(x + 0.1) && x1 >(x - 0.1) && yly + 0.1) && yl >ly - 0.1) && zl(z + 0.1) && 21 >(2 - 0.1))
    168. if(j0>0&&j0<180&&j1>0&&j1<180&&j2>0&&j2<180&&j3>0&&j3<180)
    169. {Serial.println(ANG2RAD(j0));
    170. Serial.println( j1);
    171. Serial.println( j2);
    172. Serial.println( j3);
    173. Serial.println( x1);
    174. Serial.println( yl);
    175. Serial.println( z1);
    176. for (int i = 0; i <= 4; i++){
    177. base.write(data2dArray[0][i]); //base舵机对应data2dArray数组中第1“行”元素
    178. delay(100);
    179. rArm.write(data2dArray[1][i]); //rArm舵机对应data2dArray数组中第2“行”元素
    180. delay(100);
    181. fArm.write(data2dArray[2][i]); //fArm舵机对应data2dArray数组中第3“行”元素
    182. delay(100);
    183. claw.write(data2dArray[3][i]); //claw舵机对应data2dArray数组中第4“行”元素
    184. delay(500);
    185. }
    186. for (int i = 4; i >= 0; i--){
    187. base.write(data2dArray[0][i]); //base舵机对应data2dArray数组中第1“行”元素
    188. delay(100);
    189. rArm.write(data2dArray[1][i]); //rArm舵机对应data2dArray数组中第2“行”元素
    190. delay(100);
    191. fArm.write(data2dArray[2][i]); //fArm舵机对应data2dArray数组中第3“行”元素
    192. delay(100);
    193. claw.write(data2dArray[3][i]); //claw舵机对应data2dArray数组中第4“行”元素
    194. delay(500);
    195. }
    196. }
    197. }
    198. }
    199. void armLanYaCmd(char serialCmd){//实现机械臂在蓝牙模式下工作
    200. int baseJoyPos;
    201. int rArmJoyPos;
    202. int fArmJoyPos;
    203. int clawJoyPos;
    204. switch(serialCmd){
    205. case 'a'://小臂正转
    206. fArmJoyPos = fArm.read() - moveStep;
    207. servoCmd('f',fArmJoyPos);delay(10);
    208. break;
    209. case 's'://底盘左转
    210. baseJoyPos = base.read() + moveStep;
    211. servoCmd('b',baseJoyPos);delay(10);
    212. break;
    213. case 'd'://大臂正转
    214. rArmJoyPos = rArm.read() + moveStep;
    215. servoCmd('r',rArmJoyPos);delay(10);
    216. break;
    217. case 'w'://钳子闭合
    218. clawJoyPos = claw.read() - moveStep;
    219. servoCmd('c',clawJoyPos);delay(10);
    220. break;
    221. case '4'://小臂反转
    222. fArmJoyPos = fArm.read() + moveStep;
    223. servoCmd('f',fArmJoyPos);delay(10);
    224. break;
    225. case '5'://底盘右转
    226. baseJoyPos = base.read() - moveStep;
    227. servoCmd('b',baseJoyPos);delay(10);
    228. break;
    229. case '6'://大臂反转
    230. rArmJoyPos = rArm.read() - moveStep;
    231. servoCmd('r',rArmJoyPos);delay(10);
    232. break;
    233. case '8'://钳子张开
    234. clawJoyPos = claw.read() + moveStep;
    235. servoCmd('c',clawJoyPos);delay(10);
    236. break;
    237. case 'i': //输出电机状态信息
    238. reportStatus();delay(10);
    239. break;
    240. case 'l'://电机位置初始化
    241. Arminit();delay(10);
    242. break;
    243. case 'g'://抓取功能
    244. GrabSth();delay(10);
    245. break;
    246. case 'm':
    247. Serial.println("meArm has been changed into Instruction Mode");
    248. mode = 1;
    249. break;
    250. default:
    251. Serial.println();
    252. Serial.println("【Error】出现错误!");
    253. Serial.println();
    254. break;
    255. }
    256. }
    257. void servoCmd(char serialCmd,int toPos){//电机旋转功能函数
    258. Serial.println("");
    259. Serial.print("Command:Servo ");
    260. Serial.print(serialCmd);
    261. Serial.print(" to ");
    262. Serial.print(toPos);
    263. Serial.print(" at servoVelcity value ");
    264. Serial.print(1000*pi/(180*Dtime));
    265. Serial.println(" rad/s.");
    266. int fromPos;//起始位置
    267. switch(serialCmd){
    268. case 'b':
    269. if(toPos >= baseMin && toPos <= baseMax){
    270. fromPos = base.read();
    271. vel_segmentation(fromPos,toPos,base);
    272. basePos = toPos;
    273. Serial.print("Set base servo position value: ");
    274. Serial.println(toPos);
    275. Serial.println();
    276. break;
    277. }
    278. else{
    279. Serial.println("【Warning】Base Servo Position Value Out Of Limit!");
    280. Serial.println();
    281. return;
    282. }
    283. break;
    284. case 'r':
    285. if(toPos >= rArmMin && toPos <= rArmMax){
    286. fromPos = rArm.read();
    287. vel_segmentation(fromPos,toPos,rArm);
    288. rArmPos = toPos;
    289. Serial.print("Set rArm servo position value: ");
    290. Serial.println(toPos);
    291. Serial.println();
    292. break;
    293. }
    294. else{
    295. Serial.println("【Warning】Base Servo Value Position Out Of Limit!");
    296. Serial.println();
    297. return;
    298. }
    299. break;
    300. case 'f':
    301. if(toPos >= fArmMin && toPos <= fArmMax){
    302. fromPos = fArm.read();
    303. vel_segmentation(fromPos,toPos,fArm);
    304. fArmPos = toPos;
    305. Serial.print("Set fArm servo position value: ");
    306. Serial.println(toPos);
    307. Serial.println();
    308. break;
    309. }
    310. else{
    311. Serial.println();
    312. Serial.println("【Warning】Base Servo Value Position Out Of Limit!");
    313. Serial.println();
    314. return;
    315. }
    316. break;
    317. case 'c':
    318. if(toPos >= clawMin && toPos <= clawMax){
    319. fromPos = claw.read();
    320. vel_segmentation(fromPos,toPos,claw);
    321. clawPos = toPos;
    322. Serial.print("Set claw servo position value: ");
    323. Serial.println(toPos);
    324. Serial.println();
    325. break;
    326. }
    327. else{
    328. Serial.println("【Warning】Base Servo Position Value Out Of Limit!");
    329. Serial.println();
    330. return;
    331. }
    332. break;
    333. }
    334. }
    335. void vel_segmentation(int fromPos,int toPos,Servo arm_servo){//速度控制函数
    336. //该函数通过对位置的细分和延时实现电机速度控制
    337. //这样的控制平均角速度大约为:1°/15ms = 1.16 rad/s
    338. if(fromPos < toPos){
    339. for(int i=fromPos;i<=toPos;i++){
    340. arm_servo.write(i);
    341. delay(Dtime);
    342. }
    343. }
    344. else{
    345. for(int i=fromPos;i>=toPos;i--){
    346. arm_servo.write(i);
    347. delay(Dtime);
    348. }
    349. }
    350. }
    351. void reportStatus(){//电机状态信息控制函数
    352. Serial.println();
    353. Serial.println("---Robot-Arm Status Report---");
    354. Serial.print("Base Position: ");
    355. Serial.println(basePos);
    356. Serial.print("Claw Position: ");
    357. Serial.println(clawPos);
    358. Serial.print("rArm Position: ");
    359. Serial.println(rArmPos);
    360. Serial.print("fArm Position: ");
    361. Serial.println(fArmPos);
    362. Serial.println("-----------------------------");
    363. Serial.println("Motor Model:Micro Servo 9g-SG90");
    364. Serial.println("Motor size: 23×12.2×29mm");
    365. Serial.println("Work temperature:0-60℃");
    366. Serial.println("Rated voltage: 5V");
    367. Serial.println("Rated torque: 0.176 N·m");
    368. Serial.println("-----------------------------");
    369. }
    370. void Arminit(){//电机初始化函数
    371. //将电机恢复初始状态
    372. char ServoArr[4] = {'c','f','r','b'};
    373. for(int i=0;i<4;i++){
    374. servoCmd(ServoArr[i],90);
    375. }
    376. delay(200);
    377. Serial.println("meArm has been initized!");
    378. Serial.println();
    379. }
    380. void GrabSth(){//抓取函数
    381. //抓取功能
    382. int GrabSt[4][2] = {
    383. {'b',135},
    384. {'r',150},
    385. {'f',30},
    386. {'c',40}
    387. };
    388. for(int i=0;i<4;i++){
    389. servoCmd(GrabSt[i][0],GrabSt[i][1]);
    390. }
    391. }

    ② sketch_nov05a.ino

    1. char serial_data;// 将从串口读入的消息存储在该变量中
    2. #define STOP 0
    3. #define RUN 1
    4. #define BACK 2
    5. #define LEFT 3
    6. #define RIGHT 4
    7. VoiceRecognition Voice;//声明一个语音识别对象
    8. int a1 = 6;//左电机1
    9. int a2 = 7;//左电机2
    10. int b1 = 8;//右电机1
    11. int b2 = 9;//右电机2
    12. int sensorLeft = 3; //从车头方向的最右边开始排序 探测器
    13. int sensorRight = 2;
    14. int ENA = 10;//L298N使能端左
    15. int ENB = 11;//L298N使能端右
    16. int SL;//左边灰度传感器
    17. int SR;//右边灰度传感器
    18. void setup()
    19. {
    20. Serial.begin(9600);
    21. pinMode(a1, OUTPUT);
    22. pinMode(a2, OUTPUT);
    23. pinMode(b1, OUTPUT);
    24. pinMode(b2, OUTPUT);
    25. pinMode(ENA, OUTPUT);
    26. pinMode(ENB, OUTPUT);
    27. pinMode(sensorLeft, INPUT);//寻迹模块引脚初始化
    28. pinMode(sensorRight, INPUT);
    29. Voice.init();//初始化VoiceRecognition模块
    30. Voice.addCommand("lu kou yi",1);//添加指令,参数(指定内容,指令标签)
    31. Voice.addCommand("lu kou er",2);//添加指令,参数(指定内容,指令标签)
    32. Voice.addCommand("lu kou san",3);//添加指令,参数(指定内容,指令标签)
    33. Voice.addCommand("lu kou si",4);//添加指令,参数(指定内容,指令标签)
    34. Voice.start();
    35. }
    36. void loop()
    37. {
    38. digitalWrite(ENA,HIGH);
    39. digitalWrite(ENB,HIGH);
    40. SL = digitalRead(sensorLeft);
    41. SR = digitalRead(sensorRight);
    42. switch(Voice.read())//判断识别
    43. {
    44. case 1://指令是 lu kou yi
    45. crossing1();
    46. delay(3000);
    47. WORK(STOP);//停下通过openmv识别
    48. delay(5000);
    49. WORK(RUN);//识别完毕继续前进
    50. delay(2000);//前进两秒后停止
    51. WORK(STOP);
    52. serial_data = Serial.read();//当抓取完成后发送一个w
    53. if( serial_data == 'w' )
    54. {
    55. WORK(BACK);
    56. }
    57. if(SR == HIGH & SL == HIGH)//再次识别到黑线时右转
    58. {
    59. WORK(RIGHT);
    60. }
    61. tracing();//继续前进
    62. break;
    63. case 2://指令是 lu lou er
    64. crossing2();
    65. delay(3000);
    66. WORK(STOP);//停下通过openmv识别
    67. delay(5000);
    68. WORK(RUN);//识别完毕继续前进
    69. delay(2000);//前进两秒后停止准备抓取
    70. WORK(STOP);
    71. serial_data = Serial.read();//当抓取完成后发送一个w
    72. if( serial_data == 'w' )
    73. {
    74. WORK(BACK);
    75. }
    76. if(SR == HIGH & SL == HIGH)//再次识别到黑线时左转
    77. {
    78. WORK(LEFT);
    79. }
    80. tracing();//继续前进
    81. break;
    82. case 3:
    83. tracing();
    84. if(SR == HIGH & SL == HIGH)
    85. {
    86. crossing3();
    87. }
    88. delay(3000);
    89. WORK(STOP);//停下通过openmv识别
    90. delay(5000);
    91. WORK(RUN);//识别完毕继续前进
    92. delay(2000);//前进两秒后停止准备抓取
    93. WORK(STOP);
    94. serial_data = Serial.read();//当抓取完成后发送一个w
    95. if( serial_data == 'w' )
    96. {
    97. WORK(BACK);
    98. }
    99. if(SR == HIGH & SL == HIGH)//再次识别到黑线时右转
    100. {
    101. WORK(RIHGT);
    102. }
    103. tracing();//继续前进
    104. break;
    105. case 4:
    106. tracing();
    107. if(SR == HIGH & SL == HIGH)
    108. {
    109. crossing4();
    110. }
    111. delay(3000);
    112. WORK(STOP);//停下通过openmv识别
    113. delay(5000);
    114. WORK(RUN);//识别完毕继续前进
    115. delay(2000);//前进两秒后停止准备抓取
    116. WORK(STOP);
    117. serial_data = Serial.read();//当抓取完成后发送一个w
    118. if( serial_data == 'w' )
    119. {
    120. WORK(BACK);
    121. }
    122. if(SR == HIGH & SL == HIGH)//再次识别到黑线时左转
    123. {
    124. WORK(LEFT);
    125. }
    126. tracing();//继续前进
    127. }
    128. }
    129. void WORK(int cmd)
    130. {
    131. switch(cmd)
    132. {
    133. case RUN:
    134. Serial.println("RUN"); //输出状态
    135. digitalWrite(a1, HIGH);
    136. digitalWrite(a2, LOW);
    137. analogWrite(leftPWM, 200);
    138. digitalWrite(b1, HIGH);
    139. digitalWrite(b2, LOW);
    140. analogWrite(rightPWM, 200);
    141. break;
    142. case BACK:
    143. Serial.println("BACK"); //输出状态
    144. digitalWrite(a1, LOW);
    145. digitalWrite(a2, HIGH);
    146. analogWrite(leftPWM, 200);
    147. digitalWrite(b1, LOW);
    148. digitalWrite(b2, HIGH);
    149. analogWrite(rightPWM, 200);
    150. break;
    151. case LEFT:
    152. Serial.println("TURN LEFT"); //输出状态
    153. digitalWrite(a1, HIGH);
    154. digitalWrite(a2, LOW);
    155. analogWrite(leftPWM, 100);
    156. digitalWrite(b1, LOW);
    157. digitalWrite(b2, HIGH);
    158. analogWrite(rightPWM, 200);
    159. break;
    160. case RIGHT:
    161. Serial.println("TURN RIGHT"); //输出状态
    162. digitalWrite(a1, LOW);
    163. analogWrite(leftPWM,200);
    164. digitalWrite(a2, HIGH);
    165. digitalWrite(b1, HIGH);
    166. digitalWrite(b2, LOW);
    167. analogWrite(rightPWM,100);
    168. break;
    169. default:
    170. Serial.println("STOP"); //输出状态
    171. digitalWrite(a1, LOW);
    172. digitalWrite(a2, LOW);
    173. digitalWrite(b1, LOW);
    174. digitalWrite(b2, LOW);
    175. }
    176. }
    177. void crossing1()//路口1函数
    178. {
    179. if (SL == LOW && SR == LOW)//左右两边都没有检测到黑线
    180. {
    181. WORK(RUN);
    182. }
    183. if (SL == HIGH & SR == LOW)//左侧检测到黑线
    184. {
    185. WORK(LEFT);
    186. }
    187. if (SR == HIGH & SL == LOW)//右侧检测到黑线
    188. {
    189. WORK(RIGHT);
    190. }
    191. if (SR == HIGH & SL == HIGH)//左右两边都检测到黑线
    192. {
    193. WORK(RIGHT);
    194. }
    195. }
    196. void crossing2()//路口2函数
    197. {
    198. if (SL == LOW && SR == LOW)//左右两边都没有检测到黑线
    199. {
    200. WORK(RUN);
    201. }
    202. if (SL == HIGH & SR == LOW)//左侧检测到黑线
    203. {
    204. WORK(LEFT);
    205. }
    206. if (SR == HIGH & SL == LOW)//右侧检测到黑线
    207. {
    208. WORK(RIGHT);
    209. }
    210. if (SR == HIGH & SL == HIGH)//左右两边都检测到黑线
    211. {
    212. WORK(LEFT);
    213. }
    214. }
    215. void crossing3()//路口3函数
    216. {
    217. if (SL == LOW && SR == LOW)//左右两边都没有检测到黑线
    218. {
    219. WORK(RUN);
    220. }
    221. if (SL == HIGH & SR == LOW)//左侧检测到黑线
    222. {
    223. WORK(LEFT);
    224. }
    225. if (SR == HIGH & SL == LOW)//右侧检测到黑线
    226. {
    227. WORK(RIGHT);
    228. }
    229. if (SR == HIGH & SL == HIGH)//左右两边都检测到黑线
    230. {
    231. WORK(LEFT);
    232. void crossing4()//路口函数
    233. {
    234. if (SL == LOW && SR == LOW)//左右两边都没有检测到黑线
    235. {
    236. WORK(RUN);
    237. }
    238. if (SL == HIGH & SR == LOW)//左侧检测到黑线
    239. {
    240. WORK(LEFT);
    241. }
    242. if (SR == HIGH & SL == LOW)//右侧检测到黑线
    243. {
    244. WORK(RIGHT);
    245. }
    246. if (SR == HIGH & SL == HIGH)//左右两边都检测到黑线
    247. {
    248. WORK(RIGHT);
    249. void tracing()
    250. {
    251. if (SL == LOW && SR == LOW)//左右两边都没有检测到黑线
    252. {
    253. WORK(RUN);
    254. }
    255. if (SL == HIGH & SR == LOW)//左侧检测到黑线
    256. {
    257. WORK(LEFT);
    258. }
    259. if (SR == HIGH & SL == LOW)//右侧检测到黑线
    260. {
    261. WORK(RIGHT);
    262. }
    263. if (SR == HIGH & SL == HIGH)//左右两边都检测到黑线
    264. {
    265. WORK(RUN);
    266. }
    267. }

    更多详细资料请参考 【S021】智能快递付件机器人

  • 相关阅读:
    Ajax技术【Ajax技术详解、 Ajax 的使用、Ajax请求、 JSON详解、JACKSON 的使用 】(一)-全面详解(学习总结---从入门到深化)
    adb 获取当前界面元素
    prompt 综述
    使用bitnamiredis-sentinel部署Redis 哨兵模式
    Linux进程管理2
    前端面试题:基础理论整理(篇2)
    文件部署到服务器
    C#的类型转换
    【Linux】一
    在WPF应用程序集中添加新文件时,Page和Window有什么区别
  • 原文地址:https://blog.csdn.net/Robotway/article/details/134378017