在上一篇文章中,我们已经知道了如何通过WiFi将iOS设备和EV3连接起来,那么下一步的工作就是从iOS设备中发送命令给EV3并接收EV3返回的数据。这也是本篇文章将告诉大家的。
首先要明确的一点是本开源代码库只封装了EV3直接命令(Direct Command),也就是无需在EV3上开发任何程序就能使用这些命令对EV3进行控制。
目前库中的API包含以下这些: #pragma mark - EV3 Direct Command
// Scan or stop scan each port sensor condition and data on the ev3 brick // 检测每个端口的数据 - (void)scanPorts;
- (void)stopScan;
// 清除所有命令 - (void)clearCommands;
#pragma mark - Motor Control Methods
// turn motor power at specified port and power // 控制电机运转在特定的端口和特定的功率 - (void)turnMotorAtPort:(EV3OutputPort)port power:(int)power; // 控制电机运转在特定的端口和特定的功率及特定的运转时间 - (void)turnMotorAtPort:(EV3OutputPort)port power:(int)power time:(NSTimeInterval)time;
// 控制电机运转在特定的端口和特定的功率及特定的转动角度 - (void)turnMotorAtPort:(EV3OutputPort)port power:(int)power degrees:(UInt32)degrees; // 控制电机停止在特定的端口 - (void)stopMotorAtPort:(EV3OutputPort)port;
#pragma mark - Sound Control Methods // 播放音调在特定的音量特定的频率和特定的播放时间 - (void)playToneAtVolume:(int)volume frequency:(UInt16)frequency duration:(UInt16)duration;
// 播放音乐在特定的音量特定的文件及是否重复 - (void)playSoundAtVolume:(int)volume filename:(NSString *)filename repeat:(BOOL)repeat; // 停止播放音乐 - (void)playSoundBrake;
#pragma mark - Image Control Methods // 在EV3的显示屏上画图 - (void)drawImageAtColor:(EV3ScreenColor)color x:(UInt16)x y:(UInt16)y filename:(NSString *)filename;
// 在EV3的显示屏上显示文字 - (void)drawText:(NSString *)text color:(EV3ScreenColor)color x:(UInt16)x y:(UInt16)y;
// 在EV3的显示屏上画特定大小的窗口 - (void)drawFillWindowAtColor:(EV3ScreenColor)color y0:(UInt16)y0 y1:(UInt16)y1;
最重要的两部分就是读取端口数据以及控制电机转动,至于后面的声音和显示两部分不是特别重要,可以用iPhone取代。
那么,问题来了:如何创建并发送一个命令呢?
有以下几个步骤: Step 1:根据直接命令协议创建特定命令的二进制数据 Step 2:将命令转化为特定的数据格式NSData Step 3:通过TCP socket将命令数据发送出去。
下面我们就StepByStep地剖析实现它!
== Step1:创建直接命令 == 这部分内容主要都在库中的EV3DirectCommand.m中实现。
为了让大家理解,我们先来了解一下EV3的直接命令协议!
Beside running user programs the VM is able to execute direct commands from the Communication Module. In fact direct commands are small programs that consists of regular byte codes and they are executed in parallel with a running user program.\n Special care MUST be taken when writing direct commands because the decision until now is NOT to restrict the use of "dangerous" codes and constructions (loops in a direct command are allowed).
If a new direct command from the same source is going to be executed an actual running direct command is terminated.
Because of a small header objects are limited to one VMTHREAD only - SUBCALLs and BLOCKs is of course not possible.\n This header contains information about number of global variables (for response), number of local variables and command size.
Direct commands that has data response can place the data in the global variable space. The global variable space is equal to the communication response buffer. The composition of the direct command defines at which offset the result is placed (global variable 0 is placed at offset 0 in the buffer).
Offset in the response buffer (global variables) must be aligned (float/32bits first and 8 bits last).
Direct Command Bytes: ,------,------,------,------,------,------,------,------, |Byte 0|Byte 1|Byte 2|Byte 3|Byte 4|Byte 5| |Byte n| '------'------'------'------'------'------'------'------'
Byte 0 – 1: Command size, Little Endian\n
Byte 2 – 3: Message counter, Little Endian\n
Byte 4: Command type. see following defines */
#define DIRECT_COMMAND_REPLY 0x00 // Direct command, reply required #define DIRECT_COMMAND_NO_REPLY 0x80 // Direct command, reply not required
/*
Byte 5 - 6: Number of global and local variables (compressed).
Byte 6 Byte 5 76543210 76543210 -------- -------- llllllgg gggggggg
gg gggggggg Global variables [0..MAX_COMMAND_GLOBALS]
llllll Local variables [0..MAX_COMMAND_LOCALS]
Byte 7 - n: Byte codes
Direct Command Response Bytes: ,------,------,------,------,------,------,------,------, |Byte 0|Byte 1|Byte 2|Byte 3| | | |Byte n| '------'------'------'------'------'------'------'------'
Byte 0 – 1: Reply size, Little Endian\n
Byte 2 – 3: Message counter, Little Endian\n
Byte 4: Reply type. see following defines */
#define DIRECT_REPLY 0x02 // Direct command reply #define DIRECT_REPLY_ERROR 0x04 // Direct command reply error
/*
Byte 5 - n: Response buffer (global variable values)
以上这些就是Direct Command的协议!
简单解释一下Direct Command就是前缀加上具体命令!所以搞清楚各种控制命令的格式是非常重要的!
比如我们要控制EV3的电机,还得需要知道控制电机的命令及组成方式!
更进一步地我们需要知道所有可以用的命令的格式!
从python-ev3.org 这个网站我们可以找到这些命令的格式!
当然从EV3的源代码也是可以找到的!在c_output.h文件中可以看到!
那么下面我们举一个最直接的例子来看看命令的二进制数据是怎么样的,这样大家就可以有一个直观的认识了。
== 举例:控制端口PortA的电机以功率50转动 == EV3中控制电机的命令主要有以下三个: opOUTPUT_POWER(LAYER,NOS,SPEED) // 设置电机的输出 Set power of the outputs Dispatch status unchanged Parameters: (DATA8)LAYER - Chain layer number[0..3] (DATA8)NOS - Output bit field[0x00..0x0F] output 1 to 4 (0x01, 0x02, 0x04, 0x08) (DATA8)POWER - Power[-100..100]
opOUTPUT_START(LAYER,NOS) // 启动电机 Starts the outputs Dispatch status unchanged Parameters: (DATA8)LAYER - Chain Layer number[0..3] (DATA8)NOS - Output bit field[0x00..0x0F] 端口
opOUTPUT_STOP(LAYER,NOS) // 停止电机 Stop the outputs Dispatch status unchanged Parameters: (DATA8)LAYER - Chain layer number[0..3] (DATA8)NOS - Output bit field[0x00,0x0F] (DATA8)BRAKE - Brake[0,1]
经过研究,上面的DATA8格式其实就是unsigned char格式!
可以说有了这三个命令我们就能控制电机了。 要实现这个功能需要两个命令,一个是确定端口的输出,一个是启动输出。如果要用伪代码的形式表示就是:
opOUTPUT_POWER(0x00,PortA,50) opOUTPUT_START(0x00,PortA)
先贴上这整个命令的二进制数据如下: 1100 0000 80 0000 a4 8100 8101 8132 a6 8100 8101
什么意思?
Byte 0,1: 0x1100 表示命令的长度,这里是小端对齐,所以其大小为0x0011,记住这边是16进制,所以0x0011大小为17,也就是说除了Byte0,1之外其他Byte也就是命令内容的长度为17Byte。理解了吗?大家可以数数看,是不是整个命令长度19Byte,扣除前两位,则命令内容为17Byte。
Byte 2,3: 0x0000 表示消息计数。这个的作用是什么呢?主要是为了分清楚接收的消息是对应发出的哪一个消息(命令)。举例说如果这里消息计数是1,那么返回的消息的消息计数也是1,这样就可以实现一一对应。在我们这边的应用中,我们不需要考虑对应问题,所以消息计数设置为0000就可以了。
Byte 4:0x80 命令类型(回复或者不回复)如果是0x00,则EV3接收到命令后要做出回复,如果是0x80,那么就不回复,这里我们因为是控制电机,无需返回数据,所以设置成0x80。
Byte 5,6 :0x0000 公共变量和私有变量的数量 这个在上面的协议中有解释,主要是用于确定返回数据的位置。那么这边我们无需返回任何数据,也就是无需任何变量,所以设置为0x0000。
接下来的Byte都是具体命令了。
每个命令都有专门的命令码,如下: opOUTPUT_STOP = 0xA3, // 00011 opOUTPUT_POWER = 0xA4, // 00100 opOUTPUT_START = 0xA6, // 00110 详见源代码中的bytecodes.h这个文件
接下来就是如何添加参数? 每个参数之前要先添加参数的长度! typedef enum { EV3ParameterSizeByte = 0x81, // 1 byte EV3ParameterSizeInt16 = 0x82, // 2 bytes EV3ParameterSizeInt32 = 0x83, // 4 bytes EV3ParameterSizeString = 0x84 // null-terminated string }EV3ParameterSize;
不同长度的参数前面要添加的size参数不一样!也就是说比如我这边要添加power参数,那么之前就要先添加一个size参数,由于power参数是1 byte,所以添加0x81!
那么我们再回来看二进制数据: Byte 7:0xa4 opOUTPUT_POWER的命令码 Byte 8,9,10,11,12,13: 0x8100 8101 8132 opOUTPUT_POWER对应的三个参数,0x8100表示layer,其值为0,0x8101表示port,其值01为PortA,0x8132表示功率,其值为0x32 = 50. Byte 14:0xa6 opOUTPUT_START的命令码 Byte 15,16,17,18:0x8100 8101 opOUTPUT_START对应的两个参数,0x8100表示layer,其值为0,0x8101表示port,其值01为PortA。 我们的代码库就是要将这些二进制数据创建起来,具体的实现涉及大量的二进制移位操作,大家可以自己查看文件进行分析,这里不再细说。
这个功能对应的API是 + (NSData *)turnMotorAtPort:(EV3OutputPort)port power:(int)power { EV3DirectCommander *command = [[EV3DirectCommander alloc] initWithCommandType:EV3CommandTypeDirectNoReply globalSize:0 localSize:0]; [command addOperationCode:EV3OperationOutputPower]; [command addParameterWithInt8:0]; [command addParameterWithInt8:port]; if (abs(power) > 100) { [command addParameterWithInt8:100]; } else { [command addParameterWithInt8:(Byte)power]; } [command addOperationCode:EV3OperationOutputStart]; [command addParameterWithInt8:0]; [command addParameterWithInt8:port]; return [command assembledCommandData]; }
具体实现时就是编写如下的代码: [self turnMotorAtPort:EV3OutputPortA power:50];
可以说其他的命令组成方法道理都是一样的。
大家可以自己分析一下获取传感器数据的命令是如何组成的。
== Step 2:转换为NSData数据 == 这里在EV3DirectComand.m文件通过一个方法实现: [NSData dataWithBytes:buffer length:cursor]; 仅仅是格式转换,大家即使不理解也没有关系。
== Step 3:发送命令 == 这个工作在EV3Device.m文件中实现,也很简单。 下面是控制电机的发送命令代码: - (void)turnMotorAtPort:(EV3OutputPort)port power:(int)power { // 封装命令数据 NSData *data = [EV3DirectCommander turnMotorAtPort:port power:power]; // 发送命令 [self.tcpSocket writeData:data withTimeout:-1 tag:MESSAGE_NO_REPLY]; }
大家看了源代码应该就可以明白。这里不再多讲。对于TCP方面不了解的童鞋请查看之前的文章。
好啦,关于如何创建并发送Direct Command命令就介绍到这里。可以说,如果大家理解到这里,那么对整个库的编写也就基本理解了。之后我们就不再讲解这个代码库的问题了。
我们将开始一个真正的项目:用iOS来体感控制EV3 坦克!
大家准备好了吗? 敬请期待下一篇文章!
|