1

Topic: RATC2 Client example code - C++

Hi all-

In preparation for the recent Infocomm trade-show I updated our demonstration iPhone app with new RATC2 client code. As it is a general RATC2 client written in C++ I thought others here might find it useful as an example of how to talk to RATC2 over TCP.

A few caveats of course:
1) No warranty. This is intended for demonstration and educational purposes only.
2) The error handling is very simplistic. To keep it simple I avoided the use of exceptions. All methods in the class return booleans to indicate the success or failure of each command. You are of course free to improve the error handling as suits your needs.
3) This was written primarily for, and tested primarily on, the iPhone. Consideration was made for Windows sockets APIs but little testing was done on Windows.
4) A basic knowledge of RATC/RATC2 and how it works with control aliases in NWare is assumed. If you are unfamiliar with RATC/RATC2 you should see the online help included with NWare.

That said, I think many will find this class useful 'as is' for basic RATC2 communication needs. The code is well commented but I'll add a few notes here.

There is only one top-level class: ratc2_client. This class abstracts away all socket communication and RATC2 protocol details. The user of the class need only supply the address and port of the server to connect. The socket is managed within the scope of the object. The connection will be closed and socket resource released when the object goes out of scope. A method on the class allows for the adjustment of socket communication timeout values. This value is a float representation of seconds and as such can represent sub-second values if necessary.

The class supports both change group as well individual control alias gets and sets. A sub-class of ratc2_client called 'ratc_control_value' abstracts away the need to either create or parse RATC2 commands. Objects of this sub-class are returned from all control gets and expected as input for all control sets. The ratc_control_value class allows direct access to the value and position member variables and includes constructors for creating objects suitable for either value or position set commands.

Here's a contrived example of how to use the ratc2_client class:

#include "ract2_client.h"

void example()
{
    ratc2_client client;
    
    //connect
    if (!client.connect("10.0.0.1", 1632)) {
        std::cout << "Failed to connect." << std::endl;
        return;
    }
    
    //login
    if (!client.logIn("defaultUser", "defaultPass")) {
        std::cout << "Failed to login." << std::endl;
        return;
    }
    
    //check for project name... this is optional but useful
    std::string project;
    if (!client.statusGet(project)) {
        std::cout << "Failed to get project status." << std::endl;
        return;
    } else {
        std::cout << "Connected to project: " << project << std::endl;
    }
    
    //set a control position to .5
    ratc2_client::ratc_control_value knobPosition("someKnobAlias", 0.5);
    if (!client.controlSet(knobPosition)) {
        std::cout << "Failed to set someKnobAlias position. Giving up..." << std::endl;
        return;
    } else {
        std::cout << "Set someKnobAlias to: 0.5 actual value now: " << knobPosition.position << std::endl;
    }
    
    //create a change group to monitor
    client.changeGroupControlAdd("GroupName", "control1");
    client.changeGroupControlAdd("GroupName", "control2");
    client.changeGroupControlAdd("GroupName", "control3");
    client.changeGroupControlAdd("GroupName", "control4");
    
    //monitor the change group
    ratc2_client::CONTROL_VALUE_LIST change_group_list;
    while (true) {
        if (!client.changeGroupGet("GroupName",  change_group_list)) {
            std::cout << "Connection failure? Network problems? Giving up..." << std::endl;
            return;
        }
        
        //if we have control changes process them
        if (change_group_list.size() > 0) {
            ratc2_client::CONTROL_VALUE_LIST::iterator it;
            for (it = change_group_list.begin(); it != change_group_list.end(); ++it) {
                std::cout << "Control: " << it->name << " value: '" << it->value << "' position: " << it->position << std::endl;
            }
        }
        
        //wait a while
        sleep(250);
    }
}

Here's the code itself:

ratc2_client.h

/*
 *  ratc_client.h
 *
 *  Created by Frank Vernon on 8/30/08.
 *  Copyright 2008 Peavey Electronics. All rights reserved.
 *
 */

#ifndef _H_RATC2_CLIENT_H
#define _H_RATC2_CLIENT_H

#include <string>
#include <list>

#pragma mark ratc2_client

//Simple RATC2 client 
// Note: Error handling is very simplistic. All routines return true/false
//       on success/failure but no reasons are supplied for the causes of failure
class ratc2_client {
public:
    
    //struct representing value/position for a named control alias
    struct ratc_control_value {
        std::string name;
        std::string value;
        float position;
        bool set_position;
        
