-
-
Notifications
You must be signed in to change notification settings - Fork 183
Request Response API
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 is the class 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.
Quick note: This document uses Objective-C in all code examples. However, everything described here works in Swift as well, using the Swift equivalents. The Swift version of the RequestResponseDemo example project is useful for seeing how the things described here work in Swift.
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.
The request / response API is built on top of ORSSerialPort's packet parsing API. An understanding of the packet parsing API is useful before using the request / response API. See the Packet Parsing API documentation for more information.
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:
Function | Command | Response |
---|---|---|
Turn 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.
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.
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.
Requests or commands are instances of ORSSerialRequest
. Instances of this class are created using
+ (instancetype)requestWithDataToSend:(NSData *)dataToSend userInfo:(id)userInfo timeoutInterval:(NSTimeInterval)timeout responseDescriptor:(nullable ORSSerialPacketDescriptor *)responseDescriptor;
- 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").
- responseDescriptor is an instance of
ORSSerialPacketDescriptor
describing a valid response to the request.
ORSSerialPort
itself has the following request/response API methods and properties:
- (BOOL)sendRequest:(ORSSerialRequest *)request;
@property (strong, readonly) ORSSerialRequest *pendingRequest;
@property (strong, readonly) NSArray *queuedRequests;
The first is used to send a request. The pendingRequest
property 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. The queued requests -- those waiting to be sent -- can be retrieved using the queuedRequests
property.
Additionally, there are two relevant methods that are part of the ORSSerialPortDelegate
protocol:
- (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 packet descriptor supplied when creating an ORSSerialRequest
is used by ORSSerialPort to determine if the data it has received so far makes up a valid response. See the Packet Parsing API documentation for details about this, including information about how best to implement a packet evaluator block and when that's necessary.
An example app showing how the request/response API can be used can be found in ORSSerialPort's Examples folder. It is called RequestResponseDemo, and both Objective-C and Swift versions are available. It expects to be connected to an Arduino Esplora board running the firmware found in this repository.
The app asks the Arduino for the current temperature and the LED state once per second, and plots the received temperature values on a simple plot. It also includes a checkbox that can be used to turn the Arduino's LED on and off. The LED can also be toggled by pressing the down button on the Esplora, and its state will be reflected in the app's UI.
The serial communications are implemented essentially entirely in ORSSerialBoardController.m
.