ORSSerialRequest.h API

来源:互联网 发布:合同法总论 知乎 编辑:程序博客网 时间:2024/05/16 17:21

Request Response API

Andrew Madsen edited this page on Mar 16 · 5 revisions

 Pages 8

  • Home
  • Command Line Demo
  • Getting Started
  • Installing ORSSerialPort
  • ORSSerialPortDemo
  • Request Response API
  • Swift Demo
  • System Requirements
Clone this wiki locally
 Clone in Desktop

Often, programs that communicate with hardware via a serial port send various commands: requests for data, control commands, commands to make settings changes, etc. Frequently the hardware will send specific data in response to each command.

ORSSerialPort includes API to ease implementing a system that sends requests expecting specific responses. The primary API for this class is ORSSerialRequest and associated methods on ORSSerialPort. Use of this API is entirely optional, but it can be very useful for many applications.

This document describes the request/response API in ORSSerialPort including an explanation of its usefulness, an overview of how it works, and sample code. As always, if you have questions, please don't hesitate to contact me.

Brief Overview

The ORSSerialPort request / response API provides the following:

  • An easy way to represent a command (aka. request) using ORSSerialRequest.
  • Packetization of incoming data (responses).
  • Validation of responses.
  • An adjustable timeout when an expected response is not received.
  • Request queue management.

Detailed Overview

The Setup

For the sake of discussion, imagine a piece of hardware with an LED, a temperature sensor, and a serial port. The device accepts three commands on the serial port: one to turn the LED on or off, one to read the current status of the LED, and one to read the current temperature. Each command uses the following structure:

$<command><command_value>;

In response, it always responds with data conforming to the following:

!<command><response_value>;

The device in this scenario thus supports the following commands:

FunctionCommandResponseTurn LED on$LED1;!LED1;Turn LED off$LED0;!LED0;Read LED$LED?;!LED<0 or 1>;Read temperature$TEMP?;!TEMP<x>; (<x> = temperature in degrees C)

Code is available for an Arduino Esplora to implement exactly this protocol.

The Problem

For the device above, one might write a program like the following:

- (void)readTemperature{    NSData *command = [@"$TEMP?;" dataUsingEncoding:NSASCIIStringEncoding];    [self.serialPort sendData:command];} - (void)serialPort:(ORSSerialPort *)port didReceiveData:(NSData *)data{    NSString *response = [[NSString alloc] initWithData:data usingEncoding:NSASCIIStringEncoding];    NSLog(@"response = %@", response);}

However, this won't produce the desired results. As raw incoming data is received by a computer via its serial port, the operating system delivers the data as it arrives. Often this is one or two bytes at a time. This program would likely output something like:

$> response = !T$> response = E$> response = MP$> response = 2$> response = 6$> response = ;

In the absence of an API like the one provided by ORSSerialPort, an application would have to implement a buffer, add incoming data to that buffer, and periodically check to see if a complete packet had been received:

