
在这一部分中,我们将讨论软件组件,机器是如何诞生的。 使用了什么操作系统,选择了什么语言,面临什么问题。
1. 2个字的工作方式
该系统由一台安装在打字机上的服务器和一台安装在控制台上的客户端组成。 服务器提升wifi接入点并等待,直到客户端连接。 服务器执行客户端命令,还将视频从摄像机传输到它。
2.作业系统
现在让我们谈谈所使用的操作系统。
由于整个系统都基于Raspberry pi 3 ,因此使用了其正式操作系统。 在创建时,最新版本是Stretch ,并且曾经被选择用于打字机和遥控器。 但事实证明,它存在一个错误(折磨了一周),因此无法提高wifi接入点。 因此,为了提高访问点,采用了Jessie的先前版本,但没有此类问题。
文章如何提高访问点。 非常详细,已完成所有操作。
遥控器在升高访问点时会自动连接到机器。
自动连接到我们的点,在文件/ etc / network / interfaces中添加:
auto wlan0 iface wlan0 inet dhcp wpa-ssid {ssid} wpa-psk {password}
2.语言
我选择python是因为它很简单。
3.服务器
在本节中,服务器是指我编写的用于控制机器和处理视频的软件。
服务器由两部分组成。 视频服务器和管理服务器。
3.1视频服务器
有2种选择如何使用摄像机。 第一次使用picamera模块,第二次使用mjpg-streamer软件 。 不用三思而后行,我决定同时使用它们,以及在配置设置中使用哪一个。
if conf.conf.VideoServerType == 'm' : cmd = "cd /home/pi/projects/mjpg-streamer-experimental && " cmd += './mjpg_streamer -o "./output_http.so -p {0} -w ./www" -i "./input_raspicam.so -x {1} -y {2} -fps 25 -ex auto -awb auto -vs -ISO 10"'.format(conf.conf.videoServerPort, conf.conf.VideoWidth, conf.conf.VideoHeight) print(cmd) os.system(cmd) else : with picamera.PiCamera(resolution = str(conf.conf.VideoWidth) + 'x' + str(conf.conf.VideoHeight) , framerate = conf.conf.VideoRate) as Camera: output = camera.StreamingOutput() camera.output = output Camera.start_recording(output, format = 'mjpeg') try: address = (conf.conf.ServerIP, conf.conf.videoServerPort) server = camera.StreamingServer(address, camera.StreamingHandler) server.serve_forever() finally: Camera.stop_recording()
由于它们采用相同的设置,因此它们在相同的地址工作。 从一个切换到另一个时,与遥控器的通讯没有问题。 我认为mjpg-streamer唯一的工作原理是速度更快。
3.2管理服务器
3.2.1客户端与服务器之间的交互
服务器和客户端以json字符串形式交换命令:
{'type': 'remote', 'cmd': 'Start', 'status': True, 'val': 0.0} {'type': 'remote', 'cmd': 'Y', 'status': True, 'val': 0.5} {'type': 'remote', 'cmd': 'turn', 'x': 55, 'y': 32}
- 类型-“远程”或“汽车”,取决于谁发送命令(客户端或服务器)
- cmd-一个字符串,其按钮名称与Game HAT上的按钮名称相对应,例如:
- 开始-开始按钮
- 选择-选择按钮
- Y-Y按钮
- 等
- turn-改变操纵杆状态的命令,负责转动车轮
- 状态-正确或错误,取决于是否按下按钮。 每当按钮状态事件发生状态更改时,都会调度该按钮状态事件。
- val速度和电动机的运动方向,从-1 ... 1, float型值。 仅用于运动按钮的重要参数。
- x-操纵杆沿x轴与-100 ... 100的偏差,类型为int的值
- y-操纵杆沿y轴与-100 ... 100的偏差,类型为int的值
接下来是我的耻辱,重做手无法触及的地方。 机器抬起服务器插槽,等待客户端连接到它。 此外,对于每个新连接,它都会创建一个单独的流,并且将要连接到该计算机的每个新客户端都可以控制它)。 到目前为止,这还不是很遥远,因为没有其他人拥有这样的遥控器,并且我建立了封闭的wifi网络。
def run(self): TCP_IP = conf.conf.ServerIP TCP_PORT = conf.conf.controlServerPort BUFFER_SIZE = conf.conf.ServerBufferSize self.tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.tcpServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.tcpServer.bind((TCP_IP, TCP_PORT)) threads = []
3.2.2铁管理
在使用Raspberry时,使用了GPIO.BCM引脚编号系统。
光线是通过gpio 17控制的,它连接到L293的第二个引脚。 接下来,每次命令包含以下内容:
GPIO.output(self.gpioLight, GPIO.HIGH)
GPIO.output(self.gpioLight, GPIO.LOW)
相应的命令被调用。
伺服驱动 器通过PCA9685板通过I2C总线进行控制,因此我们需要为其提供适当的库Adafruit_PCA9685 。 PCA9685通过7针连接到伺服。 伺服工作所需的PWM频率为50 Hz或20 ms。
伺服的工作原理:

