Sunday, September 6, 2015

Raspberry Pi: How to create your own WebSocket API - Part 2

Part 1: Setup xcode project

Now that I've set up the server to handle request from the client we can set up the iOS client. Either you can start with a blank project or fork/clone my github example.




  • Now install the SocketRocket websocket client library.  There are a couple ways to do it.  I used cocoapods, follow the directions for installation here.   I setup a basic uitableview within my viewcontroller.  
  • Link your tableview to your viewcontroller and call it tableView. 
  • Setup your viewDidLoad

static NSString *cellIdent = @"cellIdent";
NSMutableArray *outputs; //global array of our outputs for the piface 
NSMutableArray *inputs;  //global array of our inputs
- (void)viewDidLoad {
    [super viewDidLoad];
    _webSocket = [[SRWebSocket alloc] initWithURL:[NSURL URLWithString:@"wss://192.168.5.21:9000" ]];
    _webSocket.delegate = self;
    outputs = [@[@0,@0,@0,@0,@0,@0,@0,@0] mutableCopy];//pre populate our array with zeros
    inputs  = [@[@0,@0,@0,@0,@0,@0,@0,@0] mutableCopy];// "
    self.tableView.dataSource = self; //don't forget to set your delegate for your datasource
//we have a custom xib for our tableview cell.  register it here
    [self.tableView registerNib:[UINib nibWithNibName:@"OutputTableViewCell" bundle:nil] forCellReuseIdentifier:cellIdent];
    [self.webSocket open]; //open our websocket
}


We need to set our socket rocket delegate so we can handle messages from the server:


-(void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean{
    [self performSelector:@selector(connectWS) withObject:nil afterDelay:5];
}
-(void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error{
    [self performSelector:@selector(connectWS) withObject:nil afterDelay:5];

}
-(void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message{
    NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data
                                                         options:NSJSONReadingMutableContainers
                                                           error:nil];
    [self parseJSON:json];
}
-(void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload{
    
}
-(void)webSocketDidOpen:(SRWebSocket *)webSocket{
    
}
-(void)parseJSON:(NSDictionary*)jsonObj{
    if([jsonObj isKindOfClass:[NSArray class]]){
        for (NSDictionary *dict in jsonObj) {
            if([dict objectForKey:@"Outputs"]) [self parseOutputs:[dict objectForKey:@"Outputs"]];
            if([dict objectForKey:@"Inputs"]) [self parseInputs:[dict objectForKey:@"Inputs"]];
        }
    }
    else{
        if([jsonObj objectForKey:@"Outputs"]) [self parseOutputs:[jsonObj objectForKey:@"Outputs"]];
        if([jsonObj objectForKey:@"Inputs"]) [self parseInputs:[jsonObj objectForKey:@"Inputs"]];
    }
}
-(void)parseOutputs:(NSString*)outputStr{
    for(int index=0; index < outputStr.length; index++){
        NSString *val = [outputStr substringWithRange:NSMakeRange(index, 1)];
        [outputs setObject:@([val boolValue]) atIndexedSubscript:index];
    }
    [self.tableView reloadData];
}
-(void)parseInputs:(NSString*)inputStr{
    for(int index=0; index < inputStr.length; index++){
        NSString *val = [inputStr substringWithRange:NSMakeRange(index, 1)];
        [inputs setObject:@([val boolValue]) atIndexedSubscript:index];
    }
    [self.tableView reloadData];
}

Note what we just did here:

  1. Set up the delegates
    •  webSocketDidOpen
    • didRecieveMessage
    • didCloseWithCode
    • didFailWithError
  2. Created code to handle the JSON message
    • Convert our JSON msg into NSData and then into a NSDictionary
    • Parsing Inputs and Outputs and modifying our global arrays for each
Now we set up the table a bit more:

-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    OutputTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdent forIndexPath:indexPath];
    if(indexPath.section==0){
        int output_num = [@(indexPath.item) intValue];
        [cell setLabelText:[NSString stringWithFormat:@"Output: %d",output_num]];
        [cell setOutputStatus:[outputs[indexPath.item] boolValue]];
        [cell setOutput:output_num];
        [cell setCmd:^(int output) {
            [self.webSocket send:[NSString stringWithFormat:@"{\"Output\":\"%d\"}",output]];
        }];
    }
    else{
        int input_num = [@(indexPath.item) intValue];
        [cell setLabelText:[NSString stringWithFormat:@"Input: %d",input_num]];
        [cell setOutputStatus:[inputs[indexPath.item] boolValue]];
        [cell setCmd:nil];
    }
    return cell;
}
-(void)connectWS{
    _webSocket = [[SRWebSocket alloc] initWithURL:[NSURL URLWithString:@"wss://10.10.55.21:9000" ]];
     _webSocket.delegate = self;
    [self.webSocket open];
    
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    if(section==0)
        return outputs.count;
    if(section ==1)
        return inputs.count;
    else
        return 0;
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 2;
}

When we set the table up we use our custom cell class:

  1. Cell count/ Section count.  We have two types of cells. One we don't have a reaction to button presses (inputs we are reading). And the other we have the outputs we want to control and see status.    
    • return 2 in our numberOfSectionsInTableView
    • return our output/input row count depending on the section number in numberOfRowsInSection
  2. Connect our cells up to the status stored in the global input/output array
    • If it is an output we set up the callback to send a custom message to our WebSocket Server.  If you recall we set our server to respond to JSON messages formatted as {"Output",pinNumber}.  So we use the IndexPath.Item to determine our position in the inputs/outputs.
    • If it is an input we do not set the "setCmd" callback from our button press.  
  3. We also set up a connectWS method to have it attempt to re-connect after 5 if the websocket connection drops. (usually happens when you lose connection to wifi/cellular or if you restart the websocket server.
In this iOS app I won't connect my custom plug code since you haven't set up the server to respond to anything but output commands.  We could set it up to be {"Plug":plugNum} in the server if we needed.  

Now we only color our output and input buttons ONLY when we know the status.  Don't set the color just because you tapped the button, let the server reply with it's status.  


For simplistic purposes I didn't decorate it anymore than this in this example.  You can be creative and set up your UI however it seems easiest.  Note that iOS does not keep this connection open when you close your app.  I don't handle it elegantly in my example code.  If you wish to have your phone get updates on your sensors while off you will need to set it up as a background service and have it poll a REST-like service you can set up on your Pi with-in the same server app.  


Custom API and modules for WebSockets

WebSocket communication is one of the best ways to get realtime data to your client device.  Creating custom class modules for each sensor type connected to device can make your code and  organized and and your project expandable.