树莓派WiFi遥控小车:iOS控制端与自动避障的协作实现
简介树莓派wifi遥控和自动避障小车的iOS控制App项目面向基于树莓派开展毕业设计或课程设计的开发者尤其适合需要在iOS端实现遥控与避障控制逻辑的初学者。资源共35个文件以Objective-C源码13个.m、12个.h为主包含Xcode工程配置、界面布局文件xib/storyboard、网络请求相关配置plist/json以及说明文档可帮助理解iOS与树莓派之间的Wi-Fi通信、摇杆遥控、自动避障决策等模块的实现思路并梳理了工程内各文件的分工。包体仅95KB代码精简容易阅读和二次修改。目前已有132人学习下载适合需要快速掌握移动端控制小车思路、并在树莓派端进行联动调试的读者。通过阅读源码可了解AFNetworking库的使用、视图控制器组织方式以及自动避障与手动遥控功能在App端的交互逻辑便于根据课程设计要求裁剪或扩展为完整的毕设项目。1. 树莓派WiFi遥控小车为什么iOS端只是个“遥控器”这个zip里装的不是小车程序而是运行在iPhone上的树莓派WiFi遥控和自动避障小车的控制app。拿到RaspiCar工程时你会发现真正做避障决策的不是iOS端而是树莓派端iOS app的工作只有三件连上WiFi、把方向按钮变成HTTP请求发出去、把状态显示出来。这个项目最适合两类人一类是拿树莓派4B做课程设计需要有一个能演示的iOS界面另一类是搞物联网/嵌入式想看看移动端怎么和Linux单板机通信。理解了“iOS端只发指令、树莓派端做避障”的分工后面调代码才不会方向跑偏。2. 拆解RaspiCar工程Xcode项目结构与基于AFNetworking的WiFi指令链路拿到解压后的RaspiCar.xcodeproj先别急着双击。通过project.pbxproj能看到这是一个Objective-C工程目录里没有SceneDelegate和Swift文件说明它基于AppDelegate生命周期界面逻辑都集中在ViewController.h/.m里。RaspiCarTests是单元测试target里面只有Info.plist基本没有实际断言。AFNetworking作为第三方网络库被直接拖进工程这是老项目常见做法。2.1 工程目录与文件职责典型的文件分布如下RaspiCar.xcodeproj/project.pbxproj工程配置不建议手改RaspiCar/ViewController.h/.m主控制器摇杆、方向键、速度滑块都在这里RaspiCar/AppDelegate.h/.mApp入口RaspiCar/Info.plistATS、设备方向、本地网络权限配置RaspiCar/Base.lproj/Images.xcassetsStoryboard和图片资源AFNetworking网络请求库负责HTTP通信RaspiCarTests测试target从依赖关系看ViewController是核心。AFNetworking不是随便加的因为树莓派小车的控制本质是一个HTTP客户端与树莓派上Web服务的交互。方向键每按一次就要向树莓派的某个端口发一个带动作和速度参数的请求。AFNetworking帮你封装了请求队列、超时和回调线程比直接用NSURLSession写起来快很多。2.2 ViewController如何发出一个“前进”指令我拆过的这类项目最标准的做法是在ViewController.m里封装一个命令发送方法。它以动作名称和速度百分比为参数向树莓派的控制地址发起GET请求。参考实现如下// 发送控制指令action为forward/backward/left/right/stopspeed为0-100 - (void)sendCommand:(NSString *)action speed:(NSInteger)speed { AFHTTPSessionManager *manager [AFHTTPSessionManager manager]; manager.requestSerializer [AFHTTPRequestSerializer serializer]; manager.responseSerializer [AFHTTPResponseSerializer serializer]; manager.requestSerializer.timeoutInterval 1.5; // 遥控指令要快速失败不能等默认60秒 NSDictionary *params { action: action ?: stop, speed: (speed) }; NSString *url [NSString stringWithFormat:http://%:5000/control, self.hostIP]; [manager GET:url parameters:params progress:nil success:^(NSURLSessionDataTask *task, id responseObject) { // 响应体是纯文本OK不需要JSON解析 } failure:^(NSURLSessionDataTask *task, NSError *error) { NSLog([RaspiCar] 指令发送失败: %, error.localizedDescription); }]; }这里有几个参数是有讲究的。timeoutInterval设1.5秒是因为遥控场景下如果一条指令没及时送达下一条指令通常会在100毫秒后发出来没必要让请求卡在队列里。用GET而不用POST因为控制命令天然幂等且GET参数可以直接在树莓派端用Flask的request.args取到联调时在浏览器地址栏敲一下URL就能验证小车是否动起来。self.hostIP是你要在设置页填写的树莓派IP一般用star或本地局域网IP。2.3 WiFi链路参数与ATS配置iOS 9之后默认禁用明文HTTP而树莓派上的控制服务往往是HTTP而不是HTTPS。如果你在真机上调试会发现请求永远报Transport Security错误。解决办法是在Info.plist里做ATS例外。两种方式要么全局允许任意加载要么只对树莓派IP做例外。考虑到课程设计演示场景直接使用全局允许往往更省事。配置项值作用NSAppTransportSecurity/NSAllowsArbitraryLoadsYES允许HTTP明文请求timeoutInterval1.5s指令快速失败防止界面卡顿控制地址http://树莓派IP:5000/control与树莓派Flask服务端口保持一致WiFi模式树莓派AP或路由器同一局域网决定IP是192.168.4.x还是192.168.1.x注意如果Xcode工程在模拟器运行没问题、真机却无法连接请检查真机的WiFi是否连到了树莓派热点或同一路由器。另外老工程直接拖入AFNetworking源码时要在Build Settings里加-ObjC链接标志否则部分category方法不加载运行时会崩溃在“unrecognized selector sent to instance”。这个坑特别隐蔽遇到unrecognized selector优先排查这一项。3. 小车端协同HTTP控制接口设计与自动避障的权限出让iOS端不是孤立工作的。树莓派端必须提供一个HTTP控制接口这个接口负责解析iOS发来的动作和速度并通过GPIO驱动电机。这个资源的主题是自动避障因此树莓派端还要跑一个独立的避障线程。这一章我们讨论树莓派端怎么设计才能做到“手动遥控时避障不抢方向盘松手后小车自己会躲障碍”。3.1 树莓派端控制API的约定我用Python Flask比较多因为树莓派GPIO库RPi.GPIO和Flask配合很顺手。控制服务监听5000端口至少暴露/control和/status两个路由。/control读取参数action和speed映射到GPIO输出。参考实现# 树莓派端控制服务Flask from flask import Flask, request, jsonify import RPi.GPIO as GPIO app Flask(__name__) MOTOR_PINS {in1: 17, in2: 18, in3: 22, in4: 23} GPIO.setmode(GPIO.BCM) for pin in MOTOR_PINS.values(): GPIO.setup(pin, GPIO.OUT) GPIO.output(pin, GPIO.LOW) app.route(/control, methods[GET]) def control(): action request.args.get(action, stop) speed int(request.args.get(speed, 50)) # 动作到GPIO电平的映射 if action forward: GPIO.output(17, GPIO.HIGH) GPIO.output(18, GPIO.LOW) GPIO.output(22, GPIO.HIGH) GPIO.output(23, GPIO.LOW) elif action backward: GPIO.output(17, GPIO.LOW) GPIO.output(18, GPIO.HIGH) GPIO.output(22, GPIO.LOW) GPIO.output(23, GPIO.HIGH) elif action left: # 左轮停转右轮正转 GPIO.output(17, GPIO.LOW) GPIO.output(18, GPIO.LOW) GPIO.output(22, GPIO.HIGH) GPIO.output(23, GPIO.LOW) elif action right: GPIO.output(17, GPIO.HIGH) GPIO.output(18, GPIO.LOW) GPIO.output(22, GPIO.LOW) GPIO.output(23, GPIO.LOW) elif action stop: for pin in MOTOR_PINS.values(): GPIO.output(pin, GPIO.LOW) return OK这段代码没有处理PWM调速只说明电平映射关系。speed参数在真实项目中一般用来控制PWM占空比需要把17/18、22/23中的一路改为GPIO.PWM实例但动作映射逻辑不变。表格里可以更直观看出各动作的引脚组合动作IN1IN2IN3IN4forward高低高低backward低高低高left低低高低right高低低低stop低低低低3.2 自动避障与手动遥控如何切换自动避障的常见形态是树莓派接一个HC-SR04超声波模块循环测距距离小于阈值时自动转向。但如果iOS app正在遥控避障线程突然抢方向盘车就会像喝醉一样乱晃。更合理的方案是手动指令获得一个“保持窗口”窗口内避障只报警不干预窗口外自动接管。我用一个时间戳实现这个逻辑。import time manual_hold_until 0 # 手动控制保持时间戳 app.route(/control, methods[GET]) def control(): global manual_hold_until # 收到任何控制指令将自动避障静默2秒 manual_hold_until time.time() 2 # ... 驱动GPIO ... def avoid_obstacles(): while True: distance read_ultrasonic_distance() if distance 0.30 and time.time() manual_hold_until: # 自动转向持续0.4秒 turn_left() time.sleep(0.4) stop() time.sleep(0.1)这里的2秒窗口很关键它是遥控连续性和自动避障安全性之间的折衷。比如iOS app发送指令的间隔是100ms2秒窗口足够避免避障误入一旦app停止发送超过2秒避障线程重新获得控制权。距离阈值0.3米对应车速和电机响应时间如果你的小车速度快阈值要加大到0.4米避免惯性撞上障碍物。这个状态机实现简单答辩时也能解释清楚。3.3 WiFi模式与通信参数的坑树莓派作为AP热点时手机连接到“RaspiCar”热点树莓派端IP固定为192.168.4.1iOS端hostIP填这个地址即可。如果要接入现有路由器树莓派和手机都连同一个WiFi用hostname -I查看树莓派获取到的IP。注意不要把手机流量WiFi和树莓派热点混淆树莓派热点模式下手机无法上外网这是正常的。如果控制接口没有响应先用电脑浏览器访问http://树莓派IP:5000/control?actionstopspeed0验证服务是否在跑。还要检查Flask是否监听所有网卡app.run(host0.0.0.0, port5000)只能写0.0.0.0写127.0.0.1会导致手机无法访问。4. iOS控制App交互与报文设计手势、调速与状态回传Xcode工程的核心界面通常在ViewController里。除了用方向键按钮很多作品会加一个虚拟摇杆手指滑动即可前进后退转弯。这一章把UI交互、速度映射和状态回传的报文设计串起来并说明为什么以及怎么做。4.1 触摸手势映射控制指令虚拟摇杆的实现思路是在touchesMoved里拿到手指相对摇杆中心的位置偏移量决定方向和速度。以中心为原点dx和dy分别代表水平与垂直偏移。判断绝对值大小可以区分左右和前后。参考实现- (void)touchesMoved:(NSSetUITouch * *)touches withEvent:(UIEvent *)event { UITouch *touch [touches anyObject]; CGPoint point [touch locationInView:self.joystickView]; CGFloat dx point.x - CGRectGetMidX(self.joystickView.bounds); CGFloat dy point.y - CGRectGetMidY(self.joystickView.bounds); // 死区手指轻微抖动不触发指令 if (fabs(dx) 12 fabs(dy) 12) { [self sendCommand:stop speed:0]; return; } // 速度映射最大偏移量映射到100 NSInteger speed (NSInteger)(MIN(fabs(dx), 100) / 100 * 100); if (fabs(dx) fabs(dy)) { [self sendCommand:dx 0 ? right : left speed:speed]; } else { [self sendCommand:dy 0 ? backward : forward speed:speed]; } }这里的死区是12pt目的是防止静止时发抖导致小车频繁stop。速度映射我用偏移量绝对值除100乘100得到0-100的百分数值树莓派端再按这个百分比设置PWM。注意后退时建议把speed封顶到60因为超声波的测距头一般装在前方倒车时没有距离感知全速后退撞到充电线或墙壁的机会很大。4.2 状态回传与界面更新遥控界面最好能看到小车当前是否在线、前方障碍距离、当前是手动还是自动模式。树莓派端/status接口返回JSON是一个敏捷做法app.route(/status) def status(): distance read_ultrasonic_distance() return jsonify({ online: True, distance: round(distance, 2), mode: manual if time.time() manual_hold_until else auto })iOS端用AFHTTPSessionManager GET这个地址成功后在block里更新UI[manager GET:self.statusUrl parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id responseObject) { NSDictionary *status responseObject; dispatch_async(dispatch_get_main_queue(), ^{ self.distanceLabel.text [NSString stringWithFormat:%.2fm, [status[distance] doubleValue]]; self.modeLabel.text [status[mode] isEqualToString:manual] ? 手动遥控 : 自动避障; }); } failure:...];这里要强调AFNetworking的success回调不在主线程直接改UILabel会偶发崩溃所以必须在dispatch_async主队列里更新。状态轮询频率建议2-5Hz也就是每200-500毫秒一次。太快浪费WiFi和树莓派CPU太慢则界面感觉“卡”。4.3 控制指令的发送队列管理用手势快速滑动摇杆时手指移动频率远高于网络发送能力。如果每移动一次就发起一个GETAFNetworking内部默认是并发请求先发的慢指令可能会后置导致小车动作乱跳。我一般用一个串行队列每次发新指令前取消之前未执行的请求self.commandQueue.maxConcurrentOperationCount 1; [self.commandQueue cancelAllOperations]; [self.commandQueue addOperationWithBlock:^{ [self sendCommand:action speed:speed]; }];这样WiFi回包慢时旧的排队请求被取消只保留最新指令。配合第2章的1.5秒超时整条链路不会因为网络抖动而产生指令堆积。如果树莓派端有指令去重需求可以要求每个请求带一个自增序号树莓派端丢弃序号小于当前值的请求。不过课程设计阶段用“最新指令覆盖”就够了。5. 联调与防坑WiFi延迟、丢包重试与手动/自动避障切换5.1 先过浏览器验证树莓派端联调第一步不是打开Xcode运行app而是把树莓派服务跑起来用电脑浏览器直接访问控制URL。比如在浏览器输入http://192.168.1.20:5000/control?actionforwardspeed50小车能前进再输入actionstop能停下这之后才开始调iOS端。否则WiFi问题、Flask问题、GPIO问题叠在一起光看iOS日志很难定位。这一步可以把网络链路从问题列表里直接排除。5.2 遥控场景用连续发送代替重试刚接触这个项目的人容易想到“失败就重试”但遥控场景下重试是有害的。比如“前进”指令丢了等1.5秒超时再重发小车实际已经晚了一拍。我的做法是iOS端不重试而是以固定的100-200ms周期重复发送当前动作树莓派端每次收到新指令刷新“最后指令时间”超过2秒没有收到任何指令就自动停车。这样单包丢失不会造成小车失控只是某帧动作晚了几毫秒体验反而更顺滑。5.3 避障阈值与采样滤波技巧HC-SR04超声波模块读数偶尔会跳变比如突然冒出0.02m的杂波。避障线程如果直接拿单次测距做判断小车会在墙前突然掉头很不可靠。我一般会连续读三次求中位数作为有效距离再拿它和阈值比较。阈值配置我通常放在一个JSON文件里方便实车调试时修改重启{ distance_threshold: 0.30, manual_hold_sec: 2.0, turn_duration_sec: 0.4 }手动保持窗口和转向时间这两个参数直接决定了小车“听话”的程度。把turn_duration_sec设太短转向不到位车还是会撞上设太长绕过障碍后又偏航需要靠手动指令掰回来。实车调试时先固定阈值0.30米调节turn_duration_sec从0.3到0.6观察小车是否能避开纸箱而不掉头过度。这样场景下整个系统的可调参数都集中在树莓派端iOS app不需要反复重新编译调试效率高很多。如果后续要加摄像头图传建议视频流走MJPG-streamer的8080端口HTTP控制仍走5000端口两个服务互相独立iOS端用WKWebView直接看MJPG流控制链路延迟基本不受影响。本文还有配套的精品资源点击获取