当施加1.5 ms信号时,车轮将居中。 在1毫秒。 伺服将尽可能向右旋转2毫秒。 尽可能向左。 没有设计用于此类转弯的桥梁转向节,因此必须通过实验选择旋转角度。
可以传递给Adafruit_PCA9685 API的值的范围是0..4095、0无信号,4095满。 因此,必须从这个范围选择适合我的车轮的值。 确定精确设置的车轮的值的最简单方法是将1.5 ms转换为〜307范围内的值。
右边的最大值是245,左边的最大值是369。
来自操纵杆的值的取值范围是-100 ... 100,因此必须在245到369的范围内进行转换。同样,最简单的中心是中心,如果为0,则是307。根据以下公式,可以左右移动:
val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero))
- HardwareSetting._turnCenter-307
- 转-操纵杆的值从-100 ... 100
- HardwareSetting._turnDelta-62,中心与侧面的最大偏差之差(307-245 = 62)
- HardwareSetting.yZero-100,从操纵杆接收到的最大值
直轮:
def turnCenter(self): val = int(HardwareSetting._turnCenter) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
向左转:
def turnLeft(self, turn): val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero)) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
向右转:
def turnRight(self, turn): val = int(HardwareSetting._turnCenter + (-1 * turn * HardwareSetting._turnDelta / HardwareSetting.yZero)) self.pwm_servo.set(val) CarStatus.statusCar['car']['turn'] = val
引擎控制也通过PCA9685板通过I2C总线进行,因此我们使用Adafruit_PCA9685 。 PCA9685上的引脚10至15连接到L298N(我使用2个通道来吸收功率)。 ENA和ENB的10和11(我用PWM填充它们以控制速度)。 12、13、14、15到IN1,IN2,IN3,IN4-负责电机的旋转方向。 PWM频率在这里不是很重要,但是我也使用50赫兹(我的默认值)。
机器静止不动:
def stop(self): """ . """ self.pwm.set_pwm(self.ena, 0, self.LOW) self.pwm.set_pwm(self.enb, 0, self.LOW) self.pwm.set_pwm(self.in1, 0, self.LOW) self.pwm.set_pwm(self.in4, 0, self.LOW) self.pwm.set_pwm(self.in2, 0, self.LOW) self.pwm.set_pwm(self.in3, 0, self.LOW)
前进:
def back(self, speed): """ . Args: speed: 0 1. """ self.pwm.set_pwm(self.ena, 0, int(speed * self.HIGH)) self.pwm.set_pwm(self.enb, 0, int(speed * self.HIGH)) self.pwm.set_pwm(self.in1, 0, self.LOW) self.pwm.set_pwm(self.in4, 0, self.LOW) self.pwm.set_pwm(self.in2, 0, self.HIGH) self.pwm.set_pwm(self.in3, 0, self.HIGH)
后退运动:
def forward(self, speed): """ . Args: speed: 0 1. """ self.pwm.set_pwm(self.ena, 0, int(speed * self.HIGH)) self.pwm.set_pwm(self.enb, 0, int(speed * self.HIGH)) self.pwm.set_pwm(self.in1, 0, self.HIGH) self.pwm.set_pwm(self.in4, 0, self.HIGH) self.pwm.set_pwm(self.in2, 0, self.LOW) self.pwm.set_pwm(self.in3, 0, self.LOW)
4.客户
4.1键盘
她有某些问题,起初我想让她多事(经历了大约2周的折磨)。 但是机械按钮起作用了,触点的嘎嘎声导致了持续且不可预测的故障(我发明的控制算法无法正常工作)。 然后我的同事告诉我键盘的制作方法。 我决定做同样的事情,现在我每隔0.005秒轮询一次状态(为什么这样,谁知道)。 如果已更改,则将值发送到服务器。
def run(self): try: while True: time.sleep(0.005) for pin in self.pins : p = self.pins[pin] status = p['status'] if GPIO.input(pin) == GPIO.HIGH : p['status'] = False else : p['status'] = True if p['status'] != status : p['callback'](pin) except KeyboardInterrupt: GPIO.cleanup()
4.2游戏杆
读数通过ADS1115板通过I2C总线进行读取 ,因此,适合的库为Adafruit_PCA9685 。 操纵杆也容易产生颤抖的联系,因此我以类比键盘的方式从中读取读数。
def run(self): while True: X = self.adc.read_adc(0, gain=self.GAIN) / HardwareSetting.valueStep Y = self.adc.read_adc(1, gain=self.GAIN) / HardwareSetting.valueStep if X > HardwareSetting.xZero : X = X - HardwareSetting.xZero else : X = -1 * (HardwareSetting.xZero - X) if Y > HardwareSetting.yZero : Y = Y - HardwareSetting.yZero else : Y = -1 * (HardwareSetting.yZero - Y) if (abs(X) < 5) : X = 0 if (abs(Y) < 5) : Y = 0 if (abs(self.x - X) >= 1.0 or abs(self.y - Y) >= 1.0) : self.sendCmd(round(X), round(Y)) self.x = X self.y = Y time.sleep(0.005)
当采用3.3伏电源供电时,ADS1115的操纵杆给出的数值范围为0 ... 26500。 我将此范围从-100 ... 100。 在我大约为0的范围内,它总是波动,因此,如果值不超过5,则我认为它是0(否则它将泛滥)。 值更改后,立即将它们发送到打字机。
4.3连接到管理服务器
连接到服务器很简单:
try : tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcpClient.settimeout(2.0) tcpClient.connect((conf.conf.ServerIP, conf.conf.controlServerPort)) self.signalDisplayPrint.emit("+") carStatus.statusRemote['network']['control'] = True self.tcpClient = tcpClient except socket.error as e: self.signalDisplayPrint.emit("-") carStatus.statusRemote['network']['control'] = False time.sleep(conf.conf.timeRecconect) self.tcpClient = None continue if self.tcpClient : self.tcpClient.settimeout(None)
但是我要注意一件事。 如果未在连接中使用超时,则它可能会冻结,您将不得不等待大约几分钟(这种情况发生在客户端在服务器之前启动时)。 我通过以下方式解决了这个问题,即设置了连接超时。 建立连接后,我立即删除超时。
我还存储连接状态,以便我知道控件是否丢失并在屏幕上显示它。
4.4检查WiFi连接
我检查wifi的状态以连接到服务器。 如果可以的话,我也可以将问题通知自己。
def run(self): while True: time.sleep(1.0) self.ps = subprocess.Popen(['iwgetid'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) try: output = subprocess.check_output(('grep', 'ESSID'), stdin=self.ps.stdout) if re.search(r'djvu-car-pi3', str(output)) : self.sendStatus('wifi+') continue except subprocess.CalledProcessError: pass self.sendStatus('wifi-') self.ps.kill()
4.5连接到视频服务器
为此, 需要Qt5的所有功能, 顺便说一下,在Stretch发行版上,它是更新的,在我看来,它显示的更好。 在杰西我也尝试过。
为了显示,我使用了:
self.videoWidget = QVideoWidget()
他推论:
self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.LowLatency) self.mediaPlayer.setVideoOutput(self.videoWidget)
连接流媒体视频:
self.mediaPlayer.setMedia(QMediaContent(QUrl("http://{}:{}/?action=stream".format(conf.conf.ServerIP, conf.conf.videoServerPort)))) self.mediaPlayer.play()
再次,我对重言式表示歉意。 我监视视频连接的状态以连接到视频服务器。 如果可以的话,我也可以将问题通知自己。
这是一切都不起作用时的样子:

- W-表示没有与wifi的连接
- B-表示没有视频
- Y-表示没有控制权
否则,没有红色字母,这是摄像机的视频。 我将在以后的工作中发布照片和视频)。我希望相机的安装架会在不久的将来出现,并且我最终会正常安装它。
5配置Raspberry OS
顺便说一下,使用相机和其他必需的东西必须打开(在客户端和服务器上)。 加载操作系统后:

并开启几乎所有功能:相机,ssh,i2c,gpio

示范
只有一个视频频道(相机仍在工作)。 对于他的缺席,我深表歉意。我将在周一附上。

视频作品:
源代码
服务器和客户端源代码
守护程序服务器启动包
参考文献
第一部分
第三部分