- (void)serialPort:(ORSSerialPort *)port didReceiveData:(NSData *)data{    [self.buffer appendData:data];    if ([self bufferContainsCompletePacket:self.buffer]) {        NSString *response = [[NSString alloc] initWithData:self.buffer usingEncoding:NSASCIIStringEncoding];        NSLog(@"response = %@", response);        // Do whatever's next    }}

However, what if bad data comes in, or the device doesn't respond? The program will continue waiting for data forever. So a timeout is needed:

#define kTimeoutDuration 1.0 /* seconds */- (id)init{    self = [super init];    if (self) {        _buffer = [NSMutableData data];    }    return self;}- (void)readTemperature{    NSData *command = [@"$TEMP?;" dataUsingEncoding:NSASCIIStringEncoding];    [self.serialPort sendData:command];    self.commandSentTime = [NSDate date];} - (void)serialPort:(ORSSerialPort *)port didReceiveData:(NSData *)data{    [self.buffer appendData:data];    NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSinceDate:self.commandSentTime];    if ([self bufferContainsCompletePacket:self.buffer]) {        NSString *response = [[NSString alloc] initWithData:self.buffer usingEncoding:NSASCIIStringEncoding];        NSLog(@"response = %@", response);        // Do whatever's next    } else if (elapsedTime > kTimeoutDuration) {        NSLog(@"command timed out!);        [self.buffer setData:[NSData data]]; // Clear the buffer    } }

Even with a timeout, there are other issues with this approach. What happens if the user of the program initiates another command before a response to the previous one has been received. Should the program wait until the response is received before sending another command? If so, a command queue is needed. This quickly becomes complicated, particularly for real-world devices with many more than 2 or 3 possible commands.

The Solution

ORSSerialPort's request / response API makes solving these problems much easier. Using that API, the above program would look like:

- (void)readTemperature{    NSData *command = [@"$TEMP?;" dataUsingEncoding:NSASCIIStringEncoding];    ORSSerialRequest *request =         [ORSSerialRequest requestWithDataToSend:command                                       userInfo:nil                                timeoutInterval:kTimeoutDuration                              responseEvaluator:^BOOL(NSData *data) {            return [self bufferContainsCompletePacket:data];        }];    [self.serialPort sendRequest:request];} - (void)serialPort:(ORSSerialPort *)port didReceiveResponse:(NSData *)data toRequest:(ORSSerialRequest *)request{    NSString *response = [[NSString alloc] initWithData:data usingEncoding:NSASCIIStringEncoding];    NSLog(@"response = %@", response);}- (void)serialPort:(ORSSerialPort *)port requestDidTimeout:(ORSSerialRequest *)request{    NSLog(@"command timed out!);}

Here, ORSSerialPort will handle buffering incoming data, checking to determine if a valid response packet has been received, and timing out when appropriate. You can send multiple requests using -sendRequest: without waiting for the pending request to receive a response. ORSSerialPort will put the request(s) in a queue and send them in order, waiting until the previous request received a response (or timed out) before moving on and sending the next command.

The API in Detail

Requests or commands are are instances of ORSSerialRequest. Instances of this class are created using

+ (instancetype)requestWithDataToSend:(NSData *)dataToSend userInfo:(id)userInfo timeoutInterval:(NSTimeInterval)timeout responseEvaluator:(ORSSerialRequestResponseEvaluator)responseEvaluator.

  • dataToSend is an NSData instance containing the command data to be sent.
  • timeoutInterval is the number of seconds to wait for a response before timing out, or -1.0 to wait forever.
  • userInfo can be used to store arbitrary userInfo. You might store an enum value as an NSNumber indicating the type of the request, or a dictionary with information about the data transmitted with the request (e.g. "LED On").
  • responseEvaluator is block you supply that takes data as an argument and returns YES if the data constitutes a valid response to the request, or NO otherwise. This argument warrants further discussion.

ORSSerialPort itself has the following request/response API methods and properties:

- (BOOL)sendRequest:(ORSSerialRequest *)request;@property (strong, readonly) ORSSerialRequest *pendingRequest

The first is used to send a request. The second can be used to obtain the request for which the port is awaiting a response. If -sendRequest: is called while another request is pending, the new request is put into a queue to be sent when all previously queued requests have been sent or timed out.

Additionally, there are two relevant methods that are part of the ORSSerialPortDelegateprotocol:

- (void)serialPort:(ORSSerialPort *)serialPort didReceiveResponse:(NSData *)responseData toRequest:(ORSSerialRequest *)request;- (void)serialPort:(ORSSerialPort *)serialPort requestDidTimeout:(ORSSerialRequest *)request;

The first is called when a valid response to the most recently sent response is received. The second is called when waiting for a response timed out.

The Response Evaluator

The response evaluator block supplied when creating an ORSSerialRequest is used by ORSSerialPort to determine if the data it has received so far makes up a valid response. Your implementation of this block should be able to handle any data passed to it, including data that is not a valid response.

The implementation of the response evaluator block will depend entirely on the specifics of your data protocol. For the example device above, a simple response evaluator block for temperature read commands might look like:

^BOOL(NSData *data) {    NSString *dataAsString = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];    if (![dataAsString hasPrefix:@"!TEMP"]) return NO;    if (![dataAsString hasSuffix:@";"]) return NO;    return YES;}

This simply creates a string from the data (since expected responses are ASCII encoded text), and checks to make sure it begins with '!TEMP' and ends with a ';'.

Since you will usually need to parse a response after it has been received, to avoid duplicating very similar code, it often makes sense to factor response parsing code out into a function or method. Then, you can call this function/method in your response evaluator block as well as using it to parse successfully received responses:

- (NSNumber *)temperatureFromResponsePacket:(NSData *)data{    if (![data length]) return nil;    NSString *dataAsString = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];    if (![dataAsString hasPrefix:@"!TEMP"]) return nil;    if (![dataAsString hasSuffix:@";"]) return nil;    if ([dataAsString length] < 6) return nil;    NSString *temperatureString = [dataAsString substringWithRange:NSMakeRange(5, [dataAsString length]-6)];    return @([temperatureString integerValue]);}

Then use it in both your response evaluator block, and your response handling code:

- (void)readTemperature{    NSData *command = [@"$TEMP?;" dataUsingEncoding:NSASCIIStringEncoding];    ORSSerialRequest *request =    [ORSSerialRequest requestWithDataToSend:command                                   userInfo:nil                            timeoutInterval:kTimeoutDuration                          responseEvaluator:^BOOL(NSData *data) {            return [self temperatureFromResponsePacket:data] != nil;        }];    [self.serialPort sendRequest:request];}- (void)serialPort:(ORSSerialPort *)port didReceiveResponse:(NSData *)data toRequest:(ORSSerialRequest *)request{    NSNumber *temperature = [self temperatureFromResponsePacket:data];    NSLog(@"temperature = %@", temperature);}
0 0