        ratc_control_value();
        ratc_control_value(const char * in_ratc_response);
        ratc_control_value(const char * in_name, const char * in_value);
        ratc_control_value(const char * in_name, float in_position);
    };
    //a list of control values
    typedef std::list<ratc_control_value> CONTROL_VALUE_LIST;
    
    //construct/destruct
    ratc2_client();
    virtual ~ratc2_client();
    
    //connectioin management
    bool connect(const char * in_address, int in_port);
    void disconnect();
    
    //login to RATC server
    bool logIn(const char * in_user, const char * in_pass);
    
    //status... returns running project name
    bool statusGet(std::string & out_project);
    
    //get present value of a control alias
    bool controlGet(const char * in_control_alias, ratc_control_value & out_value);
    
    // wrapper routine to set value/position on a control alias using a ratc_control_value object
    // on return, if result is true, the io_value is updated to the actual value of the control.
    // NOTE: controls may be bound by ranges or rounded to fit allowable values thus you may want to update your UI
    // to indicate the actual value after this value returns. 
    // NOTE: There is no corresponding controlPositionSet. A position set is used if the 'in_position' 
    // version of the constructor was used to create the io_value object.
    bool controlSet(ratc_control_value & io_value);
    
    //change group add/remove/clear
    bool changeGroupControlAdd(const char * in_group_name, const char * in_control_alias);
    bool changeGroupControlRemove(const char * in_group_name, const char * in_control_alias);
    bool changeGroupClear(const char * in_group_name);

    //change group get
    // if returns false there has been a communications problem, you will likely want to try to reconnect
    bool changeGroupGet(const char * in_group_name, CONTROL_VALUE_LIST & out_responses);
    
    //adjust the read timeout if required... class defaults to -1, no timeout
    void set_read_timeout(float in_timeout_secs);
    float get_read_timeout();
    
protected:
    int client_socket;
    char read_buffer[256];
    std::string result_buffer;
    float read_timeout;
    
    //primitive methods for reading/writting RATC commands on the socket
    // you would normally use the public utility routines
    bool read_response(std::string & out_response, float in_timeout_secs = -1);
    bool send_command(const std::string & in_command);    
    bool internal_controlPositionSet(ratc_control_value & io_value);
    bool internal_controlSet(ratc_control_value & io_value);
};

#endif //_H_RATC2_CLIENT_H

ratc2_client.cpp

/*
 *  ratc2_client.cpp
 *
 *  Created by Frank Vernon on 8/30/08.
 *  Copyright 2008 Peavey Electronics. All rights reserved.
 *
 */

#include "ratc2_client.h"

#include <iostream>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <math.h>

#pragma mark ratc_control_value

ratc2_client::ratc_control_value::ratc_control_value()
:position(0), set_position(false)
{
}

//down and dirty parse of ratc command response
//example response: valueIs "level1" 0 0.000
// where: "level1" is the alias name, 0 is the value, 0.000 is the position
ratc2_client::ratc_control_value::ratc_control_value(const char * in_ratc_response)
:set_position(false), position(0.0)
{
    const char * curr_pos, * next_pos;
    curr_pos = ::strstr(in_ratc_response, " \""); //first quote
    if (curr_pos) {
        curr_pos += 2; //step over leading space and quote
        next_pos = ::strstr(curr_pos, "\" "); //second quote
        if (next_pos) {
            name.assign(curr_pos, next_pos - curr_pos); //grab name between quotes
            curr_pos = next_pos + 2; //step over trailing quote and space
            
            next_pos = ::strrchr(curr_pos, ' '); //find end of value
            if (next_pos) {
                value.assign(curr_pos, next_pos-curr_pos);
                
                curr_pos = next_pos + 1; //move to head of position
                position = ::atof(curr_pos);
            }
        }
    }
}

//create a 'value' type
ratc2_client::ratc_control_value::ratc_control_value(const char * in_name, const char * in_value)
:name(in_name), value(in_value), position(0), set_position(false)
{
}

//create a 'position' type
ratc2_client::ratc_control_value::ratc_control_value(const char * in_name, float in_position)
:name(in_name), value(""), position(in_position), set_position(true)
{
}

#pragma mark ratc2_client - public

ratc2_client::ratc2_client()
:client_socket(NULL), read_timeout(-1)
{
}
 
ratc2_client::~ratc2_client()
{
    disconnect();
}

bool 
ratc2_client::connect(const char * in_address, int in_port)
{
    disconnect();
    
    //create socket
    if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        client_socket = NULL;
        return false;
    }
    
    //create inet address structure
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(in_port);
    inet_aton(in_address, &server_address.sin_addr);
    memset(server_address.sin_zero, '\0', sizeof(server_address.sin_zero));
    
    //connect
    if (::connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
        close(client_socket);
        client_socket = NULL;
        return false;
    }
    
    //eat useless welcome message from ratc server
    send_command("\r");
    std::string response;
    read_response(response, read_timeout);
    
    return true;
}

void 
ratc2_client::disconnect()
{
    if (client_socket) {
        ::shutdown(client_socket, SHUT_RDWR);
        ::close(client_socket);
        client_socket = NULL;
    }
}

//li logIn username password : login to the ratc server
bool 
ratc2_client::logIn(const char * in_user, const char * in_pass)
{
    char temp[512];
    int result = snprintf(temp, sizeof(temp), "li %s %s\r", in_user, in_pass);
    if (result > sizeof(temp)) {
        return false;
    }
    
    send_command(temp);
    
    std::string data;
    read_response(data, read_timeout);
    
    return data.find("loggedIn") != std::string::npos;
}

//sg statusGet : return name of running project, false if not running
bool 
ratc2_client::statusGet(std::string & out_project)
{
    out_project.clear();
    
    send_command("sg\r");
    
    std::string data;
    read_response(data, read_timeout);
    
    if (data.find("statusIs") != std::string::npos) {
        std::string::size_type start = data.find(" \"");
        if (start != std::string::npos) {
            start += 2;
            std::string::size_type end = data.find("\"", start);
            if (end != std::string::npos) {
                out_project = data.substr(start, end - start);
            }
        }
    }
    
    return !out_project.empty();    
}

//cg controlGet alias : get value/position of a control alias
bool 
ratc2_client::controlGet(const char * in_control_alias, ratc_control_value & out_value)
{
    char temp[512];
    int result = snprintf(temp, sizeof(temp), "cg %s\r", in_control_alias);
    if (result > sizeof(temp)) {
        return false;
    }
    
    send_command(temp);
    
    std::string data;
    read_response(data, read_timeout);
    
    bool status = data.find("valueIs") != std::string::npos;
    
    if (status) {
        out_value = ratc_control_value(data.c_str());
    }
    
    return status;
}

// wrapper routine to set value/position on a control alias using a ratc_control_value object
// on return, if result is true, the io_value is updated to the actual value of the control
// controls may be bound by ranges or rounded to fit allowable values
bool 
ratc2_client::controlSet(ratc_control_value & io_value)
{
    if (io_value.set_position) {
        return internal_controlPositionSet(io_value);
    } else {
        return internal_controlSet(io_value);
    }
}

//cgca changeGroupControlAdd [group] control : add a Control to a Change Group
bool
ratc2_client::changeGroupControlAdd(const char * in_group_name, const char * in_control_alias)
{
    char temp[512];
    int result = snprintf(temp, sizeof(temp), "cgca %s %s\r", in_group_name, in_control_alias);
    if (result > sizeof(temp)) {
        return false;
    }
    
    send_command(temp);
    
    std::string data;
    read_response(data, read_timeout);
    
    bool status = data.find("changeGroupControlAdded") != std::string::npos;
        
    return status;
}

//cgg  changeGroupGet [group] : get changed values from a Change Group
// if returns false there has been a communications problem, you will likely want to try to reconnect
bool
ratc2_client::changeGroupGet(const char * in_group, CONTROL_VALUE_LIST & out_responses)
{    
    out_responses.clear();
    
    char temp[512];
    int result = snprintf(temp, sizeof(temp), "cgg %s\r", in_group);
    if (result > sizeof(temp)) {
        return false;
    }
    
    send_command(temp);
    
    std::string data;
    read_response(data, read_timeout);
    
    //check for expected response in response buffer
    result = snprintf(temp, sizeof(temp), "changeGroupChanges \"%s\" ", in_group);
    if (result > sizeof(temp)) {
        return false;
    }
    
    std::string::size_type pos = data.find(temp);
    if (pos == std::string::npos) {
        return false;
    }
    
    //get number of responses to expect and then read them all
    uint32_t count = atol(data.substr(pos + result).c_str());
    for (int i = 0; i < count; ++i) {
        if (!read_response(data, read_timeout)) {
            return false;
        }
        //expect 'valueIs' at head of each response
        if (data.find("valueIs") == 0) {
            out_responses.push_back(ratc_control_value(data.c_str()));
        }
    }
    
    return true;
}

void 
ratc2_client::set_read_timeout(float in_timeout_secs)
{
    read_timeout = in_timeout_secs;
}

float 
ratc2_client::get_read_timeout()
{
    return read_timeout;
}

#pragma mark ratc2_client - protected

//Internal routine to read from the socket and return the next correctly formed
// response from the ratc server.
bool 
ratc2_client::read_response(std::string & out_response, float in_timeout_secs)
{
    if (!client_socket) {
        return false;
    }
    
    out_response.clear();
    
    // Initialize time out struct in case we need it
    double int_part, fract_part;
    fract_part = ::modf(in_timeout_secs, &int_part);
    struct timeval tv;
    tv.tv_sec = int_part;
    tv.tv_usec = fract_part * 1000000.0;
    
    //read until we have line ending in the byte stream or until we timeout
    while (true) {
        //check for result already in local buffer
        std::string::size_type pos = result_buffer.find("\r\n");
        if (pos != std::string::npos) {
            out_response = result_buffer.substr(0, pos); //return response w/o CRLF
            result_buffer.erase(0, pos + 2); //remove response including CRLF
            break;
        }
        
        //do select for timeout if necessary
        if (in_timeout_secs >= 0) {
            // Initialize the fd set
            fd_set readset;
            FD_ZERO(&readset);
            FD_SET(client_socket, &readset);
            
            //do select
            //Note: Windows select() is archaic... it does not return the time remaining in the tv struct
            //        so you might have long timeouts on Windows here under some scenarios. On 'real' OSes
            //        the timeout will count down across multiple calls to select so the total elapsed time
            //        here will not exceed the value passed in by the user.
            int result = select(client_socket+1, &readset, NULL, NULL, in_timeout_secs >= 0 ? &tv : NULL);
            if (result < 0 || !FD_ISSET(client_socket, &readset)) {
                return false;
            }
        }
        
        //read available data
        int bytes_read = recv(client_socket, read_buffer, sizeof(read_buffer), 0);
        if (bytes_read == -1) {
            return false;
        }
        
        //append data to our local buffer and try again
        result_buffer.append(read_buffer, bytes_read);
    }
    
    return true;
}

//Internal routine to write to the socket
bool 
ratc2_client::send_command(const std::string & in_command)
{
    if (!client_socket) {
        return false;
    }
    
    std::string::size_type command_len = in_command.length();
    const char * data_ptr = in_command.c_str();
    int data_sent = 0;
    while (command_len) {
        int sent = ::send(client_socket, data_ptr + data_sent, command_len, 0);
        if (sent == -1) {
            return false;
        }
        data_sent += sent;
        command_len -= sent;
    }
    
    return true;
}

//cps  controlPositionSet control position : set a Control's position (0.00-1.00)
bool 
ratc2_client::internal_controlPositionSet(ratc_control_value & io_value)
{
    char temp[512];
    int result = snprintf(temp, sizeof(temp), "cps \"%s\" %g\r", io_value.name.c_str(), io_value.position);
    if (result > sizeof(temp)) {
        return false;
    }
    
    send_command(std::string(temp));
    
    std::string data;
    read_response(data, read_timeout);
    if (data.find("valueIs") != std::string::npos) {
        io_value = ratc_control_value(data.c_str());
        return true;
    } else {
        return false;
    }
}

//cs  controlSet control value : set a Control's value
bool 
ratc2_client::internal_controlSet(ratc_control_value & io_value)
{
    char temp[512];
    int result = snprintf(temp, sizeof(temp), "cs \"%s\" %s\r", io_value.name.c_str(), io_value.value.c_str());
    if (result > sizeof(temp)) {
        return false;
    }
    
    send_command(std::string(temp));
    
    std::string data;
    read_response(data, read_timeout);
    if (data.find("valueIs") != std::string::npos) {
        io_value = ratc_control_value(data.c_str());
        return true;
    } else {
        return false;
    }
}

Last edited by fvernon (2009-07-07 19:49:18)

2

Re: RATC2 Client example code - C++

fvernon wrote:

//Note: Windows select() is archaic... it does not return the time remaining in the tv struct
            //        so you might have long timeouts on Windows here under some scenarios. On 'real' OSes
            //        the timeout will count down across multiple calls to select so the total elapsed time
            //        here will not exceed the value passed in by the user.

A 'real' OS? Like Vista<grin>? I believe POSIX allows select() to leave timeout alone, or to modify it as you note. Unfortunately portable code cannot rely on that feature.

Kudos for actually implementing a line reassembly buffer, a run into a lot of people to don't realize that the stream nature of TCP offers no guarantee that a single read operation will return a full block/line/record/whatever. I like to test the code by limiting read() to a single byte.